/* Crafter®.Graphics Copyright (C) 2026 Catcrafts® catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3.0 as published by the Free Software Foundation; This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; #include "vulkan/vulkan.h" export module Crafter.Graphics:FontAtlas; import std; import :Font; import :ImageVulkan; import :Device; 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; // top-left UV in the atlas float u1 = 0, v1 = 0; // bottom-right UV in the atlas float w = 0, h = 0; // glyph quad size in atlas px (= the bitmap size) float xoff = 0, yoff = 0; // glyph bearing relative to baseline cursor float advance = 0; // horizontal advance at base size, in atlas px }; // Single-channel SDF atlas. Glyphs are rasterised with stb_truetype's // GetGlyphSDF at a fixed `kBaseSize` resolution and packed via a simple // shelf allocator. Drawing scales the glyph quad linearly; the shader // resolves edge AA via screen-space derivatives, so a single atlas // serves all sizes and DPI scales without re-bake. class FontAtlas { public: // Build-time constants. Tweak in one place if needed; values picked // to give crisp text from ~10pt to ~96pt and leave headroom in the // SDF distance band so smoothstep is in the linear regime. static constexpr int kAtlasSize = 1024; static constexpr float kBaseSize = 32.0f; // pixel-height at which we rasterise static constexpr int kPadding = 4; // distance-field padding around each glyph static constexpr int kOnEdgeValue = 128; // 8-bit value mapped to "0 distance" static constexpr float kPixelDistScale = 32.0f; // how many distance units per pixel — wider = softer AA range ImageVulkan image; bool dirty = false; // staging has unflushed writes // Allocate the GPU image and zero-clear it. Must be called once // with a one-shot init command buffer. void Initialize(VkCommandBuffer cmd); // Rasterise + pack the glyph if it isn't cached yet. Returns // false only if the atlas is out of space (V2: grow). After a // successful Ensure the bitmap lives in `image.buffer.value` and // `dirty` is true; call Update(cmd) before reading on the GPU. bool Ensure(Font& font, std::uint32_t codepoint); // Lookup is cheap (hash-table). Returns nullptr if the glyph // hasn't been Ensured. const Glyph* Lookup(Font& font, std::uint32_t codepoint) const; // If `dirty`, flushes staging into the GPU image and transitions // it back to SHADER_READ_ONLY_OPTIMAL. No-op if not dirty. void Update(VkCommandBuffer cmd); private: // Shelf packer state. struct Shelf { int y = 0; int height = 0; int cursorX = 0; }; std::vector shelves_; int nextShelfY_ = 0; // (font*, codepoint) → Glyph cache. 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{}(k.font); std::size_t h2 = std::hash{}(k.cp); return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2)); } }; std::unordered_map cache_; // Place a wxh glyph; returns true + writes top-left into outX/outY. bool ShelfPlace(int w, int h, int& outX, int& outY); }; }