Repository navigation
[Impeller] Metal backend crashes when binding a texture whose sampler was dead-code-eliminated #190034
Description
Steps to reproduce
- Write a Flutter GPU fragment shader that declares a
sampler2Dused only in code the optimizer removes, so SPIRV-Cross keeps it in the reflected resource list but drops it from the emitted Metal shader. - From Dart, bind a texture to that sampler with
renderPass.bindTexture(shader.getUniformSlot('the_sampler'), texture). - Draw on Metal with
flutter run -d macos --enable-flutter-gpu --enable-impeller.
Expected results
Binding a texture for a sampler the compiled shader does not use should be a harmless no-op, the way the OpenGLES and Vulkan backends already handle it. It should never crash the driver.
Actual results
The Metal backend passes the dead-code sentinel index uint32_t(-1) (4294967295) straight to setFragmentTexture:atIndex:, which has no range check, so the AGX driver crashes.
0 AGXMetalG15X -[AGXG15XFamilyRenderContext setFragmentTexture:atIndex:]
1 FlutterMacOS impeller::PassBindingsCacheMTL::SetTexture(impeller::ShaderStage, unsigned long long, id<MTLTexture>)
2 FlutterMacOS impeller::RenderPassMTL::BindDynamicResource(impeller::ShaderStage, impeller::DescriptorType, impeller::SampledImageSlot const&, ...)
Code sample
uniform sampler2D tex_a;
uniform sampler2D tex_b; // reflected, but dropped from the Metal shader
in vec2 v_uv;
out vec4 frag_color;
void main() {
vec4 c = texture(tex_a, v_uv);
if (false) { c += texture(tex_b, v_uv); } // makes tex_b dead
frag_color = c;
}Then bind both tex_a and tex_b from Dart and draw. Binding tex_b crashes. (This shader shape needs a quick confirmation that it lands on the reflected-but-dropped path, see the note below.)
Root cause
impellerc still lists a dead sampler in its reflection but stamps that entry's index with uint32_t(-1), because get_automatic_msl_resource_binding returns ~0u for a sampler SPIRV-Cross removed from the emitted Metal. That value is written to the shader bundle, cached on the Dart Shader, and forwarded through RenderPassMTL::BindDynamicResource to setFragmentTexture:atIndex: with no range check. Every other backend avoids the problem. Vulkan binds by the SPIR-V binding decoration, and OpenGLES binds by name and skips a location of -1 (buffer_bindings_gles.cc, "The texture binding was optimized out of the shader. Continue.").
Proposed fix
Any one of these stops the crash, and they compose. None of them add per-draw work.
- Skip a texture binding whose
ext_res_0isuint32_t(-1)when loading the shader inlib/gpu/shader_library.cc, so the bind becomes a no-op like OpenGLES. This works with already-built shader bundles. - Stop emitting
sampled_imagesentries with a sentinel index inreflector.cc, so the bundle reflection matches the compiled shader. - As a backstop, skip a bind whose index is the sentinel or out of range in
pass_bindings_cache_mtl, so no invalid binding can ever reach the driver.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fields✅ Done
Activity
[Impeller] Skip binding dead-code-eliminated resources on Metal (flut…
github-actions commentedon Aug 16, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.