glifo: Make glyph pixel-snapping decisions stable under last-ulp noise - #1785
Open
AdrianEddy wants to merge 1 commit into
Open
AdrianEddy wants to merge 1 commit into
AdrianEddy wants to merge 1 commit into
Conversation
AdrianEddy
force-pushed
the
stable-glyph-placement
branch
from
August 1, 2026 03:02
242d9ac to
fb7d3dc
Compare
AdrianEddy
force-pushed
the
stable-glyph-placement
branch
from
August 14, 2026 00:47
fb7d3dc to
8c47ff7
Compare
Glyph placement makes discrete per-pixel decisions from an f64 translation: the floor/fract split behind the atlas quad position and the subpixel-bucket cache key, and the hinting baseline round. A consumer that produces the same content twice - a live pass and a recorded or cached one - computes that translation through different floating-point op orders and gets answers agreeing only to ~1e-12, while glyph origins routinely sit exactly on the decision boundary (integer coordinates; exact halves under fractional scale factors). A bare floor/round lets that last-ulp noise decide the pixel, so the two passes can place the same glyph one device pixel apart. Bias every decision by 1e-6 - far above the noise, far below the 0.25-px subpixel buckets - so the boundary moves off those structured values and any two evaluations agreeing within 1e-6 decide identically. The floor and fract halves of the split share the biased value, and the hinting round becomes half-up so exact negative halves decide like positive ones. The subpixel quantizer now clamps an input of exactly 1.0 (reachable through the f32 narrowing of the biased fract) to the last bucket instead of wrapping to bucket 0, which would shift the glyph in the wrong direction. No behavior change away from the boundaries; the full snapshot suite passes untouched.
AdrianEddy
force-pushed
the
stable-glyph-placement
branch
from
August 14, 2026 06:22
8c47ff7 to
b4adf81
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Glyph placement makes discrete per-pixel decisions from an
f64translation: the floor/fract split behind the atlas quad position and the subpixel-bucket cache key, and the vertical-hinting baseline round. A consumer that produces the same content twice — a live pass and a recorded or cached one — computes that translation through different floating-point op orders, and the two results agree only to ~1e-12. Glyph origins routinely sit exactly on the decision boundary (integer coordinates, or exact halves under fractional scale factors), so that last-ulp noise decides the pixel: the same glyph can render one device pixel apart between the two passes. In a caching compositor this shows up as text popping by a pixel when a cached layer is swapped in for a live render — the same "two producers must agree byte-for-byte" requirement behind the damage-rendering work in #1737.The fix biases every decision by 1e-6 — far above the noise, far below the 0.25-px subpixel buckets — so the boundaries move off those structured values, and any two evaluations agreeing within 1e-6 decide identically. Details:
f64::round's half-away-from-zero is not: translating a scene by a whole pixel could flip a baseline the old way).f32narrowing of the fract and wrapped it to bucket 0 — shifting the glyph in the wrong direction. It now clamps to the last bucket.Away from the boundaries nothing changes: the full snapshot suite passes untouched. New tests pin decision stability at integers and exact halves under injected noise, plus the end-to-end lockstep of the bucket and the quad pixel.
An alternative would be snapping the translation to fixed point (FreeType-style) before all decisions — strictly stronger, but it changes the placement of every fractional position and guarantees snapshot churn. This is the minimal zero-churn version, and the helper call sites are exactly where a snap would slot in if that direction is preferred later.
This PR was generated by Claude.