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:
parent
58e9e0a747
commit
ca48f79465
7 changed files with 391 additions and 40 deletions
|
|
@ -48,39 +48,79 @@ std::uint32_t UIRenderer::ShapeText(Font& font, float pxSize,
|
|||
std::string_view utf8,
|
||||
std::array<float,4> color,
|
||||
GlyphItem* out, std::uint32_t outCapacity,
|
||||
float* outAdvance) {
|
||||
float* outAdvance,
|
||||
TextAlign align) {
|
||||
if (fontAtlas == nullptr) {
|
||||
std::println("UIRenderer::ShapeText: no FontAtlas (set fontAtlas before Initialize)");
|
||||
std::abort();
|
||||
}
|
||||
|
||||
const float scale = pxSize / FontAtlas::kBaseSize;
|
||||
float cursor = x;
|
||||
std::uint32_t written = 0;
|
||||
// Look up (or build) the origin-relative shaped run. It is stored with
|
||||
// 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);
|
||||
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 —
|
||||
// a hit skips this, which is safe because atlas entries are never
|
||||
// evicted once placed.
|
||||
const float scale = pxSize / FontAtlas::kBaseSize;
|
||||
ShapedRun run;
|
||||
float cursor = 0.0f;
|
||||
std::size_t i = 0;
|
||||
while (i < utf8.size()) {
|
||||
std::uint32_t cp = DecodeUtf8(utf8, i);
|
||||
if (cp == 0) break;
|
||||
if (cp == '\n') { continue; }
|
||||
|
||||
std::size_t i = 0;
|
||||
while (i < utf8.size() && written < outCapacity) {
|
||||
std::uint32_t cp = DecodeUtf8(utf8, i);
|
||||
if (cp == 0) break;
|
||||
if (cp == '\n') { continue; }
|
||||
fontAtlas->Ensure(font, cp);
|
||||
const Glyph* g = fontAtlas->Lookup(font, cp);
|
||||
if (g == nullptr) continue;
|
||||
|
||||
fontAtlas->Ensure(font, cp);
|
||||
const Glyph* g = fontAtlas->Lookup(font, cp);
|
||||
if (g == nullptr) continue;
|
||||
|
||||
if (g->w > 0 && g->h > 0) {
|
||||
GlyphItem& gi = out[written++];
|
||||
gi.x = cursor + g->xoff * scale;
|
||||
gi.y = baselineY + g->yoff * scale;
|
||||
gi.w = g->w * scale;
|
||||
gi.h = g->h * scale;
|
||||
gi.u0 = g->u0; gi.v0 = g->v0;
|
||||
gi.u1 = g->u1; gi.v1 = g->v1;
|
||||
gi.r = color[0]; gi.g = color[1]; gi.b = color[2]; gi.a = color[3];
|
||||
if (g->w > 0 && g->h > 0) {
|
||||
GlyphItem gi;
|
||||
gi.x = cursor + g->xoff * scale;
|
||||
gi.y = g->yoff * scale;
|
||||
gi.w = g->w * scale;
|
||||
gi.h = g->h * scale;
|
||||
gi.u0 = g->u0; gi.v0 = g->v0;
|
||||
gi.u1 = g->u1; gi.v1 = g->v1;
|
||||
gi.r = color[0]; gi.g = color[1]; gi.b = color[2]; gi.a = color[3];
|
||||
run.glyphs.push_back(gi);
|
||||
}
|
||||
cursor += g->advance * scale;
|
||||
}
|
||||
cursor += g->advance * scale;
|
||||
run.advance = cursor;
|
||||
|
||||
if (shapedRuns_.size() >= kMaxShapedRuns) shapedRuns_.clear();
|
||||
it = shapedRuns_.emplace(std::move(key), std::move(run)).first;
|
||||
}
|
||||
|
||||
if (outAdvance) *outAdvance = cursor - x;
|
||||
const ShapedRun& run = it->second;
|
||||
if (outAdvance) *outAdvance = run.advance;
|
||||
|
||||
// Fold the alignment shift into the translate so callers don't need a
|
||||
// second pass over the glyphs.
|
||||
const float alignShift = (align == TextAlign::Center) ? -run.advance * 0.5f
|
||||
: (align == TextAlign::Right) ? -run.advance
|
||||
: 0.0f;
|
||||
const float tx = x + alignShift;
|
||||
|
||||
std::uint32_t written = 0;
|
||||
for (const GlyphItem& src : run.glyphs) {
|
||||
if (written >= outCapacity) break;
|
||||
GlyphItem& gi = out[written++];
|
||||
gi = src;
|
||||
gi.x += tx;
|
||||
gi.y += baselineY;
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
void UIRenderer::InvalidateFont(const Font& font) noexcept {
|
||||
std::erase_if(shapedRuns_, [&font](const auto& kv) {
|
||||
return kv.first.font == &font;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue