fix(deck.gl-raster): Select each tile once in raster tile traversal - #689
Open
kylebarron wants to merge 2 commits into
Open
kylebarron wants to merge 2 commits into
kylebarron wants to merge 2 commits into
Conversation
…Range Both implementations floored the max edge, so in an aligned pyramid (WebMercatorQuad, or COG overviews that exactly halve) most parents also claimed their right and bottom neighbors' children, and the traversal selected those once per parent: getTileIndices returned 286 indices for 9 tiles at zoom 10 with titiler's WebMercatorQuad, and selected some children inside coarser tiles it had also selected. Tiles that only touch the box (within 1e-6 of a tile) are now left out, which also tightens root culling and MultiCOGLayer's secondary tile lookup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When pyramid levels are not aligned (e.g. COG overviews of odd sizes), a child tile can straddle two parents, and the traversal selects it once through each: a 10980 px COG still returned 16 indices for 9 tiles. Keep the first node for each z/x/y. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
Note
This PR was written by Claude (Claude Code), not by @kylebarron.
RasterTileset2Dselects the same tile many times on aligned grids such as WebMercatorQuad.Problem
Both
crsBoundsToTileRangeimplementations (TileMatrixAdaptorandAffineTilesetLevel) floor the box's max edge, so a tile that only touches the box's right or bottom edge counts as overlapping. The traversal finds a tile's children by passing the tile's bounds to the next level'scrsBoundsToTileRange. In an aligned pyramid (WebMercatorQuad, or COG overviews that exactly halve), each parent's right and bottom edges lie on child tile edges, so most parents also claim their neighbors' children:RasterTileNode.childrenbuilds a new node for every tile in the range, and each node is selected on its own, sogetTileIndicesreturns a tile once per parent that claimed it. The extra children get extra children of their own, so it compounds down the pyramid. For a 512×512WebMercatorViewportat lng/lat 0, where every level's tile edges meet:That view is the worst case for flat maps. With titiler's TileMatrixSet, ordinary flat views get about 2-10× as many indices as tiles (e.g. 110 for 20 over San Francisco at zoom 12). The OGC example's gets more (1215 for 20 there), and pitched views get far more. lonboard's
RasterLayerand #469's titiler example go through this path.What the repeats cost with deck.gl 9.4.0's
Tileset2D:No duplicate requests or draws.
getTileDataruns once per tile, andrenderLayersiterates the uniquetileset.tiles.Cache size.
selectedTileskeeps every repeat, and the default cache cap is5 * selectedTiles.length: 1430 instead of 45 for the zoom 10 view above.onViewportLoadalso receives the repeats.Traversal time per
getTileIndicescall (warm cache, 1024×768 unless noted). Pitch 60 is lonboard's default maximum for map views.Overlapping tiles. Where the selected zoom level changes, a parent can recurse while its right or bottom neighbor is selected at the coarser level. The parent's extra child lies inside that neighbor and is selected too, so the same area is fetched and drawn at two resolutions. For example, at lat 40, zoom 2, pitch 45 that's 38 extra tiles.
The other two callers of
crsBoundsToTileRangesee the same rule.createRootTiles(the culled path for large root matrices) keeps roots that only touch the viewport.resolveSecondaryTiles(MultiCOGLayer) fetches up to 2×2 secondary tiles for a primary tile whose right or bottom edge lies on a secondary tile edge: 256 secondary fetches for 121 primary tiles on a Sentinel-2-like 10 m / 20 m pair, where 121 suffice.Separately, when levels don't line up (COG overviews of odd sizes, e.g. 1373 → 2745 px), a child tile can straddle two parents and really is a child of both. The traversal selects it once through each, so even with exact ranges a 10980 px COG returns 16 indices for 9 tiles.
Changes
crsBoundsToTileRangeleaves out tiles that only touch the box. A new internal helper,overlappingTileSpan, returns[floor(start + ε), ceil(end - ε) - 1]along one axis with ε = 1e-6 of a tile. Both implementations use it and keep their existing clamping. TheRasterTilesetLeveldocs now state the rule, and the two test doubles that compute ranges follow it.getTileIndicesreturns eachz/x/yonce, keeping the first node, for levels that don't line up.Across about 75 views and tilesets checked during the investigation (flat, pitched up to 75°, rotated, world copies, globe; both WebMercatorQuads, UTM31 through proj4; power-of-two, odd-size, 1:1 and rotated COG pyramids), the new selection had no repeats and was always a subset of the old one. No sampled screen point lost coverage: every tile it no longer selects lies under a coarser tile that is still selected. The timings above come from that investigation, on code with the same source logic as this PR.
Tests
tile-children.test.ts:getTileIndiceswith both WebMercatorQuads in six views (flat at zoom 3 and 10, pitched and rotated, pitched across zoom levels, world copies, globe): no tile is selected twice or together with one of its ancestors.getTileIndiceson the 10980 px COG selects each tile once.create-root-tiles.test.ts: roots that only touch the viewport are left out (6 roots instead of 12 on a 1° grid).secondary-tile-resolver.test.ts: one 20 m tile for a 10 m tile whose edges lie on its edges, with realAffineTilesetLevels. The mock level used to count a tile that touches the box's left or top edge as overlapping, so the existing "spanning two secondary tiles" case only touched the first tile. The mock now follows the rule, and that case uses a secondary grid that the primary tile really straddles.Every new test fails on
main. With the four changed source files reverted (keeping the new helper), the three files giveTests 22 failed | 7 passed (29), and the 7 that pass are existing tests. For example:expected { minCol: +0, maxCol: 2, …(2) } to deeply equal { minCol: +0, maxCol: 1, …(2) }expected [ '11/1024/1023', …(276) ] to deeply equal []The non-aligned COG test fails against the first commit alone (
expected 16 to be 9) and passes with the second.pnpm --filter @developmentseed/deck.gl-raster test: 152/152 pass.typecheckpasses (TypeScript 6.0.3; CI covers 7).pnpm --filter @developmentseed/deck.gl-geotiff test(33/33) andpnpm --filter @developmentseed/deck.gl-zarr test(16/16) pass.pnpm exec biome ci .passes.Notes
MultiCOGLayernow fetches one secondary tile, not up to 2×2, for a primary tile whose right or bottom edge lies on a secondary tile edge. With linear filtering those edges now clamp to the texture edge, as the left and top edges always did. I haven't checked this visually. Root culling is also tighter.RasterTileNodeperz/x/ywithin a traversal would also stop shared children in non-aligned pyramids from being traversed twice. I went with the smaller change.overlappingTileSpanis not exported.RasterTilesetLevelis public, so other implementations have to follow the documented rule on their own. Should the helper be exported?tile-children.test.tsreads the OGC WebMercatorQuad example from the morecantile spec submodule, as fix(deck.gl-raster): Infer missing TileMatrixSet bounds and honorextent#684's tests do. CI checks out submodules, but a clone without them fails these tests. If it should be inlined, it's probably worth doing for both PRs together.extent#684. Itsraster-tileset-2d-extent.test.tsdedupsgetTileIndicesoutput because "the traversal can return the same tile more than once". Whichever of the two PRs lands second can drop that and assert there are no repeats.Ref #523 (per-frame traversal cost, which these extra nodes inflate), Ref #684
🤖 Written by Claude Code