precision
Reconstructing the absolute coordinate was the bug
Line strokes shook at deep zoom in non-Mercator projections: the shader rebuilt an absolute longitude in f32 degrees, then a later stage subtracted the camera again — catastrophic cancellation. The fix subtracts first; the proof needs no GPU.
By the X-GIS team 3 min read
At longitude ~127°, one f32 ULP of a degree value is roughly 1.7 metres on the
ground. Our line shader’s non-Mercator branch rebuilt an absolute longitude
in f32 degrees from tile-local coordinates, handed it to the projection, and
the projection’s first act was radians(abs_lon) − radians(clon) — subtract
the camera longitude back out. Two nearly-equal large numbers, differenced in
f32: catastrophic cancellation, and strokes that visibly shook at high zoom in
every non-Mercator projection. The fill and point paths had already been fixed
to carry a split-precision tile-local tail; the line path was the sibling that
kept the old degree round-trip.
The bug’s shape is worth naming precisely, because it recurs in any coordinate pipeline: a value that is born relative (tile-local, sub-metre precise) gets promoted to absolute form (huge magnitude, precision destroyed), travels one stage, and is demoted back to relative by subtracting the same large anchor. The information the final stage needs — the small difference — existed exactly at the start and was reconstructed, lossily, at the end.
The wrong first move
For a render bug, the reflexive verification plan is “reproduce on screen, fix, compare screenshots.” For this class it is close to useless: the error is sub-metre at z16–z20, visible mainly as temporal shimmer under camera motion, and a screenshot diff of it proves nothing about the zooms and longitudes you didn’t capture. The issue itself had been filed from static analysis with the honest caveat “NOT yet runtime/pixel-reproduced” — and it stayed open, because “we’ll verify it when we can render it” is where precision bugs go to wait. Precision is math wearing a pixel costume; the admissible proof is an error budget, not an eyeball.
The fix
Feed the projection the difference that already exists. The renderer sets
cam_h + cam_l (a split-precision pair) to camMercX − tileMercX, so
corner − (cam_h + cam_l) is the camera-relative position — the ~1.4×10⁷ m
anchor magnitude cancels in the split arithmetic, before f32 ever sees a large
number:
const clon = projParams.yconst relMercX = p.corner.x.sub(TILE.field.cam_h.x).sub(TILE.field.cam_l.x)const dLon = relMercX.div(DEG2RAD.mul(EARTH_R))const projParamsRel = vec4(projParams.x, f32(0), projParams.z, projParams.w)const flatRel = flat_rel(dLon, absLat, projParamsRel, tileRefLonRel)The projection depends only on lon − clon, so recentring the whole call onto
clon = 0 is exact in real arithmetic — the same function evaluated at
shifted arguments — and it deletes the cancellation instead of guarding it.
Latitude deliberately keeps the absolute path: it has no linear
camera-relative form, and its magnitude keeps its residual sub-metre. The fix
is honest about being half a fix.
How we know it holds
No GPU was involved, by design. The gate evaluates the old and new formulas
in a CPU model of the shader — every operation wrapped in Math.fround, so
f32 rounding happens where the hardware would round — against an f64
reference projection, across five projections at z16/z18/z20. The old path
leaks >0.2 m of longitude error; the new path is <0.01 m, a >20× reduction,
asserted as a budget, not a screenshot. Because a numeric model can drift
from what the compiler actually emits, two further tests pin the emitted WGSL
itself — that flat_rel receives the camera-relative operand and the
recentred parameters. Stashing the fix fails exactly those two (the pre-fix
emit contains the absolute-degree reconstruction); the suite stays green
otherwise. The end-to-end measurement keeps the honesty visible: total error
0.29–0.43 m after versus 0.48–0.64 m before — the longitude term is gone, the
documented latitude residual remains.
What generalizes
When a pipeline subtracts a large anchor at stage N, the fix is almost never higher precision at stage N — it is refusing to re-add the anchor at stage N−1. Difference first, then round; any absolute form in the middle is a precision tax paid for nothing. And for this bug class, flip the verification instinct: an error-budget test over the whole input domain is strictly stronger evidence than any number of screenshots, because the screenshot samples one camera and the budget bounds all of them.
Read next
floating-point
7 min read
The split constant was never the bug
GPU double-single splits floats with 8193; the textbook says 4097. I was sure 8193 was cargo-culted from a wider type — until two million float32 products came back error-free for both. The split constant that breaks is one too small; the real bug was never the split.
verification
5 min read
Verified only where the bug showed: a hot-path fix that shipped a zoom-in regression
A WebGL2 flicker fix (#1139) was render-parity-verified on exactly one scenario — the zoom-OUT hold-tiles case that reproduced the original flicker, retained-content 0.19 → 0.79 matching WebGPU — and never on zoom-in or per-frame cost. It shipped an unmemoized per-frame classifyTile run twice per tile plus synchronous uploads, and stuttered with progressively-black fills on a Seoul zoom-in. Reverted 36 minutes later (#1140). A hot-path fix must be verified in its worst regime, not the one that showed the original bug.
rendering
7 min read
Draped at the wrong tile: a deferred write turned a shared uniform pool into a data race
A draped landcover polygon rendered hundreds of kilometres offshore — but only on WebGPU. Every vector slice of one source shares one Material uniform pool, and each slice resets the pool cursor to 0, so a later slice's tile i overwrote an earlier slice's tile-i buffer. WebGPU's queue.writeBuffer defers to submit (last-writer-wins), so every draw read the final bytes; WebGL2's immediate bufferSubData never showed it — and the differential test was structurally blind, because both frames aliased identically.