metadata
The cull we didn't dare add was already safe
Two engines rendered the same style differently because one honoured a metadata field the other deliberately skipped. The comment defending the skip described a danger an existing clamp had already neutralized — and the "working" half of the cull turned out to be silently keyed past its own lookup.
By the X-GIS team 4 min read
Zoom into Australia on the MapLibre demotiles style and, at z5, MapLibre drops
the Tropic of Capricorn — the dashed reference line and its label vanish. Our
engine kept drawing both forever. Nobody’s projection math differed; the
divergence was one TileJSON field. The style’s geolines source-layer
declares vector_layers maxzoom 4 while the source maxzoom is 6, so the
native z5/z6 tiles simply omit that layer. MapLibre renders the empty native
slice — lines gone. We ignored per-layer maxzoom and over-zoomed the z4 data
indefinitely — lines forever.
Parity bugs between two consumers of the same data are usually not two algorithms disagreeing; they are one metadata field honoured asymmetrically.
The wrong first move
The skip wasn’t an oversight. The code defended it, at length:
// `maxzoom` is intentionally NOT used as a cull bound — it's// a SOFT bound on raw archive data, but sub-tile generation// continues to upscale the maxzoom data for over-zoom views.// Culling on maxzoom would freeze rendering past z=15 on// protomaps v4 (every layer reports maxzoom=15), defeating the// whole over-zoom pipeline.The fear was legitimate: on a basemap where every layer reports maxzoom 15,
a naive currentZ > layer.maxzoom cull would blank the entire map past z15.
So the field stayed half-honoured — minzoom culled, maxzoom “intentionally
NOT” — and the comment sat there as the reason no one touched it.
But the danger had an unstated precondition, and the precondition no longer
held. currentZ — the resolved native tile level — is already clamped to
the source maxLevel before this code runs. On protomaps, currentZ can never
exceed 15, so currentZ > 15 can never fire; over-zoom is untouched. The
comparison fires only when a layer’s data-max is strictly below the source
max — demotiles’ 4 < 6 — which is precisely the case where the native tile
exists and omits the layer, i.e. precisely what MapLibre renders. The
comment’s scenario was unreachable, and had been since the clamp existed.
What actually happened
The cull became a symmetric band check, with the safety argument stated as a reachability condition rather than a warning:
export function sliceOutsideDataZoomRange( range: { minzoom: number; maxzoom: number } | null | undefined, nativeZ: number,): boolean { if (!range) return false return nativeZ < range.minzoom || nativeZ > range.maxzoom}Wiring it exposed a second, quieter bug in the half that everyone believed
worked. Layers with a style filter get a slice key of the form
sourceLayer::<hash> — but the metadata lookup keys on the bare source-layer
name. The demotiles geolines layers all carry a filter, so the existing
minzoom cull had been a silent no-op on them the whole time: a lookup that
returns undefined and a guard that treats undefined as “always present.”
The suffix is now stripped before the lookup, which means the minzoom cull is
enforced on filtered layers for the first time — a latent behaviour change we
flagged for the visual pass rather than assumed harmless.
How we know it holds
Three regression points, one per stakeholder in that old comment. The
demotiles case: at z5 the geolines slice must produce no selection —
reverting only the cull fails with a nine-tile Selection where null was
expected, the exact pre-fix behaviour. The boundary: z4 (at the layer’s
declared max) still renders — the band is inclusive, matching MapLibre. The
feared case: protomaps roads at z18 are not culled, pinning that the clamp
makes the over-zoom scenario unreachable. The converter side is pinned too —
the style’s own maxzoom (24) passes through untouched, so the cull decision
stays owned by the tile metadata, not the style. On-screen confirmation
against MapLibre remains on the checklist for a GPU machine; the decision
function and both call sites are what these tests prove.
What generalizes
“Intentionally NOT” comments have an expiry condition that nobody re-checks, because the comment reads as a conclusion rather than a claim. This one was protecting against a case an existing clamp had made unreachable — the reason not to fix had expired while the comment kept guarding it. When you find one, extract its precondition and test it; if the precondition holds structurally, encode it in the code path (or a pinned regression) and retire the prose. And distrust the half of a guard that “already works”: the working minzoom cull here had been no-oping on every filtered layer, failing in the only direction nobody screenshots — rendering more than it should.
Read next
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.
rendering
7 min read
The hemisphere that was never selected: a fast-path bbox that collapsed to a point
Half the globe rendered as background again — same symptom as the drape-suppression bug, opposite root cause. This time the trans-antimeridian tiles were never selected: the overzoom fast-path unprojects the viewport corners to a lon/lat box, but on a small globe disc the corners miss the sphere, the box collapses to the sub-camera point, and it emits the single tile column under the camera — which a contiguous longitude range can never wrap across the dateline. Told apart from the draw bug by seam location and persistence.