diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 5377dfba3be17..c27adfbb54a1f 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -439,7 +439,8 @@ impl Plugin for PbrPlugin { prepare_clusters.in_set(RenderSet::PrepareResources), ), ) - .init_resource::(); + .init_resource::() + .init_resource::(); render_app.world_mut().add_observer(add_light_view_entities); render_app diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index 00d60993c4da4..ff3c33841b43c 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -818,7 +818,9 @@ pub fn check_dir_light_mesh_visibility( for entities in defer_queue.iter_mut() { let mut iter = query.iter_many_mut(world, entities.iter()); while let Some(mut view_visibility) = iter.fetch_next() { - view_visibility.set(); + if !**view_visibility { + view_visibility.set(); + } } } }); @@ -940,12 +942,16 @@ pub fn check_point_light_mesh_visibility( if has_no_frustum_culling || frustum.intersects_obb(aabb, &model_to_world, true, true) { - view_visibility.set(); + if !**view_visibility { + view_visibility.set(); + } visible_entities.push(entity); } } } else { - view_visibility.set(); + if !**view_visibility { + view_visibility.set(); + } for visible_entities in cubemap_visible_entities_local_queue.iter_mut() { visible_entities.push(entity); @@ -1025,11 +1031,15 @@ pub fn check_point_light_mesh_visibility( if has_no_frustum_culling || frustum.intersects_obb(aabb, &model_to_world, true, true) { - view_visibility.set(); + if !**view_visibility { + view_visibility.set(); + } spot_visible_entities_local_queue.push(entity); } } else { - view_visibility.set(); + if !**view_visibility { + view_visibility.set(); + } spot_visible_entities_local_queue.push(entity); } }, diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 0ffe305bd7cb1..112c1cb36828c 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -6,7 +6,7 @@ use crate::meshlet::{ InstanceManager, }; use crate::*; -use bevy_asset::{Asset, AssetId, AssetServer}; +use bevy_asset::{Asset, AssetId, AssetServer, UntypedAssetId}; use bevy_core_pipeline::{ core_3d::{ AlphaMask3d, Camera3d, Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, @@ -33,17 +33,18 @@ use bevy_render::{ batching::gpu_preprocessing::GpuPreprocessingSupport, camera::TemporalJitter, extract_resource::ExtractResource, - mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, + mesh::{self, Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, render_phase::*, render_resource::*, renderer::RenderDevice, + sync_world::MainEntity, view::{ExtractedView, Msaa, RenderVisibilityRanges, ViewVisibility}, Extract, }; use bevy_render::{mesh::allocator::MeshAllocator, sync_world::MainEntityHashMap}; use bevy_render::{texture::FallbackImage, view::RenderVisibleEntities}; -use bevy_utils::hashbrown::hash_map::Entry; +use bevy_utils::HashMap; use core::{hash::Hash, marker::PhantomData}; use tracing::error; @@ -270,7 +271,13 @@ where fn build(&self, app: &mut App) { app.init_asset::() .register_type::>() - .add_plugins(RenderAssetPlugin::>::default()); + .add_plugins(RenderAssetPlugin::>::default()) + .add_systems( + PostUpdate, + mark_meshes_as_changed_if_their_materials_changed:: + .ambiguous_with_all() + .after(mesh::mark_3d_meshes_as_changed_if_their_assets_changed), + ); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app @@ -282,7 +289,10 @@ where .add_render_command::>() .add_render_command::>() .init_resource::>>() - .add_systems(ExtractSchedule, extract_mesh_materials::) + .add_systems( + ExtractSchedule, + extract_mesh_materials::.before(ExtractMeshesSet), + ) .add_systems( Render, queue_material_meshes:: @@ -581,26 +591,64 @@ pub const fn screen_space_specular_transmission_pipeline_key( } } -pub fn extract_mesh_materials( +/// A system that ensures that +/// [`crate::render::mesh::extract_meshes_for_gpu_building`] re-extracts meshes +/// whose materials changed. +/// +/// As [`crate::render::mesh::collect_meshes_for_gpu_building`] only considers +/// meshes that were newly extracted, and it writes information from the +/// [`RenderMeshMaterialIds`] into the +/// [`crate::render::mesh::MeshInputUniform`], we must tell +/// [`crate::render::mesh::extract_meshes_for_gpu_building`] to re-extract a +/// mesh if its material changed. Otherwise, the material binding information in +/// the [`crate::render::mesh::MeshInputUniform`] might not be updated properly. +/// The easiest way to ensure that +/// [`crate::render::mesh::extract_meshes_for_gpu_building`] re-extracts a mesh +/// is to mark its [`Mesh3d`] as changed, so that's what this system does. +fn mark_meshes_as_changed_if_their_materials_changed( + mut changed_meshes_query: Query<&mut Mesh3d, Changed>>, +) where + M: Material, +{ + for mut mesh in &mut changed_meshes_query { + mesh.set_changed(); + } +} + +/// Fills the [`RenderMaterialInstances`] and [`RenderMeshMaterialIds`] +/// resources from the meshes in the scene. +fn extract_mesh_materials( mut material_instances: ResMut>, mut material_ids: ResMut, - mut material_bind_group_allocator: ResMut>, - query: Extract)>>, + changed_meshes_query: Extract< + Query< + (Entity, &ViewVisibility, &MeshMaterial3d), + Or<(Changed, Changed>)>, + >, + >, + mut removed_visibilities_query: Extract>, + mut removed_materials_query: Extract>>, ) { - material_instances.clear(); - - for (entity, view_visibility, material) in &query { + for (entity, view_visibility, material) in &changed_meshes_query { if view_visibility.get() { material_instances.insert(entity.into(), material.id()); + material_ids.insert(entity.into(), material.id().into()); + } else { + material_instances.remove(&MainEntity::from(entity)); + material_ids.remove(entity.into()); + } + } - // Allocate a slot for this material in the bind group. - let material_id = material.id().untyped(); - material_ids - .mesh_to_material - .insert(entity.into(), material_id); - if let Entry::Vacant(entry) = material_ids.material_to_binding.entry(material_id) { - entry.insert(material_bind_group_allocator.allocate()); - } + for entity in removed_visibilities_query + .read() + .chain(removed_materials_query.read()) + { + // Only queue a mesh for removal if we didn't pick it up above. + // It's possible that a necessary component was removed and re-added in + // the same frame. + if !changed_meshes_query.contains(entity) { + material_instances.remove(&MainEntity::from(entity)); + material_ids.remove(entity.into()); } } } @@ -1019,6 +1067,14 @@ pub struct MaterialProperties { pub reads_view_transmission_texture: bool, } +/// A resource that maps each untyped material ID to its binding. +/// +/// This duplicates information in `RenderAssets`, but it doesn't have the +/// `M` type parameter, so it can be used in untyped contexts like +/// [`crate::render::mesh::collect_meshes_for_gpu_building`]. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct RenderMaterialBindings(HashMap); + /// Data prepared for a [`Material`] instance. pub struct PreparedMaterial { pub binding: MaterialBindingId, @@ -1033,8 +1089,8 @@ impl RenderAsset for PreparedMaterial { SRes, SRes>, SRes, - SRes, SResMut>, + SResMut, M::Param, ); @@ -1045,19 +1101,15 @@ impl RenderAsset for PreparedMaterial { render_device, pipeline, default_opaque_render_method, - mesh_material_ids, ref mut bind_group_allocator, + ref mut render_material_bindings, ref mut material_param, ): &mut SystemParamItem, ) -> Result> { - // Fetch the material binding ID, so that we can write it in to the - // `PreparedMaterial`. - let Some(material_binding_id) = mesh_material_ids - .material_to_binding - .get(&material_id.untyped()) - else { - return Err(PrepareAssetError::RetryNextUpdate(material)); - }; + // Allocate a material binding ID if needed. + let material_binding_id = *render_material_bindings + .entry(material_id.into()) + .or_insert_with(|| bind_group_allocator.allocate()); let method = match material.opaque_render_method() { OpaqueRendererMethod::Forward => OpaqueRendererMethod::Forward, @@ -1077,10 +1129,10 @@ impl RenderAsset for PreparedMaterial { false, ) { Ok(unprepared) => { - bind_group_allocator.init(render_device, *material_binding_id, unprepared); + bind_group_allocator.init(render_device, material_binding_id, unprepared); Ok(PreparedMaterial { - binding: *material_binding_id, + binding: material_binding_id, properties: MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), @@ -1110,13 +1162,13 @@ impl RenderAsset for PreparedMaterial { Ok(prepared_bind_group) => { // Store the resulting bind group directly in the slot. bind_group_allocator.init_custom( - *material_binding_id, + material_binding_id, prepared_bind_group.bind_group, prepared_bind_group.data, ); Ok(PreparedMaterial { - binding: *material_binding_id, + binding: material_binding_id, properties: MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), @@ -1142,21 +1194,21 @@ impl RenderAsset for PreparedMaterial { } fn unload_asset( - asset_id: AssetId, - (_, _, _, mesh_material_ids, ref mut bind_group_allocator, _): &mut SystemParamItem< - Self::Param, - >, + source_asset: AssetId, + ( + _, + _, + _, + ref mut bind_group_allocator, + ref mut render_material_bindings, + _, + ): &mut SystemParamItem, ) { - // Mark this material's slot in the binding array as free. - - let Some(material_binding_id) = mesh_material_ids - .material_to_binding - .get(&asset_id.untyped()) + let Some(material_binding_id) = render_material_bindings.remove(&source_asset.untyped()) else { return; }; - - bind_group_allocator.free(*material_binding_id); + bind_group_allocator.free(material_binding_id); } } diff --git a/crates/bevy_pbr/src/meshlet/instance_manager.rs b/crates/bevy_pbr/src/meshlet/instance_manager.rs index f19cb0a008efe..1f62ed64700ad 100644 --- a/crates/bevy_pbr/src/meshlet/instance_manager.rs +++ b/crates/bevy_pbr/src/meshlet/instance_manager.rs @@ -1,7 +1,8 @@ use super::{meshlet_mesh_manager::MeshletMeshManager, MeshletMesh, MeshletMesh3d}; use crate::{ Material, MeshFlags, MeshTransforms, MeshUniform, NotShadowCaster, NotShadowReceiver, - PreviousGlobalTransform, RenderMaterialInstances, RenderMeshMaterialIds, + PreviousGlobalTransform, RenderMaterialBindings, RenderMaterialInstances, + RenderMeshMaterialIds, }; use bevy_asset::{AssetEvent, AssetServer, Assets, UntypedAssetId}; use bevy_ecs::{ @@ -90,6 +91,7 @@ impl InstanceManager { previous_transform: Option<&PreviousGlobalTransform>, render_layers: Option<&RenderLayers>, mesh_material_ids: &RenderMeshMaterialIds, + render_material_bindings: &RenderMaterialBindings, not_shadow_receiver: bool, not_shadow_caster: bool, ) { @@ -110,15 +112,11 @@ impl InstanceManager { flags: flags.bits(), }; - let Some(mesh_material_asset_id) = mesh_material_ids.mesh_to_material.get(&instance) else { - return; - }; - let Some(mesh_material_binding_id) = mesh_material_ids - .material_to_binding - .get(mesh_material_asset_id) - else { - return; - }; + let mesh_material = mesh_material_ids.mesh_material(instance); + let mesh_material_binding_id = render_material_bindings + .get(&mesh_material) + .cloned() + .unwrap_or_default(); let mesh_uniform = MeshUniform::new( &transforms, @@ -190,6 +188,7 @@ pub fn extract_meshlet_mesh_entities( // TODO: Replace main_world and system_state when Extract>> is possible mut main_world: ResMut, mesh_material_ids: Res, + render_material_bindings: Res, mut system_state: Local< Option< SystemState<( @@ -259,6 +258,7 @@ pub fn extract_meshlet_mesh_entities( previous_transform, render_layers, &mesh_material_ids, + &render_material_bindings, not_shadow_receiver, not_shadow_caster, ); diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index dd92ef7828eec..f5e79ae1695b4 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -38,8 +38,8 @@ use bevy_render::{ renderer::{RenderAdapter, RenderDevice, RenderQueue}, texture::DefaultImageSampler, view::{ - NoFrustumCulling, NoIndirectDrawing, RenderVisibilityRanges, ViewTarget, ViewUniformOffset, - ViewVisibility, VisibilityRange, + self, NoFrustumCulling, NoIndirectDrawing, RenderVisibilityRanges, ViewTarget, + ViewUniformOffset, ViewVisibility, VisibilityRange, }, Extract, }; @@ -160,6 +160,10 @@ impl Plugin for MeshRenderPlugin { .init_resource::() .init_resource::() .init_resource::() + .configure_sets( + ExtractSchedule, + ExtractMeshesSet.after(view::extract_visibility_ranges), + ) .add_systems( ExtractSchedule, ( @@ -172,7 +176,7 @@ impl Plugin for MeshRenderPlugin { .add_systems( Render, ( - set_mesh_motion_vector_flags.in_set(RenderSet::PrepareAssets), + set_mesh_motion_vector_flags.in_set(RenderSet::PrepareMeshes), prepare_skins.in_set(RenderSet::PrepareResources), prepare_morphs.in_set(RenderSet::PrepareResources), prepare_mesh_bind_group.in_set(RenderSet::PrepareBindGroups), @@ -220,9 +224,7 @@ impl Plugin for MeshRenderPlugin { gpu_preprocessing::delete_old_work_item_buffers:: .in_set(RenderSet::PrepareResources), collect_meshes_for_gpu_building - .in_set(RenderSet::PrepareAssets) - .after(allocator::allocate_and_free_meshes) - .after(extract_skins) + .in_set(RenderSet::PrepareMeshes) // This must be before // `set_mesh_motion_vector_flags` so it doesn't // overwrite those flags. @@ -696,10 +698,7 @@ pub struct RenderMeshInstancesGpu(MainEntityHashMap); #[derive(Resource, Default)] pub struct RenderMeshMaterialIds { /// Maps the mesh instance to the material ID. - pub(crate) mesh_to_material: MainEntityHashMap, - /// Maps the material ID to the binding ID, which describes the location of - /// that material bind group data in memory. - pub(crate) material_to_binding: HashMap, + mesh_to_material: MainEntityHashMap, } impl RenderMeshMaterialIds { @@ -709,15 +708,19 @@ impl RenderMeshMaterialIds { /// Meshes almost always have materials, but in very specific circumstances /// involving custom pipelines they won't. (See the /// `specialized_mesh_pipelines` example.) - fn mesh_material_binding_id(&self, entity: MainEntity) -> MaterialBindingId { + pub(crate) fn mesh_material(&self, entity: MainEntity) -> UntypedAssetId { self.mesh_to_material .get(&entity) - .and_then(|mesh_material_asset_id| { - self.material_to_binding - .get(mesh_material_asset_id) - .cloned() - }) - .unwrap_or_default() + .cloned() + .unwrap_or(AssetId::::invalid().into()) + } + + pub(crate) fn insert(&mut self, mesh_entity: MainEntity, material_id: UntypedAssetId) { + self.mesh_to_material.insert(mesh_entity, material_id); + } + + pub(crate) fn remove(&mut self, main_entity: MainEntity) { + self.mesh_to_material.remove(&main_entity); } } @@ -920,6 +923,7 @@ impl RenderMeshInstanceGpuBuilder { previous_input_buffer: &mut InstanceInputUniformBuffer, mesh_allocator: &MeshAllocator, mesh_material_ids: &RenderMeshMaterialIds, + render_material_bindings: &RenderMaterialBindings, render_lightmaps: &RenderLightmaps, skin_indices: &SkinIndices, ) -> u32 { @@ -951,7 +955,11 @@ impl RenderMeshInstanceGpuBuilder { }; // Look up the material index. - let mesh_material_binding_id = mesh_material_ids.mesh_material_binding_id(entity); + let mesh_material = mesh_material_ids.mesh_material(entity); + let mesh_material_binding_id = render_material_bindings + .get(&mesh_material) + .cloned() + .unwrap_or_default(); self.shared.material_bindings_index = mesh_material_binding_id; let lightmap_slot = match render_lightmaps.render_lightmaps.get(&entity) { @@ -1394,6 +1402,7 @@ pub fn collect_meshes_for_gpu_building( mut render_mesh_instance_queues: ResMut, mesh_allocator: Res, mesh_material_ids: Res, + render_material_bindings: Res, render_lightmaps: Res, skin_indices: Res, ) { @@ -1432,6 +1441,7 @@ pub fn collect_meshes_for_gpu_building( previous_input_buffer, &mesh_allocator, &mesh_material_ids, + &render_material_bindings, &render_lightmaps, &skin_indices, ); @@ -1458,6 +1468,7 @@ pub fn collect_meshes_for_gpu_building( previous_input_buffer, &mesh_allocator, &mesh_material_ids, + &render_material_bindings, &render_lightmaps, &skin_indices, ); diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index b267d4bf5a379..f98efcf1e788a 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -126,6 +126,8 @@ pub enum RenderSet { ExtractCommands, /// Prepare assets that have been created/modified/removed this frame. PrepareAssets, + /// Prepares extracted meshes. + PrepareMeshes, /// Create any additional views such as those used for shadow mapping. ManageViews, /// Queue drawable entities as phase items in render phases ready for @@ -174,6 +176,7 @@ impl Render { schedule.configure_sets( ( ExtractCommands, + PrepareMeshes, ManageViews, Queue, PhaseSort, @@ -185,7 +188,7 @@ impl Render { .chain(), ); - schedule.configure_sets((ExtractCommands, PrepareAssets, Prepare).chain()); + schedule.configure_sets((ExtractCommands, PrepareAssets, PrepareMeshes, Prepare).chain()); schedule.configure_sets( QueueMeshes .in_set(Queue) diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index 8725c0e3343cf..6f9339bb06198 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -21,7 +21,7 @@ use bevy_ecs::{ SystemParamItem, }, }; -pub use components::{Mesh2d, Mesh3d}; +pub use components::{mark_3d_meshes_as_changed_if_their_assets_changed, Mesh2d, Mesh3d}; use wgpu::IndexFormat; /// Adds the [`Mesh`] as an asset and makes sure that they are extracted and prepared for the GPU. @@ -40,7 +40,7 @@ impl Plugin for MeshPlugin { .add_plugins(MeshAllocatorPlugin) .add_systems( PostUpdate, - components::mark_3d_meshes_as_changed_if_their_assets_changed + mark_3d_meshes_as_changed_if_their_assets_changed .ambiguous_with(VisibilitySystems::CalculateBounds), ); diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index a4dfc376682c4..3566c74a5e743 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -28,15 +28,15 @@ use bevy_render::{ ViewBinnedRenderPhases, ViewSortedRenderPhases, }, render_resource::{ - AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout, PipelineCache, - RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipeline, + AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout, BindingResources, + PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines, }, renderer::RenderDevice, + sync_world::{MainEntity, MainEntityHashMap}, view::{ExtractedView, Msaa, RenderVisibleEntities, ViewVisibility}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_render::{render_resource::BindingResources, sync_world::MainEntityHashMap}; use core::{hash::Hash, marker::PhantomData}; use derive_more::derive::From; use tracing::error; @@ -281,15 +281,56 @@ impl Default for RenderMaterial2dInstances { pub fn extract_mesh_materials_2d( mut material_instances: ResMut>, - query: Extract), With>>, + changed_meshes_query: Extract< + Query< + (Entity, &ViewVisibility, &MeshMaterial2d), + Or<(Changed, Changed>)>, + >, + >, + mut removed_visibilities_query: Extract>, + mut removed_materials_query: Extract>>, ) { - material_instances.clear(); - - for (entity, view_visibility, material) in &query { + for (entity, view_visibility, material) in &changed_meshes_query { if view_visibility.get() { - material_instances.insert(entity.into(), material.id()); + add_mesh_instance(entity, material, &mut material_instances); + } else { + remove_mesh_instance(entity, &mut material_instances); + } + } + + for entity in removed_visibilities_query + .read() + .chain(removed_materials_query.read()) + { + // Only queue a mesh for removal if we didn't pick it up above. + // It's possible that a necessary component was removed and re-added in + // the same frame. + if !changed_meshes_query.contains(entity) { + remove_mesh_instance(entity, &mut material_instances); } } + + // Adds or updates a mesh instance in the [`RenderMaterial2dInstances`] + // array. + fn add_mesh_instance( + entity: Entity, + material: &MeshMaterial2d, + material_instances: &mut RenderMaterial2dInstances, + ) where + M: Material2d, + { + material_instances.insert(entity.into(), material.id()); + } + + // Removes a mesh instance from the [`RenderMaterial2dInstances`] array. + fn remove_mesh_instance( + entity: Entity, + material_instances: &mut RenderMaterial2dInstances, + ) where + M: Material2d, + { + material_instances.remove(&MainEntity::from(entity)); + } } /// Render pipeline data for a given [`Material2d`]