/* 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" #include "../lib/stb_truetype.h" module Crafter.Graphics:FontAtlas_impl; import :FontAtlas; import :Font; import :ImageVulkan; import :Device; import std; using namespace Crafter; void FontAtlas::Initialize(VkCommandBuffer cmd) { 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 ); // Staging buffer is mapped; clear it so empty atlas regions sample as // distance < onedge (i.e. fully outside any glyph). std::memset(image.buffer.value, 0, kAtlasSize * kAtlasSize); dirty = true; } bool FontAtlas::ShelfPlace(int w, int h, int& outX, int& outY) { // Try existing shelves first — same height heuristic keeps fragmentation low. for (Shelf& s : shelves_) { if (h <= s.height && s.cursorX + w <= kAtlasSize) { outX = s.cursorX; outY = s.y; s.cursorX += w; return true; } } // New shelf below current ones. 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) { Key key{&font, codepoint}; if (cache_.contains(key)) return true; float fontScale = stbtt_ScaleForPixelHeight(&font.font, kBaseSize); // Advance is always present, even for empty glyphs (e.g. space). 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 false; // V1: silently drop overflow; V2: grow atlas } // Blit row-by-row into the mapped staging buffer. for (int row = 0; row < sh; ++row) { std::memcpy( image.buffer.value + (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; dirty = true; } // For empty glyphs (whitespace) we still cache the entry — the size-0 // fields tell the emitter to skip the quad but advance the cursor. cache_.emplace(key, g); return true; } 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(VkCommandBuffer cmd) { if (!dirty) return; image.Update(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); dirty = false; }