/* 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; #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(x0); y = static_cast(y0); w = static_cast(x1 - x0); h = static_cast(y1 - y0); return true; } }; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM ImageVulkan image; #else WebGPUTextureRef textureHandle = 0; std::vector staging; #endif // `dirty` stays the cheap "is there anything to flush?" flag the // renderer polls each frame; `dirtyRect` carries the bounds Update // copies. The two are always set and cleared together (dirty == // !dirtyRect.Empty()). 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: struct Shelf { int y = 0; int height = 0; int cursorX = 0; }; std::vector 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{}(k.font); std::size_t h2 = std::hash{}(k.cp); return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2)); } }; std::unordered_map cache_; bool ShelfPlace(int w, int h, int& outX, int& outY); }; }