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:
catbot 2026-06-18 13:35:54 +00:00
commit 16d291b0ad
2 changed files with 51 additions and 28 deletions

View file

@ -102,9 +102,12 @@ export namespace Crafter {
std::vector<std::uint8_t> staging;
#endif
// `dirty` stays the cheap "is there anything to flush?" flag the
// renderer polls each frame; `dirtyRect` carries the bounds Update
// copies. The two are always set and cleared together (dirty ==
// !dirtyRect.Empty()).
// renderer polls each frame; it is the OR of every dirty span Update
// copies. `dirtyRect` is the *whole-atlas* span — used only for the
// 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;
DirtyRect dirtyRect;
@ -131,7 +134,11 @@ export namespace Crafter {
}
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_;
int nextShelfY_ = 0;
@ -149,6 +156,8 @@ export namespace Crafter {
};
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);
};
}