Skip to content

Cache key: a font handle does not identify a font - #176

Merged
revarbat merged 2 commits into
mainfrom
fix/font-handle-cache-key
Sep 13, 2026
Merged

revarbat merged 2 commits into
mainfrom
fix/font-handle-cache-key

Conversation

@revarbat

Copy link
Copy Markdown
Member

Reported as BelfrySCAD#434 — "fonts seem to be cached between objects and tabs".

Cause

text() put the resolved FontHandle in its CSG params. A handle numbers faces within one FontProvider, and every Evaluator makes its own — so two evaluators sharing a ManifoldCache, which is exactly what two GUI tabs are, hand the cache the same number for different fonts. The second render is served the first one's glyphs.

Measured rather than deduced:

providerA.resolveFont("Arial:style=Regular") -> 4
providerB.resolveFont("Arial:style=Bold")    -> 4

End to end through one shared cache:

font before after alone in its own cache
Arial Regular 204 tris / 9.196 204 / 9.196 204 / 9.196
Arial Bold 204 / 9.196 164 / 9.298 164 / 9.298
Courier Regular 204 / 9.196 320 / 6.978 320 / 6.978
Courier Bold 204 / 9.196 296 / 7.819 296 / 7.819

Fix

The spec goes into the params alongside the handle, so the key tells two fonts apart. The handle still does the lookup within its own evaluator. Two specs that resolve to the same face merely miss the cache rather than collide — the safe direction.

On the tests

The tests here pin the two properties provable here: the handle colliding across providers, and cacheKey distinguishing nodes that differ only by spec.

The end-to-end regression test lives in BelfrySCAD, not here. I wrote one against these C++ helpers first and it passed with the fix reverted — the scenario doesn't collide through evalSrcWithCache, so it would have been a green test that can't fail. It's in the repo where the reproduction is verified in both directions instead.

1260 C++ tests, 42 binding tests.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JFTVMTH6yEktHDPF5csksb

revarbat and others added 2 commits September 13, 2026 13:30
text() put the resolved FontHandle in its CSG params. A handle numbers
faces WITHIN one FontProvider, and every Evaluator makes its own, so two
evaluators sharing a ManifoldCache -- which is exactly what two GUI tabs
are -- handed the cache the same number for different fonts. The second
render was served the first one's glyphs.

Measured, not deduced: two providers, and

    providerA.resolveFont("Arial:style=Regular") -> 4
    providerB.resolveFont("Arial:style=Bold")    -> 4

End to end through one shared cache, before:

    Arial Regular   204 tris  width 9.196
    Arial Bold      204 tris  width 9.196   <- Regular's glyph
    Courier Regular 204 tris  width 9.196   <- Regular's glyph
    Courier Bold    204 tris  width 9.196   <- Regular's glyph

after, each matches what it renders in a cache of its own (204/9.196,
164/9.298, 320/6.978, 296/7.819).

The fix is the SPEC alongside the handle in the params, so the key tells
them apart. The handle still does the lookup within its own evaluator. Two
specs that resolve to one face merely miss the cache rather than collide,
which is the safe direction.

Tests here pin the two properties that are provable here: the handle
collision across providers, and cacheKey distinguishing two nodes that
differ only by spec. The end-to-end regression test lives in BelfrySCAD --
written against these C++ helpers the scenario does not collide, so a test
here would have passed either way, and a green test that cannot fail is
worse than none.

Reported as BelfrySCAD#434.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JFTVMTH6yEktHDPF5csksb
The test claimed a fresh provider gives the first font it resolves the
same handle the previous provider gave ITS first font. That is what macOS
does (both 4, which is the collision this change is about) but not what
the Linux runner does: with only the bundled faces present the numbering
lines up instead, 0 and 1 either way, and the test failed there on aReg=0
vs bBold=1.

What is universally true is the part that stays: within ONE provider two
fonts get two handles. That a second provider CAN reuse a number is the
motivation, is recorded as a comment with both platforms' numbers, and is
covered end to end by BelfrySCAD's own test -- where the collision
reproduces in both directions rather than depending on which fonts the
machine has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JFTVMTH6yEktHDPF5csksb
@revarbat
revarbat merged commit 270765f into main Sep 13, 2026
3 checks passed
@revarbat
revarbat deleted the fix/font-handle-cache-key branch September 13, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant