perf(font): upload only the dirty sub-rect of the glyph atlas (#51)
FontAtlas::Update re-uploaded the entire 1024x1024 R8 atlas (1 MiB) whenever a single glyph was rasterized, so warm-up / language-switch / size-churn cost roughly one full-atlas copy per frame that added a codepoint. Accumulate a dirty bounding box (FontAtlas::DirtyRect) in MarkDirty as glyphs are placed, and copy only that sub-rect. On Vulkan this is a new ImageVulkan::UpdateRegion: the staging buffer keeps the full atlas row stride, so bufferRowLength stays the atlas width and bufferOffset points at the rect origin while imageOffset/imageExtent carve out the rect. Layout transitions stay whole-image (cheap); only the copy extent shrinks. WebGPU passes the rect straight to wgpuWriteAtlasRegion, which already took x/y/w/h. The freshly-zeroed atlas marks its whole extent dirty in Initialize so the first Update clears the image once; thereafter every untouched texel stays the zero it was uploaded as, keeping the image byte-identical to the staging copy. Tested: new FontAtlasDirtyRect unit test covers the accumulation / clamp / lockstep-with-`dirty` math (no GPU needed); HelloUI renders text correctly on both Vulkan (NVIDIA, validation-clean) and WebGPU. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
459f10afa8
commit
97c79c23ee
5 changed files with 276 additions and 6 deletions
|
|
@ -39,13 +39,17 @@ void FontAtlas::Initialize(GraphicsCommandBuffer cmd) {
|
|||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
|
||||
);
|
||||
std::memset(image.buffer.value, 0, kAtlasSize * kAtlasSize);
|
||||
dirty = true;
|
||||
#else
|
||||
(void)cmd;
|
||||
staging.assign(kAtlasSize * kAtlasSize, 0);
|
||||
textureHandle = WebGPU::wgpuCreateAtlasTexture(kAtlasSize, kAtlasSize);
|
||||
dirty = true;
|
||||
#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) {
|
||||
|
|
@ -112,7 +116,7 @@ bool FontAtlas::Ensure(Font& font, std::uint32_t codepoint) {
|
|||
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;
|
||||
MarkDirty(px, py, sw, sh);
|
||||
}
|
||||
|
||||
cache_.emplace(key, g);
|
||||
|
|
@ -126,16 +130,28 @@ const Glyph* FontAtlas::Lookup(Font& font, std::uint32_t codepoint) const {
|
|||
|
||||
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.Update(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
image.UpdateRegion(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, x, y, w, h);
|
||||
#else
|
||||
(void)cmd;
|
||||
// Full-atlas upload. Future: track dirty region.
|
||||
// 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,
|
||||
0, 0, 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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue