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

Merged
catbot merged 2 commits from claude/issue-57 into master 2026-06-16 19:04:09 +02:00
Member

Problem

Font::GetLineWidth called stbtt_GetCodepointHMetrics for every glyph on every call, with no caching. InputField_HitTestCursor rescans the whole field once per character, so caret hit-testing is O(n²) in HMetrics lookups — duplicating the advance work the FontAtlas already pays in EnsureAndGet.

Fix

Per-Font codepoint → advance cache (Font::AdvanceUnits): ASCII in a flat 128-entry array (the common caret path), everything else in a map.

Correctness: 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 fontSize, so it cannot be reused — the cache stores units and multiplies by ScaleForPixelHeight(size) per call. The per-glyph (int) truncation in GetLineWidth is preserved exactly.

Tests

New CPU-only FontAdvanceCache regression test (no Vulkan device — Font only touches stb_truetype):

  • AdvanceUnits is size-independent (proves units, not pixels, are stored)
  • GetLineWidth == sum of per-glyph (int)(units * scale) at every size
  • cache hits are byte-identical to the first (miss) call
  • width scales with size (rules out a baked-at-one-size advance)
  • empty string is zero
  • non-ASCII codepoints exercise the map path

crafter-build test8 passed (run with --jobs=1; the parallel run hits a pre-existing concurrent-git pull race on the shared Vulkan-Headers external-dep cache, unrelated to this change).

Resolves #57

## Problem `Font::GetLineWidth` called `stbtt_GetCodepointHMetrics` for every glyph on every call, with no caching. `InputField_HitTestCursor` rescans the whole field once per character, so caret hit-testing is O(n²) in HMetrics lookups — duplicating the advance work the FontAtlas already pays in `EnsureAndGet`. ## Fix Per-`Font` codepoint → advance cache (`Font::AdvanceUnits`): ASCII in a flat 128-entry array (the common caret path), everything else in a map. **Correctness:** 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 `fontSize`, so it cannot be reused — the cache stores units and multiplies by `ScaleForPixelHeight(size)` per call. The per-glyph `(int)` truncation in `GetLineWidth` is preserved exactly. ## Tests New CPU-only `FontAdvanceCache` regression test (no Vulkan device — `Font` only touches stb_truetype): - `AdvanceUnits` is size-independent (proves units, not pixels, are stored) - `GetLineWidth` == sum of per-glyph `(int)(units * scale)` at every size - cache hits are byte-identical to the first (miss) call - width scales with size (rules out a baked-at-one-size advance) - empty string is zero - non-ASCII codepoints exercise the map path `crafter-build test` → **8 passed** (run with `--jobs=1`; the parallel run hits a pre-existing concurrent-`git pull` race on the shared Vulkan-Headers external-dep cache, unrelated to this change). Resolves #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>
catbot merged commit f07bbc20f1 into master 2026-06-16 19:04:09 +02:00
catbot deleted branch claude/issue-57 2026-06-16 19:04:09 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Catcrafts/Crafter.Graphics!92
No description provided.