diff --git a/Cargo.toml b/Cargo.toml index 688147ff9ba3e..19a2afc8e84c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -579,7 +579,7 @@ ron = "0.10" flate2 = "1.0" serde = { version = "1", features = ["derive"] } serde_json = "1.0.140" -bytemuck = "1.7" +bytemuck = "1" bevy_render = { path = "crates/bevy_render", version = "0.17.0-dev", default-features = false } # The following explicit dependencies are needed for proc macros to work inside of examples as they are part of the bevy crate itself. bevy_ecs = { path = "crates/bevy_ecs", version = "0.17.0-dev", default-features = false } @@ -1051,6 +1051,17 @@ description = "Showcases different blend modes" category = "3D Rendering" wasm = true +[[example]] +name = "manual_material" +path = "examples/3d/manual_material.rs" +doc-scrape-examples = true + +[package.metadata.example.manual_material] +name = "Manual Material Implementation" +description = "Demonstrates how to implement a material manually using the mid-level render APIs" +category = "3D Rendering" +wasm = true + [[example]] name = "edit_material_on_gltf" path = "examples/3d/edit_material_on_gltf.rs" diff --git a/assets/shaders/manual_material.wgsl b/assets/shaders/manual_material.wgsl new file mode 100644 index 0000000000000..557a1412c73d2 --- /dev/null +++ b/assets/shaders/manual_material.wgsl @@ -0,0 +1,11 @@ +#import bevy_pbr::forward_io::VertexOutput + +@group(3) @binding(0) var material_color_texture: texture_2d; +@group(3) @binding(1) var material_color_sampler: sampler; + +@fragment +fn fragment( + mesh: VertexOutput, +) -> @location(0) vec4 { + return textureSample(material_color_texture, material_color_sampler, mesh.uv); +} diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 69854f7de4818..92f4ee0228bc6 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -50,9 +50,7 @@ impl Plugin for LineGizmo3dPlugin { .init_resource::>() .configure_sets( Render, - GizmoRenderSystems::QueueLineGizmos3d - .in_set(RenderSystems::Queue) - .ambiguous_with(bevy_pbr::queue_material_meshes::), + GizmoRenderSystems::QueueLineGizmos3d.in_set(RenderSystems::Queue), ) .add_systems( Render, diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index f2e973eae58fe..fb17d842856ef 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -51,7 +51,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea ] } # other -bitflags = "2.3" +bitflags = { version = "2.3", features = ["bytemuck"] } fixedbitset = "0.5" thiserror = { version = "2", default-features = false } derive_more = { version = "2", default-features = false, features = ["from"] } diff --git a/crates/bevy_pbr/src/extended_material.rs b/crates/bevy_pbr/src/extended_material.rs index e01dd0ff14ef3..165debee6f328 100644 --- a/crates/bevy_pbr/src/extended_material.rs +++ b/crates/bevy_pbr/src/extended_material.rs @@ -1,6 +1,6 @@ use alloc::borrow::Cow; -use bevy_asset::{Asset, Handle}; +use bevy_asset::Asset; use bevy_ecs::system::SystemParamItem; use bevy_platform::{collections::HashSet, hash::FixedHasher}; use bevy_reflect::{impl_type_path, Reflect}; @@ -9,8 +9,8 @@ use bevy_render::{ mesh::MeshVertexBufferLayoutRef, render_resource::{ AsBindGroup, AsBindGroupError, BindGroupLayout, BindGroupLayoutEntry, BindlessDescriptor, - BindlessResourceType, BindlessSlabResourceLimit, RenderPipelineDescriptor, Shader, - ShaderRef, SpecializedMeshPipelineError, UnpreparedBindGroup, + BindlessResourceType, BindlessSlabResourceLimit, RenderPipelineDescriptor, ShaderRef, + SpecializedMeshPipelineError, UnpreparedBindGroup, }, renderer::RenderDevice, }; @@ -19,10 +19,6 @@ use crate::{Material, MaterialPipeline, MaterialPipelineKey, MeshPipeline, MeshP pub struct MaterialExtensionPipeline { pub mesh_pipeline: MeshPipeline, - pub material_layout: BindGroupLayout, - pub vertex_shader: Option>, - pub fragment_shader: Option>, - pub bindless: bool, } pub struct MaterialExtensionKey { @@ -150,12 +146,19 @@ where } } +#[derive(bytemuck::Pod, bytemuck::Zeroable, Copy, Clone, PartialEq, Eq, Hash)] +#[repr(C, packed)] +pub struct MaterialExtensionBindGroupData { + pub base: B, + pub extension: E, +} + // We don't use the `TypePath` derive here due to a bug where `#[reflect(type_path = false)]` // causes the `TypePath` derive to not generate an implementation. impl_type_path!((in bevy_pbr::extended_material) ExtendedMaterial); impl AsBindGroup for ExtendedMaterial { - type Data = (::Data, ::Data); + type Data = MaterialExtensionBindGroupData; type Param = (::Param, ::Param); fn bindless_slot_count() -> Option { @@ -179,20 +182,24 @@ impl AsBindGroup for ExtendedMaterial { } } + fn bind_group_data(&self) -> Self::Data { + MaterialExtensionBindGroupData { + base: self.base.bind_group_data(), + extension: self.extension.bind_group_data(), + } + } + fn unprepared_bind_group( &self, layout: &BindGroupLayout, render_device: &RenderDevice, (base_param, extended_param): &mut SystemParamItem<'_, '_, Self::Param>, mut force_non_bindless: bool, - ) -> Result, AsBindGroupError> { + ) -> Result { force_non_bindless = force_non_bindless || Self::bindless_slot_count().is_none(); // add together the bindings of the base material and the user material - let UnpreparedBindGroup { - mut bindings, - data: base_data, - } = B::unprepared_bind_group( + let UnpreparedBindGroup { mut bindings } = B::unprepared_bind_group( &self.base, layout, render_device, @@ -209,10 +216,7 @@ impl AsBindGroup for ExtendedMaterial { bindings.extend(extended_bindgroup.bindings.0); - Ok(UnpreparedBindGroup { - bindings, - data: (base_data, extended_bindgroup.data), - }) + Ok(UnpreparedBindGroup { bindings }) } fn bind_group_layout_entries( @@ -373,57 +377,28 @@ impl Material for ExtendedMaterial { } fn specialize( - pipeline: &MaterialPipeline, + pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayoutRef, key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { // Call the base material's specialize function - let MaterialPipeline:: { - mesh_pipeline, - material_layout, - vertex_shader, - fragment_shader, - bindless, - .. - } = pipeline.clone(); - let base_pipeline = MaterialPipeline:: { - mesh_pipeline, - material_layout, - vertex_shader, - fragment_shader, - bindless, - marker: Default::default(), - }; let base_key = MaterialPipelineKey:: { mesh_key: key.mesh_key, - bind_group_data: key.bind_group_data.0, + bind_group_data: key.bind_group_data.base, }; - B::specialize(&base_pipeline, descriptor, layout, base_key)?; + B::specialize(pipeline, descriptor, layout, base_key)?; // Call the extended material's specialize function afterwards - let MaterialPipeline:: { - mesh_pipeline, - material_layout, - vertex_shader, - fragment_shader, - bindless, - .. - } = pipeline.clone(); - E::specialize( &MaterialExtensionPipeline { - mesh_pipeline, - material_layout, - vertex_shader, - fragment_shader, - bindless, + mesh_pipeline: pipeline.mesh_pipeline.clone(), }, descriptor, layout, MaterialExtensionKey { mesh_key: key.mesh_key, - bind_group_data: key.bind_group_data.1, + bind_group_data: key.bind_group_data.extension, }, ) } diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 517ef0eb5cbe2..b72c58c2d2b08 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -239,6 +239,9 @@ impl Plugin for PbrPlugin { use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder, debug_flags: self.debug_flags, }, + MaterialsPlugin { + debug_flags: self.debug_flags, + }, MaterialPlugin:: { prepass_enabled: self.prepass_enabled, debug_flags: self.debug_flags, diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index f73f1392bb1d5..fb86f411d438f 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -1,12 +1,8 @@ use crate::material_bind_groups::{ FallbackBindlessResources, MaterialBindGroupAllocator, MaterialBindingId, }; -#[cfg(feature = "meshlet")] -use crate::meshlet::{ - prepare_material_meshlet_meshes_main_opaque_pass, queue_material_meshlet_meshes, - InstanceManager, -}; use crate::*; +use alloc::sync::Arc; use bevy_asset::prelude::AssetChanged; use bevy_asset::{Asset, AssetEventSystems, AssetId, AssetServer, UntypedAssetId}; use bevy_core_pipeline::deferred::{AlphaMask3dDeferred, Opaque3dDeferred}; @@ -35,14 +31,17 @@ use bevy_platform::hash::FixedHasher; use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; use bevy_render::camera::extract_cameras; +use bevy_render::erased_render_asset::{ + ErasedRenderAsset, ErasedRenderAssetPlugin, ErasedRenderAssets, PrepareAssetError, +}; use bevy_render::mesh::mark_3d_meshes_as_changed_if_their_assets_changed; -use bevy_render::render_asset::prepare_assets; +use bevy_render::render_asset::{prepare_assets, RenderAssets}; use bevy_render::renderer::RenderQueue; use bevy_render::{ batching::gpu_preprocessing::GpuPreprocessingSupport, extract_resource::ExtractResource, mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, - render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, + prelude::*, render_phase::*, render_resource::*, renderer::RenderDevice, @@ -53,7 +52,9 @@ use bevy_render::{ use bevy_render::{mesh::allocator::MeshAllocator, sync_world::MainEntityHashMap}; use bevy_render::{texture::FallbackImage, view::RenderVisibleEntities}; use bevy_utils::Parallel; +use core::any::TypeId; use core::{hash::Hash, marker::PhantomData}; +use smallvec::SmallVec; use tracing::error; /// Materials are used alongside [`MaterialPlugin`], [`Mesh3d`], and [`MeshMaterial3d`] @@ -239,7 +240,7 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { )] #[inline] fn specialize( - pipeline: &MaterialPipeline, + pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayoutRef, key: MaterialPipelineKey, @@ -248,6 +249,74 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { } } +#[derive(Default)] +pub struct MaterialsPlugin { + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, +} + +impl Plugin for MaterialsPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((PrepassPipelinePlugin, PrepassPlugin::new(self.debug_flags))); + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .init_resource::() + .init_resource::>() + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems( + Render, + ( + specialize_material_meshes + .in_set(RenderSystems::PrepareMeshes) + .after(prepare_assets::) + .after(collect_meshes_for_gpu_building) + .after(set_mesh_motion_vector_flags), + queue_material_meshes.in_set(RenderSystems::QueueMeshes), + ), + ) + .add_systems( + Render, + ( + prepare_material_bind_groups, + write_material_bind_group_buffers, + ) + .chain() + .in_set(RenderSystems::PrepareBindGroups), + ) + .add_systems( + Render, + ( + check_views_lights_need_specialization.in_set(RenderSystems::PrepareAssets), + // specialize_shadows also needs to run after prepare_assets::, + // which is fine since ManageViews is after PrepareAssets + specialize_shadows + .in_set(RenderSystems::ManageViews) + .after(prepare_lights), + queue_shadows.in_set(RenderSystems::QueueMeshes), + ), + ); + } + } + + fn finish(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::>() + .init_resource::() + .init_resource::() + .init_resource::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::(); + } + } +} + /// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material`] /// asset type. pub struct MaterialPlugin { @@ -283,7 +352,7 @@ where app.init_asset::() .register_type::>() .init_resource::>() - .add_plugins((RenderAssetPlugin::>::default(),)) + .add_plugins((ErasedRenderAssetPlugin::>::default(),)) .add_systems( PostUpdate, ( @@ -302,109 +371,41 @@ where } if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app - .init_resource::>() - .init_resource::>() - .init_resource::>() - .init_resource::() - .add_render_command::>() - .add_render_command::>() - .add_render_command::>() - .add_render_command::>() - .add_render_command::>() - .init_resource::>>() - .add_systems( - ExtractSchedule, - ( - extract_mesh_materials::.in_set(MaterialExtractionSystems), - early_sweep_material_instances:: - .after(MaterialExtractionSystems) - .before(late_sweep_material_instances), - extract_entities_needs_specialization::.after(extract_cameras), - ), - ) - .add_systems( - Render, - ( - specialize_material_meshes:: - .in_set(RenderSystems::PrepareMeshes) - .after(prepare_assets::>) - .after(prepare_assets::) - .after(collect_meshes_for_gpu_building) - .after(set_mesh_motion_vector_flags), - queue_material_meshes:: - .in_set(RenderSystems::QueueMeshes) - .after(prepare_assets::>), - ), - ) - .add_systems( - Render, - ( - prepare_material_bind_groups::, - write_material_bind_group_buffers::, - ) - .chain() - .in_set(RenderSystems::PrepareBindGroups) - .after(prepare_assets::>), - ); - - if self.shadows_enabled { - render_app - .init_resource::() - .init_resource::() - .init_resource::>() - .add_systems( - Render, - ( - check_views_lights_need_specialization - .in_set(RenderSystems::PrepareAssets), - // specialize_shadows:: also needs to run after prepare_assets::>, - // which is fine since ManageViews is after PrepareAssets - specialize_shadows:: - .in_set(RenderSystems::ManageViews) - .after(prepare_lights), - queue_shadows:: - .in_set(RenderSystems::QueueMeshes) - .after(prepare_assets::>), - ), - ); - } - - #[cfg(feature = "meshlet")] render_app.add_systems( - Render, - queue_material_meshlet_meshes:: - .in_set(RenderSystems::QueueMeshes) - .run_if(resource_exists::), - ); - - #[cfg(feature = "meshlet")] - render_app.add_systems( - Render, - prepare_material_meshlet_meshes_main_opaque_pass:: - .in_set(RenderSystems::QueueMeshes) - .after(prepare_assets::>) - .before(queue_material_meshlet_meshes::) - .run_if(resource_exists::), + ExtractSchedule, + ( + extract_mesh_materials::.in_set(MaterialExtractionSystems), + early_sweep_material_instances:: + .after(MaterialExtractionSystems) + .before(late_sweep_material_instances), + extract_entities_needs_specialization::.after(extract_cameras), + ), ); } - - if self.shadows_enabled || self.prepass_enabled { - // PrepassPipelinePlugin is required for shadow mapping and the optional PrepassPlugin - app.add_plugins(PrepassPipelinePlugin::::default()); - } - - if self.prepass_enabled { - app.add_plugins(PrepassPlugin::::new(self.debug_flags)); - } } fn finish(&self, app: &mut App) { - if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app - .init_resource::>() - .init_resource::>(); - } + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.world_mut().resource_scope( + |world, mut bind_group_allocators: Mut| { + let render_device = world.resource::(); + bind_group_allocators.insert( + TypeId::of::(), + MaterialBindGroupAllocator::new( + render_device, + M::label(), + material_uses_bindless_resources::(render_device) + .then(|| M::bindless_descriptor()) + .flatten(), + M::bind_group_layout(render_device), + M::bindless_slot_count(), + ), + ); + }, + ); } } @@ -422,91 +423,54 @@ pub struct MaterialPipelineKey { pub bind_group_data: M::Data, } -impl Eq for MaterialPipelineKey where M::Data: PartialEq {} - -impl PartialEq for MaterialPipelineKey -where - M::Data: PartialEq, -{ - fn eq(&self, other: &Self) -> bool { - self.mesh_key == other.mesh_key && self.bind_group_data == other.bind_group_data - } -} - -impl Clone for MaterialPipelineKey -where - M::Data: Clone, -{ - fn clone(&self) -> Self { - Self { - mesh_key: self.mesh_key, - bind_group_data: self.bind_group_data.clone(), - } - } -} - -impl Hash for MaterialPipelineKey -where - M::Data: Hash, -{ - fn hash(&self, state: &mut H) { - self.mesh_key.hash(state); - self.bind_group_data.hash(state); - } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ErasedMaterialPipelineKey { + pub mesh_key: MeshPipelineKey, + pub material_key: SmallVec<[u8; 8]>, + pub type_id: TypeId, } /// Render pipeline data for a given [`Material`]. -#[derive(Resource)] -pub struct MaterialPipeline { +#[derive(Resource, Clone)] +pub struct MaterialPipeline { pub mesh_pipeline: MeshPipeline, - pub material_layout: BindGroupLayout, - pub vertex_shader: Option>, - pub fragment_shader: Option>, - /// Whether this material *actually* uses bindless resources, taking the - /// platform support (or lack thereof) of bindless resources into account. - pub bindless: bool, - pub marker: PhantomData, } -impl Clone for MaterialPipeline { - fn clone(&self) -> Self { - Self { - mesh_pipeline: self.mesh_pipeline.clone(), - material_layout: self.material_layout.clone(), - vertex_shader: self.vertex_shader.clone(), - fragment_shader: self.fragment_shader.clone(), - bindless: self.bindless, - marker: PhantomData, - } - } +pub struct MaterialPipelineSpecializer { + pub(crate) pipeline: MaterialPipeline, + pub(crate) properties: Arc, } -impl SpecializedMeshPipeline for MaterialPipeline -where - M::Data: PartialEq + Eq + Hash + Clone, -{ - type Key = MaterialPipelineKey; +impl SpecializedMeshPipeline for MaterialPipelineSpecializer { + type Key = ErasedMaterialPipelineKey; fn specialize( &self, key: Self::Key, layout: &MeshVertexBufferLayoutRef, ) -> Result { - let mut descriptor = self.mesh_pipeline.specialize(key.mesh_key, layout)?; - if let Some(vertex_shader) = &self.vertex_shader { + let mut descriptor = self + .pipeline + .mesh_pipeline + .specialize(key.mesh_key, layout)?; + if let Some(vertex_shader) = self.properties.get_shader(MaterialVertexShader) { descriptor.vertex.shader = vertex_shader.clone(); } - if let Some(fragment_shader) = &self.fragment_shader { + if let Some(fragment_shader) = self.properties.get_shader(MaterialFragmentShader) { descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone(); } - descriptor.layout.insert(3, self.material_layout.clone()); + descriptor + .layout + .insert(3, self.properties.material_layout.as_ref().unwrap().clone()); - M::specialize(self, &mut descriptor, layout, key)?; + if let Some(specialize) = self.properties.specialize { + specialize(&self.pipeline, &mut descriptor, layout, key)?; + } // If bindless mode is on, add a `BINDLESS` define. - if self.bindless { + if self.properties.bindless { descriptor.vertex.shader_defs.push("BINDLESS".into()); if let Some(ref mut fragment) = descriptor.fragment { fragment.shader_defs.push("BINDLESS".into()); @@ -517,46 +481,30 @@ where } } -impl FromWorld for MaterialPipeline { +impl FromWorld for MaterialPipeline { fn from_world(world: &mut World) -> Self { - let asset_server = world.resource::(); - let render_device = world.resource::(); - MaterialPipeline { mesh_pipeline: world.resource::().clone(), - material_layout: M::bind_group_layout(render_device), - vertex_shader: match M::vertex_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - fragment_shader: match M::fragment_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - bindless: material_uses_bindless_resources::(render_device), - marker: PhantomData, } } } -type DrawMaterial = ( +pub type DrawMaterial = ( SetItemPipeline, SetMeshViewBindGroup<0>, SetMeshViewBindingArrayBindGroup<1>, SetMeshBindGroup<2>, - SetMaterialBindGroup, + SetMaterialBindGroup<3>, DrawMesh, ); /// Sets the bind group for a given [`Material`] at the configured `I` index. -pub struct SetMaterialBindGroup(PhantomData); -impl RenderCommand

for SetMaterialBindGroup { +pub struct SetMaterialBindGroup; +impl RenderCommand

for SetMaterialBindGroup { type Param = ( - SRes>>, + SRes>, SRes, - SRes>, + SRes, ); type ViewQuery = (); type ItemQuery = (); @@ -575,15 +523,17 @@ impl RenderCommand

for SetMaterial ) -> RenderCommandResult { let materials = materials.into_inner(); let material_instances = material_instances.into_inner(); - let material_bind_group_allocator = material_bind_group_allocator.into_inner(); + let material_bind_group_allocators = material_bind_group_allocator.into_inner(); let Some(material_instance) = material_instances.instances.get(&item.main_entity()) else { return RenderCommandResult::Skip; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { + let Some(material_bind_group_allocator) = + material_bind_group_allocators.get(&material_instance.asset_id.type_id()) + else { return RenderCommandResult::Skip; }; - let Some(material) = materials.get(material_asset_id) else { + let Some(material) = materials.get(material_instance.asset_id) else { return RenderCommandResult::Skip; }; let Some(material_bind_group) = material_bind_group_allocator.get(material.binding.group) @@ -607,7 +557,7 @@ pub struct RenderMaterialInstances { /// A monotonically-increasing counter, which we use to sweep /// [`RenderMaterialInstances::instances`] when the entities and/or required /// components are removed. - current_change_tick: Tick, + pub current_change_tick: Tick, } impl RenderMaterialInstances { @@ -631,10 +581,10 @@ impl RenderMaterialInstances { /// material type, for simplicity. pub struct RenderMaterialInstance { /// The material asset. - pub(crate) asset_id: UntypedAssetId, + pub asset_id: UntypedAssetId, /// The [`RenderMaterialInstances::current_change_tick`] at which this /// material instance was last modified. - last_change_tick: Tick, + pub last_change_tick: Tick, } /// A [`SystemSet`] that contains all `extract_mesh_materials` systems. @@ -814,14 +764,14 @@ pub(crate) fn late_sweep_material_instances( pub fn extract_entities_needs_specialization( entities_needing_specialization: Extract>>, - mut entity_specialization_ticks: ResMut>, + mut entity_specialization_ticks: ResMut, mut removed_mesh_material_components: Extract>>, - mut specialized_material_pipeline_cache: ResMut>, + mut specialized_material_pipeline_cache: ResMut, mut specialized_prepass_material_pipeline_cache: Option< - ResMut>, + ResMut, >, mut specialized_shadow_material_pipeline_cache: Option< - ResMut>, + ResMut, >, views: Query<&ExtractedView>, ticks: SystemChangeTick, @@ -876,57 +826,27 @@ impl Default for EntitiesNeedingSpecialization { } } -#[derive(Resource, Deref, DerefMut, Clone, Debug)] -pub struct EntitySpecializationTicks { +#[derive(Resource, Deref, DerefMut, Default, Clone, Debug)] +pub struct EntitySpecializationTicks { #[deref] pub entities: MainEntityHashMap, - _marker: PhantomData, -} - -impl Default for EntitySpecializationTicks { - fn default() -> Self { - Self { - entities: MainEntityHashMap::default(), - _marker: Default::default(), - } - } } /// Stores the [`SpecializedMaterialViewPipelineCache`] for each view. -#[derive(Resource, Deref, DerefMut)] -pub struct SpecializedMaterialPipelineCache { +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedMaterialPipelineCache { // view entity -> view pipeline cache #[deref] - map: HashMap>, - marker: PhantomData, + map: HashMap, } /// Stores the cached render pipeline ID for each entity in a single view, as /// well as the last time it was changed. -#[derive(Deref, DerefMut)] -pub struct SpecializedMaterialViewPipelineCache { +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedMaterialViewPipelineCache { // material entity -> (tick, pipeline_id) #[deref] map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, - marker: PhantomData, -} - -impl Default for SpecializedMaterialPipelineCache { - fn default() -> Self { - Self { - map: HashMap::default(), - marker: PhantomData, - } - } -} - -impl Default for SpecializedMaterialViewPipelineCache { - fn default() -> Self { - Self { - map: MainEntityHashMap::default(), - marker: PhantomData, - } - } } pub fn check_entities_needing_specialization( @@ -956,21 +876,19 @@ pub fn check_entities_needing_specialization( par_local.drain_into(&mut entities_needing_specialization); } -pub fn specialize_material_meshes( +pub fn specialize_material_meshes( render_meshes: Res>, - render_materials: Res>>, + render_materials: Res>, render_mesh_instances: Res, render_material_instances: Res, render_lightmaps: Res, render_visibility_ranges: Res, ( - material_bind_group_allocator, opaque_render_phases, alpha_mask_render_phases, transmissive_render_phases, transparent_render_phases, ): ( - Res>, Res>, Res>, Res>, @@ -978,16 +896,14 @@ pub fn specialize_material_meshes( ), views: Query<(&ExtractedView, &RenderVisibleEntities)>, view_key_cache: Res, - entity_specialization_ticks: Res>, + entity_specialization_ticks: Res, view_specialization_ticks: Res, - mut specialized_material_pipeline_cache: ResMut>, - mut pipelines: ResMut>>, - pipeline: Res>, + mut specialized_material_pipeline_cache: ResMut, + mut pipelines: ResMut>, + pipeline: Res, pipeline_cache: Res, ticks: SystemChangeTick, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ +) { // Record the retained IDs of all shadow views so that we can expire old // pipeline IDs. let mut all_views: HashSet = HashSet::default(); @@ -1019,9 +935,6 @@ pub fn specialize_material_meshes( else { continue; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { - continue; - }; let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) else { continue; @@ -1040,12 +953,7 @@ pub fn specialize_material_meshes( let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { continue; }; - let Some(material) = render_materials.get(material_asset_id) else { - continue; - }; - let Some(material_bind_group) = - material_bind_group_allocator.get(material.binding.group) - else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; @@ -1086,13 +994,21 @@ pub fn specialize_material_meshes( } } - let key = MaterialPipelineKey { + let erased_key = ErasedMaterialPipelineKey { + type_id: material_instance.asset_id.type_id(), mesh_key, - bind_group_data: material_bind_group - .get_extra_data(material.binding.slot) - .clone(), + material_key: material.properties.material_key.clone(), }; - let pipeline_id = pipelines.specialize(&pipeline_cache, &pipeline, key, &mesh.layout); + let material_pipeline_specializer = MaterialPipelineSpecializer { + pipeline: pipeline.clone(), + properties: material.properties.clone(), + }; + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &material_pipeline_specializer, + erased_key, + &mesh.layout, + ); let pipeline_id = match pipeline_id { Ok(id) => id, Err(err) => { @@ -1113,8 +1029,8 @@ pub fn specialize_material_meshes( /// For each view, iterates over all the meshes visible from that view and adds /// them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as appropriate. -pub fn queue_material_meshes( - render_materials: Res>>, +pub fn queue_material_meshes( + render_materials: Res>, render_mesh_instances: Res, render_material_instances: Res, mesh_allocator: Res, @@ -1124,10 +1040,8 @@ pub fn queue_material_meshes( mut transmissive_render_phases: ResMut>, mut transparent_render_phases: ResMut>, views: Query<(&ExtractedView, &RenderVisibleEntities)>, - specialized_material_pipeline_cache: ResMut>, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ + specialized_material_pipeline_cache: ResMut, +) { for (view, visible_entities) in &views { let ( Some(opaque_phase), @@ -1170,19 +1084,20 @@ pub fn queue_material_meshes( else { continue; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { - continue; - }; let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) else { continue; }; - let Some(material) = render_materials.get(material_asset_id) else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; // Fetch the slabs that this mesh resides in. let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let Some(draw_function) = material.properties.get_draw_function(MaterialDrawFunction) + else { + continue; + }; match material.properties.render_phase_type { RenderPhaseType::Transmissive => { @@ -1190,7 +1105,7 @@ pub fn queue_material_meshes( + material.properties.depth_bias; transmissive_phase.add(Transmissive3d { entity: (*render_entity, *visible_entity), - draw_function: material.properties.draw_function_id, + draw_function, pipeline: pipeline_id, distance, batch_range: 0..1, @@ -1209,7 +1124,7 @@ pub fn queue_material_meshes( } let batch_set_key = Opaque3dBatchSetKey { pipeline: pipeline_id, - draw_function: material.properties.draw_function_id, + draw_function, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), index_slab, @@ -1233,7 +1148,7 @@ pub fn queue_material_meshes( // Alpha mask RenderPhaseType::AlphaMask => { let batch_set_key = OpaqueNoLightmap3dBatchSetKey { - draw_function: material.properties.draw_function_id, + draw_function, pipeline: pipeline_id, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), @@ -1259,7 +1174,7 @@ pub fn queue_material_meshes( + material.properties.depth_bias; transparent_phase.add(Transparent3d { entity: (*render_entity, *visible_entity), - draw_function: material.properties.draw_function_id, + draw_function, pipeline: pipeline_id, distance, batch_range: 0..1, @@ -1322,7 +1237,47 @@ pub enum OpaqueRendererMethod { Auto, } +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MaterialVertexShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MaterialFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct PrepassVertexShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct PrepassFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct DeferredVertexShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct DeferredFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MeshletFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MeshletPrepassFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MeshletDeferredFragmentShader; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MaterialDrawFunction; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct PrepassDrawFunction; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct DeferredDrawFunction; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct ShadowsDrawFunction; + /// Common [`Material`] properties, calculated for a specific material instance. +#[derive(Default)] pub struct MaterialProperties { /// Is this material should be rendered by the deferred renderer when. /// [`AlphaMode::Opaque`] or [`AlphaMode::Mask`] @@ -1344,13 +1299,54 @@ pub struct MaterialProperties { /// rendering to take place in a separate [`Transmissive3d`] pass. pub reads_view_transmission_texture: bool, pub render_phase_type: RenderPhaseType, - pub draw_function_id: DrawFunctionId, - pub prepass_draw_function_id: Option, - pub deferred_draw_function_id: Option, + pub material_layout: Option, + pub draw_functions: HashMap, + pub shaders: HashMap>, + /// Whether this material *actually* uses bindless resources, taking the + /// platform support (or lack thereof) of bindless resources into account. + pub bindless: bool, + pub specialize: Option< + fn( + &MaterialPipeline, + &mut RenderPipelineDescriptor, + &MeshVertexBufferLayoutRef, + ErasedMaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError>, + >, + /// The key for this material, typically a bitfield of flags that are used to modify + /// the pipeline descriptor used for this material. + pub material_key: SmallVec<[u8; 8]>, } -#[derive(Clone, Copy)] +impl MaterialProperties { + pub fn get_shader(&self, label: impl ShaderLabel) -> Option> { + self.shaders.get(&label.intern()).cloned() + } + + pub fn add_shader( + &mut self, + label: impl ShaderLabel, + shader: Handle, + ) -> Option> { + self.shaders.insert(label.intern(), shader) + } + + pub fn get_draw_function(&self, label: impl DrawFunctionLabel) -> Option { + self.draw_functions.get(&label.intern()).copied() + } + + pub fn add_draw_function( + &mut self, + label: impl DrawFunctionLabel, + draw_function: DrawFunctionId, + ) -> Option { + self.draw_functions.insert(label.intern(), draw_function) + } +} + +#[derive(Clone, Copy, Default)] pub enum RenderPhaseType { + #[default] Opaque, AlphaMask, Transmissive, @@ -1366,20 +1362,23 @@ pub enum RenderPhaseType { pub struct RenderMaterialBindings(HashMap); /// Data prepared for a [`Material`] instance. -pub struct PreparedMaterial { +pub struct PreparedMaterial { pub binding: MaterialBindingId, - pub properties: MaterialProperties, - pub phantom: PhantomData, + pub properties: Arc, } -impl RenderAsset for PreparedMaterial { +// orphan rules T_T +impl ErasedRenderAsset for MeshMaterial3d +where + M::Data: Clone, +{ type SourceAsset = M; + type ErasedAsset = PreparedMaterial; type Param = ( SRes, - SRes>, SRes, - SResMut>, + SResMut, SResMut, SRes>, SRes>, @@ -1389,6 +1388,8 @@ impl RenderAsset for PreparedMaterial { SRes>, SRes>, SRes>, + SRes>, + SRes, M::Param, ); @@ -1397,9 +1398,8 @@ impl RenderAsset for PreparedMaterial { material_id: AssetId, ( render_device, - pipeline, default_opaque_render_method, - bind_group_allocator, + bind_group_allocators, render_material_bindings, opaque_draw_functions, alpha_mask_draw_functions, @@ -1409,26 +1409,27 @@ impl RenderAsset for PreparedMaterial { alpha_mask_prepass_draw_functions, opaque_deferred_draw_functions, alpha_mask_deferred_draw_functions, + shadow_draw_functions, + asset_server, material_param, ): &mut SystemParamItem, - _: Option<&Self>, - ) -> Result> { - let draw_opaque_pbr = opaque_draw_functions.read().id::>(); - let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::>(); - let draw_transmissive_pbr = transmissive_draw_functions.read().id::>(); - let draw_transparent_pbr = transparent_draw_functions.read().id::>(); - let draw_opaque_prepass = opaque_prepass_draw_functions - .read() - .get_id::>(); + ) -> Result> { + let material_layout = M::bind_group_layout(render_device); + let draw_opaque_pbr = opaque_draw_functions.read().id::(); + let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::(); + let draw_transmissive_pbr = transmissive_draw_functions.read().id::(); + let draw_transparent_pbr = transparent_draw_functions.read().id::(); + let draw_opaque_prepass = opaque_prepass_draw_functions.read().get_id::(); let draw_alpha_mask_prepass = alpha_mask_prepass_draw_functions .read() - .get_id::>(); + .get_id::(); let draw_opaque_deferred = opaque_deferred_draw_functions .read() - .get_id::>(); + .get_id::(); let draw_alpha_mask_deferred = alpha_mask_deferred_draw_functions .read() - .get_id::>(); + .get_id::(); + let shadow_draw_function_id = shadow_draw_functions.read().get_id::(); let render_method = match material.opaque_render_method() { OpaqueRendererMethod::Forward => OpaqueRendererMethod::Forward, @@ -1471,13 +1472,81 @@ impl RenderAsset for PreparedMaterial { _ => None, }; - match material.unprepared_bind_group( - &pipeline.material_layout, - render_device, - material_param, - false, - ) { + let mut draw_functions = HashMap::new(); + draw_functions.insert(MaterialDrawFunction.intern(), draw_function_id); + if let Some(prepass_draw_function_id) = prepass_draw_function_id { + draw_functions.insert(PrepassDrawFunction.intern(), prepass_draw_function_id); + } + if let Some(deferred_draw_function_id) = deferred_draw_function_id { + draw_functions.insert(DeferredDrawFunction.intern(), deferred_draw_function_id); + } + if let Some(shadow_draw_function_id) = shadow_draw_function_id { + draw_functions.insert(ShadowsDrawFunction.intern(), shadow_draw_function_id); + } + + let mut shaders = HashMap::new(); + let mut add_shader = |label: InternedShaderLabel, shader_ref: ShaderRef| { + let mayber_shader = match shader_ref { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }; + if let Some(shader) = mayber_shader { + shaders.insert(label, shader); + } + }; + add_shader(MaterialVertexShader.intern(), M::vertex_shader()); + add_shader(MaterialFragmentShader.intern(), M::fragment_shader()); + add_shader(PrepassVertexShader.intern(), M::prepass_vertex_shader()); + add_shader(PrepassFragmentShader.intern(), M::prepass_fragment_shader()); + add_shader(DeferredVertexShader.intern(), M::deferred_vertex_shader()); + add_shader( + DeferredFragmentShader.intern(), + M::deferred_fragment_shader(), + ); + + #[cfg(feature = "meshlet")] + { + add_shader( + MeshletFragmentShader.intern(), + M::meshlet_mesh_fragment_shader(), + ); + add_shader( + MeshletPrepassFragmentShader.intern(), + M::meshlet_mesh_prepass_fragment_shader(), + ); + add_shader( + MeshletDeferredFragmentShader.intern(), + M::meshlet_mesh_deferred_fragment_shader(), + ); + } + + let bindless = material_uses_bindless_resources::(render_device); + let bind_group_data = material.bind_group_data(); + let material_key = SmallVec::from(bytemuck::bytes_of(&bind_group_data)); + fn specialize( + pipeline: &MaterialPipeline, + descriptor: &mut RenderPipelineDescriptor, + mesh_layout: &MeshVertexBufferLayoutRef, + erased_key: ErasedMaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError> { + let material_key = bytemuck::from_bytes(erased_key.material_key.as_slice()); + M::specialize( + pipeline, + descriptor, + mesh_layout, + MaterialPipelineKey { + mesh_key: erased_key.mesh_key, + bind_group_data: *material_key, + }, + ) + } + + match material.unprepared_bind_group(&material_layout, render_device, material_param, false) + { Ok(unprepared) => { + let bind_group_allocator = + bind_group_allocators.get_mut(&TypeId::of::()).unwrap(); // Allocate or update the material. let binding = match render_material_bindings.entry(material_id.into()) { Entry::Occupied(mut occupied_entry) => { @@ -1486,31 +1555,32 @@ impl RenderAsset for PreparedMaterial { // change. For now, we just delete and recreate the bind // group. bind_group_allocator.free(*occupied_entry.get()); - let new_binding = bind_group_allocator - .allocate_unprepared(unprepared, &pipeline.material_layout); + let new_binding = + bind_group_allocator.allocate_unprepared(unprepared, &material_layout); *occupied_entry.get_mut() = new_binding; new_binding } Entry::Vacant(vacant_entry) => *vacant_entry.insert( - bind_group_allocator - .allocate_unprepared(unprepared, &pipeline.material_layout), + bind_group_allocator.allocate_unprepared(unprepared, &material_layout), ), }; Ok(PreparedMaterial { binding, - properties: MaterialProperties { + properties: Arc::new(MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), reads_view_transmission_texture, render_phase_type, - draw_function_id, - prepass_draw_function_id, render_method, mesh_pipeline_key_bits, - deferred_draw_function_id, - }, - phantom: PhantomData, + material_layout: Some(material_layout), + draw_functions, + shaders, + bindless, + specialize: Some(specialize::), + material_key, + }), }) } @@ -1523,12 +1593,10 @@ impl RenderAsset for PreparedMaterial { // and is requesting a fully-custom bind group. Invoke // `as_bind_group` as requested, and store the resulting bind // group in the slot. - match material.as_bind_group( - &pipeline.material_layout, - render_device, - material_param, - ) { + match material.as_bind_group(&material_layout, render_device, material_param) { Ok(prepared_bind_group) => { + let bind_group_allocator = + bind_group_allocators.get_mut(&TypeId::of::()).unwrap(); // Store the resulting bind group directly in the slot. let material_binding_id = bind_group_allocator.allocate_prepared(prepared_bind_group); @@ -1536,18 +1604,20 @@ impl RenderAsset for PreparedMaterial { Ok(PreparedMaterial { binding: material_binding_id, - properties: MaterialProperties { + properties: Arc::new(MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), reads_view_transmission_texture, render_phase_type, - draw_function_id, - prepass_draw_function_id, render_method, mesh_pipeline_key_bits, - deferred_draw_function_id, - }, - phantom: PhantomData, + material_layout: Some(material_layout), + draw_functions, + shaders, + bindless, + specialize: Some(specialize::), + material_key, + }), }) } @@ -1565,7 +1635,7 @@ impl RenderAsset for PreparedMaterial { fn unload_asset( source_asset: AssetId, - (_, _, _, bind_group_allocator, render_material_bindings, ..): &mut SystemParamItem< + (_, _, bind_group_allocators, render_material_bindings, ..): &mut SystemParamItem< Self::Param, >, ) { @@ -1573,7 +1643,8 @@ impl RenderAsset for PreparedMaterial { else { return; }; - bind_group_allocator.free(material_binding_id); + let bind_group_allactor = bind_group_allocators.get_mut(&TypeId::of::()).unwrap(); + bind_group_allactor.free(material_binding_id); } } @@ -1594,15 +1665,15 @@ impl From for MaterialBindGroupId { /// Creates and/or recreates any bind groups that contain materials that were /// modified this frame. -pub fn prepare_material_bind_groups( - mut allocator: ResMut>, +pub fn prepare_material_bind_groups( + mut allocators: ResMut, render_device: Res, fallback_image: Res, fallback_resources: Res, -) where - M: Material, -{ - allocator.prepare_bind_groups(&render_device, &fallback_resources, &fallback_image); +) { + for (_, allocator) in allocators.iter_mut() { + allocator.prepare_bind_groups(&render_device, &fallback_resources, &fallback_image); + } } /// Uploads the contents of all buffers that the [`MaterialBindGroupAllocator`] @@ -1610,12 +1681,12 @@ pub fn prepare_material_bind_groups( /// /// Non-bindless allocators don't currently manage any buffers, so this method /// only has an effect for bindless allocators. -pub fn write_material_bind_group_buffers( - mut allocator: ResMut>, +pub fn write_material_bind_group_buffers( + mut allocators: ResMut, render_device: Res, render_queue: Res, -) where - M: Material, -{ - allocator.write_buffers(&render_device, &render_queue); +) { + for (_, allocator) in allocators.iter_mut() { + allocator.write_buffers(&render_device, &render_queue); + } } diff --git a/crates/bevy_pbr/src/material_bind_groups.rs b/crates/bevy_pbr/src/material_bind_groups.rs index 39028fed2dca0..780ac8e10c235 100644 --- a/crates/bevy_pbr/src/material_bind_groups.rs +++ b/crates/bevy_pbr/src/material_bind_groups.rs @@ -4,8 +4,7 @@ //! allocator manages each bind group, assigning slots to materials as //! appropriate. -use core::{cmp::Ordering, iter, marker::PhantomData, mem, ops::Range}; - +use crate::Material; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ resource::Resource, @@ -13,6 +12,7 @@ use bevy_ecs::{ }; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_render::render_resource::BindlessSlabResourceLimit; use bevy_render::{ render_resource::{ BindGroup, BindGroupEntry, BindGroupLayout, BindingNumber, BindingResource, @@ -26,11 +26,14 @@ use bevy_render::{ settings::WgpuFeatures, texture::FallbackImage, }; -use bevy_utils::default; +use bevy_utils::{default, TypeIdMap}; use bytemuck::Pod; +use core::hash::Hash; +use core::{cmp::Ordering, iter, mem, ops::Range}; use tracing::{error, trace}; -use crate::Material; +#[derive(Resource, Deref, DerefMut, Default)] +pub struct MaterialBindGroupAllocators(TypeIdMap); /// A resource that places materials into bind groups and tracks their /// resources. @@ -38,25 +41,20 @@ use crate::Material; /// Internally, Bevy has separate allocators for bindless and non-bindless /// materials. This resource provides a common interface to the specific /// allocator in use. -#[derive(Resource)] -pub enum MaterialBindGroupAllocator -where - M: Material, -{ +pub enum MaterialBindGroupAllocator { /// The allocator used when the material is bindless. - Bindless(Box>), + Bindless(Box), /// The allocator used when the material is non-bindless. - NonBindless(Box>), + NonBindless(Box), } /// The allocator that places bindless materials into bind groups and tracks /// their resources. -pub struct MaterialBindGroupBindlessAllocator -where - M: Material, -{ +pub struct MaterialBindGroupBindlessAllocator { + /// The label of the bind group allocator to use for allocated buffers. + label: Option<&'static str>, /// The slabs, each of which contains a bind group. - slabs: Vec>, + slabs: Vec, /// The layout of the bind groups that we produce. bind_group_layout: BindGroupLayout, /// Information about the bindless resources in the material. @@ -79,10 +77,7 @@ where } /// A single bind group and the bookkeeping necessary to allocate into it. -pub struct MaterialBindlessSlab -where - M: Material, -{ +pub struct MaterialBindlessSlab { /// The current bind group, if it's up to date. /// /// If this is `None`, then the bind group is dirty and needs to be @@ -98,7 +93,7 @@ where /// /// Because the slab binary searches this table, the entries within must be /// sorted by bindless index. - bindless_index_tables: Vec>, + bindless_index_tables: Vec, /// The binding arrays containing samplers. samplers: HashMap>, @@ -110,12 +105,6 @@ where /// `#[data]` attribute of `AsBindGroup`). data_buffers: HashMap, - /// Holds extra CPU-accessible data that the material provides. - /// - /// Typically, this data is used for constructing the material key, for - /// pipeline specialization purposes. - extra_data: Vec>, - /// A list of free slot IDs. free_slots: Vec, /// The total number of materials currently allocated in this slab. @@ -130,10 +119,7 @@ where /// This is conventionally assigned to bind group binding 0, but it can be /// changed by altering the [`Self::binding_number`], which corresponds to the /// `#[bindless(index_table(binding(B)))]` attribute in `AsBindGroup`. -struct MaterialBindlessIndexTable -where - M: Material, -{ +struct MaterialBindlessIndexTable { /// The buffer containing the mappings. buffer: RetainedRawBufferVec, /// The range of bindless indices that this bindless index table covers. @@ -146,7 +132,6 @@ where index_range: Range, /// The binding number that this index table is assigned to in the shader. binding_number: BindingNumber, - phantom: PhantomData, } /// A single binding array for storing bindless resources and the bookkeeping @@ -189,13 +174,12 @@ where } /// The allocator that stores bind groups for non-bindless materials. -pub struct MaterialBindGroupNonBindlessAllocator -where - M: Material, -{ +pub struct MaterialBindGroupNonBindlessAllocator { + /// The label of the bind group allocator to use for allocated buffers. + label: Option<&'static str>, /// A mapping from [`MaterialBindGroupIndex`] to the bind group allocated in /// each slot. - bind_groups: Vec>>, + bind_groups: Vec>, /// The bind groups that are dirty and need to be prepared. /// /// To prepare the bind groups, call @@ -203,15 +187,11 @@ where to_prepare: HashSet, /// A list of free bind group indices. free_indices: Vec, - phantom: PhantomData, } /// A single bind group that a [`MaterialBindGroupNonBindlessAllocator`] is /// currently managing. -enum MaterialNonBindlessAllocatedBindGroup -where - M: Material, -{ +enum MaterialNonBindlessAllocatedBindGroup { /// An unprepared bind group. /// /// The allocator prepares all outstanding unprepared bind groups when @@ -219,13 +199,13 @@ where /// called. Unprepared { /// The unprepared bind group, including extra data. - bind_group: UnpreparedBindGroup, + bind_group: UnpreparedBindGroup, /// The layout of that bind group. layout: BindGroupLayout, }, /// A bind group that's already been prepared. Prepared { - bind_group: PreparedBindGroup, + bind_group: PreparedBindGroup, #[expect(dead_code, reason = "These buffers are only referenced by bind groups")] uniform_buffers: Vec, }, @@ -351,35 +331,27 @@ trait GetBindingResourceId { } /// The public interface to a slab, which represents a single bind group. -pub struct MaterialSlab<'a, M>(MaterialSlabImpl<'a, M>) -where - M: Material; +pub struct MaterialSlab<'a>(MaterialSlabImpl<'a>); /// The actual implementation of a material slab. /// /// This has bindless and non-bindless variants. -enum MaterialSlabImpl<'a, M> -where - M: Material, -{ +enum MaterialSlabImpl<'a> { /// The implementation of the slab interface we use when the slab /// is bindless. - Bindless(&'a MaterialBindlessSlab), + Bindless(&'a MaterialBindlessSlab), /// The implementation of the slab interface we use when the slab /// is non-bindless. - NonBindless(MaterialNonBindlessSlab<'a, M>), + NonBindless(MaterialNonBindlessSlab<'a>), } /// A single bind group that the [`MaterialBindGroupNonBindlessAllocator`] /// manages. -enum MaterialNonBindlessSlab<'a, M> -where - M: Material, -{ +enum MaterialNonBindlessSlab<'a> { /// A slab that has a bind group. - Prepared(&'a PreparedBindGroup), + Prepared(&'a PreparedBindGroup), /// A slab that doesn't yet have a bind group. - Unprepared(&'a UnpreparedBindGroup), + Unprepared, } /// Manages an array of untyped plain old data on GPU and allocates individual @@ -480,26 +452,33 @@ impl GetBindingResourceId for TextureView { } } -impl MaterialBindGroupAllocator -where - M: Material, -{ +impl MaterialBindGroupAllocator { /// Creates a new [`MaterialBindGroupAllocator`] managing the data for a /// single material. - fn new(render_device: &RenderDevice) -> MaterialBindGroupAllocator { - if material_uses_bindless_resources::(render_device) { + pub fn new( + render_device: &RenderDevice, + label: Option<&'static str>, + bindless_descriptor: Option, + bind_group_layout: BindGroupLayout, + slab_capacity: Option, + ) -> MaterialBindGroupAllocator { + if let Some(bindless_descriptor) = bindless_descriptor { MaterialBindGroupAllocator::Bindless(Box::new(MaterialBindGroupBindlessAllocator::new( render_device, + label, + bindless_descriptor, + bind_group_layout, + slab_capacity, ))) } else { MaterialBindGroupAllocator::NonBindless(Box::new( - MaterialBindGroupNonBindlessAllocator::new(), + MaterialBindGroupNonBindlessAllocator::new(label), )) } } /// Returns the slab with the given index, if one exists. - pub fn get(&self, group: MaterialBindGroupIndex) -> Option> { + pub fn get(&self, group: MaterialBindGroupIndex) -> Option { match *self { MaterialBindGroupAllocator::Bindless(ref bindless_allocator) => bindless_allocator .get(group) @@ -520,7 +499,7 @@ where /// you need to prepare the bind group yourself. pub fn allocate_unprepared( &mut self, - unprepared_bind_group: UnpreparedBindGroup, + unprepared_bind_group: UnpreparedBindGroup, bind_group_layout: &BindGroupLayout, ) -> MaterialBindingId { match *self { @@ -545,7 +524,7 @@ where /// this method if you need to prepare the bind group yourself. pub fn allocate_prepared( &mut self, - prepared_bind_group: PreparedBindGroup, + prepared_bind_group: PreparedBindGroup, ) -> MaterialBindingId { match *self { MaterialBindGroupAllocator::Bindless(_) => { @@ -613,14 +592,11 @@ where } } -impl MaterialBindlessIndexTable -where - M: Material, -{ +impl MaterialBindlessIndexTable { /// Creates a new [`MaterialBindlessIndexTable`] for a single slab. fn new( bindless_index_table_descriptor: &BindlessIndexTableDescriptor, - ) -> MaterialBindlessIndexTable { + ) -> MaterialBindlessIndexTable { // Preallocate space for one bindings table, so that there will always be a buffer. let mut buffer = RetainedRawBufferVec::new(BufferUsages::STORAGE); for _ in *bindless_index_table_descriptor.indices.start @@ -633,7 +609,6 @@ where buffer, index_range: bindless_index_table_descriptor.indices.clone(), binding_number: bindless_index_table_descriptor.binding_number, - phantom: PhantomData, } } @@ -747,15 +722,16 @@ where } } -impl MaterialBindGroupBindlessAllocator -where - M: Material, -{ +impl MaterialBindGroupBindlessAllocator { /// Creates a new [`MaterialBindGroupBindlessAllocator`] managing the data /// for a single bindless material. - fn new(render_device: &RenderDevice) -> MaterialBindGroupBindlessAllocator { - let bindless_descriptor = M::bindless_descriptor() - .expect("Non-bindless materials should use the non-bindless allocator"); + fn new( + render_device: &RenderDevice, + label: Option<&'static str>, + bindless_descriptor: BindlessDescriptor, + bind_group_layout: BindGroupLayout, + slab_capacity: Option, + ) -> MaterialBindGroupBindlessAllocator { let fallback_buffers = bindless_descriptor .buffers .iter() @@ -776,11 +752,12 @@ where .collect(); MaterialBindGroupBindlessAllocator { + label, slabs: vec![], - bind_group_layout: M::bind_group_layout(render_device), + bind_group_layout, bindless_descriptor, fallback_buffers, - slab_capacity: M::bindless_slot_count() + slab_capacity: slab_capacity .expect("Non-bindless materials should use the non-bindless allocator") .resolve(), } @@ -796,7 +773,7 @@ where /// created, and the material is allocated into it. fn allocate_unprepared( &mut self, - mut unprepared_bind_group: UnpreparedBindGroup, + mut unprepared_bind_group: UnpreparedBindGroup, ) -> MaterialBindingId { for (slab_index, slab) in self.slabs.iter_mut().enumerate() { trace!("Trying to allocate in slab {}", slab_index); @@ -842,7 +819,7 @@ where /// /// A [`MaterialBindGroupIndex`] can be fetched from a /// [`MaterialBindingId`]. - fn get(&self, group: MaterialBindGroupIndex) -> Option<&MaterialBindlessSlab> { + fn get(&self, group: MaterialBindGroupIndex) -> Option<&MaterialBindlessSlab> { self.slabs.get(group.0 as usize) } @@ -858,6 +835,7 @@ where for slab in &mut self.slabs { slab.prepare( render_device, + self.label, &self.bind_group_layout, fallback_bindless_resources, &self.fallback_buffers, @@ -878,20 +856,7 @@ where } } -impl FromWorld for MaterialBindGroupAllocator -where - M: Material, -{ - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); - MaterialBindGroupAllocator::new(render_device) - } -} - -impl MaterialBindlessSlab -where - M: Material, -{ +impl MaterialBindlessSlab { /// Attempts to allocate the given unprepared bind group in this slab. /// /// If the allocation succeeds, this method returns the slot that the @@ -900,9 +865,9 @@ where /// so that it can try to allocate again. fn try_allocate( &mut self, - unprepared_bind_group: UnpreparedBindGroup, + unprepared_bind_group: UnpreparedBindGroup, slot_capacity: u32, - ) -> Result> { + ) -> Result { // Locate pre-existing resources, and determine how many free slots we need. let Some(allocation_candidate) = self.check_allocation(&unprepared_bind_group) else { return Err(unprepared_bind_group); @@ -942,12 +907,6 @@ where bindless_index_table.set(slot, &allocated_resource_slots); } - // Insert extra data. - if self.extra_data.len() < (*slot as usize + 1) { - self.extra_data.resize_with(*slot as usize + 1, || None); - } - self.extra_data[*slot as usize] = Some(unprepared_bind_group.data); - // Invalidate the cached bind group. self.bind_group = None; @@ -958,7 +917,7 @@ where /// bind group can be allocated in this slab. fn check_allocation( &self, - unprepared_bind_group: &UnpreparedBindGroup, + unprepared_bind_group: &UnpreparedBindGroup, ) -> Option { let mut allocation_candidate = BindlessAllocationCandidate { pre_existing_resources: HashMap::default(), @@ -1189,9 +1148,6 @@ where } } - // Clear out the extra data. - self.extra_data[slot.0 as usize] = None; - // Invalidate the cached bind group. self.bind_group = None; @@ -1204,6 +1160,7 @@ where fn prepare( &mut self, render_device: &RenderDevice, + label: Option<&'static str>, bind_group_layout: &BindGroupLayout, fallback_bindless_resources: &FallbackBindlessResources, fallback_buffers: &HashMap, @@ -1224,6 +1181,7 @@ where // Create the bind group if needed. self.prepare_bind_group( render_device, + label, bind_group_layout, fallback_bindless_resources, fallback_buffers, @@ -1238,6 +1196,7 @@ where fn prepare_bind_group( &mut self, render_device: &RenderDevice, + label: Option<&'static str>, bind_group_layout: &BindGroupLayout, fallback_bindless_resources: &FallbackBindlessResources, fallback_buffers: &HashMap, @@ -1304,11 +1263,8 @@ where }); } - self.bind_group = Some(render_device.create_bind_group( - M::label(), - bind_group_layout, - &bind_group_entries, - )); + self.bind_group = + Some(render_device.create_bind_group(label, bind_group_layout, &bind_group_entries)); } /// Writes any buffers that we're managing to the GPU. @@ -1548,19 +1504,11 @@ where self.bind_group.as_ref() } - /// Returns the extra data associated with this material. - fn get_extra_data(&self, slot: MaterialBindGroupSlot) -> &M::Data { - self.extra_data - .get(slot.0 as usize) - .and_then(|data| data.as_ref()) - .expect("Extra data not present") - } - /// Returns the bindless index table containing the given bindless index. fn get_bindless_index_table( &self, bindless_index: BindlessIndex, - ) -> Option<&MaterialBindlessIndexTable> { + ) -> Option<&MaterialBindlessIndexTable> { let table_index = self .bindless_index_tables .binary_search_by(|bindless_index_table| { @@ -1689,15 +1637,12 @@ where }) } -impl MaterialBindlessSlab -where - M: Material, -{ +impl MaterialBindlessSlab { /// Creates a new [`MaterialBindlessSlab`] for a material with the given /// bindless descriptor. /// /// We use this when no existing slab could hold a material to be allocated. - fn new(bindless_descriptor: &BindlessDescriptor) -> MaterialBindlessSlab { + fn new(bindless_descriptor: &BindlessDescriptor) -> MaterialBindlessSlab { let mut buffers = HashMap::default(); let mut samplers = HashMap::default(); let mut textures = HashMap::default(); @@ -1780,7 +1725,7 @@ where let bindless_index_tables = bindless_descriptor .index_tables .iter() - .map(|bindless_index_table| MaterialBindlessIndexTable::new(bindless_index_table)) + .map(MaterialBindlessIndexTable::new) .collect(); MaterialBindlessSlab { @@ -1790,7 +1735,6 @@ where textures, buffers, data_buffers, - extra_data: vec![], free_slots: vec![], live_allocation_count: 0, allocated_resource_count: 0, @@ -1822,18 +1766,15 @@ impl FromWorld for FallbackBindlessResources { } } -impl MaterialBindGroupNonBindlessAllocator -where - M: Material, -{ +impl MaterialBindGroupNonBindlessAllocator { /// Creates a new [`MaterialBindGroupNonBindlessAllocator`] managing the /// bind groups for a single non-bindless material. - fn new() -> MaterialBindGroupNonBindlessAllocator { + fn new(label: Option<&'static str>) -> MaterialBindGroupNonBindlessAllocator { MaterialBindGroupNonBindlessAllocator { + label, bind_groups: vec![], to_prepare: HashSet::default(), free_indices: vec![], - phantom: PhantomData, } } @@ -1842,10 +1783,7 @@ where /// /// The returned [`MaterialBindingId`] can later be used to fetch the bind /// group. - fn allocate( - &mut self, - bind_group: MaterialNonBindlessAllocatedBindGroup, - ) -> MaterialBindingId { + fn allocate(&mut self, bind_group: MaterialNonBindlessAllocatedBindGroup) -> MaterialBindingId { let group_id = self .free_indices .pop() @@ -1874,7 +1812,7 @@ where /// [`MaterialBindingId`]. fn allocate_unprepared( &mut self, - unprepared_bind_group: UnpreparedBindGroup, + unprepared_bind_group: UnpreparedBindGroup, bind_group_layout: BindGroupLayout, ) -> MaterialBindingId { self.allocate(MaterialNonBindlessAllocatedBindGroup::Unprepared { @@ -1885,10 +1823,7 @@ where /// Inserts an prepared bind group into this allocator and returns a /// [`MaterialBindingId`]. - fn allocate_prepared( - &mut self, - prepared_bind_group: PreparedBindGroup, - ) -> MaterialBindingId { + fn allocate_prepared(&mut self, prepared_bind_group: PreparedBindGroup) -> MaterialBindingId { self.allocate(MaterialNonBindlessAllocatedBindGroup::Prepared { bind_group: prepared_bind_group, uniform_buffers: vec![], @@ -1905,15 +1840,15 @@ where } /// Returns a wrapper around the bind group with the given index. - fn get(&self, group: MaterialBindGroupIndex) -> Option> { + fn get(&self, group: MaterialBindGroupIndex) -> Option { self.bind_groups[group.0 as usize] .as_ref() .map(|bind_group| match bind_group { MaterialNonBindlessAllocatedBindGroup::Prepared { bind_group, .. } => { MaterialNonBindlessSlab::Prepared(bind_group) } - MaterialNonBindlessAllocatedBindGroup::Unprepared { bind_group, .. } => { - MaterialNonBindlessSlab::Unprepared(bind_group) + MaterialNonBindlessAllocatedBindGroup::Unprepared { .. } => { + MaterialNonBindlessSlab::Unprepared } }) } @@ -1972,7 +1907,7 @@ where // Create the bind group. let bind_group = render_device.create_bind_group( - M::label(), + self.label, &bind_group_layout, &bind_group_entries, ); @@ -1982,7 +1917,6 @@ where bind_group: PreparedBindGroup { bindings: unprepared_bind_group.bindings, bind_group, - data: unprepared_bind_group.data, }, uniform_buffers, }); @@ -1990,28 +1924,7 @@ where } } -impl<'a, M> MaterialSlab<'a, M> -where - M: Material, -{ - /// Returns the extra data associated with this material. - /// - /// When deriving `AsBindGroup`, this data is given by the - /// `#[bind_group_data(DataType)]` attribute on the material structure. - pub fn get_extra_data(&self, slot: MaterialBindGroupSlot) -> &M::Data { - match self.0 { - MaterialSlabImpl::Bindless(material_bindless_slab) => { - material_bindless_slab.get_extra_data(slot) - } - MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Prepared( - prepared_bind_group, - )) => &prepared_bind_group.data, - MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Unprepared( - unprepared_bind_group, - )) => &unprepared_bind_group.data, - } - } - +impl<'a> MaterialSlab<'a> { /// Returns the [`BindGroup`] corresponding to this slab, if it's been /// prepared. /// @@ -2026,7 +1939,7 @@ where MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Prepared( prepared_bind_group, )) => Some(&prepared_bind_group.bind_group), - MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Unprepared(_)) => None, + MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Unprepared) => None, } } } diff --git a/crates/bevy_pbr/src/meshlet/instance_manager.rs b/crates/bevy_pbr/src/meshlet/instance_manager.rs index 661d4791aeac7..6aa5f226658fd 100644 --- a/crates/bevy_pbr/src/meshlet/instance_manager.rs +++ b/crates/bevy_pbr/src/meshlet/instance_manager.rs @@ -1,8 +1,8 @@ use super::{meshlet_mesh_manager::MeshletMeshManager, MeshletMesh, MeshletMesh3d}; use crate::{ - material::DUMMY_MESH_MATERIAL, Material, MaterialBindingId, MeshFlags, MeshTransforms, - MeshUniform, NotShadowCaster, NotShadowReceiver, PreviousGlobalTransform, - RenderMaterialBindings, RenderMaterialInstances, + material::DUMMY_MESH_MATERIAL, MaterialBindingId, MeshFlags, MeshTransforms, MeshUniform, + NotShadowCaster, NotShadowReceiver, PreviousGlobalTransform, RenderMaterialBindings, + RenderMaterialInstances, }; use bevy_asset::{AssetEvent, AssetServer, Assets, UntypedAssetId}; use bevy_ecs::{ @@ -40,9 +40,9 @@ pub struct InstanceManager { /// Per-view per-instance visibility bit. Used for [`RenderLayers`] and [`NotShadowCaster`] support. pub view_instance_visibility: EntityHashMap>>, - /// Next material ID available for a [`Material`]. + /// Next material ID available. next_material_id: u32, - /// Map of [`Material`] to material ID. + /// Map of material asset to material ID. material_id_lookup: HashMap, /// Set of material IDs used in the scene. material_ids_present_in_scene: HashSet, @@ -272,7 +272,7 @@ pub fn extract_meshlet_mesh_entities( /// For each entity in the scene, record what material ID its material was assigned in the `prepare_material_meshlet_meshes` systems, /// and note that the material is used by at least one entity in the scene. -pub fn queue_material_meshlet_meshes( +pub fn queue_material_meshlet_meshes( mut instance_manager: ResMut, render_material_instances: Res, ) { @@ -280,16 +280,14 @@ pub fn queue_material_meshlet_meshes( for (i, (instance, _, _)) in instance_manager.instances.iter().enumerate() { if let Some(material_instance) = render_material_instances.instances.get(instance) { - if let Ok(material_asset_id) = material_instance.asset_id.try_typed::() { - if let Some(material_id) = instance_manager - .material_id_lookup - .get(&material_asset_id.untyped()) - { - instance_manager - .material_ids_present_in_scene - .insert(*material_id); - instance_manager.instance_material_ids.get_mut()[i] = *material_id; - } + if let Some(material_id) = instance_manager + .material_id_lookup + .get(&material_instance.asset_id) + { + instance_manager + .material_ids_present_in_scene + .insert(*material_id); + instance_manager.instance_material_ids.get_mut()[i] = *material_id; } } } diff --git a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs index 90d35d05142a2..edb487bbb07e5 100644 --- a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs +++ b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs @@ -2,11 +2,7 @@ use super::{ instance_manager::InstanceManager, resource_manager::ResourceManager, MESHLET_MESH_MATERIAL_SHADER_HANDLE, }; -use crate::{ - environment_map::EnvironmentMapLight, irradiance_volume::IrradianceVolume, - material_bind_groups::MaterialBindGroupAllocator, *, -}; -use bevy_asset::AssetServer; +use crate::{environment_map::EnvironmentMapLight, irradiance_volume::IrradianceVolume, *}; use bevy_core_pipeline::{ core_3d::Camera3d, prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, @@ -14,14 +10,13 @@ use bevy_core_pipeline::{ }; use bevy_derive::{Deref, DerefMut}; use bevy_platform::collections::{HashMap, HashSet}; +use bevy_render::erased_render_asset::ErasedRenderAssets; use bevy_render::{ camera::TemporalJitter, mesh::{Mesh, MeshVertexBufferLayout, MeshVertexBufferLayoutRef, MeshVertexBufferLayouts}, - render_asset::RenderAssets, render_resource::*, view::ExtractedView, }; -use core::hash::Hash; /// A list of `(Material ID, Pipeline, BindGroup)` for a view for use in [`super::MeshletMainOpaquePass3dNode`]. #[derive(Component, Deref, DerefMut, Default)] @@ -29,17 +24,16 @@ pub struct MeshletViewMaterialsMainOpaquePass(pub Vec<(u32, CachedRenderPipeline /// Prepare [`Material`] pipelines for [`super::MeshletMesh`] entities for use in [`super::MeshletMainOpaquePass3dNode`], /// and register the material with [`InstanceManager`]. -pub fn prepare_material_meshlet_meshes_main_opaque_pass( +pub fn prepare_material_meshlet_meshes_main_opaque_pass( resource_manager: ResMut, mut instance_manager: ResMut, mut cache: Local>, pipeline_cache: Res, - material_pipeline: Res>, + material_pipeline: Res, mesh_pipeline: Res, - render_materials: Res>>, + render_materials: Res>, render_material_instances: Res, - material_bind_group_allocator: Res>, - asset_server: Res, + material_bind_group_allocators: Res, mut mesh_vertex_buffer_layouts: ResMut, mut views: Query< ( @@ -62,9 +56,7 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( ), With, >, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ +) { let fake_vertex_buffer_layout = &fake_vertex_buffer_layout(&mut mesh_vertex_buffer_layouts); for ( @@ -151,17 +143,12 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( for material_id in render_material_instances .instances .values() - .flat_map(|instance| instance.asset_id.try_typed::().ok()) + .map(|instance| instance.asset_id) .collect::>() { let Some(material) = render_materials.get(material_id) else { continue; }; - let Some(material_bind_group) = - material_bind_group_allocator.get(material.binding.group) - else { - continue; - }; if material.properties.render_method != OpaqueRendererMethod::Forward || material.properties.alpha_mode != AlphaMode::Opaque @@ -170,15 +157,18 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( continue; } - let Ok(material_pipeline_descriptor) = material_pipeline.specialize( - MaterialPipelineKey { - mesh_key: view_key, - bind_group_data: material_bind_group - .get_extra_data(material.binding.slot) - .clone(), - }, - fake_vertex_buffer_layout, - ) else { + let erased_key = ErasedMaterialPipelineKey { + mesh_key: view_key, + material_key: material.properties.material_key.clone(), + type_id: material_id.type_id(), + }; + let material_pipeline_specializer = MaterialPipelineSpecializer { + pipeline: material_pipeline.clone(), + properties: material.properties.clone(), + }; + let Ok(material_pipeline_descriptor) = + material_pipeline_specializer.specialize(erased_key, fake_vertex_buffer_layout) + else { continue; }; let material_fragment = material_pipeline_descriptor.fragment.unwrap(); @@ -191,7 +181,12 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( layout.main_layout.clone(), layout.binding_array_layout.clone(), resource_manager.material_shade_bind_group_layout.clone(), - material_pipeline.material_layout.clone(), + material + .properties + .material_layout + .as_ref() + .unwrap() + .clone(), ]; let pipeline_descriptor = RenderPipelineDescriptor { @@ -214,10 +209,9 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( }), multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: match M::meshlet_mesh_fragment_shader() { - ShaderRef::Default => MESHLET_MESH_MATERIAL_SHADER_HANDLE, - ShaderRef::Handle(handle) => handle, - ShaderRef::Path(path) => asset_server.load(path), + shader: match material.properties.get_shader(MeshletFragmentShader) { + Some(shader) => shader.clone(), + None => MESHLET_MESH_MATERIAL_SHADER_HANDLE, }, shader_defs, entry_point: material_fragment.entry_point, @@ -225,8 +219,12 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( }), zero_initialize_workgroup_memory: false, }; - - let material_id = instance_manager.get_material_id(material_id.untyped()); + let Some(material_bind_group_allocator) = + material_bind_group_allocators.get(&material_id.type_id()) + else { + continue; + }; + let material_id = instance_manager.get_material_id(material_id); let pipeline_id = *cache.entry(view_key).or_insert_with(|| { pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone()) @@ -258,17 +256,16 @@ pub struct MeshletViewMaterialsDeferredGBufferPrepass( /// Prepare [`Material`] pipelines for [`super::MeshletMesh`] entities for use in [`super::MeshletPrepassNode`], /// and [`super::MeshletDeferredGBufferPrepassNode`] and register the material with [`InstanceManager`]. -pub fn prepare_material_meshlet_meshes_prepass( +pub fn prepare_material_meshlet_meshes_prepass( resource_manager: ResMut, mut instance_manager: ResMut, mut cache: Local>, pipeline_cache: Res, - prepass_pipeline: Res>, - render_materials: Res>>, + prepass_pipeline: Res, + material_bind_group_allocators: Res, + render_materials: Res>, render_material_instances: Res, mut mesh_vertex_buffer_layouts: ResMut, - material_bind_group_allocator: Res>, - asset_server: Res, mut views: Query< ( &mut MeshletViewMaterialsPrepass, @@ -278,9 +275,7 @@ pub fn prepare_material_meshlet_meshes_prepass( ), With, >, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ +) { let fake_vertex_buffer_layout = &fake_vertex_buffer_layout(&mut mesh_vertex_buffer_layouts); for ( @@ -305,14 +300,14 @@ pub fn prepare_material_meshlet_meshes_prepass( for material_id in render_material_instances .instances .values() - .flat_map(|instance| instance.asset_id.try_typed::().ok()) + .map(|instance| instance.asset_id) .collect::>() { let Some(material) = render_materials.get(material_id) else { continue; }; - let Some(material_bind_group) = - material_bind_group_allocator.get(material.binding.group) + let Some(material_bind_group_allocator) = + material_bind_group_allocators.get(&material_id.type_id()) else { continue; }; @@ -333,15 +328,18 @@ pub fn prepare_material_meshlet_meshes_prepass( continue; } - let Ok(material_pipeline_descriptor) = prepass_pipeline.specialize( - MaterialPipelineKey { - mesh_key: view_key, - bind_group_data: material_bind_group - .get_extra_data(material.binding.slot) - .clone(), - }, - fake_vertex_buffer_layout, - ) else { + let erased_key = ErasedMaterialPipelineKey { + mesh_key: view_key, + material_key: material.properties.material_key.clone(), + type_id: material_id.type_id(), + }; + let material_pipeline_specializer = PrepassPipelineSpecializer { + pipeline: prepass_pipeline.clone(), + properties: material.properties.clone(), + }; + let Ok(material_pipeline_descriptor) = + material_pipeline_specializer.specialize(erased_key, fake_vertex_buffer_layout) + else { continue; }; let material_fragment = material_pipeline_descriptor.fragment.unwrap(); @@ -359,13 +357,19 @@ pub fn prepare_material_meshlet_meshes_prepass( }; let fragment_shader = if view_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { - M::meshlet_mesh_deferred_fragment_shader() + material + .properties + .get_shader(MeshletDeferredFragmentShader) + .unwrap_or(MESHLET_MESH_MATERIAL_SHADER_HANDLE) } else { - M::meshlet_mesh_prepass_fragment_shader() + material + .properties + .get_shader(MeshletPrepassFragmentShader) + .unwrap_or(MESHLET_MESH_MATERIAL_SHADER_HANDLE) }; - let entry_point = match fragment_shader { - ShaderRef::Default => "prepass_fragment".into(), + let entry_point = match &fragment_shader { + x if x == &MESHLET_MESH_MATERIAL_SHADER_HANDLE => "prepass_fragment".into(), _ => material_fragment.entry_point, }; @@ -374,7 +378,12 @@ pub fn prepare_material_meshlet_meshes_prepass( layout: vec![ view_layout, resource_manager.material_shade_bind_group_layout.clone(), - prepass_pipeline.internal.material_layout.clone(), + material + .properties + .material_layout + .as_ref() + .unwrap() + .clone(), ], push_constant_ranges: vec![], vertex: VertexState { @@ -393,11 +402,7 @@ pub fn prepare_material_meshlet_meshes_prepass( }), multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: match fragment_shader { - ShaderRef::Default => MESHLET_MESH_MATERIAL_SHADER_HANDLE, - ShaderRef::Handle(handle) => handle, - ShaderRef::Path(path) => asset_server.load(path), - }, + shader: fragment_shader, shader_defs, entry_point, targets: material_fragment.targets, @@ -405,7 +410,7 @@ pub fn prepare_material_meshlet_meshes_prepass( zero_initialize_workgroup_memory: false, }; - let material_id = instance_manager.get_material_id(material_id.untyped()); + let material_id = instance_manager.get_material_id(material_id); let pipeline_id = *cache.entry(view_key).or_insert_with(|| { pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone()) diff --git a/crates/bevy_pbr/src/meshlet/mod.rs b/crates/bevy_pbr/src/meshlet/mod.rs index 2375894613e52..60cf680ac0879 100644 --- a/crates/bevy_pbr/src/meshlet/mod.rs +++ b/crates/bevy_pbr/src/meshlet/mod.rs @@ -283,6 +283,10 @@ impl Plugin for MeshletPlugin { .in_set(RenderSystems::ManageViews), prepare_meshlet_per_frame_resources.in_set(RenderSystems::PrepareResources), prepare_meshlet_view_bind_groups.in_set(RenderSystems::PrepareBindGroups), + queue_material_meshlet_meshes.in_set(RenderSystems::QueueMeshes), + prepare_material_meshlet_meshes_main_opaque_pass + .in_set(RenderSystems::QueueMeshes) + .before(queue_material_meshlet_meshes), ), ); } diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index cbd8445483a7f..0207a81ed06ef 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -1185,7 +1185,8 @@ impl AsBindGroupShaderType for StandardMaterial { bitflags! { /// The pipeline key for `StandardMaterial`, packed into 64 bits. - #[derive(Clone, Copy, PartialEq, Eq, Hash)] + #[repr(C)] + #[derive(Clone, Copy, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable)] pub struct StandardMaterialKey: u64 { const CULL_FRONT = 0x000001; const CULL_BACK = 0x000002; @@ -1404,7 +1405,7 @@ impl Material for StandardMaterial { } fn specialize( - _pipeline: &MaterialPipeline, + _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, _layout: &MeshVertexBufferLayoutRef, key: MaterialPipelineKey, diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 8732f92e8277e..13b44edbdd56c 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -2,12 +2,13 @@ mod prepass_bindings; use crate::{ alpha_mode_pipeline_key, binding_arrays_are_usable, buffer_layout, - collect_meshes_for_gpu_building, material_bind_groups::MaterialBindGroupAllocator, - queue_material_meshes, set_mesh_motion_vector_flags, setup_morph_and_skinning_defs, skin, - DrawMesh, EntitySpecializationTicks, Material, MaterialPipeline, MaterialPipelineKey, + collect_meshes_for_gpu_building, set_mesh_motion_vector_flags, setup_morph_and_skinning_defs, + skin, DeferredDrawFunction, DeferredFragmentShader, DeferredVertexShader, DrawMesh, + EntitySpecializationTicks, ErasedMaterialPipelineKey, MaterialPipeline, MaterialProperties, MeshLayouts, MeshPipeline, MeshPipelineKey, OpaqueRendererMethod, PreparedMaterial, - RenderLightmaps, RenderMaterialInstances, RenderMeshInstanceFlags, RenderMeshInstances, - RenderPhaseType, SetMaterialBindGroup, SetMeshBindGroup, ShadowView, StandardMaterial, + PrepassDrawFunction, PrepassFragmentShader, PrepassVertexShader, RenderLightmaps, + RenderMaterialInstances, RenderMeshInstanceFlags, RenderMeshInstances, RenderPhaseType, + SetMaterialBindGroup, SetMeshBindGroup, ShadowView, }; use bevy_app::{App, Plugin, PreUpdate}; use bevy_render::{ @@ -24,7 +25,7 @@ use bevy_render::{ }; pub use prepass_bindings::*; -use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::CORE_3D_DEPTH_FORMAT, deferred::*, prelude::Camera3d, prepass::*, }; @@ -55,30 +56,22 @@ use crate::meshlet::{ MeshletMesh3d, }; +use alloc::sync::Arc; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::component::Tick; use bevy_ecs::system::SystemChangeTick; use bevy_platform::collections::HashMap; +use bevy_render::erased_render_asset::ErasedRenderAssets; use bevy_render::sync_world::MainEntityHashMap; use bevy_render::view::RenderVisibleEntities; use bevy_render::RenderSystems::{PrepareAssets, PrepareResources}; -use core::{hash::Hash, marker::PhantomData}; /// Sets up everything required to use the prepass pipeline. /// /// This does not add the actual prepasses, see [`PrepassPlugin`] for that. -pub struct PrepassPipelinePlugin(PhantomData); +pub struct PrepassPipelinePlugin; -impl Default for PrepassPipelinePlugin { - fn default() -> Self { - Self(Default::default()) - } -} - -impl Plugin for PrepassPipelinePlugin -where - M::Data: PartialEq + Eq + Hash + Clone, -{ +impl Plugin for PrepassPipelinePlugin { fn build(&self, app: &mut App) { embedded_asset!(app, "prepass.wgsl"); @@ -93,10 +86,9 @@ where render_app .add_systems( Render, - prepare_prepass_view_bind_group::.in_set(RenderSystems::PrepareBindGroups), + prepare_prepass_view_bind_group.in_set(RenderSystems::PrepareBindGroups), ) - .init_resource::>>() - .allow_ambiguous_resource::>>(); + .init_resource::>(); } fn finish(&self, app: &mut App) { @@ -105,34 +97,27 @@ where }; render_app - .init_resource::>() + .init_resource::() .init_resource::(); } } -/// Sets up the prepasses for a [`Material`]. +/// Sets up the prepasses for a material. /// /// This depends on the [`PrepassPipelinePlugin`]. -pub struct PrepassPlugin { +pub struct PrepassPlugin { /// Debugging flags that can optionally be set when constructing the renderer. pub debug_flags: RenderDebugFlags, - pub phantom: PhantomData, } -impl PrepassPlugin { +impl PrepassPlugin { /// Creates a new [`PrepassPlugin`] with the given debug flags. pub fn new(debug_flags: RenderDebugFlags) -> Self { - PrepassPlugin { - debug_flags, - phantom: PhantomData, - } + PrepassPlugin { debug_flags } } } -impl Plugin for PrepassPlugin -where - M::Data: PartialEq + Eq + Hash + Clone, -{ +impl Plugin for PrepassPlugin { fn build(&self, app: &mut App) { let no_prepass_plugin_loaded = app .world() @@ -174,36 +159,30 @@ where render_app .init_resource::() .init_resource::() - .init_resource::>() - .add_render_command::>() - .add_render_command::>() - .add_render_command::>() - .add_render_command::>() + .init_resource::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::() .add_systems( Render, ( check_prepass_views_need_specialization.in_set(PrepareAssets), - specialize_prepass_material_meshes:: + specialize_prepass_material_meshes .in_set(RenderSystems::PrepareMeshes) - .after(prepare_assets::>) .after(prepare_assets::) .after(collect_meshes_for_gpu_building) .after(set_mesh_motion_vector_flags), - queue_prepass_material_meshes:: - .in_set(RenderSystems::QueueMeshes) - .after(prepare_assets::>) - // queue_material_meshes only writes to `material_bind_group_id`, which `queue_prepass_material_meshes` doesn't read - .ambiguous_with(queue_material_meshes::), + queue_prepass_material_meshes.in_set(RenderSystems::QueueMeshes), ), ); #[cfg(feature = "meshlet")] render_app.add_systems( Render, - prepare_material_meshlet_meshes_prepass:: + prepare_material_meshlet_meshes_prepass .in_set(RenderSystems::QueueMeshes) - .after(prepare_assets::>) - .before(queue_material_meshlet_meshes::) + .before(queue_material_meshlet_meshes) .run_if(resource_exists::), ); } @@ -261,24 +240,20 @@ pub fn update_mesh_previous_global_transforms( } } -#[derive(Resource)] -pub struct PrepassPipeline { +#[derive(Resource, Clone)] +pub struct PrepassPipeline { pub internal: PrepassPipelineInternal, - pub material_pipeline: MaterialPipeline, + pub material_pipeline: MaterialPipeline, } /// Internal fields of the `PrepassPipeline` that don't need the generic bound /// This is done as an optimization to not recompile the same code multiple time +#[derive(Clone)] pub struct PrepassPipelineInternal { pub view_layout_motion_vectors: BindGroupLayout, pub view_layout_no_motion_vectors: BindGroupLayout, pub mesh_layouts: MeshLayouts, pub empty_layout: BindGroupLayout, - pub material_layout: BindGroupLayout, - pub prepass_material_vertex_shader: Option>, - pub prepass_material_fragment_shader: Option>, - pub deferred_material_vertex_shader: Option>, - pub deferred_material_fragment_shader: Option>, pub default_prepass_shader: Handle, /// Whether skins will use uniform buffers on account of storage buffers @@ -292,12 +267,10 @@ pub struct PrepassPipelineInternal { pub binding_arrays_are_usable: bool, } -impl FromWorld for PrepassPipeline { +impl FromWorld for PrepassPipeline { fn from_world(world: &mut World) -> Self { let render_device = world.resource::(); let render_adapter = world.resource::(); - let asset_server = world.resource::(); - let visibility_ranges_buffer_binding_type = render_device .get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT); @@ -358,28 +331,7 @@ impl FromWorld for PrepassPipeline { view_layout_motion_vectors, view_layout_no_motion_vectors, mesh_layouts: mesh_pipeline.mesh_layouts.clone(), - prepass_material_vertex_shader: match M::prepass_vertex_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - prepass_material_fragment_shader: match M::prepass_fragment_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - deferred_material_vertex_shader: match M::deferred_vertex_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, - deferred_material_fragment_shader: match M::deferred_fragment_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), - }, default_prepass_shader: load_embedded_asset!(world, "prepass.wgsl"), - material_layout: M::bind_group_layout(render_device), skins_use_uniform_buffers: skin::skins_use_uniform_buffers(render_device), depth_clip_control_supported, binding_arrays_are_usable: binding_arrays_are_usable(render_device, render_adapter), @@ -387,16 +339,18 @@ impl FromWorld for PrepassPipeline { }; PrepassPipeline { internal, - material_pipeline: world.resource::>().clone(), + material_pipeline: world.resource::().clone(), } } } -impl SpecializedMeshPipeline for PrepassPipeline -where - M::Data: PartialEq + Eq + Hash + Clone, -{ - type Key = MaterialPipelineKey; +pub struct PrepassPipelineSpecializer { + pub(crate) pipeline: PrepassPipeline, + pub(crate) properties: Arc, +} + +impl SpecializedMeshPipeline for PrepassPipelineSpecializer { + type Key = ErasedMaterialPipelineKey; fn specialize( &self, @@ -404,17 +358,27 @@ where layout: &MeshVertexBufferLayoutRef, ) -> Result { let mut shader_defs = Vec::new(); - if self.material_pipeline.bindless { + if self.properties.bindless { shader_defs.push("BINDLESS".into()); } - let mut descriptor = self - .internal - .specialize(key.mesh_key, shader_defs, layout)?; + let mut descriptor = self.pipeline.internal.specialize( + key.mesh_key, + shader_defs, + layout, + &self.properties, + )?; // This is a bit risky because it's possible to change something that would // break the prepass but be fine in the main pass. // Since this api is pretty low-level it doesn't matter that much, but it is a potential issue. - M::specialize(&self.material_pipeline, &mut descriptor, layout, key)?; + if let Some(specialize) = self.properties.specialize { + specialize( + &self.pipeline.material_pipeline, + &mut descriptor, + layout, + key, + )?; + } Ok(descriptor) } @@ -426,6 +390,7 @@ impl PrepassPipelineInternal { mesh_key: MeshPipelineKey, shader_defs: Vec, layout: &MeshVertexBufferLayoutRef, + material_properties: &MaterialProperties, ) -> Result { let mut shader_defs = shader_defs; let mut bind_group_layouts = vec![ @@ -445,7 +410,13 @@ impl PrepassPipelineInternal { // NOTE: Eventually, it would be nice to only add this when the shaders are overloaded by the Material. // The main limitation right now is that bind group order is hardcoded in shaders. - bind_group_layouts.push(self.material_layout.clone()); + bind_group_layouts.push( + material_properties + .material_layout + .as_ref() + .unwrap() + .clone(), + ); #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] shader_defs.push("WEBGL2".into()); shader_defs.push("VERTEX_OUTPUT_INSTANCE_INDEX".into()); @@ -581,17 +552,28 @@ impl PrepassPipelineInternal { let fragment_required = !targets.is_empty() || emulate_unclipped_depth || (mesh_key.contains(MeshPipelineKey::MAY_DISCARD) - && self.prepass_material_fragment_shader.is_some()); + && material_properties + .shaders + .get(&PrepassFragmentShader.intern()) + .is_some()); let fragment = fragment_required.then(|| { // Use the fragment shader from the material let frag_shader_handle = if mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { - match self.deferred_material_fragment_shader.clone() { + match material_properties + .shaders + .get(&DeferredFragmentShader.intern()) + .cloned() + { Some(frag_shader_handle) => frag_shader_handle, None => self.default_prepass_shader.clone(), } } else { - match self.prepass_material_fragment_shader.clone() { + match material_properties + .shaders + .get(&PrepassFragmentShader.intern()) + .cloned() + { Some(frag_shader_handle) => frag_shader_handle, None => self.default_prepass_shader.clone(), } @@ -607,13 +589,13 @@ impl PrepassPipelineInternal { // Use the vertex shader from the material if present let vert_shader_handle = if mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { - if let Some(handle) = &self.deferred_material_vertex_shader { - handle.clone() + if let Some(handle) = material_properties.get_shader(DeferredVertexShader) { + handle } else { self.default_prepass_shader.clone() } - } else if let Some(handle) = &self.prepass_material_vertex_shader { - handle.clone() + } else if let Some(handle) = material_properties.get_shader(PrepassVertexShader) { + handle } else { self.default_prepass_shader.clone() }; @@ -736,7 +718,7 @@ pub struct PrepassViewBindGroup { impl FromWorld for PrepassViewBindGroup { fn from_world(world: &mut World) -> Self { - let pipeline = world.resource::>(); + let pipeline = world.resource::(); let render_device = world.resource::(); let empty_bind_group = render_device.create_bind_group( @@ -752,9 +734,9 @@ impl FromWorld for PrepassViewBindGroup { } } -pub fn prepare_prepass_view_bind_group( +pub fn prepare_prepass_view_bind_group( render_device: Res, - prepass_pipeline: Res>, + prepass_pipeline: Res, view_uniforms: Res, globals_buffer: Res, previous_view_uniforms: Res, @@ -792,40 +774,20 @@ pub fn prepare_prepass_view_bind_group( } /// Stores the [`SpecializedPrepassMaterialViewPipelineCache`] for each view. -#[derive(Resource, Deref, DerefMut)] -pub struct SpecializedPrepassMaterialPipelineCache { +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedPrepassMaterialPipelineCache { // view_entity -> view pipeline cache #[deref] - map: HashMap>, - marker: PhantomData, + map: HashMap, } /// Stores the cached render pipeline ID for each entity in a single view, as /// well as the last time it was changed. -#[derive(Deref, DerefMut)] -pub struct SpecializedPrepassMaterialViewPipelineCache { +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedPrepassMaterialViewPipelineCache { // material entity -> (tick, pipeline_id) #[deref] map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, - marker: PhantomData, -} - -impl Default for SpecializedPrepassMaterialPipelineCache { - fn default() -> Self { - Self { - map: HashMap::default(), - marker: PhantomData, - } - } -} - -impl Default for SpecializedPrepassMaterialViewPipelineCache { - fn default() -> Self { - Self { - map: HashMap::default(), - marker: PhantomData, - } - } } #[derive(Resource, Deref, DerefMut, Default, Clone)] @@ -870,14 +832,13 @@ pub fn check_prepass_views_need_specialization( } } -pub fn specialize_prepass_material_meshes( +pub fn specialize_prepass_material_meshes( render_meshes: Res>, - render_materials: Res>>, + render_materials: Res>, render_mesh_instances: Res, render_material_instances: Res, render_lightmaps: Res, render_visibility_ranges: Res, - material_bind_group_allocator: Res>, view_key_cache: Res, views: Query<( &ExtractedView, @@ -906,18 +867,15 @@ pub fn specialize_prepass_material_meshes( view_specialization_ticks, entity_specialization_ticks, ): ( - ResMut>, + ResMut, SystemChangeTick, - Res>, - ResMut>>, + Res, + ResMut>, Res, Res, - Res>, + Res, ), -) where - M: Material, - M::Data: PartialEq + Eq + Hash + Clone, -{ +) { for (extracted_view, visible_entities, msaa, motion_vector_prepass, deferred_prepass) in &views { if !opaque_deferred_render_phases.contains_key(&extracted_view.retained_view_entity) @@ -944,9 +902,6 @@ pub fn specialize_prepass_material_meshes( else { continue; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { - continue; - }; let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) else { continue; @@ -962,13 +917,7 @@ pub fn specialize_prepass_material_meshes( if !needs_specialization { continue; } - let Some(material) = render_materials.get(material_asset_id) else { - continue; - }; - let Some(material_bind_group) = - material_bind_group_allocator.get(material.binding.group) - else { - warn!("Couldn't get bind group for material"); + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { @@ -1045,15 +994,19 @@ pub fn specialize_prepass_material_meshes( } } + let erased_key = ErasedMaterialPipelineKey { + mesh_key, + material_key: material.properties.material_key.clone(), + type_id: material_instance.asset_id.type_id(), + }; + let prepass_pipeline_specializer = PrepassPipelineSpecializer { + pipeline: prepass_pipeline.clone(), + properties: material.properties.clone(), + }; let pipeline_id = pipelines.specialize( &pipeline_cache, - &prepass_pipeline, - MaterialPipelineKey { - mesh_key, - bind_group_data: material_bind_group - .get_extra_data(material.binding.slot) - .clone(), - }, + &prepass_pipeline_specializer, + erased_key, &mesh.layout, ); let pipeline_id = match pipeline_id { @@ -1070,9 +1023,9 @@ pub fn specialize_prepass_material_meshes( } } -pub fn queue_prepass_material_meshes( +pub fn queue_prepass_material_meshes( render_mesh_instances: Res, - render_materials: Res>>, + render_materials: Res>, render_material_instances: Res, mesh_allocator: Res, gpu_preprocessing_support: Res, @@ -1081,10 +1034,8 @@ pub fn queue_prepass_material_meshes( mut opaque_deferred_render_phases: ResMut>, mut alpha_mask_deferred_render_phases: ResMut>, views: Query<(&ExtractedView, &RenderVisibleEntities)>, - specialized_material_pipeline_cache: Res>, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ + specialized_material_pipeline_cache: Res, +) { for (extracted_view, visible_entities) in &views { let ( mut opaque_phase, @@ -1137,14 +1088,11 @@ pub fn queue_prepass_material_meshes( else { continue; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { - continue; - }; let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) else { continue; }; - let Some(material) = render_materials.get(material_asset_id) else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); @@ -1162,7 +1110,7 @@ pub fn queue_prepass_material_meshes( OpaqueNoLightmap3dBatchSetKey { draw_function: material .properties - .deferred_draw_function_id + .get_draw_function(DeferredDrawFunction) .unwrap(), pipeline: *pipeline_id, material_bind_group_index: Some(material.binding.group.0), @@ -1187,7 +1135,7 @@ pub fn queue_prepass_material_meshes( OpaqueNoLightmap3dBatchSetKey { draw_function: material .properties - .prepass_draw_function_id + .get_draw_function(PrepassDrawFunction) .unwrap(), pipeline: *pipeline_id, material_bind_group_index: Some(material.binding.group.0), @@ -1212,7 +1160,10 @@ pub fn queue_prepass_material_meshes( let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); let batch_set_key = OpaqueNoLightmap3dBatchSetKey { - draw_function: material.properties.deferred_draw_function_id.unwrap(), + draw_function: material + .properties + .get_draw_function(DeferredDrawFunction) + .unwrap(), pipeline: *pipeline_id, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), @@ -1236,7 +1187,10 @@ pub fn queue_prepass_material_meshes( let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); let batch_set_key = OpaqueNoLightmap3dBatchSetKey { - draw_function: material.properties.prepass_draw_function_id.unwrap(), + draw_function: material + .properties + .get_draw_function(PrepassDrawFunction) + .unwrap(), pipeline: *pipeline_id, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), @@ -1331,11 +1285,11 @@ impl RenderCommand

for SetPrepassViewEmptyBindG } } -pub type DrawPrepass = ( +pub type DrawPrepass = ( SetItemPipeline, SetPrepassViewBindGroup<0>, SetPrepassViewEmptyBindGroup<1>, SetMeshBindGroup<2>, - SetMaterialBindGroup, + SetMaterialBindGroup<3>, DrawMesh, ); diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 83d28a7da76f5..ddf9d831f0911 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1,5 +1,4 @@ use self::assign::ClusterableObjectType; -use crate::material_bind_groups::MaterialBindGroupAllocator; use crate::*; use bevy_asset::UntypedAssetId; use bevy_color::ColorToComponents; @@ -15,6 +14,7 @@ use bevy_ecs::{ use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_platform::collections::{HashMap, HashSet}; use bevy_platform::hash::FixedHasher; +use bevy_render::erased_render_asset::ErasedRenderAssets; use bevy_render::experimental::occlusion_culling::{ OcclusionCulling, OcclusionCullingSubview, OcclusionCullingSubviewEntities, }; @@ -44,7 +44,7 @@ use bevy_render::{ }; use bevy_transform::{components::GlobalTransform, prelude::Transform}; use bevy_utils::default; -use core::{hash::Hash, marker::PhantomData, ops::Range}; +use core::{hash::Hash, ops::Range}; #[cfg(feature = "trace")] use tracing::info_span; use tracing::{error, warn}; @@ -1717,37 +1717,17 @@ pub struct LightKeyCache(HashMap); #[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] pub struct LightSpecializationTicks(HashMap); -#[derive(Resource, Deref, DerefMut)] -pub struct SpecializedShadowMaterialPipelineCache { +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedShadowMaterialPipelineCache { // view light entity -> view pipeline cache #[deref] - map: HashMap>, - marker: PhantomData, + map: HashMap, } -#[derive(Deref, DerefMut)] -pub struct SpecializedShadowMaterialViewPipelineCache { +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedShadowMaterialViewPipelineCache { #[deref] map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, - marker: PhantomData, -} - -impl Default for SpecializedShadowMaterialPipelineCache { - fn default() -> Self { - Self { - map: HashMap::default(), - marker: PhantomData, - } - } -} - -impl Default for SpecializedShadowMaterialViewPipelineCache { - fn default() -> Self { - Self { - map: MainEntityHashMap::default(), - marker: PhantomData, - } - } } pub fn check_views_lights_need_specialization( @@ -1789,23 +1769,16 @@ pub fn check_views_lights_need_specialization( } } -pub fn specialize_shadows( - prepass_pipeline: Res>, - ( - render_meshes, - render_mesh_instances, - render_materials, - render_material_instances, - material_bind_group_allocator, - ): ( +pub fn specialize_shadows( + prepass_pipeline: Res, + (render_meshes, render_mesh_instances, render_materials, render_material_instances): ( Res>, Res, - Res>>, + Res>, Res, - Res>, ), shadow_render_phases: Res>, - mut pipelines: ResMut>>, + mut pipelines: ResMut>, pipeline_cache: Res, render_lightmaps: Res, view_lights: Query<(Entity, &ViewLightEntities), With>, @@ -1817,13 +1790,11 @@ pub fn specialize_shadows( >, spot_light_entities: Query<&RenderVisibleMeshEntities, With>, light_key_cache: Res, - mut specialized_material_pipeline_cache: ResMut>, + mut specialized_material_pipeline_cache: ResMut, light_specialization_ticks: Res, - entity_specialization_ticks: Res>, + entity_specialization_ticks: Res, ticks: SystemChangeTick, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ +) { // Record the retained IDs of all shadow views so that we can expire old // pipeline IDs. let mut all_shadow_views: HashSet = HashSet::default(); @@ -1881,14 +1852,12 @@ pub fn specialize_shadows( .or_default(); for (_, visible_entity) in visible_entities.iter().copied() { - let Some(material_instances) = + let Some(material_instance) = render_material_instances.instances.get(&visible_entity) else { continue; }; - let Ok(material_asset_id) = material_instances.asset_id.try_typed::() else { - continue; - }; + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(visible_entity) else { @@ -1905,7 +1874,7 @@ pub fn specialize_shadows( if !needs_specialization { continue; } - let Some(material) = render_materials.get(material_asset_id) else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; if !mesh_instance @@ -1914,11 +1883,6 @@ pub fn specialize_shadows( { continue; } - let Some(material_bind_group) = - material_bind_group_allocator.get(material.binding.group) - else { - continue; - }; let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { continue; }; @@ -1946,18 +1910,21 @@ pub fn specialize_shadows( | AlphaMode::AlphaToCoverage => MeshPipelineKey::MAY_DISCARD, _ => MeshPipelineKey::NONE, }; + let erased_key = ErasedMaterialPipelineKey { + mesh_key, + material_key: material.properties.material_key.clone(), + type_id: material_instance.asset_id.type_id(), + }; + let material_pipeline_specializer = PrepassPipelineSpecializer { + pipeline: prepass_pipeline.clone(), + properties: material.properties.clone(), + }; let pipeline_id = pipelines.specialize( &pipeline_cache, - &prepass_pipeline, - MaterialPipelineKey { - mesh_key, - bind_group_data: material_bind_group - .get_extra_data(material.binding.slot) - .clone(), - }, + &material_pipeline_specializer, + erased_key, &mesh.layout, ); - let pipeline_id = match pipeline_id { Ok(id) => id, Err(err) => { @@ -1979,10 +1946,9 @@ pub fn specialize_shadows( /// For each shadow cascade, iterates over all the meshes "visible" from it and /// adds them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as /// appropriate. -pub fn queue_shadows( - shadow_draw_functions: Res>, +pub fn queue_shadows( render_mesh_instances: Res, - render_materials: Res>>, + render_materials: Res>, render_material_instances: Res, mut shadow_render_phases: ResMut>, gpu_preprocessing_support: Res, @@ -1995,11 +1961,8 @@ pub fn queue_shadows( With, >, spot_light_entities: Query<&RenderVisibleMeshEntities, With>, - specialized_material_pipeline_cache: Res>, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ - let draw_shadow_mesh = shadow_draw_functions.read().id::>(); + specialized_material_pipeline_cache: Res, +) { for (entity, view_lights) in &view_lights { for view_light_entity in view_lights.lights.iter().copied() { let Ok((light_entity, extracted_view_light)) = @@ -2070,10 +2033,12 @@ pub fn queue_shadows( else { continue; }; - let Ok(material_asset_id) = material_instance.asset_id.try_typed::() else { + let Some(material) = render_materials.get(material_instance.asset_id) else { continue; }; - let Some(material) = render_materials.get(material_asset_id) else { + let Some(draw_function) = + material.properties.get_draw_function(ShadowsDrawFunction) + else { continue; }; @@ -2082,7 +2047,7 @@ pub fn queue_shadows( let batch_set_key = ShadowBatchSetKey { pipeline: *pipeline_id, - draw_function: draw_shadow_mesh, + draw_function, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), index_slab, diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 50d7e98a48011..fa30a2e5a8218 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -2039,7 +2039,7 @@ impl GetFullBatchData for MeshPipeline { } bitflags::bitflags! { - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)] #[repr(transparent)] // NOTE: Apparently quadro drivers support up to 64x MSAA. /// MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA. diff --git a/crates/bevy_render/macros/src/as_bind_group.rs b/crates/bevy_render/macros/src/as_bind_group.rs index 7bac6796acbc8..b426088e22794 100644 --- a/crates/bevy_render/macros/src/as_bind_group.rs +++ b/crates/bevy_render/macros/src/as_bind_group.rs @@ -1061,17 +1061,21 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { render_device: &#render_path::renderer::RenderDevice, (images, fallback_image, storage_buffers): &mut #ecs_path::system::SystemParamItem<'_, '_, Self::Param>, force_no_bindless: bool, - ) -> Result<#render_path::render_resource::UnpreparedBindGroup, #render_path::render_resource::AsBindGroupError> { + ) -> Result<#render_path::render_resource::UnpreparedBindGroup, #render_path::render_resource::AsBindGroupError> { #uniform_binding_type_declarations let bindings = #render_path::render_resource::BindingResources(vec![#(#binding_impls,)*]); Ok(#render_path::render_resource::UnpreparedBindGroup { bindings, - data: #get_prepared_data, }) } + #[allow(clippy::unused_unit)] + fn bind_group_data(&self) -> Self::Data { + #get_prepared_data + } + fn bind_group_layout_entries( render_device: &#render_path::renderer::RenderDevice, force_no_bindless: bool diff --git a/crates/bevy_render/macros/src/lib.rs b/crates/bevy_render/macros/src/lib.rs index 75cbdfa959b3a..c58a8fd146bf4 100644 --- a/crates/bevy_render/macros/src/lib.rs +++ b/crates/bevy_render/macros/src/lib.rs @@ -101,3 +101,29 @@ pub fn derive_render_sub_graph(input: TokenStream) -> TokenStream { .push(format_ident!("RenderSubGraph").into()); derive_label(input, "RenderSubGraph", &trait_path) } + +#[proc_macro_derive(ShaderLabel)] +pub fn derive_shader_label(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let mut trait_path = bevy_render_path(); + trait_path + .segments + .push(format_ident!("render_phase").into()); + trait_path + .segments + .push(format_ident!("ShaderLabel").into()); + derive_label(input, "ShaderLabel", &trait_path) +} + +#[proc_macro_derive(DrawFunctionLabel)] +pub fn derive_draw_function_label(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let mut trait_path = bevy_render_path(); + trait_path + .segments + .push(format_ident!("render_phase").into()); + trait_path + .segments + .push(format_ident!("DrawFunctionLabel").into()); + derive_label(input, "DrawFunctionLabel", &trait_path) +} diff --git a/crates/bevy_render/src/erased_render_asset.rs b/crates/bevy_render/src/erased_render_asset.rs new file mode 100644 index 0000000000000..ac2423990b8c7 --- /dev/null +++ b/crates/bevy_render/src/erased_render_asset.rs @@ -0,0 +1,431 @@ +use crate::{ + render_resource::AsBindGroupError, ExtractSchedule, MainWorld, Render, RenderApp, + RenderSystems, Res, +}; +use bevy_app::{App, Plugin, SubApp}; +pub use bevy_asset::RenderAssetUsages; +use bevy_asset::{Asset, AssetEvent, AssetId, Assets, UntypedAssetId}; +use bevy_ecs::{ + prelude::{Commands, EventReader, IntoScheduleConfigs, ResMut, Resource}, + schedule::{ScheduleConfigs, SystemSet}, + system::{ScheduleSystem, StaticSystemParam, SystemParam, SystemParamItem, SystemState}, + world::{FromWorld, Mut}, +}; +use bevy_platform::collections::{HashMap, HashSet}; +use bevy_render::render_asset::RenderAssetBytesPerFrameLimiter; +use core::marker::PhantomData; +use thiserror::Error; +use tracing::{debug, error}; + +#[derive(Debug, Error)] +pub enum PrepareAssetError { + #[error("Failed to prepare asset")] + RetryNextUpdate(E), + #[error("Failed to build bind group: {0}")] + AsBindGroupError(AsBindGroupError), +} + +/// The system set during which we extract modified assets to the render world. +#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)] +pub struct AssetExtractionSystems; + +/// Deprecated alias for [`AssetExtractionSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `AssetExtractionSystems`.")] +pub type ExtractAssetsSet = AssetExtractionSystems; + +/// Describes how an asset gets extracted and prepared for rendering. +/// +/// In the [`ExtractSchedule`] step the [`ErasedRenderAsset::SourceAsset`] is transferred +/// from the "main world" into the "render world". +/// +/// After that in the [`RenderSystems::PrepareAssets`] step the extracted asset +/// is transformed into its GPU-representation of type [`ErasedRenderAsset`]. +pub trait ErasedRenderAsset: Send + Sync + 'static { + /// The representation of the asset in the "main world". + type SourceAsset: Asset + Clone; + /// The target representation of the asset in the "render world". + type ErasedAsset: Send + Sync + 'static + Sized; + + /// Specifies all ECS data required by [`ErasedRenderAsset::prepare_asset`]. + /// + /// For convenience use the [`lifetimeless`](bevy_ecs::system::lifetimeless) [`SystemParam`]. + type Param: SystemParam; + + /// Whether or not to unload the asset after extracting it to the render world. + #[inline] + fn asset_usage(_source_asset: &Self::SourceAsset) -> RenderAssetUsages { + RenderAssetUsages::default() + } + + /// Size of the data the asset will upload to the gpu. Specifying a return value + /// will allow the asset to be throttled via [`RenderAssetBytesPerFrameLimiter`]. + #[inline] + #[expect( + unused_variables, + reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." + )] + fn byte_len(erased_asset: &Self::SourceAsset) -> Option { + None + } + + /// Prepares the [`ErasedRenderAsset::SourceAsset`] for the GPU by transforming it into a [`ErasedRenderAsset`]. + /// + /// ECS data may be accessed via `param`. + fn prepare_asset( + source_asset: Self::SourceAsset, + asset_id: AssetId, + param: &mut SystemParamItem, + ) -> Result>; + + /// Called whenever the [`ErasedRenderAsset::SourceAsset`] has been removed. + /// + /// You can implement this method if you need to access ECS data (via + /// `_param`) in order to perform cleanup tasks when the asset is removed. + /// + /// The default implementation does nothing. + fn unload_asset( + _source_asset: AssetId, + _param: &mut SystemParamItem, + ) { + } +} + +/// This plugin extracts the changed assets from the "app world" into the "render world" +/// and prepares them for the GPU. They can then be accessed from the [`ErasedRenderAssets`] resource. +/// +/// Therefore it sets up the [`ExtractSchedule`] and +/// [`RenderSystems::PrepareAssets`] steps for the specified [`ErasedRenderAsset`]. +/// +/// The `AFTER` generic parameter can be used to specify that `A::prepare_asset` should not be run until +/// `prepare_assets::` has completed. This allows the `prepare_asset` function to depend on another +/// prepared [`ErasedRenderAsset`], for example `Mesh::prepare_asset` relies on `ErasedRenderAssets::` for morph +/// targets, so the plugin is created as `ErasedRenderAssetPlugin::::default()`. +pub struct ErasedRenderAssetPlugin< + A: ErasedRenderAsset, + AFTER: ErasedRenderAssetDependency + 'static = (), +> { + phantom: PhantomData (A, AFTER)>, +} + +impl Default + for ErasedRenderAssetPlugin +{ + fn default() -> Self { + Self { + phantom: Default::default(), + } + } +} + +impl Plugin + for ErasedRenderAssetPlugin +{ + fn build(&self, app: &mut App) { + app.init_resource::>(); + } + + fn finish(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::>() + .init_resource::>() + .init_resource::>() + .add_systems( + ExtractSchedule, + extract_erased_render_asset::.in_set(AssetExtractionSystems), + ); + AFTER::register_system( + render_app, + prepare_erased_assets::.in_set(RenderSystems::PrepareAssets), + ); + } + } +} + +// helper to allow specifying dependencies between render assets +pub trait ErasedRenderAssetDependency { + fn register_system(render_app: &mut SubApp, system: ScheduleConfigs); +} + +impl ErasedRenderAssetDependency for () { + fn register_system(render_app: &mut SubApp, system: ScheduleConfigs) { + render_app.add_systems(Render, system); + } +} + +impl ErasedRenderAssetDependency for A { + fn register_system(render_app: &mut SubApp, system: ScheduleConfigs) { + render_app.add_systems(Render, system.after(prepare_erased_assets::)); + } +} + +/// Temporarily stores the extracted and removed assets of the current frame. +#[derive(Resource)] +pub struct ExtractedAssets { + /// The assets extracted this frame. + /// + /// These are assets that were either added or modified this frame. + pub extracted: Vec<(AssetId, A::SourceAsset)>, + + /// IDs of the assets that were removed this frame. + /// + /// These assets will not be present in [`ExtractedAssets::extracted`]. + pub removed: HashSet>, + + /// IDs of the assets that were modified this frame. + pub modified: HashSet>, + + /// IDs of the assets that were added this frame. + pub added: HashSet>, +} + +impl Default for ExtractedAssets { + fn default() -> Self { + Self { + extracted: Default::default(), + removed: Default::default(), + modified: Default::default(), + added: Default::default(), + } + } +} + +/// Stores all GPU representations ([`ErasedRenderAsset`]) +/// of [`ErasedRenderAsset::SourceAsset`] as long as they exist. +#[derive(Resource)] +pub struct ErasedRenderAssets(HashMap); + +impl Default for ErasedRenderAssets { + fn default() -> Self { + Self(Default::default()) + } +} + +impl ErasedRenderAssets { + pub fn get(&self, id: impl Into) -> Option<&ERA> { + self.0.get(&id.into()) + } + + pub fn get_mut(&mut self, id: impl Into) -> Option<&mut ERA> { + self.0.get_mut(&id.into()) + } + + pub fn insert(&mut self, id: impl Into, value: ERA) -> Option { + self.0.insert(id.into(), value) + } + + pub fn remove(&mut self, id: impl Into) -> Option { + self.0.remove(&id.into()) + } + + pub fn iter(&self) -> impl Iterator { + self.0.iter().map(|(k, v)| (*k, v)) + } + + pub fn iter_mut(&mut self) -> impl Iterator { + self.0.iter_mut().map(|(k, v)| (*k, v)) + } +} + +#[derive(Resource)] +struct CachedExtractErasedRenderAssetSystemState { + state: SystemState<( + EventReader<'static, 'static, AssetEvent>, + ResMut<'static, Assets>, + )>, +} + +impl FromWorld for CachedExtractErasedRenderAssetSystemState { + fn from_world(world: &mut bevy_ecs::world::World) -> Self { + Self { + state: SystemState::new(world), + } + } +} + +/// This system extracts all created or modified assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type +/// into the "render world". +pub(crate) fn extract_erased_render_asset( + mut commands: Commands, + mut main_world: ResMut, +) { + main_world.resource_scope( + |world, mut cached_state: Mut>| { + let (mut events, mut assets) = cached_state.state.get_mut(world); + + let mut needs_extracting = >::default(); + let mut removed = >::default(); + let mut modified = >::default(); + + for event in events.read() { + #[expect( + clippy::match_same_arms, + reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon." + )] + match event { + AssetEvent::Added { id } => { + needs_extracting.insert(*id); + } + AssetEvent::Modified { id } => { + needs_extracting.insert(*id); + modified.insert(*id); + } + AssetEvent::Removed { .. } => { + // We don't care that the asset was removed from Assets in the main world. + // An asset is only removed from ErasedRenderAssets when its last handle is dropped (AssetEvent::Unused). + } + AssetEvent::Unused { id } => { + needs_extracting.remove(id); + modified.remove(id); + removed.insert(*id); + } + AssetEvent::LoadedWithDependencies { .. } => { + // TODO: handle this + } + } + } + + let mut extracted_assets = Vec::new(); + let mut added = >::default(); + for id in needs_extracting.drain() { + if let Some(asset) = assets.get(id) { + let asset_usage = A::asset_usage(asset); + if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) { + if asset_usage == RenderAssetUsages::RENDER_WORLD { + if let Some(asset) = assets.remove(id) { + extracted_assets.push((id, asset)); + added.insert(id); + } + } else { + extracted_assets.push((id, asset.clone())); + added.insert(id); + } + } + } + } + + commands.insert_resource(ExtractedAssets:: { + extracted: extracted_assets, + removed, + modified, + added, + }); + cached_state.state.apply(world); + }, + ); +} + +// TODO: consider storing inside system? +/// All assets that should be prepared next frame. +#[derive(Resource)] +pub struct PrepareNextFrameAssets { + assets: Vec<(AssetId, A::SourceAsset)>, +} + +impl Default for PrepareNextFrameAssets { + fn default() -> Self { + Self { + assets: Default::default(), + } + } +} + +/// This system prepares all assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type +/// which where extracted this frame for the GPU. +pub fn prepare_erased_assets( + mut extracted_assets: ResMut>, + mut render_assets: ResMut>, + mut prepare_next_frame: ResMut>, + param: StaticSystemParam<::Param>, + bpf: Res, +) { + let mut wrote_asset_count = 0; + + let mut param = param.into_inner(); + let queued_assets = core::mem::take(&mut prepare_next_frame.assets); + for (id, extracted_asset) in queued_assets { + if extracted_assets.removed.contains(&id) || extracted_assets.added.contains(&id) { + // skip previous frame's assets that have been removed or updated + continue; + } + + let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) { + // we could check if available bytes > byte_len here, but we want to make some + // forward progress even if the asset is larger than the max bytes per frame. + // this way we always write at least one (sized) asset per frame. + // in future we could also consider partial asset uploads. + if bpf.exhausted() { + prepare_next_frame.assets.push((id, extracted_asset)); + continue; + } + size + } else { + 0 + }; + + match A::prepare_asset(extracted_asset, id, &mut param) { + Ok(prepared_asset) => { + render_assets.insert(id, prepared_asset); + bpf.write_bytes(write_bytes); + wrote_asset_count += 1; + } + Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { + prepare_next_frame.assets.push((id, extracted_asset)); + } + Err(PrepareAssetError::AsBindGroupError(e)) => { + error!( + "{} Bind group construction failed: {e}", + core::any::type_name::() + ); + } + } + } + + for removed in extracted_assets.removed.drain() { + render_assets.remove(removed); + A::unload_asset(removed, &mut param); + } + + for (id, extracted_asset) in extracted_assets.extracted.drain(..) { + // we remove previous here to ensure that if we are updating the asset then + // any users will not see the old asset after a new asset is extracted, + // even if the new asset is not yet ready or we are out of bytes to write. + render_assets.remove(id); + + let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) { + if bpf.exhausted() { + prepare_next_frame.assets.push((id, extracted_asset)); + continue; + } + size + } else { + 0 + }; + + match A::prepare_asset(extracted_asset, id, &mut param) { + Ok(prepared_asset) => { + render_assets.insert(id, prepared_asset); + bpf.write_bytes(write_bytes); + wrote_asset_count += 1; + } + Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { + prepare_next_frame.assets.push((id, extracted_asset)); + } + Err(PrepareAssetError::AsBindGroupError(e)) => { + error!( + "{} Bind group construction failed: {e}", + core::any::type_name::() + ); + } + } + } + + if bpf.exhausted() && !prepare_next_frame.assets.is_empty() { + debug!( + "{} write budget exhausted with {} assets remaining (wrote {})", + core::any::type_name::(), + prepare_next_frame.assets.len(), + wrote_asset_count + ); + } +} diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index aa1e2da676082..8bf7f41195c53 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -26,6 +26,7 @@ pub mod alpha; pub mod batching; pub mod camera; pub mod diagnostic; +pub mod erased_render_asset; pub mod experimental; pub mod extract_component; pub mod extract_instances; diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index 0a5ad3e4ece41..c35062eb85c40 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -499,14 +499,14 @@ impl RenderAssetBytesPerFrameLimiter { } /// Decreases the available bytes for the current frame. - fn write_bytes(&self, bytes: usize) { + pub(crate) fn write_bytes(&self, bytes: usize) { if self.max_bytes.is_some() && bytes > 0 { self.bytes_written.fetch_add(bytes, Ordering::Relaxed); } } /// Returns `true` if there are no remaining bytes available for writing this frame. - fn exhausted(&self) -> bool { + pub(crate) fn exhausted(&self) -> bool { if let Some(max_bytes) = self.max_bytes { let bytes_written = self.bytes_written.load(Ordering::Relaxed); bytes_written >= max_bytes diff --git a/crates/bevy_render/src/render_phase/mod.rs b/crates/bevy_render/src/render_phase/mod.rs index 272418f67f03c..c58318f654578 100644 --- a/crates/bevy_render/src/render_phase/mod.rs +++ b/crates/bevy_render/src/render_phase/mod.rs @@ -61,7 +61,9 @@ use crate::{ render_resource::{CachedRenderPipelineId, GpuArrayBufferIndex, PipelineCache}, Render, RenderApp, RenderSystems, }; +use bevy_ecs::intern::Interned; use bevy_ecs::{ + define_label, prelude::*, system::{lifetimeless::SRes, SystemParamItem}, }; @@ -69,6 +71,33 @@ use core::{fmt::Debug, hash::Hash, iter, marker::PhantomData, ops::Range, slice: use smallvec::SmallVec; use tracing::warn; +pub use bevy_render_macros::ShaderLabel; + +define_label!( + #[diagnostic::on_unimplemented( + note = "consider annotating `{Self}` with `#[derive(ShaderLabel)]`" + )] + /// Labels used to uniquely identify types of material shaders + ShaderLabel, + SHADER_LABEL_INTERNER +); + +/// A shorthand for `Interned`. +pub type InternedShaderLabel = Interned; + +pub use bevy_render_macros::DrawFunctionLabel; + +define_label!( + #[diagnostic::on_unimplemented( + note = "consider annotating `{Self}` with `#[derive(DrawFunctionLabel)]`" + )] + /// Labels used to uniquely identify types of material shaders + DrawFunctionLabel, + DRAW_FUNCTION_LABEL_INTERNER +); + +pub type InternedDrawFunctionLabel = Interned; + /// Stores the rendering instructions for a single phase that uses bins in all /// views. /// diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index 17de8455dac5d..04b77471795fa 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -481,22 +481,27 @@ impl Deref for BindGroup { /// is_shaded: bool, /// } /// -/// #[derive(Copy, Clone, Hash, Eq, PartialEq)] +/// // Materials keys are intended to be small, cheap to hash, and +/// // uniquely identify a specific material permutation, which +/// // is why they are required to be `bytemuck::Pod` and `bytemuck::Zeroable` +/// // when using the `AsBindGroup` derive macro. +/// #[repr(C)] +/// #[derive(Copy, Clone, Hash, Eq, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] /// struct CoolMaterialKey { -/// is_shaded: bool, +/// is_shaded: u32, /// } /// /// impl From<&CoolMaterial> for CoolMaterialKey { /// fn from(material: &CoolMaterial) -> CoolMaterialKey { /// CoolMaterialKey { -/// is_shaded: material.is_shaded, +/// is_shaded: material.is_shaded as u32, /// } /// } /// } /// ``` pub trait AsBindGroup { /// Data that will be stored alongside the "prepared" bind group. - type Data: Send + Sync; + type Data: bytemuck::Pod + bytemuck::Zeroable + Send + Sync; type Param: SystemParam + 'static; @@ -531,8 +536,8 @@ pub trait AsBindGroup { layout: &BindGroupLayout, render_device: &RenderDevice, param: &mut SystemParamItem<'_, '_, Self::Param>, - ) -> Result, AsBindGroupError> { - let UnpreparedBindGroup { bindings, data } = + ) -> Result { + let UnpreparedBindGroup { bindings } = Self::unprepared_bind_group(self, layout, render_device, param, false)?; let entries = bindings @@ -548,10 +553,11 @@ pub trait AsBindGroup { Ok(PreparedBindGroup { bindings, bind_group, - data, }) } + fn bind_group_data(&self) -> Self::Data; + /// Returns a vec of (binding index, `OwnedBindingResource`). /// /// In cases where `OwnedBindingResource` is not available (as for bindless @@ -569,7 +575,7 @@ pub trait AsBindGroup { render_device: &RenderDevice, param: &mut SystemParamItem<'_, '_, Self::Param>, force_no_bindless: bool, - ) -> Result, AsBindGroupError>; + ) -> Result; /// Creates the bind group layout matching all bind groups returned by /// [`AsBindGroup::as_bind_group`] @@ -613,16 +619,14 @@ pub enum AsBindGroupError { } /// A prepared bind group returned as a result of [`AsBindGroup::as_bind_group`]. -pub struct PreparedBindGroup { +pub struct PreparedBindGroup { pub bindings: BindingResources, pub bind_group: BindGroup, - pub data: T, } /// a map containing `OwnedBindingResource`s, keyed by the target binding index -pub struct UnpreparedBindGroup { +pub struct UnpreparedBindGroup { pub bindings: BindingResources, - pub data: T, } /// A pair of binding index and binding resource, used as part of diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index fa784bd9afcc0..06914690ca127 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -410,7 +410,7 @@ where fn clone(&self) -> Self { Self { mesh_key: self.mesh_key, - bind_group_data: self.bind_group_data.clone(), + bind_group_data: self.bind_group_data, } } } @@ -753,7 +753,7 @@ pub fn specialize_material2d_meshes( &material2d_pipeline, Material2dKey { mesh_key, - bind_group_data: material_2d.key.clone(), + bind_group_data: material_2d.key, }, &mesh.layout, ); @@ -969,6 +969,7 @@ impl RenderAsset for PreparedMaterial2d { ): &mut SystemParamItem, _: Option<&Self>, ) -> Result> { + let bind_group_data = material.bind_group_data(); match material.as_bind_group(&pipeline.material2d_layout, render_device, material_param) { Ok(prepared) => { let mut mesh_pipeline_key_bits = Mesh2dPipelineKey::empty(); @@ -987,7 +988,7 @@ impl RenderAsset for PreparedMaterial2d { Ok(PreparedMaterial2d { bindings: prepared.bindings, bind_group: prepared.bind_group, - key: prepared.data, + key: bind_group_data, properties: Material2dProperties { depth_bias: material.depth_bias(), alpha_mode: material.alpha_mode(), diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 5d2201e6090f3..94de7afd233d7 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -583,11 +583,12 @@ impl RenderAsset for PreparedUiMaterial { (render_device, pipeline, material_param): &mut SystemParamItem, _: Option<&Self>, ) -> Result> { + let bind_group_data = material.bind_group_data(); match material.as_bind_group(&pipeline.ui_layout, render_device, material_param) { Ok(prepared) => Ok(PreparedUiMaterial { bindings: prepared.bindings, bind_group: prepared.bind_group, - key: prepared.data, + key: bind_group_data, }), Err(AsBindGroupError::RetryNextUpdate) => { Err(PrepareAssetError::RetryNextUpdate(material)) @@ -637,7 +638,7 @@ pub fn queue_ui_material_nodes( &ui_material_pipeline, UiMaterialKey { hdr: view.hdr, - bind_group_data: material.key.clone(), + bind_group_data: material.key, }, ); if transparent_phase.items.capacity() < extracted_uinodes.uinodes.len() { diff --git a/crates/bevy_ui/src/ui_material.rs b/crates/bevy_ui/src/ui_material.rs index 9f56e834a4c73..89a8df948cb3e 100644 --- a/crates/bevy_ui/src/ui_material.rs +++ b/crates/bevy_ui/src/ui_material.rs @@ -141,7 +141,7 @@ where fn clone(&self) -> Self { Self { hdr: self.hdr, - bind_group_data: self.bind_group_data.clone(), + bind_group_data: self.bind_group_data, } } } diff --git a/examples/3d/lines.rs b/examples/3d/lines.rs index d755aa434a2bb..0ee62f599c20b 100644 --- a/examples/3d/lines.rs +++ b/examples/3d/lines.rs @@ -77,7 +77,7 @@ impl Material for LineMaterial { } fn specialize( - _pipeline: &MaterialPipeline, + _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, _layout: &MeshVertexBufferLayoutRef, _key: MaterialPipelineKey, diff --git a/examples/3d/manual_material.rs b/examples/3d/manual_material.rs new file mode 100644 index 0000000000000..f93265a50a296 --- /dev/null +++ b/examples/3d/manual_material.rs @@ -0,0 +1,315 @@ +//! A simple 3D scene with light shining over a cube sitting on a plane. + +use bevy::{ + asset::{AsAssetId, AssetEventSystems}, + core_pipeline::core_3d::Opaque3d, + ecs::system::{ + lifetimeless::{SRes, SResMut}, + SystemChangeTick, SystemParamItem, + }, + pbr::{ + DrawMaterial, EntitiesNeedingSpecialization, EntitySpecializationTicks, + MaterialBindGroupAllocator, MaterialBindGroupAllocators, MaterialDrawFunction, + MaterialFragmentShader, MaterialProperties, PreparedMaterial, RenderMaterialBindings, + RenderMaterialInstance, RenderMaterialInstances, SpecializedMaterialPipelineCache, + }, + platform::collections::hash_map::Entry, + prelude::*, + render::{ + erased_render_asset::{ErasedRenderAsset, ErasedRenderAssetPlugin, PrepareAssetError}, + render_asset::RenderAssets, + render_phase::DrawFunctions, + render_resource::{ + binding_types::{sampler, texture_2d}, + AsBindGroup, BindGroupLayout, BindGroupLayoutEntries, BindingResources, + OwnedBindingResource, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, + TextureSampleType, TextureViewDimension, UnpreparedBindGroup, + }, + renderer::RenderDevice, + sync_world::MainEntity, + texture::GpuImage, + view::ExtractedView, + Extract, RenderApp, + }, + utils::Parallel, +}; +use std::{any::TypeId, sync::Arc}; + +const SHADER_ASSET_PATH: &str = "shaders/manual_material.wgsl"; + +fn main() { + App::new() + .add_plugins((DefaultPlugins, ImageMaterialPlugin)) + .add_systems(Startup, setup) + .run(); +} + +struct ImageMaterialPlugin; + +impl Plugin for ImageMaterialPlugin { + fn build(&self, app: &mut App) { + app.init_asset::() + .add_plugins(ErasedRenderAssetPlugin::::default()) + .add_systems( + PostUpdate, + check_entities_needing_specialization.after(AssetEventSystems), + ) + .init_resource::>(); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.add_systems( + ExtractSchedule, + ( + extract_image_materials, + extract_image_materials_needing_specialization, + ), + ); + + render_app.world_mut().resource_scope( + |world: &mut World, mut bind_group_allocators: Mut| { + world.resource_scope(|world: &mut World, render_device: Mut| { + let bind_group_layout = render_device.create_bind_group_layout( + "image_material_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + sampler(SamplerBindingType::NonFiltering), + ), + ), + ); + let sampler = render_device.create_sampler(&SamplerDescriptor::default()); + world.insert_resource(ImageMaterialBindGroupLayout(bind_group_layout.clone())); + world.insert_resource(ImageMaterialBindGroupSampler(sampler)); + + bind_group_allocators.insert( + TypeId::of::(), + MaterialBindGroupAllocator::new( + &render_device, + None, + None, + bind_group_layout, + None, + ), + ); + }); + }, + ); + } +} + +#[derive(Resource)] +struct ImageMaterialBindGroupLayout(BindGroupLayout); + +#[derive(Resource)] +struct ImageMaterialBindGroupSampler(Sampler); + +#[derive(Component)] +struct ImageMaterial3d(Handle); + +impl AsAssetId for ImageMaterial3d { + type Asset = ImageMaterial; + + fn as_asset_id(&self) -> AssetId { + self.0.id() + } +} + +#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] +struct ImageMaterial { + image: Handle, +} + +impl ErasedRenderAsset for ImageMaterial { + type SourceAsset = ImageMaterial; + type ErasedAsset = PreparedMaterial; + type Param = ( + SRes>, + SRes, + SRes, + SResMut, + SResMut, + SRes>, + SRes, + ); + + fn prepare_asset( + source_asset: Self::SourceAsset, + asset_id: AssetId, + ( + opaque_draw_functions, + material_layout, + asset_server, + bind_group_allocators, + render_material_bindings, + gpu_images, + image_material_sampler, + ): &mut SystemParamItem, + ) -> std::result::Result> { + let material_layout = material_layout.0.clone(); + let draw_function_id = opaque_draw_functions.read().id::(); + let bind_group_allocator = bind_group_allocators + .get_mut(&TypeId::of::()) + .unwrap(); + let Some(image) = gpu_images.get(&source_asset.image) else { + return Err(PrepareAssetError::RetryNextUpdate(source_asset)); + }; + let unprepared = UnpreparedBindGroup { + bindings: BindingResources(vec![ + ( + 0, + OwnedBindingResource::TextureView( + TextureViewDimension::D2, + image.texture_view.clone(), + ), + ), + ( + 1, + OwnedBindingResource::Sampler( + SamplerBindingType::NonFiltering, + image_material_sampler.0.clone(), + ), + ), + ]), + }; + let binding = match render_material_bindings.entry(asset_id.into()) { + Entry::Occupied(mut occupied_entry) => { + bind_group_allocator.free(*occupied_entry.get()); + let new_binding = + bind_group_allocator.allocate_unprepared(unprepared, &material_layout); + *occupied_entry.get_mut() = new_binding; + new_binding + } + Entry::Vacant(vacant_entry) => *vacant_entry + .insert(bind_group_allocator.allocate_unprepared(unprepared, &material_layout)), + }; + + let mut properties = MaterialProperties { + material_layout: Some(material_layout), + ..Default::default() + }; + properties.add_draw_function(MaterialDrawFunction, draw_function_id); + properties.add_shader(MaterialFragmentShader, asset_server.load(SHADER_ASSET_PATH)); + + Ok(PreparedMaterial { + binding, + properties: Arc::new(properties), + }) + } +} + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + asset_server: Res, +) { + // cube + commands.spawn(( + Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))), + ImageMaterial3d(materials.add(ImageMaterial { + image: asset_server.load("branding/icon.png"), + })), + Transform::from_xyz(0.0, 0.5, 0.0), + )); + // light + commands.spawn(( + PointLight { + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(4.0, 8.0, 4.0), + )); + // camera + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), + )); +} + +fn extract_image_materials( + mut material_instances: ResMut, + changed_meshes_query: Extract< + Query< + (Entity, &ViewVisibility, &ImageMaterial3d), + Or<(Changed, Changed)>, + >, + >, +) { + let last_change_tick = material_instances.current_change_tick; + + for (entity, view_visibility, material) in &changed_meshes_query { + if view_visibility.get() { + material_instances.instances.insert( + entity.into(), + RenderMaterialInstance { + asset_id: material.0.id().untyped(), + last_change_tick, + }, + ); + } else { + material_instances + .instances + .remove(&MainEntity::from(entity)); + } + } +} + +fn check_entities_needing_specialization( + needs_specialization: Query< + Entity, + ( + Or<( + Changed, + AssetChanged, + Changed, + AssetChanged, + )>, + With, + ), + >, + mut par_local: Local>>, + mut entities_needing_specialization: ResMut>, +) { + entities_needing_specialization.clear(); + + needs_specialization + .par_iter() + .for_each(|entity| par_local.borrow_local_mut().push(entity)); + + par_local.drain_into(&mut entities_needing_specialization); +} + +fn extract_image_materials_needing_specialization( + entities_needing_specialization: Extract>>, + mut entity_specialization_ticks: ResMut, + mut removed_mesh_material_components: Extract>, + mut specialized_material_pipeline_cache: ResMut, + views: Query<&ExtractedView>, + ticks: SystemChangeTick, +) { + // Clean up any despawned entities, we do this first in case the removed material was re-added + // the same frame, thus will appear both in the removed components list and have been added to + // the `EntitiesNeedingSpecialization` collection by triggering the `Changed` filter + for entity in removed_mesh_material_components.read() { + entity_specialization_ticks.remove(&MainEntity::from(entity)); + for view in views { + if let Some(cache) = + specialized_material_pipeline_cache.get_mut(&view.retained_view_entity) + { + cache.remove(&MainEntity::from(entity)); + } + } + } + + for entity in entities_needing_specialization.iter() { + // Update the entity's specialization tick with this run's tick + entity_specialization_ticks.insert((*entity).into(), ticks.this_run()); + } +} diff --git a/examples/README.md b/examples/README.md index b966e75d1dc07..a8b9bfb3b406b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -165,6 +165,7 @@ Example | Description [Lines](../examples/3d/lines.rs) | Create a custom material to draw 3d lines [Load glTF](../examples/3d/load_gltf.rs) | Loads and renders a glTF file as a scene [Load glTF extras](../examples/3d/load_gltf_extras.rs) | Loads and renders a glTF file as a scene, including the gltf extras +[Manual Material Implementation](../examples/3d/manual_material.rs) | Demonstrates how to implement a material manually using the mid-level render APIs [Mesh Ray Cast](../examples/3d/mesh_ray_cast.rs) | Demonstrates ray casting with the `MeshRayCast` system parameter [Meshlet](../examples/3d/meshlet.rs) | Meshlet rendering for dense high-poly scenes (experimental) [Mixed lighting](../examples/3d/mixed_lighting.rs) | Demonstrates how to combine baked and dynamic lighting diff --git a/examples/shader/custom_vertex_attribute.rs b/examples/shader/custom_vertex_attribute.rs index 4cc8ea7dc5fe4..46ab930851bde 100644 --- a/examples/shader/custom_vertex_attribute.rs +++ b/examples/shader/custom_vertex_attribute.rs @@ -74,7 +74,7 @@ impl Material for CustomMaterial { } fn specialize( - _pipeline: &MaterialPipeline, + _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, layout: &MeshVertexBufferLayoutRef, _key: MaterialPipelineKey, diff --git a/examples/shader/shader_defs.rs b/examples/shader/shader_defs.rs index 950ec182b4609..1944040142d2f 100644 --- a/examples/shader/shader_defs.rs +++ b/examples/shader/shader_defs.rs @@ -61,12 +61,12 @@ impl Material for CustomMaterial { } fn specialize( - _pipeline: &MaterialPipeline, + _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, _layout: &MeshVertexBufferLayoutRef, key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { - if key.bind_group_data.is_red { + if key.bind_group_data.is_red == 1 { let fragment = descriptor.fragment.as_mut().unwrap(); fragment.shader_defs.push("IS_RED".into()); } @@ -86,16 +86,19 @@ struct CustomMaterial { // This key is used to identify a specific permutation of this material pipeline. // In this case, we specialize on whether or not to configure the "IS_RED" shader def. // Specialization keys should be kept as small / cheap to hash as possible, -// as they will be used to look up the pipeline for each drawn entity with this material type. -#[derive(Eq, PartialEq, Hash, Clone)] +// as they will be used to look up the pipeline for each drawn entity with this material type, +// Which is why they are required to be `bytemuck::Pod` and `bytemuck::Zeroable` for materials +// that use the `AsBindGroup` derive macro. +#[repr(C)] +#[derive(Eq, PartialEq, Hash, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] struct CustomMaterialKey { - is_red: bool, + is_red: u32, } impl From<&CustomMaterial> for CustomMaterialKey { fn from(material: &CustomMaterial) -> Self { Self { - is_red: material.is_red, + is_red: material.is_red as u32, } } } diff --git a/examples/shader/shader_material_glsl.rs b/examples/shader/shader_material_glsl.rs index 1f25302a727b9..9692020389416 100644 --- a/examples/shader/shader_material_glsl.rs +++ b/examples/shader/shader_material_glsl.rs @@ -79,7 +79,7 @@ impl Material for CustomMaterial { // and fragment shaders use the "fragment" entry point (for WGSL shaders). // GLSL uses "main" as the entry point, so we must override the defaults here fn specialize( - _pipeline: &MaterialPipeline, + _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, _layout: &MeshVertexBufferLayoutRef, _key: MaterialPipelineKey, diff --git a/examples/shader/shader_material_wesl.rs b/examples/shader/shader_material_wesl.rs index 108093de78664..c6db6175424de 100644 --- a/examples/shader/shader_material_wesl.rs +++ b/examples/shader/shader_material_wesl.rs @@ -101,15 +101,16 @@ struct CustomMaterial { party_mode: bool, } -#[derive(Eq, PartialEq, Hash, Clone)] +#[repr(C)] +#[derive(Eq, PartialEq, Hash, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] struct CustomMaterialKey { - party_mode: bool, + party_mode: u32, } impl From<&CustomMaterial> for CustomMaterialKey { fn from(material: &CustomMaterial) -> Self { Self { - party_mode: material.party_mode, + party_mode: material.party_mode as u32, } } } @@ -120,7 +121,7 @@ impl Material for CustomMaterial { } fn specialize( - _pipeline: &MaterialPipeline, + _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, _layout: &MeshVertexBufferLayoutRef, key: MaterialPipelineKey, @@ -128,7 +129,7 @@ impl Material for CustomMaterial { let fragment = descriptor.fragment.as_mut().unwrap(); fragment.shader_defs.push(ShaderDefVal::Bool( "PARTY_MODE".to_string(), - key.bind_group_data.party_mode, + key.bind_group_data.party_mode == 1, )); Ok(()) } diff --git a/examples/shader/texture_binding_array.rs b/examples/shader/texture_binding_array.rs index e4939edf6ae86..bfb439d81f818 100644 --- a/examples/shader/texture_binding_array.rs +++ b/examples/shader/texture_binding_array.rs @@ -104,7 +104,7 @@ impl AsBindGroup for BindlessMaterial { layout: &BindGroupLayout, render_device: &RenderDevice, (image_assets, fallback_image): &mut SystemParamItem<'_, '_, Self::Param>, - ) -> Result, AsBindGroupError> { + ) -> Result { // retrieve the render resources from handles let mut images = vec![]; for handle in self.textures.iter().take(MAX_TEXTURE_COUNT) { @@ -135,17 +135,18 @@ impl AsBindGroup for BindlessMaterial { Ok(PreparedBindGroup { bindings: BindingResources(vec![]), bind_group, - data: (), }) } + fn bind_group_data(&self) -> Self::Data {} + fn unprepared_bind_group( &self, _layout: &BindGroupLayout, _render_device: &RenderDevice, _param: &mut SystemParamItem<'_, '_, Self::Param>, _force_no_bindless: bool, - ) -> Result, AsBindGroupError> { + ) -> Result { // We implement `as_bind_group`` directly because bindless texture // arrays can't be owned. // Or rather, they can be owned, but then you can't make a `&'a [&'a