Skip to content

Invalidate the lighting bind memo in the instanced draw path - #277

Open
Groseuros wants to merge 3 commits into
bdero:masterfrom
Groseuros:fix/instanced-lighting-bind-memo
Open

Invalidate the lighting bind memo in the instanced draw path#277
Groseuros wants to merge 3 commits into
bdero:masterfrom
Groseuros:fix/instanced-lighting-bind-memo

Conversation

@Groseuros

Copy link
Copy Markdown

EngineLightingUniforms memoises its bind set and documents that "the scene encoder invalidates
the memo whenever it clears the pass's bindings". _encode does that (scene_encoder.dart:379-380).
_encodeInstanced clears at :441 without invalidating, so from the second instanced draw onward
the engine lighting samplers are wiped and never rebound — those draws, and any non-instanced mesh
encoded after them, shade black while still writing depth.

_renderPass.clearBindings();
EngineLightingUniforms.invalidateBindMemo();   // added

Needs two or more InstancedMeshes sharing a pipeline. With one, the clear hits an empty pass and
the memo misses anyway, so everything binds — probably why it hasn't come up.

Repro: cuboid instances split across K InstancedMeshes with PhysicallyBasedMaterial, plus a
ground-plane Node and one DirectionalLight. Correct at K=1; at K≥2 only one type renders and the
ground disappears.

Only visible on the GLES backend. Windows/Vulkan, an Android flagship on Vulkan, and web/WebGL2
all render correctly without the patch, so they are tolerating the missing binds rather than
unaffected — which is likely why this survived, since Android only falls back to GLES where Vulkan
is unavailable. On GLES it also under-reports frame cost, because the unshaded geometry still writes
depth: the same scene measured 3.8 ms raster before the fix and 4.9 ms after.

No output or timing change on Vulkan or WebGL2 after the patch. Cost is ~12 binds per instanced mesh
per frame, scaling with mesh count rather than instance count.

Happy to instead mirror _encode exactly and only clear on an actual pipeline change, if you'd
prefer that over the minimal fix.

flutter_scene 0.20.0 and current master (70125a88); Flutter master bc2162e270.

Groseuros and others added 3 commits July 29, 2026 19:47
SceneEncoder._encodeInstanced clears the render pass bindings without the
paired EngineLightingUniforms.invalidateBindMemo() that _encode performs at
the equivalent site. EngineLightingUniforms memoises its bind set on
(pass, shader, lighting, environment) and documents that the encoder
invalidates that memo whenever it clears the pass; the instanced path did not,
so the memo kept reporting bindings that had just been wiped.

In a run of same-pipeline opaque draws only the first instanced draw is
therefore fully bound. Every later one — and any non-instanced mesh encoded
after it — loses prefiltered_radiance, brdf_lut, shadow_map, sh_coefficients,
punctual_lights, ssao_texture and FogInfo, and shades black. The geometry is
still submitted and still writes depth, so it reads as missing rather than
unlit.

A single InstancedMesh escapes this: its clear hits an empty pass and the memo
misses anyway on a fresh RenderPass, so everything binds, and a following mesh
sharing the pipeline skips its own clear and inherits the live bindings. Two or
more instanced meshes is where it starts.

Reproduced with cuboid instances split across K PhysicallyBasedMaterial
InstancedMeshes plus a ground plane: correct at K=1, and at K>=2 only one type
and no ground on Impeller's Android GLES backend. Vulkan and WebGL2 tolerate
the missing binds and render correctly, so the defect is latent there rather
than absent. Setting a non-zero emissiveFactor — which travels in FragInfo and
is re-bound on every draw — makes the missing geometry reappear, confirming the
draws execute and only the memoised engine bindings are lost.
Pairs `clearBindings` with the engine-lighting memo invalidation in a single
`_clearBindings`, so the instanced path cannot drop the invalidation again.

Adds an instanced_lighting smoke scene (two instanced meshes plus a plain mesh
sharing a lit pipeline), which reproduced the loss on Impeller GLES and now
matches Metal and WebGL2. Also corrects the web shim's clearBindings comment,
which claimed to drop uniform and texture bindings it actually leaves bound.
The shim only dropped vertex and index bindings. Uniform blocks and texture
units are GL context state, so they stayed attached and a draw that should
have rebound a slot silently read the previous draw's resource. That is why
the instanced lighting loss rendered correctly on WebGL2 while breaking on
GLES.

The pass now records the binding points it hands out and releases them on
clear, so a missing rebind fails on web the way it does natively.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants