Crafter.Graphics/interfaces/Crafter.Graphics-FontAtlas.cppm
Jorijn van der Graaf b16f1e99d5
fix(font-atlas): the atlas destroys its image
FontAtlas::Initialize creates an ImageVulkan and nothing ever called its
Destroy(): every atlas left its 1024x1024 image, memory and view behind.
The atlas now owns the image for its lifetime and is no longer copyable.
ImageVulkan's handles start as VK_NULL_HANDLE and Destroy() resets them,
so an atlas that was never initialised, or an image destroyed by hand
before the atlas goes, is a no-op in the destructor.

The owner waits for the GPU before the atlas goes and, in sets mode,
destroys the descriptor heap that holds the image's view before the atlas.
2026-10-18 01:12:21 +02:00

160 lines
6.4 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;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
FontAtlas() = default;
FontAtlas(const FontAtlas&) = delete;
FontAtlas& operator=(const FontAtlas&) = delete;
// The image goes with the atlas. The owner waits for the GPU first
// and, in sets mode, destroys the heap holding the image's view before
// the atlas.
~FontAtlas() {
if (image.image != VK_NULL_HANDLE) image.Destroy();
}
#endif
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);
};
}