perf(ui): per-shelf dirty spans for tight font-atlas uploads (#129) #147
2 changed files with 51 additions and 28 deletions
|
|
@ -59,11 +59,13 @@ void FontAtlas::Initialize(GraphicsCommandBuffer cmd) {
|
|||
MarkDirty(0, 0, kAtlasSize, kAtlasSize);
|
||||
}
|
||||
|
||||
bool FontAtlas::ShelfPlace(int w, int h, int& outX, int& outY) {
|
||||
for (Shelf& s : shelves_) {
|
||||
bool FontAtlas::ShelfPlace(int w, int h, int& outX, int& outY, int& outShelf) {
|
||||
for (std::size_t i = 0; i < shelves_.size(); ++i) {
|
||||
Shelf& s = shelves_[i];
|
||||
if (h <= s.height && s.cursorX + w <= kAtlasSize) {
|
||||
outX = s.cursorX;
|
||||
outY = s.y;
|
||||
outShelf = static_cast<int>(i);
|
||||
s.cursorX += w;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -75,6 +77,7 @@ bool FontAtlas::ShelfPlace(int w, int h, int& outX, int& outY) {
|
|||
s.cursorX = w;
|
||||
outX = 0;
|
||||
outY = s.y;
|
||||
outShelf = static_cast<int>(shelves_.size());
|
||||
shelves_.push_back(s);
|
||||
nextShelfY_ += h;
|
||||
return true;
|
||||
|
|
@ -106,8 +109,8 @@ const Glyph* FontAtlas::EnsureAndGet(Font& font, std::uint32_t codepoint) {
|
|||
g.yoff = static_cast<float>(yoff);
|
||||
|
||||
if (sdf && sw > 0 && sh > 0) {
|
||||
int px = 0, py = 0;
|
||||
if (!ShelfPlace(sw, sh, px, py)) {
|
||||
int px = 0, py = 0, shelf = 0;
|
||||
if (!ShelfPlace(sw, sh, px, py, shelf)) {
|
||||
stbtt_FreeSDF(sdf, 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.u1 = static_cast<float>(px + sw) / 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;
|
||||
|
|
@ -141,27 +147,35 @@ 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);
|
||||
|
||||
// Issue one tight copy per dirty span and reset it. Clamping to the atlas
|
||||
// keeps each extent provably valid even though Add() already works in
|
||||
// in-bounds glyph coordinates; UploadBox skips spans that never armed.
|
||||
// The spans are the whole-atlas zero-clear (dirtyRect, Initialize only)
|
||||
// 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;
|
||||
if (!r.UploadBox(kAtlasSize, x, y, w, h)) return;
|
||||
#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
|
||||
(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)
|
||||
);
|
||||
// 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
|
||||
r.Reset();
|
||||
};
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
(void)cmd;
|
||||
#endif
|
||||
upload(dirtyRect);
|
||||
for (Shelf& s : shelves_) upload(s.dirty);
|
||||
|
||||
dirty = false;
|
||||
dirtyRect.Reset();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue