Crafter.Graphics/interfaces/Crafter.Graphics-FontAtlas.cppm

148 lines
6 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "vulkan/vulkan.h"
#endif
export module Crafter.Graphics:FontAtlas;
import std;
import :Font;
import :GraphicsTypes;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
import :ImageVulkan;
import :Device;
#else
import :WebGPU;
#endif
export namespace Crafter {
// Per-glyph metrics. UVs are 0..1 in atlas space; on-screen sizes /
// offsets / advance are in *atlas pixels at the base size* and scale
// linearly with the requested font size at draw time.
struct Glyph {
float u0 = 0, v0 = 0;
float u1 = 0, v1 = 0;
float w = 0, h = 0;
float xoff = 0, yoff = 0;
float advance = 0;
};
class FontAtlas {
public:
static constexpr int kAtlasSize = 1024;
static constexpr float kBaseSize = 32.0f;
static constexpr int kPadding = 4;
static constexpr int kOnEdgeValue = 128;
static constexpr float kPixelDistScale = 32.0f;
// Bounding box of the atlas pixels touched since the last upload,
// used to copy only the sub-rect that actually changed instead of
// the whole 1 MiB atlas. Half-open: covers [minX, maxX) × [minY,
// maxY). `active` distinguishes "nothing dirty" from a zero-origin
// rect.
struct DirtyRect {
int minX = 0, minY = 0, maxX = 0, maxY = 0;
bool active = false;
bool Empty() const noexcept { return !active; }
void Reset() noexcept { active = false; }
// Grow the box to include [x, x+w) × [y, y+h). No-op for an
// empty rect (a glyph that rasterized to nothing marks nothing).
void Add(int x, int y, int w, int h) noexcept {
if (w <= 0 || h <= 0) return;
if (!active) {
minX = x; minY = y; maxX = x + w; maxY = y + h;
active = true;
} else {
minX = std::min(minX, x);
minY = std::min(minY, y);
maxX = std::max(maxX, x + w);
maxY = std::max(maxY, y + h);
}
}
// Clamp the box to [0, limit] on both axes and emit it as an
// (origin, extent) pair for a GPU copy. Returns false when
// empty, leaving the outputs untouched.
bool UploadBox(int limit, std::uint32_t& x, std::uint32_t& y, std::uint32_t& w, std::uint32_t& h) const noexcept {
if (!active) return false;
const int x0 = std::clamp(minX, 0, limit);
const int y0 = std::clamp(minY, 0, limit);
const int x1 = std::clamp(maxX, 0, limit);
const int y1 = std::clamp(maxY, 0, limit);
x = static_cast<std::uint32_t>(x0);
y = static_cast<std::uint32_t>(y0);
w = static_cast<std::uint32_t>(x1 - x0);
h = static_cast<std::uint32_t>(y1 - y0);
return true;
}
};
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
ImageVulkan<std::uint8_t> image;
#else
WebGPUTextureRef textureHandle = 0;
std::vector<std::uint8_t> staging;
#endif
// `dirty` stays the cheap "is there anything to flush?" flag the
// renderer polls each frame; it is the OR of every dirty span Update
// copies. `dirtyRect` is the *whole-atlas* span — used only for the
// one-shot zero-clear in Initialize; per-glyph dirt is tracked tight
// per shelf (Shelf::dirty) instead of inflating one tall union box
// across scattered shelves (#129). `dirty` and the spans are always
// armed and cleared together.
bool dirty = false;
DirtyRect dirtyRect;
void Initialize(GraphicsCommandBuffer cmd);
// Returns the row-major byte pointer the CPU writes pixels into.
// Same shape on both backends.
std::uint8_t* PixelPtr() noexcept;
bool Ensure(Font& font, std::uint32_t codepoint);
// Ensure the glyph is in the atlas and return a pointer to it in one
// hash+probe. Returns nullptr only when a brand-new glyph cannot be
// placed (ShelfPlace failure); a glyph with no SDF coverage (e.g.
// space) still returns a valid pointer.
const Glyph* EnsureAndGet(Font& font, std::uint32_t codepoint);
const Glyph* Lookup(Font& font, std::uint32_t codepoint) const;
void Update(GraphicsCommandBuffer cmd);
// Flag the [x, x+w) × [y, y+h) atlas region as needing re-upload,
// keeping `dirty` in lockstep with the accumulated `dirtyRect`.
void MarkDirty(int x, int y, int w, int h) noexcept {
dirtyRect.Add(x, y, w, h);
dirty = !dirtyRect.Empty();
}
private:
// A shelf packs glyphs left-to-right at a fixed top (`y`). Its `dirty`
// span therefore stays naturally tight: a contiguous X run capped by
// the shelf height — far smaller than a union box spanning every
// shelf a frame happened to touch.
struct Shelf { int y = 0; int height = 0; int cursorX = 0; DirtyRect dirty; };
std::vector<Shelf> shelves_;
int nextShelfY_ = 0;
struct Key {
const Font* font;
std::uint32_t cp;
bool operator==(const Key&) const = default;
};
struct KeyHash {
std::size_t operator()(const Key& k) const noexcept {
std::size_t h1 = std::hash<const void*>{}(k.font);
std::size_t h2 = std::hash<std::uint32_t>{}(k.cp);
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
}
};
std::unordered_map<Key, Glyph, KeyHash> cache_;
// On success, outShelf is the index into shelves_ of the placed
// glyph, so the caller can mark that shelf's dirty span.
bool ShelfPlace(int w, int h, int& outX, int& outY, int& outShelf);
};
}