160 lines
5 KiB
C++
160 lines
5 KiB
C++
/*
|
|
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
|
|
);
|
|
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<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;
|
|
}
|
|
std::uint8_t* dst = PixelPtr();
|
|
for (int row = 0; row < sh; ++row) {
|
|
std::memcpy(
|
|
dst + (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;
|
|
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<std::int32_t>(x), static_cast<std::int32_t>(y),
|
|
static_cast<std::int32_t>(w), static_cast<std::int32_t>(h)
|
|
);
|
|
#endif
|
|
dirty = false;
|
|
dirtyRect.Reset();
|
|
}
|