Skip to content

perf(raster-tileset): Reduce re-renders of RasterLayer for same per-tile transforms#543

Merged
kylebarron merged 2 commits into
mainfrom
raster-tileset-tile-transform-memo
May 14, 2026
Merged

perf(raster-tileset): Reduce re-renders of RasterLayer for same per-tile transforms#543
kylebarron merged 2 commits into
mainfrom
raster-tileset-tile-transform-memo

Conversation

@kylebarron
Copy link
Copy Markdown
Member

@kylebarron kylebarron commented May 14, 2026

I validated by hand with some console.log statements that this avoids the recreation of the MeshLayer model for the raster layer for each tile, at least in the cog-basic example when toggling the debug layer.

I think the core issue was that with new arrow functions, every change to the tile would cause a new re-render of the RasterLayer.


Summary

Compute each tile's forwardTransform / inverseTransform once at tile construction (inside RasterTileset2D.getTileMetadata) and attach them to the tile's TileMetadata. RasterTileLayer._renderSubLayers reads them off the tile instead of recomputing on every render.

Why

Before this change, every call to level.tileTransform(col, row) constructed two fresh arrow functions:

return {
  forwardTransform: (x, y) => affine.apply(tileAffine, x, y),
  inverseTransform: (x, y) => affine.apply(invTileAffine, x, y),
};

RasterTileLayer._renderSubLayers calls this on every render for every visible tile, so the references churned constantly. That instability tripped reprojectionFnsChanged in RasterLayer.updateState, which re-ran _generateMesh, which produced a fresh state.mesh wrapper, which tripped props.mesh !== oldProps.mesh in SimpleMeshLayer.updateState, which destroyed and rebuilt the GPU Model — incurring full shader assembly (assembleGLSLShaderPair, extractShaderUniformBlockFieldNames, etc.) per tile per render.

In a usgs-topo mosaic with ~411 active tile sublayers, this dominated frame time during any layer prop change (e.g. toggling a debug overlay).

Why tile-attached vs. level-cached

Earlier draft of this PR cached tileTransform(col, row) on the level itself. That worked, but the cache had no eviction — entries accumulated for every (col, row) ever queried across the level's lifetime.

Attaching to TileMetadata instead gives correct lifetime semantics for free:

  • Tile is constructed by TileLayergetTileMetadata runs once → transforms computed once
  • While the tile is in TileLayer's cache, every render reads the same references off the tile
  • When the tile is evicted from TileLayer's cache, the transforms go with it
  • Cache size is automatically bounded by TileLayer.maxCacheSize — no separate growth concern

Transforms are also co-located with the (x, y, z) identity that defines them, which is conceptually cleaner.

Verified

In cog-basic with diagnostic logs in place: toggling the debug overlay used to fire RasterLayer._generateMesh and MeshTextureLayer.getShaders once per tile per toggle. With this change, neither fires — only RasterLayer.renderLayers runs, and it's cheap because it just reads stable references off the tile.

Test plan

  • Unit test asserts RasterTileset2D.getTileMetadata attaches functioning forwardTransform / inverseTransform to the metadata
  • Existing tile-transform behavior tests still pass (94 / 94)
  • Visual regression check in usgs-topo-cutline example
  • Profile-driven verification in a real mosaic app (usgs-topo)

Supersedes

Replaces #540 (mesh-shape refactor in RasterLayer), which was treating a symptom of this same instability. #540 should be closed in favor of this PR. #541 (shader-assembler memoization) becomes optional defensive caching with this fix in place.

🤖 Generated with Claude Code

@github-actions github-actions Bot added the perf label May 14, 2026
…ileMetadata

`AffineTilesetLevel.tileTransform` and `TileMatrixAdaptor.tileTransform`
each constructed two new arrow functions per call. `RasterTileLayer.
_renderSubLayers` invoked them per tile on every render, so the returned
references churned constantly. That instability tripped
`reprojectionFnsChanged` in `RasterLayer.updateState`, which re-ran
`_generateMesh`, which produced a fresh `state.mesh` wrapper, which tripped
`props.mesh !== oldProps.mesh` in `SimpleMeshLayer.updateState`, which
destroyed and rebuilt the GPU `Model` — incurring full shader assembly per
tile per render.

Move transform computation upstream into `RasterTileset2D.getTileMetadata`,
which deck.gl calls once per tile at construction time. The resulting
forward/inverse functions live on the tile's metadata, so consumers receive
the same references across all renders for the tile's lifetime. When the
tile is evicted from `TileLayer`'s cache, the transforms go with it —
lifetime is automatically correct, and the cache size tracks
`TileLayer.maxCacheSize` rather than growing unbounded.

`_renderSubLayers` now reads `tile.forwardTransform` / `tile.inverseTransform`
instead of recomputing per render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kylebarron kylebarron force-pushed the raster-tileset-tile-transform-memo branch from 23a79e9 to 9049c0f Compare May 14, 2026 15:19
@kylebarron kylebarron changed the title perf(raster-tileset): memoize tileTransform output per (col, row) perf(raster-tileset): attach per-tile transforms to TileMetadata May 14, 2026
@kylebarron kylebarron marked this pull request as ready for review May 14, 2026 15:53
@kylebarron kylebarron changed the title perf(raster-tileset): attach per-tile transforms to TileMetadata perf(raster-tileset): Reduce re-renders of RasterLayer for same per-tile transforms May 14, 2026
@kylebarron kylebarron enabled auto-merge (squash) May 14, 2026 15:54
@kylebarron kylebarron merged commit 9480a26 into main May 14, 2026
6 checks passed
@kylebarron kylebarron deleted the raster-tileset-tile-transform-memo branch May 14, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant