perf(font): cache per-codepoint advances in font units (#57)

Font::GetLineWidth called stbtt_GetCodepointHMetrics for every glyph on
every invocation. InputField_HitTestCursor rescans the whole field once
per character, making caret hit-testing O(n²) in HMetrics lookups.

Memoise advances per Font via Font::AdvanceUnits: ASCII in a flat array,
the rest in a map. Advances are cached in unscaled font *units* and
rescaled per call — Glyph::advance in the FontAtlas is baked at kBaseSize
and is wrong at any other size, so it cannot be reused. The per-glyph
(int) truncation in GetLineWidth is preserved exactly.

Adds the FontAdvanceCache CPU-only regression test covering size
independence of the cached unit advance, the per-glyph-truncated rescale
pipeline, cache-hit determinism, width scaling with size, and the
non-ASCII map path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-16 17:00:48 +00:00
commit c169986835
5 changed files with 203 additions and 3 deletions

View file

@ -63,5 +63,17 @@ namespace Crafter {
float LineHeight(float size);
float AscentPx(float size);
float ScaleForSize(float size);
// Horizontal advance for `cp` in unscaled font units, cached per Font.
// Stored in font units (not pixels) so it can be rescaled for any
// `size`; Glyph::advance in the FontAtlas is baked at kBaseSize and is
// wrong at other sizes, so it cannot be reused here. ASCII lands in a
// flat array (the common path for caret hit-testing, which rescans the
// whole field per character), everything else in the map.
std::int32_t AdvanceUnits(std::uint32_t cp);
private:
std::array<std::int32_t, 128> asciiAdvance_; // -1 = uncached
std::unordered_map<std::uint32_t, std::int32_t> advanceUnits_;
};
}