Crafter.Graphics/implementations/Crafter.Graphics-FontAtlas.cpp

129 lines
4.2 KiB
C++

/*
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<int>(codepoint), &advanceUnits, &lsb);
int sw = 0, sh = 0, xoff = 0, yoff = 0;
unsigned char* sdf = stbtt_GetCodepointSDF(
&font.font, fontScale, static_cast<int>(codepoint),
kPadding, static_cast<unsigned char>(kOnEdgeValue), kPixelDistScale,
&sw, &sh, &xoff, &yoff
);
Glyph g{};
g.advance = advanceUnits * fontScale;
g.xoff = static_cast<float>(xoff);
g.yoff = static_cast<float>(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<std::size_t>(sw)
);
}
stbtt_FreeSDF(sdf, nullptr);
g.w = static_cast<float>(sw);
g.h = static_cast<float>(sh);
g.u0 = static_cast<float>(px) / kAtlasSize;
g.v0 = static_cast<float>(py) / kAtlasSize;
g.u1 = static_cast<float>(px + sw) / kAtlasSize;
g.v1 = static_cast<float>(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;
}