/* Crafter®.Graphics Copyright (C) 2026 Catcrafts® catcrafts.net */ module; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM #include "vulkan/vulkan.h" #endif #include "../lib/stb_truetype.h" module Crafter.Graphics:FontAtlas_impl; import :FontAtlas; import :Font; import :GraphicsTypes; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM import :ImageVulkan; import :Device; #else import :WebGPU; #endif import std; using namespace Crafter; std::uint8_t* FontAtlas::PixelPtr() noexcept { #ifndef CRAFTER_GRAPHICS_WINDOW_DOM return image.buffer.value; #else return staging.data(); #endif } void FontAtlas::Initialize(GraphicsCommandBuffer cmd) { #ifndef CRAFTER_GRAPHICS_WINDOW_DOM image.Create( kAtlasSize, kAtlasSize, /*mipLevels*/ 1, cmd, VK_FORMAT_R8_UNORM, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, // The atlas is sampled by the UI text *compute* shader, not an RT // pipeline — scope the upload barriers to the real consumer. VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT ); std::memset(image.buffer.value, 0, kAtlasSize * kAtlasSize); #else (void)cmd; staging.assign(kAtlasSize * kAtlasSize, 0); textureHandle = WebGPU::wgpuCreateAtlasTexture(kAtlasSize, kAtlasSize); #endif // The freshly-zeroed atlas backs an image whose contents are undefined // (Vulkan) / unwritten (WebGPU). Mark the whole extent dirty so the // first Update clears it once; thereafter only glyph sub-rects flip // dirty, and every untouched texel stays the zero it was uploaded as — // so partial uploads keep the image byte-identical to the staging copy. MarkDirty(0, 0, kAtlasSize, kAtlasSize); } bool FontAtlas::ShelfPlace(int w, int h, int& outX, int& outY) { for (Shelf& s : shelves_) { if (h <= s.height && s.cursorX + w <= kAtlasSize) { outX = s.cursorX; outY = s.y; s.cursorX += w; return true; } } if (nextShelfY_ + h > kAtlasSize) return false; Shelf s{}; s.y = nextShelfY_; s.height = h; s.cursorX = w; outX = 0; outY = s.y; shelves_.push_back(s); nextShelfY_ += h; return true; } bool FontAtlas::Ensure(Font& font, std::uint32_t codepoint) { return EnsureAndGet(font, codepoint) != nullptr; } const Glyph* FontAtlas::EnsureAndGet(Font& font, std::uint32_t codepoint) { Key key{&font, codepoint}; if (auto it = cache_.find(key); it != cache_.end()) return &it->second; float fontScale = stbtt_ScaleForPixelHeight(&font.font, kBaseSize); int advanceUnits = 0, lsb = 0; stbtt_GetCodepointHMetrics(&font.font, static_cast(codepoint), &advanceUnits, &lsb); int sw = 0, sh = 0, xoff = 0, yoff = 0; unsigned char* sdf = stbtt_GetCodepointSDF( &font.font, fontScale, static_cast(codepoint), kPadding, static_cast(kOnEdgeValue), kPixelDistScale, &sw, &sh, &xoff, &yoff ); Glyph g{}; g.advance = advanceUnits * fontScale; g.xoff = static_cast(xoff); g.yoff = static_cast(yoff); if (sdf && sw > 0 && sh > 0) { int px = 0, py = 0; if (!ShelfPlace(sw, sh, px, py)) { stbtt_FreeSDF(sdf, nullptr); return nullptr; } std::uint8_t* dst = PixelPtr(); for (int row = 0; row < sh; ++row) { std::memcpy( dst + (py + row) * kAtlasSize + px, sdf + row * sw, static_cast(sw) ); } stbtt_FreeSDF(sdf, nullptr); g.w = static_cast(sw); g.h = static_cast(sh); g.u0 = static_cast(px) / kAtlasSize; g.v0 = static_cast(py) / kAtlasSize; g.u1 = static_cast(px + sw) / kAtlasSize; g.v1 = static_cast(py + sh) / kAtlasSize; MarkDirty(px, py, sw, sh); } return &cache_.emplace(key, g).first->second; } const Glyph* FontAtlas::Lookup(Font& font, std::uint32_t codepoint) const { auto it = cache_.find(Key{&font, codepoint}); return it == cache_.end() ? nullptr : &it->second; } void FontAtlas::Update(GraphicsCommandBuffer cmd) { if (!dirty) return; // Clamp the accumulated box to the atlas and convert to (origin, // extent). Add() works in glyph coordinates that are always in-bounds, // but clamping keeps the copy extent provably valid. The `dirty` guard // above guarantees the rect is non-empty, so UploadBox always fills the // outputs. std::uint32_t x = 0, y = 0, w = 0, h = 0; dirtyRect.UploadBox(kAtlasSize, x, y, w, h); #ifndef CRAFTER_GRAPHICS_WINDOW_DOM image.UpdateRegion(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, x, y, w, h); #else (void)cmd; // The staging buffer keeps the full atlas row stride, so srcBytesPerRow // stays kAtlasSize and (dstX, dstY) index the sub-rect's first texel. WebGPU::wgpuWriteAtlasRegion( textureHandle, staging.data(), kAtlasSize, kAtlasSize, kAtlasSize, static_cast(x), static_cast(y), static_cast(w), static_cast(h) ); #endif dirty = false; dirtyRect.Reset(); }