Bevy version and features
0.19.0
What you did
Create a Material/MaterialExtension with a prepass_vertex_shader that has custom bindings.
What went wrong
Rendering crashes with:
ERROR bevy_render::error_handler: Caught rendering error: Validation Error
Caused by:
In Device::create_render_pipeline, label = 'pbr_prepass_pipeline'
Error matching ShaderStages(VERTEX) shader requirements against the pipeline
Shader global ResourceBinding { group: 3, binding: 100 } is not available in the pipeline layout
Binding is missing from the pipeline layout
Additional information
Here is a near minimal example that causes crashes. It is a bit non-sensical since I removed a lot of stuff that actually matters, but still causes crashes. If I downgrade to Bevy 0.18.0, there are no crashes. In my actual game, I do a lot of vertex shader logic and it needs bindings. Then I use the same bindings in the prepass shader to make sure shadows are handled correctly, and not using the original geometry.
Example:
use bevy::pbr::{ExtendedMaterial, MaterialExtension};
use bevy::prelude::*;
use bevy::render::render_resource::AsBindGroup;
use bevy::shader::ShaderRef;
type BugMaterial = ExtendedMaterial<StandardMaterial, ReadsMaterialInPrepass>;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(MaterialPlugin::<BugMaterial>::default())
.add_systems(Startup, setup)
.run();
}
#[derive(Asset, TypePath, AsBindGroup, Clone)]
struct ReadsMaterialInPrepass {
#[uniform(100)]
offset: Vec4,
}
impl MaterialExtension for ReadsMaterialInPrepass {
fn prepass_vertex_shader() -> ShaderRef {
"prepass.wgsl".into()
}
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<BugMaterial>>,
) {
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(BugMaterial {
base: StandardMaterial::default(),
extension: ReadsMaterialInPrepass { offset: Vec4::ZERO },
})),
));
commands.spawn(Camera3d::default());
commands.spawn(DirectionalLight {
shadow_maps_enabled: true,
..default()
});
}
And the shader:
#import bevy_pbr::{
mesh_functions,
prepass_io::{Vertex, VertexOutput},
view_transformations::position_world_to_clip,
}
@group(#{MATERIAL_BIND_GROUP}) @binding(100)
var<uniform> offset: vec4<f32>;
@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
var out: VertexOutput;
let model = mesh_functions::get_world_from_local(vertex.instance_index);
let world = mesh_functions::mesh_position_local_to_world(
model,
vec4<f32>(vertex.position + offset.xyz, 1.0),
);
return out;
}
I don't know much about the logic behind this, but I did throw the problem at Claude to see if I was crazy, and it claims:
Details
Introduced by **#22813 "Batch prepasses that use depth only."** For a depth-only opaque prepass, the material bind group layout is replaced with an empty layout:
crates/bevy_pbr/src/prepass/mod.rs, PrepassPipeline::specialize:
if is_depth_only_opaque_prepass(mesh_key) && !emulate_unclipped_depth {
bind_group_layouts.push(self.empty_layout.clone()); // <-- no material bindings
} else {
bind_group_layouts.push(material_properties.material_layout.as_ref().unwrap().clone());
}
with
fn is_depth_only_opaque_prepass(mesh_key: MeshPipelineKey) -> bool {
mesh_key.intersection(MeshPipelineKey::ALL_PREPASS_BITS) == MeshPipelineKey::DEPTH_PREPASS
}
The PR also added an opt-out flag, MeshPipelineKey::PREPASS_READS_MATERIAL:
- It is part of
ALL_PREPASS_BITS (so setting it makes is_depth_only_opaque_prepass return false, i.e. keeps the material layout).
- The shadow path checks it in
specialize_shadows (crates/bevy_pbr/src/render/light.rs):
let is_depth_only_opaque = !item.mesh_key
.intersects(MeshPipelineKey::MAY_DISCARD | MeshPipelineKey::PREPASS_READS_MATERIAL)
&& !emulate_unclipped_depth;
But PREPASS_READS_MATERIAL is never set by any code. git grep PREPASS_READS_MATERIAL on both release-0.19.0 and main shows only its definition, its inclusion in ALL_PREPASS_BITS, and the read in light.rs — there is no write. So there is currently no way (engine-side or user-side) for a material to declare that its prepass reads material data, and such materials hit the layout/shader mismatch above.
There is also no user-side workaround:
- The shadow
mesh_key is built from light_key | mesh.key_bits | <alpha discard bits> in specialize_shadows; it does not include the material's mesh_pipeline_key_bits, so a material cannot inject PREPASS_READS_MATERIAL through its own key.
AlphaMode::Mask only sets MAY_DISCARD, which is_depth_only_opaque_prepass (the layout decision) ignores.
Proposed fix
Set PREPASS_READS_MATERIAL when a material overrides the prepass vertex or fragment shader (those are the cases that can read material data in the prepass). For the shadow path, in specialize_shadows where the discard bits are added:
if material.properties.get_shader(PrepassVertexShader).is_some()
|| material.properties.get_shader(PrepassFragmentShader).is_some()
{
mesh_key |= MeshPipelineKey::PREPASS_READS_MATERIAL;
}
and the equivalent where the camera-prepass material key is built in specialize_prepass_material_meshes.
Alternatively (more explicit/precise), add an opt-in to the Material trait, e.g. fn prepass_reads_material() -> bool { false }, and OR PREPASS_READS_MATERIAL into the key when it returns true. This avoids disabling batching for custom prepass shaders that don't actually touch material data.
Bevy version and features
0.19.0
What you did
Create a Material/MaterialExtension with a
prepass_vertex_shaderthat has custom bindings.What went wrong
Rendering crashes with:
Additional information
Here is a near minimal example that causes crashes. It is a bit non-sensical since I removed a lot of stuff that actually matters, but still causes crashes. If I downgrade to Bevy 0.18.0, there are no crashes. In my actual game, I do a lot of vertex shader logic and it needs bindings. Then I use the same bindings in the prepass shader to make sure shadows are handled correctly, and not using the original geometry.
Example:
And the shader:
I don't know much about the logic behind this, but I did throw the problem at Claude to see if I was crazy, and it claims:
Details
Introduced by **#22813 "Batch prepasses that use depth only."** For a depth-only opaque prepass, the material bind group layout is replaced with an empty layout:crates/bevy_pbr/src/prepass/mod.rs,PrepassPipeline::specialize:with
The PR also added an opt-out flag,
MeshPipelineKey::PREPASS_READS_MATERIAL:ALL_PREPASS_BITS(so setting it makesis_depth_only_opaque_prepassreturnfalse, i.e. keeps the material layout).specialize_shadows(crates/bevy_pbr/src/render/light.rs):But
PREPASS_READS_MATERIALis never set by any code.git grep PREPASS_READS_MATERIALon bothrelease-0.19.0andmainshows only its definition, its inclusion inALL_PREPASS_BITS, and the read inlight.rs— there is no write. So there is currently no way (engine-side or user-side) for a material to declare that its prepass reads material data, and such materials hit the layout/shader mismatch above.There is also no user-side workaround:
mesh_keyis built fromlight_key | mesh.key_bits | <alpha discard bits>inspecialize_shadows; it does not include the material'smesh_pipeline_key_bits, so a material cannot injectPREPASS_READS_MATERIALthrough its own key.AlphaMode::Maskonly setsMAY_DISCARD, whichis_depth_only_opaque_prepass(the layout decision) ignores.Proposed fix
Set
PREPASS_READS_MATERIALwhen a material overrides the prepass vertex or fragment shader (those are the cases that can read material data in the prepass). For the shadow path, inspecialize_shadowswhere the discard bits are added:and the equivalent where the camera-prepass material key is built in
specialize_prepass_material_meshes.Alternatively (more explicit/precise), add an opt-in to the
Materialtrait, e.g.fn prepass_reads_material() -> bool { false }, and ORPREPASS_READS_MATERIALinto the key when it returnstrue. This avoids disabling batching for custom prepass shaders that don't actually touch material data.