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

167 lines
5.4 KiB
C++
Raw Normal View History

2026-05-01 23:35:37 +02:00
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
*/
module;
2026-05-18 04:58:52 +02:00
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
2026-05-01 23:35:37 +02:00
#include "vulkan/vulkan.h"
2026-05-18 04:58:52 +02:00
#endif
2026-05-01 23:35:37 +02:00
#include "../lib/stb_truetype.h"
2026-05-02 21:08:20 +02:00
module Crafter.Graphics:FontAtlas_impl;
import :FontAtlas;
2026-05-01 23:35:37 +02:00
import :Font;
2026-05-18 04:58:52 +02:00
import :GraphicsTypes;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
2026-05-01 23:35:37 +02:00
import :ImageVulkan;
import :Device;
2026-05-18 04:58:52 +02:00
#else
import :WebGPU;
#endif
2026-05-01 23:35:37 +02:00
import std;
using namespace Crafter;
2026-05-18 04:58:52 +02:00
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
2026-05-01 23:35:37 +02:00
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,
// Streamed: glyphs are rasterized into buffer.value on the CPU and
// re-uploaded every frame via UpdateRegion, so the persistent staging
// map must outlive the first upload rather than being released (#114).
/*streamed*/ true
2026-05-01 23:35:37 +02:00
);
std::memset(image.buffer.value, 0, kAtlasSize * kAtlasSize);
2026-05-18 04:58:52 +02:00
#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);
2026-05-01 23:35:37 +02:00
}
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) {
2026-05-01 23:35:37 +02:00
Key key{&font, codepoint};
if (auto it = cache_.find(key); it != cache_.end()) return &it->second;
2026-05-01 23:35:37 +02:00
float fontScale = stbtt_ScaleForPixelHeight(&font.font, kBaseSize);
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 nullptr;
2026-05-01 23:35:37 +02:00
}
2026-05-18 04:58:52 +02:00
std::uint8_t* dst = PixelPtr();
2026-05-01 23:35:37 +02:00
for (int row = 0; row < sh; ++row) {
std::memcpy(
2026-05-18 04:58:52 +02:00
dst + (py + row) * kAtlasSize + px,
2026-05-01 23:35:37 +02:00
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;
MarkDirty(px, py, sw, sh);
2026-05-01 23:35:37 +02:00
}
return &cache_.emplace(key, g).first->second;
2026-05-01 23:35:37 +02:00
}
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;
}
2026-05-18 04:58:52 +02:00
void FontAtlas::Update(GraphicsCommandBuffer cmd) {
2026-05-01 23:35:37 +02:00
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);
2026-05-18 04:58:52 +02:00
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
image.UpdateRegion(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, x, y, w, h);
2026-05-18 04:58:52 +02:00
#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.
2026-05-18 04:58:52 +02:00
WebGPU::wgpuWriteAtlasRegion(
textureHandle, staging.data(),
kAtlasSize, kAtlasSize, kAtlasSize,
static_cast<std::int32_t>(x), static_cast<std::int32_t>(y),
static_cast<std::int32_t>(w), static_cast<std::int32_t>(h)
2026-05-18 04:58:52 +02:00
);
#endif
2026-05-01 23:35:37 +02:00
dirty = false;
dirtyRect.Reset();
2026-05-01 23:35:37 +02:00
}