perf(ui): LRU-evict the shaped-run cache instead of clearing it (#123) #143
5 changed files with 99 additions and 30 deletions
Merge remote-tracking branch 'origin/master' into claude/issue-123
# Conflicts: # implementations/Crafter.Graphics-UI-Shared.cpp # interfaces/Crafter.Graphics-UI.cppm
commit
88541bf44e
|
|
@ -75,8 +75,11 @@ std::uint32_t UIRenderer::ShapeText(Font& font, float pxSize,
|
|||
// the pen starting at x=0, baseline=0 so a cache hit only needs the
|
||||
// translate below. The whole string is shaped (no outCapacity limit) so
|
||||
// a later call with a larger buffer still gets the full run.
|
||||
ShapedRunKey key{&font, pxSize, color, std::string(utf8)};
|
||||
auto it = shapedRuns_.find(key);
|
||||
// Probe with a borrowing view key (text is a string_view onto utf8) so a
|
||||
// cache hit copies and hashes no string. The owning std::string is only
|
||||
// built on a miss, below, for the emplace.
|
||||
ShapedRunViewKey viewKey{&font, pxSize, color, utf8};
|
||||
auto it = shapedRuns_.find(viewKey);
|
||||
if (it == shapedRuns_.end()) {
|
||||
// Miss: shape from scratch. Ensure() each glyph so brand-new ones
|
||||
// rasterise into the atlas and mark it dirty for the next upload —
|
||||
|
|
@ -119,6 +122,9 @@ std::uint32_t UIRenderer::ShapeText(Font& font, float pxSize,
|
|||
shapedRunsLru_.pop_back();
|
||||
shapedRuns_.erase(*victim);
|
||||
}
|
||||
// Materialise the owning key (the probe above borrowed utf8) only now,
|
||||
// on the miss path, for the emplace.
|
||||
ShapedRunKey key{&font, pxSize, color, std::string(utf8)};
|
||||
it = shapedRuns_.emplace(std::move(key), std::move(run)).first;
|
||||
it->second.lruIt = shapedRunsLru_.insert(shapedRunsLru_.begin(), &it->first);
|
||||
} else {
|
||||
|
|
@ -164,6 +170,6 @@ void UIRenderer::InvalidateFont(const Font& font) noexcept {
|
|||
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)})
|
||||
return shapedRuns_.find(ShapedRunViewKey{&font, pxSize, color, utf8})
|
||||
!= shapedRuns_.end();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -350,25 +350,63 @@ export namespace Crafter {
|
|||
// only needs a translate by (x + alignShift, baselineY). The full
|
||||
// string is part of the key (compared on lookup) so hash collisions
|
||||
// can't return the wrong run.
|
||||
//
|
||||
// The map owns its key string, but lookups happen every frame on the
|
||||
// onBuild path. To avoid copying the string into a key on each cache
|
||||
// hit, the hash and equality functors are transparent (is_transparent)
|
||||
// and accept a borrowing view key whose text is a std::string_view;
|
||||
// the owning std::string is only materialised on a miss, for emplace.
|
||||
struct ShapedRunKey {
|
||||
const Font* font;
|
||||
float pxSize;
|
||||
std::array<float, 4> color;
|
||||
std::string text;
|
||||
bool operator==(const ShapedRunKey&) const = default;
|
||||
};
|
||||
struct ShapedRunViewKey {
|
||||
const Font* font;
|
||||
float pxSize;
|
||||
std::array<float, 4> color;
|
||||
std::string_view text;
|
||||
};
|
||||
struct ShapedRunKeyHash {
|
||||
std::size_t operator()(const ShapedRunKey& k) const noexcept {
|
||||
std::size_t h = std::hash<const void*>{}(k.font);
|
||||
using is_transparent = void;
|
||||
static std::size_t Hash(const Font* font, float pxSize,
|
||||
const std::array<float, 4>& color,
|
||||
std::string_view text) noexcept {
|
||||
std::size_t h = std::hash<const void*>{}(font);
|
||||
auto mix = [&h](std::size_t v) noexcept {
|
||||
h ^= v + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2);
|
||||
};
|
||||
mix(std::hash<std::uint32_t>{}(std::bit_cast<std::uint32_t>(k.pxSize)));
|
||||
for (float c : k.color)
|
||||
mix(std::hash<std::uint32_t>{}(std::bit_cast<std::uint32_t>(pxSize)));
|
||||
for (float c : color)
|
||||
mix(std::hash<std::uint32_t>{}(std::bit_cast<std::uint32_t>(c)));
|
||||
mix(std::hash<std::string_view>{}(k.text));
|
||||
mix(std::hash<std::string_view>{}(text));
|
||||
return h;
|
||||
}
|
||||
std::size_t operator()(const ShapedRunKey& k) const noexcept {
|
||||
return Hash(k.font, k.pxSize, k.color, k.text);
|
||||
}
|
||||
std::size_t operator()(const ShapedRunViewKey& k) const noexcept {
|
||||
return Hash(k.font, k.pxSize, k.color, k.text);
|
||||
}
|
||||
};
|
||||
struct ShapedRunKeyEqual {
|
||||
using is_transparent = void;
|
||||
static bool Eq(const Font* fa, float pa, const std::array<float, 4>& ca,
|
||||
std::string_view ta,
|
||||
const Font* fb, float pb, const std::array<float, 4>& cb,
|
||||
std::string_view tb) noexcept {
|
||||
return fa == fb && pa == pb && ca == cb && ta == tb;
|
||||
}
|
||||
bool operator()(const ShapedRunKey& a, const ShapedRunKey& b) const noexcept {
|
||||
return Eq(a.font, a.pxSize, a.color, a.text, b.font, b.pxSize, b.color, b.text);
|
||||
}
|
||||
bool operator()(const ShapedRunViewKey& a, const ShapedRunKey& b) const noexcept {
|
||||
return Eq(a.font, a.pxSize, a.color, a.text, b.font, b.pxSize, b.color, b.text);
|
||||
}
|
||||
bool operator()(const ShapedRunKey& a, const ShapedRunViewKey& b) const noexcept {
|
||||
return Eq(a.font, a.pxSize, a.color, a.text, b.font, b.pxSize, b.color, b.text);
|
||||
}
|
||||
};
|
||||
struct ShapedRun {
|
||||
std::vector<GlyphItem> glyphs; // origin-relative
|
||||
|
|
@ -387,7 +425,7 @@ export namespace Crafter {
|
|||
// unaffected either way; this just avoids the periodic full-UI reshape
|
||||
// spike the old clear() caused.
|
||||
static constexpr std::size_t kMaxShapedRuns = 8192;
|
||||
std::unordered_map<ShapedRunKey, ShapedRun, ShapedRunKeyHash> shapedRuns_;
|
||||
std::unordered_map<ShapedRunKey, ShapedRun, ShapedRunKeyHash, ShapedRunKeyEqual> shapedRuns_;
|
||||
// LRU recency order for shapedRuns_; front = most recently used. Holds
|
||||
// pointers to the keys owned by the map — stable across rehash because
|
||||
// unordered_map is node-based — so eviction never re-hashes the world.
|
||||
|
|
|
|||
|
|
@ -70,6 +70,13 @@ shared vec4 s_v1[UI_CHUNK];
|
|||
shared vec4 s_v2[UI_CHUNK];
|
||||
shared vec4 s_v3[UI_CHUNK];
|
||||
shared uvec4 s_v4[UI_CHUNK];
|
||||
// Per-item constants precomputed once at cooperative-load time, REUSED across
|
||||
// phases like the s_v* scratch (the per-pixel inner loops read them instead of
|
||||
// recomputing per pixel × item):
|
||||
// images: s_inv = 1.0/rect.zw
|
||||
// text: s_inv = 1.0/rect.zw, s_band = SDF AA scale (a divide + two maxes)
|
||||
shared vec2 s_inv[UI_CHUNK];
|
||||
shared float s_band[UI_CHUNK];
|
||||
shared uint s_keep[UI_CHUNK];
|
||||
shared uint s_order[UI_CHUNK];
|
||||
shared uint s_count;
|
||||
|
|
@ -232,6 +239,7 @@ void main() {
|
|||
s_v1[lid] = uiImageHeap[heap].items[idx].uv;
|
||||
s_v2[lid] = uiImageHeap[heap].items[idx].tint;
|
||||
s_v4[lid] = uiImageHeap[heap].items[idx].slots;
|
||||
s_inv[lid] = 1.0 / s_v0[lid].zw;
|
||||
keep = uiAabbOverlapsTile(s_v0[lid].xy, s_v0[lid].xy + s_v0[lid].zw,
|
||||
tileMin, tileMax);
|
||||
}
|
||||
|
|
@ -249,7 +257,7 @@ void main() {
|
|||
if (sp.x < lo.x || sp.y < lo.y) continue;
|
||||
if (sp.x >= hi.x || sp.y >= hi.y) continue;
|
||||
|
||||
vec2 t = (sp - s_v0[c].xy) / s_v0[c].zw;
|
||||
vec2 t = (sp - s_v0[c].xy) * s_inv[c];
|
||||
vec2 uv = mix(s_v1[c].xy, s_v1[c].zw, t);
|
||||
|
||||
uint texSlot = s_v4[c].x;
|
||||
|
|
@ -282,6 +290,13 @@ void main() {
|
|||
s_v0[lid] = uiGlyphHeap[heap].items[idx].rect;
|
||||
s_v1[lid] = uiGlyphHeap[heap].items[idx].uv;
|
||||
s_v2[lid] = uiGlyphHeap[heap].items[idx].color;
|
||||
s_inv[lid] = 1.0 / s_v0[lid].zw;
|
||||
// SDF AA band — atlas-px per screen-px, constant per glyph
|
||||
// (uvSpan * kAtlasSize(1024) / screenSpan), max'd to 1px floor.
|
||||
vec2 uvSpan = s_v1[lid].zw - s_v1[lid].xy;
|
||||
vec2 atlasPerScreen = (uvSpan * 1024.0) * s_inv[lid];
|
||||
float scalePx = max(atlasPerScreen.x, atlasPerScreen.y);
|
||||
s_band[lid] = max(scalePx, 0.0001);
|
||||
keep = uiAabbOverlapsTile(s_v0[lid].xy, s_v0[lid].xy + s_v0[lid].zw,
|
||||
tileMin, tileMax);
|
||||
}
|
||||
|
|
@ -299,7 +314,7 @@ void main() {
|
|||
if (sp.x < lo.x || sp.y < lo.y) continue;
|
||||
if (sp.x >= hi.x || sp.y >= hi.y) continue;
|
||||
|
||||
vec2 t = (sp - s_v0[c].xy) / s_v0[c].zw;
|
||||
vec2 t = (sp - s_v0[c].xy) * s_inv[c];
|
||||
vec2 uv = mix(s_v1[c].xy, s_v1[c].zw, t);
|
||||
|
||||
// Font slots are push constants — provably dynamically uniform,
|
||||
|
|
@ -312,12 +327,8 @@ void main() {
|
|||
|
||||
float dAtlas = (ON_EDGE - sdf) * DIST_SCALE;
|
||||
|
||||
vec2 uvSpan = s_v1[c].zw - s_v1[c].xy;
|
||||
vec2 atlasPerScreen = (uvSpan * 1024.0) / s_v0[c].zw;
|
||||
float scalePx = max(atlasPerScreen.x, atlasPerScreen.y);
|
||||
float band = max(scalePx, 0.0001);
|
||||
|
||||
float a = clamp(0.5 - dAtlas / band, 0.0, 1.0);
|
||||
// band (the AA scale) was precomputed once per glyph at load.
|
||||
float a = clamp(0.5 - dAtlas / s_band[c], 0.0, 1.0);
|
||||
if (a <= 0.0) continue;
|
||||
|
||||
vec4 col = s_v2[c];
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ shared vec4 s_rect[UI_CHUNK];
|
|||
shared vec4 s_uv[UI_CHUNK];
|
||||
shared vec4 s_tint[UI_CHUNK];
|
||||
shared uvec4 s_slots[UI_CHUNK];
|
||||
// 1.0/rect.zw, precomputed once per item at cooperative-load time so the
|
||||
// per-pixel inner loop multiplies instead of recomputing a vec2 reciprocal of
|
||||
// the (loop-invariant, but shared-mem + varying-index, so non-hoistable by the
|
||||
// compiler) rect size for every pixel × item.
|
||||
shared vec2 s_invRectSize[UI_CHUNK];
|
||||
shared uint s_keep[UI_CHUNK];
|
||||
shared uint s_order[UI_CHUNK];
|
||||
shared uint s_count;
|
||||
|
|
@ -43,6 +48,7 @@ void main() {
|
|||
s_uv[lid] = uiImageHeap[pc.hdr.itemBuffer].items[idx].uv;
|
||||
s_tint[lid] = uiImageHeap[pc.hdr.itemBuffer].items[idx].tint;
|
||||
s_slots[lid] = uiImageHeap[pc.hdr.itemBuffer].items[idx].slots;
|
||||
s_invRectSize[lid] = 1.0 / s_rect[lid].zw;
|
||||
keep = uiAabbOverlapsTile(s_rect[lid].xy, s_rect[lid].xy + s_rect[lid].zw,
|
||||
tileMin, tileMax);
|
||||
}
|
||||
|
|
@ -67,7 +73,7 @@ void main() {
|
|||
if (sp.x < lo.x || sp.y < lo.y) continue;
|
||||
if (sp.x >= hi.x || sp.y >= hi.y) continue;
|
||||
|
||||
vec2 t = (sp - s_rect[c].xy) / s_rect[c].zw;
|
||||
vec2 t = (sp - s_rect[c].xy) * s_invRectSize[c];
|
||||
vec2 uv = mix(s_uv[c].xy, s_uv[c].zw, t);
|
||||
|
||||
uint texSlot = s_slots[c].x;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ const float DIST_SCALE = 32.0;
|
|||
shared vec4 s_rect[UI_CHUNK];
|
||||
shared vec4 s_uv[UI_CHUNK];
|
||||
shared vec4 s_color[UI_CHUNK];
|
||||
// Per-glyph constants precomputed once at cooperative-load time and read by the
|
||||
// per-pixel inner loop, instead of being recomputed for every pixel × glyph:
|
||||
// s_invRectSize = 1.0/rect.zw (turns the per-pixel divide into a multiply)
|
||||
// s_band = the SDF AA scale (a divide + two maxes that depend only on
|
||||
// the glyph's uv span and rect size — fully loop-invariant)
|
||||
shared vec2 s_invRectSize[UI_CHUNK];
|
||||
shared float s_band[UI_CHUNK];
|
||||
shared uint s_keep[UI_CHUNK];
|
||||
shared uint s_order[UI_CHUNK];
|
||||
shared uint s_count;
|
||||
|
|
@ -49,6 +56,15 @@ void main() {
|
|||
s_rect[lid] = uiGlyphHeap[pc.hdr.itemBuffer].items[idx].rect;
|
||||
s_uv[lid] = uiGlyphHeap[pc.hdr.itemBuffer].items[idx].uv;
|
||||
s_color[lid] = uiGlyphHeap[pc.hdr.itemBuffer].items[idx].color;
|
||||
s_invRectSize[lid] = 1.0 / s_rect[lid].zw;
|
||||
// Atlas-px per screen-px along this glyph's transform — keeps AA
|
||||
// crisp at any rendering size. uvSpan * atlasSize / screenSpan,
|
||||
// with FontAtlas::kAtlasSize = 1024. Constant for the whole glyph.
|
||||
vec2 uvSpan = s_uv[lid].zw - s_uv[lid].xy;
|
||||
vec2 atlasPerScreen = (uvSpan * 1024.0) * s_invRectSize[lid];
|
||||
float scalePx = max(atlasPerScreen.x, atlasPerScreen.y);
|
||||
// 1-screen-px AA band, expressed in atlas-pixel units of dAtlas.
|
||||
s_band[lid] = max(scalePx, 0.0001);
|
||||
keep = uiAabbOverlapsTile(s_rect[lid].xy, s_rect[lid].xy + s_rect[lid].zw,
|
||||
tileMin, tileMax);
|
||||
}
|
||||
|
|
@ -73,7 +89,7 @@ void main() {
|
|||
if (sp.x < lo.x || sp.y < lo.y) continue;
|
||||
if (sp.x >= hi.x || sp.y >= hi.y) continue;
|
||||
|
||||
vec2 t = (sp - s_rect[c].xy) / s_rect[c].zw;
|
||||
vec2 t = (sp - s_rect[c].xy) * s_invRectSize[c];
|
||||
vec2 uv = mix(s_uv[c].xy, s_uv[c].zw, t);
|
||||
|
||||
// Font slots are push constants — provably dynamically uniform, so no
|
||||
|
|
@ -88,16 +104,8 @@ void main() {
|
|||
// Distance in atlas-pixels (negative inside the glyph).
|
||||
float dAtlas = (ON_EDGE - sdf) * DIST_SCALE;
|
||||
|
||||
// Atlas-px per screen-px along this glyph's transform — keeps AA crisp
|
||||
// at any rendering size. uvSpan * atlasSize / screenSpan.
|
||||
vec2 uvSpan = s_uv[c].zw - s_uv[c].xy;
|
||||
// FontAtlas::kAtlasSize = 1024.
|
||||
vec2 atlasPerScreen = (uvSpan * 1024.0) / s_rect[c].zw;
|
||||
float scalePx = max(atlasPerScreen.x, atlasPerScreen.y);
|
||||
// 1-screen-px AA band, expressed in atlas-pixel units of dAtlas.
|
||||
float band = max(scalePx, 0.0001);
|
||||
|
||||
float a = clamp(0.5 - dAtlas / band, 0.0, 1.0);
|
||||
// band (the AA scale) was precomputed once per glyph at load.
|
||||
float a = clamp(0.5 - dAtlas / s_band[c], 0.0, 1.0);
|
||||
if (a <= 0.0) continue;
|
||||
|
||||
vec4 col = s_color[c];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue