perf(ui): LRU-evict the shaped-run cache instead of clearing it (#123)
At kMaxShapedRuns (8192) UIRenderer::ShapeText did a full shapedRuns_.clear(). A stream of unique strings (FPS counters, timers) alongside many stable labels periodically nuked every stable entry, forcing a full-UI reshape the next frame. Track recency with an auxiliary std::list<const ShapedRunKey*> (front = most recently used) holding pointers to the keys owned by the node-based map. A hit splices its entry to the front; an overflow pops the least-recently-used entry from the back and erases just that one — both O(1). The hot set of labels, reshaped every frame, stays at the front and survives, so the churn of unique strings only recycles the cold tail. Output is byte-identical either way. InvalidateFont now drops matching entries from both the map and the LRU list. Adds ShapedRunCacheSize()/IsShapedRunCached() introspection hooks (the policy isn't observable through output or atlas.dirty) and a ShapeTextCache test that asserts the cache plateaus at a fixed cap (evict-one, not clear-all), the hot label survives a churn past the cap, and an untouched cold string is evicted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2783e47674
commit
1d7d9f7b8e
3 changed files with 115 additions and 6 deletions
|
|
@ -109,8 +109,22 @@ std::uint32_t UIRenderer::ShapeText(Font& font, float pxSize,
|
|||
}
|
||||
run.advance = cursor;
|
||||
|
||||
if (shapedRuns_.size() >= kMaxShapedRuns) shapedRuns_.clear();
|
||||
// Evict the least-recently-used run (LRU list back) on overflow rather
|
||||
// than clearing the whole cache. The back is whatever has gone longest
|
||||
// without a reshape — a stale unique string, never a stable label
|
||||
// (those are reshaped every frame and sit at the front), so the hot set
|
||||
// survives and the next frame doesn't trigger a full-UI reshape.
|
||||
if (shapedRuns_.size() >= kMaxShapedRuns) {
|
||||
const ShapedRunKey* victim = shapedRunsLru_.back();
|
||||
shapedRunsLru_.pop_back();
|
||||
shapedRuns_.erase(*victim);
|
||||
}
|
||||
it = shapedRuns_.emplace(std::move(key), std::move(run)).first;
|
||||
it->second.lruIt = shapedRunsLru_.insert(shapedRunsLru_.begin(), &it->first);
|
||||
} else {
|
||||
// Hit: promote to most-recently-used. splice is O(1) and keeps lruIt
|
||||
// (and every other iterator) valid.
|
||||
shapedRunsLru_.splice(shapedRunsLru_.begin(), shapedRunsLru_, it->second.lruIt);
|
||||
}
|
||||
|
||||
const ShapedRun& run = it->second;
|
||||
|
|
@ -135,7 +149,21 @@ std::uint32_t UIRenderer::ShapeText(Font& font, float pxSize,
|
|||
}
|
||||
|
||||
void UIRenderer::InvalidateFont(const Font& font) noexcept {
|
||||
std::erase_if(shapedRuns_, [&font](const auto& kv) {
|
||||
return kv.first.font == &font;
|
||||
});
|
||||
// Drop matching runs from both the map and the LRU order list, keeping the
|
||||
// two in sync (the list points at keys owned by the map).
|
||||
for (auto it = shapedRuns_.begin(); it != shapedRuns_.end(); ) {
|
||||
if (it->first.font == &font) {
|
||||
shapedRunsLru_.erase(it->second.lruIt);
|
||||
it = shapedRuns_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool UIRenderer::IsShapedRunCached(const Font& font, float pxSize,
|
||||
std::array<float,4> color,
|
||||
std::string_view utf8) const {
|
||||
return shapedRuns_.find(ShapedRunKey{&font, pxSize, color, std::string(utf8)})
|
||||
!= shapedRuns_.end();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue