perf(text): cache shaped runs in ShapeText (#52)

ShapeText re-decoded UTF-8, re-looked-up every glyph, and recomputed
layout every frame for static strings (DrawText / EmitCenteredLabel run
inside per-frame onBuild). DrawText and EmitCenteredLabel also did a
second full pass over the glyphs to apply alignment.

Memoize the shaped run, keyed by (Font*, pxSize, color, utf8), as an
origin-relative vector<GlyphItem>. A cache hit skips decode/lookup/layout
and just translates the cached run into the output buffer by
(x + alignShift, baselineY). The alignment shift (Center = -advance/2,
Right = -advance) is folded into ShapeText's single pass, dropping the
second loop in both callers. TextAlign moves to the :UI partition so
ShapeText can take it.

Pure CPU memoization — the glyph buffer is fully rewritten and re-flushed
every frame, so a hit is byte-equivalent. A miss still calls Ensure so new
glyphs rasterise and dirty the atlas; hits don't (atlas entries are never
evicted). InvalidateFont drops a font's runs before it is destroyed (the
cache, like FontAtlas's glyph cache, keys on the Font pointer). A soft cap
bounds memory against pathological per-frame-unique text.

Adds the ShapeTextCache test (15 checks): hit == miss byte-for-byte, hits
don't touch the atlas, translate/alignment/truncation/InvalidateFont.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-16 15:40:52 +00:00
commit ca48f79465
7 changed files with 391 additions and 40 deletions

View file

@ -58,17 +58,12 @@ namespace {
GlyphItem* writePos = buf.glyphs + before;
float baseline = centerY + fontSize * 0.32f;
float advance = 0.0f;
// ShapeText folds the -advance/2 centering shift into its own pass.
std::uint32_t n = buf.renderer->ShapeText(
font, fontSize, centerX, baseline,
label, color, writePos, cap, &advance
label, color, writePos, cap, &advance, TextAlign::Center
);
*buf.glyphCount = before + n;
// Center: shift each glyph's x by -advance/2.
float shift = -advance * 0.5f;
for (std::uint32_t i = 0; i < n; ++i) {
writePos[i].x += shift;
}
return advance;
}
}
@ -202,16 +197,12 @@ float Crafter::DrawText(UIBuffer& buf, std::string_view text,
GlyphItem* writePos = buf.glyphs + before;
float advance = 0.0f;
// Alignment is applied inside ShapeText (folded into its single pass).
std::uint32_t n = buf.renderer->ShapeText(
font, fontSize, x, baselineY,
text, color, writePos, cap, &advance
text, color, writePos, cap, &advance, align
);
*buf.glyphCount = before + n;
if (align != TextAlign::Left && n > 0) {
float shift = (align == TextAlign::Center) ? -advance * 0.5f : -advance;
for (std::uint32_t i = 0; i < n; ++i) writePos[i].x += shift;
}
return advance;
}