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

@ -413,6 +413,35 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
memImpls.emplace_back("tests/MemoryTypeFallback/main");
mc.GetInterfacesAndImplementations(ifaces, memImpls);
cfg.tests.push_back(std::move(memTest));
// Issue #57: Font::GetLineWidth memoises per-codepoint advances in
// font units (Font::AdvanceUnits) and rescales per call, instead of
// calling stbtt_GetCodepointHMetrics for every glyph on every caret
// query. Pure CPU — Font only touches stb_truetype — so this drives
// the public API directly with no Vulkan device. The font file is
// copied next to the binary; the test also probes the project root.
Test advTest;
Configuration& vc = advTest.config;
vc.path = cfg.path;
vc.name = "FontAdvanceCache";
vc.outputName = "FontAdvanceCache";
vc.type = ConfigurationType::Executable;
vc.target = cfg.target;
vc.march = cfg.march;
vc.mtune = cfg.mtune;
vc.debug = cfg.debug;
vc.sysroot = cfg.sysroot;
vc.dependencies = cfg.dependencies;
vc.externalDependencies = cfg.externalDependencies;
vc.compileFlags = cfg.compileFlags;
vc.linkFlags = cfg.linkFlags;
vc.defines = cfg.defines;
vc.cFiles = cfg.cFiles;
vc.files = { fs::path("tests/FontAdvanceCache/font.ttf") };
std::vector<fs::path> advImpls(impls.begin(), impls.end());
advImpls.emplace_back("tests/FontAdvanceCache/main");
vc.GetInterfacesAndImplementations(ifaces, advImpls);
cfg.tests.push_back(std::move(advTest));
}
return cfg;