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>
607 KiB
607 KiB