perf(raster-layer): avoid re-compiling shader Model as much as possible#540
Merged
Conversation
Store the full `{indices, attributes}` mesh wrapper in `state.mesh` and pass
it directly to `MeshTextureLayer`, rather than rebuilding the wrapper on
every `renderLayers()` call.
`SimpleMeshLayer.updateState` rebuilds its `Model` whenever
`props.mesh !== oldProps.mesh` (reference equality). With the previous
implementation, every render produced a new wrapper object, so every tile
sublayer destroyed and recreated its Model every frame — incurring full
`assembleGLSLShaderPair` and vertex-buffer setup per tile per frame. With
many sublayers active (e.g. a multi-source mosaic of small-tile COGs), this
dominated frame time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
Adds two unit tests for the mesh stabilization invariants:
1. `state.mesh` after `_generateMesh()` matches SimpleMeshLayer's
`{indices: {value, size}, attributes: {POSITION, TEXCOORD_0}}` shape.
2. Two consecutive `renderLayers()` calls pass the *same* `state.mesh`
reference to `MeshTextureLayer` — the property `SimpleMeshLayer` checks
via `props.mesh !== oldProps.mesh` before rebuilding its Model.
The tests bypass deck.gl's `LayerManager` (constructing a bare layer with
stubbed `setState` / `getSubLayerProps`) and mock `MeshTextureLayer` so
its props are captured directly. Verifies the fix at the unit level
independent of any rendering context.
Also adds a temporary `console.log` in `MeshTextureLayer.getShaders()` so
the fix can be evaluated empirically in a real app: each call corresponds
to a Model construction, so a healthy after-state should show the log
fire only on tile load, not per frame.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…teState SimpleMeshLayer rebuilds its Model when `props.mesh !== oldProps.mesh` or `changeFlags.extensionsChanged`. Logging which trigger fires (and whether oldProps.mesh existed) lets us distinguish first-time initialization, mesh-reference instability, and unexpected extensions changes when evaluating the mesh-reference fix in a real app. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
Member
Author
|
|
With the stable-mesh-ref fix in this branch, `SimpleMeshLayer` no longer destroys and rebuilds its Model on every render. That's the perf win we want — but the existing Model holds a shader compiled from the *initial* renderPipeline module list. When the caller swaps modules (e.g. a render- mode switch in the naip-mosaic example: trueColor → NDVI), the new modules never make it into the shader, so the GPU silently keeps running the previous pipeline. Override `MeshTextureLayer.updateState` to detect renderPipeline module- list changes and force a single Model rebuild in that case. Per-frame prop-only changes (e.g. an NDVI slider updating `ndviFilter`'s `ndviMin/ndviMax`) still take the cheap `shaderInputs.setProps` path through `draw()`. Also drops the temporary diagnostic `console.log` statements from adee590 that were added to evaluate the mesh-ref fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch the module-change detection in `hasRenderPipelineChanged` from
reference equality on the module object to equality on `module.name`.
Name is the canonical identifier luma.gl uses to dedupe modules during
shader assembly — two modules with the same name can't coexist in a
pipeline anyway — so "same names in the same order" is the more honest
proxy for "the shape of the assembled shader is the same."
It also avoids false-positive Model rebuilds on HMR / module
re-evaluation, where the user's `const fooModule = {...}` gets a new
identity but the same name and source.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…edicate
Previously we mirrored SimpleMeshLayer's mesh/extensions rebuild predicate
inline to know whether super.updateState already replaced the Model.
Capture `this.state.model` before super.updateState and compare after
instead — directly meaningful ("did super swap the Model out?") and
robust if SimpleMeshLayer adds new rebuild triggers upstream.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…line Rather than duplicating SimpleMeshLayer's destroy/getModel/invalidateAll sequence, just set `changeFlags.extensionsChanged = true` before calling super.updateState when the renderPipeline module list changes. super already runs that exact rebuild path on the flag, and that path calls our overridden getShaders() — which composes the new shader from the current renderPipeline. One source of truth for the rebuild dance, no `as any` cast on `props.mesh`, and resilient to upstream changes in how the rebuild is performed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
It's very important to reduce shader recompiles as much as possible. Changing a pixel filter slider should be nearly instant; it should just be updating the uniforms to pass into the existing GPU pipeline.
Previously we were re-compiling the shader model on every render, because the mesh instance was not passing instance equality.
Significant performance improvement in the NAIP mosaic pixel filter slider:
Before:
Screen.Recording.2026-05-14.at.1.06.47.PM.mov
After:
Screen.Recording.2026-05-14.at.1.14.07.PM.mov
Summary
Two-part fix that lets
RasterLayer/MeshTextureLayerreuse the same luma.glModel(and its compiled shader) across renders, while still rebuilding it the rare times the shader source actually has to change.RasterLayer— store the full{indices, attributes}mesh wrapper instate.meshand pass it directly toMeshTextureLayer, so the reference stays stable across renders.MeshTextureLayer— overrideupdateStateto force a Model rebuild when therenderPipelinemodule list changes (which the upstreamSimpleMeshLayerhas no way to know about, since it only checksmeshandextensions).Why
SimpleMeshLayer.updateStaterebuilds itsModelwheneverprops.mesh !== oldProps.mesh(reference equality check). Before this change,RasterLayer.renderLayers()built a fresh wrapper object on every render — so every tile sublayer destroyed and recreated its Model every frame, running fullassembleGLSLShaderPairand vertex-buffer setup per tile per frame.In the naip-mosaic example, moving the NDVI range slider dropped a single redraw frame to ~120ms in profiles, with
warnIfGLSLUniformBlocksAreNotStd140(11.5ms) andextractShaderUniformBlockFieldNames(9.0ms) dominating self-time — both functions only run insideassembleGLSLShaderPair.Relation to #543
#543 fixed an upstream churn source (
tileTransformreturning fresh arrow functions every call) and ended the constant_generateMeshre-runs inRasterLayer. That was sufficient for scenarios where the changing prop didn't propagate toRasterLayer'srenderPipeline(e.g. thedebugoverlay toggle tested there). It's not sufficient when the changing prop isrenderPipelineitself — which is the common case for any interactive uniform (sliders, color pickers, etc.). The mesh wrapper is still rebuilt inrenderLayers()for those, and the Model still churns. This PR closes that gap.Why the mesh-only fix wasn't enough
The first iteration of this PR (
18f32ca) only stabilized the mesh reference. That broke render-mode switching: with the Model preserved, the shader source froze at whateverrenderPipelinewas active when the Model was first built. SwitchingrenderMode: trueColor → ndviin the naip-mosaic example produced new modules inrenderPipeline, but the GPU silently kept running the original RGB shader.The second commit fixes this in
MeshTextureLayer.updateState: it compares old vs. new module identities (plusimagepresence) and forces a single Model rebuild when they differ. Prop-only changes within the same module list — the slider case — skip the rebuild and update uniforms viashaderInputs.setPropsindraw().Verified
In the naip-mosaic example:
Test plan
state.meshreference is stable across renders when mesh-affecting props haven't changed (0d21f83)MeshTextureLayermodule-list-change rebuild pathusgs-topo-cutlineexampleDECKGL_FILTER_COLORshader compile error previously seen in usgs-topo (likely orthogonal — that error was bisected as preexisting in initial testing of this PR)🤖 Generated with Claude Code