perf(ui): per-shelf dirty spans for tight atlas uploads (#129)
FontAtlas::Update used a single union bounding box for the dirty rect. Scattered new glyphs landing on different shelves produced a tall union box that re-uploaded mostly-unchanged texels between the shelves. Track a dirty span per shelf instead and issue one tight UpdateRegion / wgpuWriteAtlasRegion copy per armed span. A shelf packs glyphs left-to-right at a fixed top, so each span stays a contiguous X run capped by the shelf height. The whole-atlas zero-clear in Initialize keeps using the existing dirtyRect span. `dirty` is now the OR of every span and is cleared together with them in Update. Verified: full test suite green (23 passed); HelloUI text renders crisply on the WebGPU backend through the new multi-span upload path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e1222c0ab7
commit
16d291b0ad
2 changed files with 51 additions and 28 deletions
|
|
@ -59,11 +59,13 @@ void FontAtlas::Initialize(GraphicsCommandBuffer cmd) {
|
||||||
MarkDirty(0, 0, kAtlasSize, kAtlasSize);
|
MarkDirty(0, 0, kAtlasSize, kAtlasSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FontAtlas::ShelfPlace(int w, int h, int& outX, int& outY) {
|
bool FontAtlas::ShelfPlace(int w, int h, int& outX, int& outY, int& outShelf) {
|
||||||
for (Shelf& s : shelves_) {
|
for (std::size_t i = 0; i < shelves_.size(); ++i) {
|
||||||
|
Shelf& s = shelves_[i];
|
||||||
if (h <= s.height && s.cursorX + w <= kAtlasSize) {
|
if (h <= s.height && s.cursorX + w <= kAtlasSize) {
|
||||||
outX = s.cursorX;
|
outX = s.cursorX;
|
||||||
outY = s.y;
|
outY = s.y;
|
||||||
|
outShelf = static_cast<int>(i);
|
||||||
s.cursorX += w;
|
s.cursorX += w;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -75,6 +77,7 @@ bool FontAtlas::ShelfPlace(int w, int h, int& outX, int& outY) {
|
||||||
s.cursorX = w;
|
s.cursorX = w;
|
||||||
outX = 0;
|
outX = 0;
|
||||||
outY = s.y;
|
outY = s.y;
|
||||||
|
outShelf = static_cast<int>(shelves_.size());
|
||||||
shelves_.push_back(s);
|
shelves_.push_back(s);
|
||||||
nextShelfY_ += h;
|
nextShelfY_ += h;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -106,8 +109,8 @@ const Glyph* FontAtlas::EnsureAndGet(Font& font, std::uint32_t codepoint) {
|
||||||
g.yoff = static_cast<float>(yoff);
|
g.yoff = static_cast<float>(yoff);
|
||||||
|
|
||||||
if (sdf && sw > 0 && sh > 0) {
|
if (sdf && sw > 0 && sh > 0) {
|
||||||
int px = 0, py = 0;
|
int px = 0, py = 0, shelf = 0;
|
||||||
if (!ShelfPlace(sw, sh, px, py)) {
|
if (!ShelfPlace(sw, sh, px, py, shelf)) {
|
||||||
stbtt_FreeSDF(sdf, nullptr);
|
stbtt_FreeSDF(sdf, nullptr);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +130,10 @@ const Glyph* FontAtlas::EnsureAndGet(Font& font, std::uint32_t codepoint) {
|
||||||
g.v0 = static_cast<float>(py) / kAtlasSize;
|
g.v0 = static_cast<float>(py) / kAtlasSize;
|
||||||
g.u1 = static_cast<float>(px + sw) / kAtlasSize;
|
g.u1 = static_cast<float>(px + sw) / kAtlasSize;
|
||||||
g.v1 = static_cast<float>(py + sh) / kAtlasSize;
|
g.v1 = static_cast<float>(py + sh) / kAtlasSize;
|
||||||
MarkDirty(px, py, sw, sh);
|
// Mark the *shelf's* span, not one global box — keeps each upload
|
||||||
|
// tight when glyphs land on different shelves the same frame (#129).
|
||||||
|
shelves_[shelf].dirty.Add(px, py, sw, sh);
|
||||||
|
dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cache_.emplace(key, g).first->second;
|
return &cache_.emplace(key, g).first->second;
|
||||||
|
|
@ -141,18 +147,18 @@ const Glyph* FontAtlas::Lookup(Font& font, std::uint32_t codepoint) const {
|
||||||
void FontAtlas::Update(GraphicsCommandBuffer cmd) {
|
void FontAtlas::Update(GraphicsCommandBuffer cmd) {
|
||||||
if (!dirty) return;
|
if (!dirty) return;
|
||||||
|
|
||||||
// Clamp the accumulated box to the atlas and convert to (origin,
|
// Issue one tight copy per dirty span and reset it. Clamping to the atlas
|
||||||
// extent). Add() works in glyph coordinates that are always in-bounds,
|
// keeps each extent provably valid even though Add() already works in
|
||||||
// but clamping keeps the copy extent provably valid. The `dirty` guard
|
// in-bounds glyph coordinates; UploadBox skips spans that never armed.
|
||||||
// above guarantees the rect is non-empty, so UploadBox always fills the
|
// The spans are the whole-atlas zero-clear (dirtyRect, Initialize only)
|
||||||
// outputs.
|
// plus each shelf's accumulated glyph run — so scattered glyphs upload as
|
||||||
|
// several small rects, not one tall union covering the gaps between them.
|
||||||
|
auto upload = [&](DirtyRect& r) {
|
||||||
std::uint32_t x = 0, y = 0, w = 0, h = 0;
|
std::uint32_t x = 0, y = 0, w = 0, h = 0;
|
||||||
dirtyRect.UploadBox(kAtlasSize, x, y, w, h);
|
if (!r.UploadBox(kAtlasSize, x, y, w, h)) return;
|
||||||
|
|
||||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
image.UpdateRegion(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, x, y, w, h);
|
image.UpdateRegion(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, x, y, w, h);
|
||||||
#else
|
#else
|
||||||
(void)cmd;
|
|
||||||
// The staging buffer keeps the full atlas row stride, so srcBytesPerRow
|
// The staging buffer keeps the full atlas row stride, so srcBytesPerRow
|
||||||
// stays kAtlasSize and (dstX, dstY) index the sub-rect's first texel.
|
// stays kAtlasSize and (dstX, dstY) index the sub-rect's first texel.
|
||||||
WebGPU::wgpuWriteAtlasRegion(
|
WebGPU::wgpuWriteAtlasRegion(
|
||||||
|
|
@ -162,6 +168,14 @@ void FontAtlas::Update(GraphicsCommandBuffer cmd) {
|
||||||
static_cast<std::int32_t>(w), static_cast<std::int32_t>(h)
|
static_cast<std::int32_t>(w), static_cast<std::int32_t>(h)
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
r.Reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
(void)cmd;
|
||||||
|
#endif
|
||||||
|
upload(dirtyRect);
|
||||||
|
for (Shelf& s : shelves_) upload(s.dirty);
|
||||||
|
|
||||||
dirty = false;
|
dirty = false;
|
||||||
dirtyRect.Reset();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,9 +102,12 @@ export namespace Crafter {
|
||||||
std::vector<std::uint8_t> staging;
|
std::vector<std::uint8_t> staging;
|
||||||
#endif
|
#endif
|
||||||
// `dirty` stays the cheap "is there anything to flush?" flag the
|
// `dirty` stays the cheap "is there anything to flush?" flag the
|
||||||
// renderer polls each frame; `dirtyRect` carries the bounds Update
|
// renderer polls each frame; it is the OR of every dirty span Update
|
||||||
// copies. The two are always set and cleared together (dirty ==
|
// copies. `dirtyRect` is the *whole-atlas* span — used only for the
|
||||||
// !dirtyRect.Empty()).
|
// one-shot zero-clear in Initialize; per-glyph dirt is tracked tight
|
||||||
|
// per shelf (Shelf::dirty) instead of inflating one tall union box
|
||||||
|
// across scattered shelves (#129). `dirty` and the spans are always
|
||||||
|
// armed and cleared together.
|
||||||
bool dirty = false;
|
bool dirty = false;
|
||||||
DirtyRect dirtyRect;
|
DirtyRect dirtyRect;
|
||||||
|
|
||||||
|
|
@ -131,7 +134,11 @@ export namespace Crafter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Shelf { int y = 0; int height = 0; int cursorX = 0; };
|
// A shelf packs glyphs left-to-right at a fixed top (`y`). Its `dirty`
|
||||||
|
// span therefore stays naturally tight: a contiguous X run capped by
|
||||||
|
// the shelf height — far smaller than a union box spanning every
|
||||||
|
// shelf a frame happened to touch.
|
||||||
|
struct Shelf { int y = 0; int height = 0; int cursorX = 0; DirtyRect dirty; };
|
||||||
std::vector<Shelf> shelves_;
|
std::vector<Shelf> shelves_;
|
||||||
int nextShelfY_ = 0;
|
int nextShelfY_ = 0;
|
||||||
|
|
||||||
|
|
@ -149,6 +156,8 @@ export namespace Crafter {
|
||||||
};
|
};
|
||||||
std::unordered_map<Key, Glyph, KeyHash> cache_;
|
std::unordered_map<Key, Glyph, KeyHash> cache_;
|
||||||
|
|
||||||
bool ShelfPlace(int w, int h, int& outX, int& outY);
|
// On success, outShelf is the index into shelves_ of the placed
|
||||||
|
// glyph, so the caller can mark that shelf's dirty span.
|
||||||
|
bool ShelfPlace(int w, int h, int& outX, int& outY, int& outShelf);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue