diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 2bfd54d847d84..aeaa0d0ab429c 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -32,7 +32,7 @@ use crate::{ }; use bevy_app::{Animation, App, Plugin, PostUpdate}; -use bevy_asset::{Asset, AssetApp, Assets}; +use bevy_asset::{Asset, AssetApp, AssetEvents, Assets}; use bevy_ecs::{ entity::{VisitEntities, VisitEntitiesMut}, prelude::*, @@ -1244,7 +1244,7 @@ impl Plugin for AnimationPlugin { .add_systems( PostUpdate, ( - graph::thread_animation_graphs, + graph::thread_animation_graphs.before(AssetEvents), advance_transitions, advance_animations, // TODO: `animate_targets` can animate anything, so diff --git a/crates/bevy_asset/src/asset_changed.rs b/crates/bevy_asset/src/asset_changed.rs index 04439d9c5ee36..a9b46a29f9f9e 100644 --- a/crates/bevy_asset/src/asset_changed.rs +++ b/crates/bevy_asset/src/asset_changed.rs @@ -293,7 +293,7 @@ mod tests { use std::println; use crate::{AssetApp, Assets}; - use bevy_app::{App, AppExit, Last, Startup, TaskPoolPlugin, Update}; + use bevy_app::{App, AppExit, PostUpdate, Startup, TaskPoolPlugin, Update}; use bevy_ecs::schedule::IntoSystemConfigs; use bevy_ecs::{ component::Component, @@ -410,7 +410,7 @@ mod tests { .init_asset::() .insert_resource(Counter(vec![0, 0, 0, 0])) .add_systems(Update, add_some) - .add_systems(Last, count_update.after(AssetEvents)); + .add_systems(PostUpdate, count_update.after(AssetEvents)); // First run of the app, `add_systems(Startup…)` runs. app.update(); // run_count == 0 @@ -445,7 +445,7 @@ mod tests { }, ) .add_systems(Update, update_some) - .add_systems(Last, count_update.after(AssetEvents)); + .add_systems(PostUpdate, count_update.after(AssetEvents)); // First run of the app, `add_systems(Startup…)` runs. app.update(); // run_count == 0 diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 85fe7a87ed132..a3211fe2b36ab 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -212,7 +212,7 @@ use alloc::{ sync::Arc, vec::Vec, }; -use bevy_app::{App, Last, Plugin, PreUpdate}; +use bevy_app::{App, Plugin, PostUpdate, PreUpdate}; use bevy_ecs::prelude::Component; use bevy_ecs::{ reflect::AppTypeRegistry, @@ -580,7 +580,7 @@ impl AssetApp for App { .add_event::>() .register_type::>() .add_systems( - Last, + PostUpdate, Assets::::asset_events .run_if(Assets::::asset_events_condition) .in_set(AssetEvents), diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 96ccdf781e840..5b863eaa96d93 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -1,4 +1,3 @@ -use self::{irradiance_volume::IrradianceVolume, prelude::EnvironmentMapLight}; use crate::material_bind_groups::{MaterialBindGroupAllocator, MaterialBindingId}; #[cfg(feature = "meshlet")] use crate::meshlet::{ @@ -6,20 +5,22 @@ use crate::meshlet::{ InstanceManager, }; use crate::*; -use bevy_asset::{Asset, AssetId, AssetServer, UntypedAssetId}; +use bevy_asset::prelude::AssetChanged; +use bevy_asset::{Asset, AssetEvents, AssetId, AssetServer, UntypedAssetId}; +use bevy_core_pipeline::deferred::{AlphaMask3dDeferred, Opaque3dDeferred}; +use bevy_core_pipeline::prepass::{AlphaMask3dPrepass, Opaque3dPrepass}; use bevy_core_pipeline::{ core_3d::{ - AlphaMask3d, Camera3d, Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, - ScreenSpaceTransmissionQuality, Transmissive3d, Transparent3d, + AlphaMask3d, Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, ScreenSpaceTransmissionQuality, + Transmissive3d, Transparent3d, }, - oit::OrderIndependentTransparencySettings, - prepass::{ - DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass, - OpaqueNoLightmap3dBatchSetKey, OpaqueNoLightmap3dBinKey, - }, - tonemapping::{DebandDither, Tonemapping}, + prepass::{OpaqueNoLightmap3dBatchSetKey, OpaqueNoLightmap3dBinKey}, + tonemapping::Tonemapping, }; use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::component::Tick; +use bevy_ecs::entity::EntityHash; +use bevy_ecs::system::SystemChangeTick; use bevy_ecs::{ prelude::*, system::{ @@ -30,11 +31,11 @@ use bevy_ecs::{ use bevy_platform_support::collections::HashMap; use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; +use bevy_render::mesh::mark_3d_meshes_as_changed_if_their_assets_changed; use bevy_render::{ batching::gpu_preprocessing::GpuPreprocessingSupport, - camera::TemporalJitter, extract_resource::ExtractResource, - mesh::{self, Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, + mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, render_phase::*, render_resource::*, @@ -271,16 +272,29 @@ where fn build(&self, app: &mut App) { app.init_asset::() .register_type::>() - .add_plugins(RenderAssetPlugin::>::default()) + .init_resource::>() + .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), + ( + mark_meshes_as_changed_if_their_materials_changed::.ambiguous_with_all(), + check_entities_needing_specialization::.after(AssetEvents), + ) + .after(mark_3d_meshes_as_changed_if_their_assets_changed), ); + if self.shadows_enabled { + app.add_systems( + PostUpdate, + check_light_entities_needing_specialization:: + .after(check_entities_needing_specialization::), + ); + } + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app + .init_resource::>() + .init_resource::>() .init_resource::>() .init_resource::>() .add_render_command::>() @@ -291,13 +305,22 @@ where .init_resource::>>() .add_systems( ExtractSchedule, - extract_mesh_materials::.before(ExtractMeshesSet), + ( + extract_mesh_materials::.before(ExtractMeshesSet), + extract_entities_needs_specialization::, + ), ) .add_systems( Render, - queue_material_meshes:: - .in_set(RenderSet::QueueMeshes) - .after(prepare_assets::>), + ( + specialize_material_meshes:: + .in_set(RenderSet::PrepareAssets) + .after(prepare_assets::>) + .after(prepare_assets::), + queue_material_meshes:: + .in_set(RenderSet::QueueMeshes) + .after(prepare_assets::>), + ), ) .add_systems( Render, @@ -307,12 +330,22 @@ where ); if self.shadows_enabled { - render_app.add_systems( - Render, - queue_shadows:: - .in_set(RenderSet::QueueMeshes) - .after(prepare_assets::>), - ); + render_app + .init_resource::() + .init_resource::() + .init_resource::>() + .add_systems( + Render, + ( + check_views_lights_need_specialization.in_set(RenderSet::PrepareAssets), + specialize_shadows:: + .in_set(RenderSet::PrepareAssets) + .after(prepare_assets::>), + queue_shadows:: + .in_set(RenderSet::QueueMeshes) + .after(prepare_assets::>), + ), + ); } #[cfg(feature = "meshlet")] @@ -653,180 +686,146 @@ fn extract_mesh_materials( } } -/// 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( - ( - opaque_draw_functions, - alpha_mask_draw_functions, - transmissive_draw_functions, - transparent_draw_functions, - ): ( - Res>, - Res>, - Res>, - Res>, - ), - material_pipeline: Res>, - mut pipelines: ResMut>>, - pipeline_cache: Res, - render_meshes: Res>, - render_materials: Res>>, - render_mesh_instances: Res, - render_material_instances: Res>, - render_lightmaps: Res, - render_visibility_ranges: Res, - (mesh_allocator, material_bind_group_allocator, gpu_preprocessing_support): ( - Res, - Res>, - Res, - ), - mut opaque_render_phases: ResMut>, - mut alpha_mask_render_phases: ResMut>, - mut transmissive_render_phases: ResMut>, - mut transparent_render_phases: ResMut>, - views: Query<( - &ExtractedView, - &RenderVisibleEntities, - &Msaa, - Option<&Tonemapping>, - Option<&DebandDither>, - Option<&ShadowFilteringMethod>, - Has, - ( - Has, - Has, - Has, - Has, - ), - Option<&Camera3d>, - Has, - Option<&Projection>, - Has, - ( - Has>, - Has>, - ), - Has, - )>, +pub fn extract_entities_needs_specialization( + entities_needing_specialization: Extract>>, + mut entity_specialization_ticks: ResMut>, + ticks: SystemChangeTick, ) where - M::Data: PartialEq + Eq + Hash + Clone, + M: Material, { - for ( - view, - visible_entities, - msaa, - tonemapping, - dither, - shadow_filter_method, - ssao, - (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), - camera_3d, - temporal_jitter, - projection, - distance_fog, - (has_environment_maps, has_irradiance_volumes), - has_oit, - ) in &views - { - let ( - Some(opaque_phase), - Some(alpha_mask_phase), - Some(transmissive_phase), - Some(transparent_phase), - ) = ( - opaque_render_phases.get_mut(&view.retained_view_entity), - alpha_mask_render_phases.get_mut(&view.retained_view_entity), - transmissive_render_phases.get_mut(&view.retained_view_entity), - transparent_render_phases.get_mut(&view.retained_view_entity), - ) - else { - continue; - }; - - 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 mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) - | MeshPipelineKey::from_hdr(view.hdr); - - if normal_prepass { - view_key |= MeshPipelineKey::NORMAL_PREPASS; - } + 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()); + } +} - if depth_prepass { - view_key |= MeshPipelineKey::DEPTH_PREPASS; - } +#[derive(Resource, Deref, DerefMut, Clone, Debug)] +pub struct EntitiesNeedingSpecialization { + #[deref] + pub entities: Vec, + _marker: PhantomData, +} - if motion_vector_prepass { - view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; +impl Default for EntitiesNeedingSpecialization { + fn default() -> Self { + Self { + entities: Default::default(), + _marker: Default::default(), } + } +} - if deferred_prepass { - view_key |= MeshPipelineKey::DEFERRED_PREPASS; - } +#[derive(Resource, Deref, DerefMut, Clone, Debug)] +pub struct EntitySpecializationTicks { + #[deref] + pub entities: MainEntityHashMap, + _marker: PhantomData, +} - if temporal_jitter { - view_key |= MeshPipelineKey::TEMPORAL_JITTER; +impl Default for EntitySpecializationTicks { + fn default() -> Self { + Self { + entities: MainEntityHashMap::default(), + _marker: Default::default(), } + } +} - if has_environment_maps { - view_key |= MeshPipelineKey::ENVIRONMENT_MAP; - } +#[derive(Resource, Deref, DerefMut)] +pub struct SpecializedMaterialPipelineCache { + // (view_entity, material_entity) -> (tick, pipeline_id) + #[deref] + map: HashMap<(MainEntity, MainEntity), (Tick, CachedRenderPipelineId), EntityHash>, + marker: PhantomData, +} - if has_irradiance_volumes { - view_key |= MeshPipelineKey::IRRADIANCE_VOLUME; +impl Default for SpecializedMaterialPipelineCache { + fn default() -> Self { + Self { + map: HashMap::default(), + marker: PhantomData, } + } +} - if has_oit { - view_key |= MeshPipelineKey::OIT_ENABLED; - } +pub fn check_entities_needing_specialization( + needs_specialization: Query< + Entity, + Or<( + Changed, + AssetChanged, + Changed>, + AssetChanged>, + )>, + >, + mut entities_needing_specialization: ResMut>, +) where + M: Material, +{ + entities_needing_specialization.clear(); + for entity in &needs_specialization { + entities_needing_specialization.push(entity); + } +} - if let Some(projection) = projection { - view_key |= match projection { - Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE, - Projection::Orthographic(_) => MeshPipelineKey::VIEW_PROJECTION_ORTHOGRAPHIC, - Projection::Custom(_) => MeshPipelineKey::VIEW_PROJECTION_NONSTANDARD, - }; +pub fn specialize_material_meshes( + render_meshes: 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>, + Res>, + ), + views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>, + view_key_cache: Res, + entity_specialization_ticks: Res>, + view_specialization_ticks: Res, + mut specialized_material_pipeline_cache: ResMut>, + mut pipelines: ResMut>>, + pipeline: Res>, + pipeline_cache: Res, + ticks: SystemChangeTick, +) where + M::Data: PartialEq + Eq + Hash + Clone, +{ + for (view_entity, view, visible_entities) in &views { + if !transparent_render_phases.contains_key(&view.retained_view_entity) + && !opaque_render_phases.contains_key(&view.retained_view_entity) + && !alpha_mask_render_phases.contains_key(&view.retained_view_entity) + && !transmissive_render_phases.contains_key(&view.retained_view_entity) + { + continue; } - match shadow_filter_method.unwrap_or(&ShadowFilteringMethod::default()) { - ShadowFilteringMethod::Hardware2x2 => { - view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2; - } - ShadowFilteringMethod::Gaussian => { - view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN; - } - ShadowFilteringMethod::Temporal => { - view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL; - } - } + let Some(view_key) = view_key_cache.get(view_entity) else { + continue; + }; - if !view.hdr { - if let Some(tonemapping) = tonemapping { - view_key |= MeshPipelineKey::TONEMAP_IN_SHADER; - view_key |= tonemapping_pipeline_key(*tonemapping); - } - if let Some(DebandDither::Enabled) = dither { - view_key |= MeshPipelineKey::DEBAND_DITHER; + for (_, visible_entity) in visible_entities.iter::() { + let view_tick = view_specialization_ticks.get(view_entity).unwrap(); + let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap(); + let last_specialized_tick = specialized_material_pipeline_cache + .get(&(*view_entity, *visible_entity)) + .map(|(tick, _)| *tick); + let needs_specialization = last_specialized_tick.is_none_or(|tick| { + view_tick.is_newer_than(tick, ticks.this_run()) + || entity_tick.is_newer_than(tick, ticks.this_run()) + }); + if !needs_specialization { + continue; } - } - if ssao { - view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; - } - if distance_fog { - view_key |= MeshPipelineKey::DISTANCE_FOG; - } - if let Some(camera_3d) = camera_3d { - view_key |= screen_space_specular_transmission_pipeline_key( - camera_3d.screen_space_specular_transmission_quality, - ); - } - - let rangefinder = view.rangefinder3d(); - for (render_entity, visible_entity) in visible_entities.iter::() { let Some(material_asset_id) = render_material_instances.get(visible_entity) else { continue; }; @@ -849,15 +848,13 @@ pub fn queue_material_meshes( let mut mesh_pipeline_key_bits = material.properties.mesh_pipeline_key_bits; mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key( material.properties.alpha_mode, - msaa, + &Msaa::from_samples(view_key.msaa_samples()), )); - let mut mesh_key = view_key + let mut mesh_key = *view_key | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits()) | mesh_pipeline_key_bits; - let mut lightmap_slab = None; if let Some(lightmap) = render_lightmaps.render_lightmaps.get(visible_entity) { - lightmap_slab = Some(*lightmap.slab_index); mesh_key |= MeshPipelineKey::LIGHTMAPPED; if lightmap.bicubic_sampling { @@ -869,7 +866,7 @@ pub fn queue_material_meshes( mesh_key |= MeshPipelineKey::VISIBILITY_RANGE_DITHER; } - if motion_vector_prepass { + if view_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { // If the previous frame have skins or morph targets, note that. if mesh_instance .flags @@ -885,17 +882,13 @@ pub fn queue_material_meshes( } } - let pipeline_id = pipelines.specialize( - &pipeline_cache, - &material_pipeline, - MaterialPipelineKey { - mesh_key, - bind_group_data: material_bind_group - .get_extra_data(material.binding.slot) - .clone(), - }, - &mesh.layout, - ); + let key = MaterialPipelineKey { + mesh_key, + bind_group_data: material_bind_group + .get_extra_data(material.binding.slot) + .clone(), + }; + let pipeline_id = pipelines.specialize(&pipeline_cache, &pipeline, key, &mesh.layout); let pipeline_id = match pipeline_id { Ok(id) => id, Err(err) => { @@ -904,90 +897,136 @@ pub fn queue_material_meshes( } }; + specialized_material_pipeline_cache.insert( + (*view_entity, *visible_entity), + (ticks.this_run(), pipeline_id), + ); + } + } +} + +/// 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>>, + render_mesh_instances: Res, + render_material_instances: Res>, + mesh_allocator: Res, + gpu_preprocessing_support: Res, + mut opaque_render_phases: ResMut>, + mut alpha_mask_render_phases: ResMut>, + mut transmissive_render_phases: ResMut>, + mut transparent_render_phases: ResMut>, + views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>, + specialized_material_pipeline_cache: ResMut>, +) where + M::Data: PartialEq + Eq + Hash + Clone, +{ + for (view_entity, view, visible_entities) in &views { + let ( + Some(opaque_phase), + Some(alpha_mask_phase), + Some(transmissive_phase), + Some(transparent_phase), + ) = ( + opaque_render_phases.get_mut(&view.retained_view_entity), + alpha_mask_render_phases.get_mut(&view.retained_view_entity), + transmissive_render_phases.get_mut(&view.retained_view_entity), + transparent_render_phases.get_mut(&view.retained_view_entity), + ) + else { + continue; + }; + + let rangefinder = view.rangefinder3d(); + for (render_entity, visible_entity) in visible_entities.iter::() { + let Some(pipeline_id) = specialized_material_pipeline_cache + .get(&(*view_entity, *visible_entity)) + .map(|(_, pipeline_id)| *pipeline_id) + else { + continue; + }; + let Some(material_asset_id) = render_material_instances.get(visible_entity) 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 { + continue; + }; + // Fetch the slabs that this mesh resides in. let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); - match mesh_key - .intersection(MeshPipelineKey::BLEND_RESERVED_BITS | MeshPipelineKey::MAY_DISCARD) - { - MeshPipelineKey::BLEND_OPAQUE | MeshPipelineKey::BLEND_ALPHA_TO_COVERAGE => { - if material.properties.reads_view_transmission_texture { - let distance = rangefinder.distance_translation(&mesh_instance.translation) - + material.properties.depth_bias; - transmissive_phase.add(Transmissive3d { - entity: (*render_entity, *visible_entity), - draw_function: draw_transmissive_pbr, - pipeline: pipeline_id, - distance, - batch_range: 0..1, - extra_index: PhaseItemExtraIndex::None, - indexed: index_slab.is_some(), - }); - } else if material.properties.render_method == OpaqueRendererMethod::Forward { - let batch_set_key = Opaque3dBatchSetKey { - pipeline: pipeline_id, - draw_function: draw_opaque_pbr, - material_bind_group_index: Some(material.binding.group.0), - vertex_slab: vertex_slab.unwrap_or_default(), - index_slab, - lightmap_slab, - }; - let bin_key = Opaque3dBinKey { - asset_id: mesh_instance.mesh_asset_id.into(), - }; - opaque_phase.add( - batch_set_key, - bin_key, - (*render_entity, *visible_entity), - BinnedRenderPhaseType::mesh( - mesh_instance.should_batch(), - &gpu_preprocessing_support, - ), - ); + match material.properties.render_phase_type { + RenderPhaseType::Transmissive => { + let distance = rangefinder.distance_translation(&mesh_instance.translation) + + material.properties.depth_bias; + transmissive_phase.add(Transmissive3d { + entity: (*render_entity, *visible_entity), + draw_function: material.properties.draw_function_id, + pipeline: pipeline_id, + distance, + batch_range: 0..1, + extra_index: PhaseItemExtraIndex::None, + indexed: index_slab.is_some(), + }); + } + RenderPhaseType::Opaque => { + if material.properties.render_method == OpaqueRendererMethod::Deferred { + continue; } + let batch_set_key = Opaque3dBatchSetKey { + pipeline: pipeline_id, + draw_function: material.properties.draw_function_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + lightmap_slab: mesh_instance.shared.lightmap_slab_index.map(|index| *index), + }; + let bin_key = Opaque3dBinKey { + asset_id: mesh_instance.mesh_asset_id.into(), + }; + opaque_phase.add( + batch_set_key, + bin_key, + (*render_entity, *visible_entity), + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + ); } // Alpha mask - MeshPipelineKey::MAY_DISCARD => { - if material.properties.reads_view_transmission_texture { - let distance = rangefinder.distance_translation(&mesh_instance.translation) - + material.properties.depth_bias; - transmissive_phase.add(Transmissive3d { - entity: (*render_entity, *visible_entity), - draw_function: draw_transmissive_pbr, - pipeline: pipeline_id, - distance, - batch_range: 0..1, - extra_index: PhaseItemExtraIndex::None, - indexed: index_slab.is_some(), - }); - } else if material.properties.render_method == OpaqueRendererMethod::Forward { - let batch_set_key = OpaqueNoLightmap3dBatchSetKey { - draw_function: draw_alpha_mask_pbr, - pipeline: pipeline_id, - material_bind_group_index: Some(material.binding.group.0), - vertex_slab: vertex_slab.unwrap_or_default(), - index_slab, - }; - let bin_key = OpaqueNoLightmap3dBinKey { - asset_id: mesh_instance.mesh_asset_id.into(), - }; - alpha_mask_phase.add( - batch_set_key, - bin_key, - (*render_entity, *visible_entity), - BinnedRenderPhaseType::mesh( - mesh_instance.should_batch(), - &gpu_preprocessing_support, - ), - ); - } + RenderPhaseType::AlphaMask => { + let batch_set_key = OpaqueNoLightmap3dBatchSetKey { + draw_function: material.properties.draw_function_id, + pipeline: pipeline_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; + let bin_key = OpaqueNoLightmap3dBinKey { + asset_id: mesh_instance.mesh_asset_id.into(), + }; + alpha_mask_phase.add( + batch_set_key, + bin_key, + (*render_entity, *visible_entity), + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + ); } - _ => { + RenderPhaseType::Transparent => { let distance = rangefinder.distance_translation(&mesh_instance.translation) + material.properties.depth_bias; transparent_phase.add(Transparent3d { entity: (*render_entity, *visible_entity), - draw_function: draw_transparent_pbr, + draw_function: material.properties.draw_function_id, pipeline: pipeline_id, distance, batch_range: 0..1, @@ -1070,6 +1109,18 @@ pub struct MaterialProperties { /// This allows taking color output from the [`Opaque3d`] pass as an input, (for screen-space transmission) but requires /// 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, +} + +#[derive(Clone, Copy)] +pub enum RenderPhaseType { + Opaque, + AlphaMask, + Transmissive, + Transparent, } /// A resource that maps each untyped material ID to its binding. @@ -1096,6 +1147,14 @@ impl RenderAsset for PreparedMaterial { SRes, SResMut>, SResMut, + SRes>, + SRes>, + SRes>, + SRes>, + SRes>, + SRes>, + SRes>, + SRes>, M::Param, ); @@ -1108,6 +1167,14 @@ impl RenderAsset for PreparedMaterial { default_opaque_render_method, ref mut bind_group_allocator, ref mut render_material_bindings, + opaque_draw_functions, + alpha_mask_draw_functions, + transmissive_draw_functions, + transparent_draw_functions, + opaque_prepass_draw_functions, + alpha_mask_prepass_draw_functions, + opaque_deferred_draw_functions, + alpha_mask_deferred_draw_functions, ref mut material_param, ): &mut SystemParamItem, ) -> Result> { @@ -1116,17 +1183,64 @@ impl RenderAsset for PreparedMaterial { .entry(material_id.into()) .or_insert_with(|| bind_group_allocator.allocate()); - let method = match material.opaque_render_method() { + 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::>(); + let draw_opaque_deferred = opaque_deferred_draw_functions + .read() + .get_id::>(); + let draw_alpha_mask_deferred = alpha_mask_deferred_draw_functions + .read() + .get_id::>(); + + let render_method = match material.opaque_render_method() { OpaqueRendererMethod::Forward => OpaqueRendererMethod::Forward, OpaqueRendererMethod::Deferred => OpaqueRendererMethod::Deferred, OpaqueRendererMethod::Auto => default_opaque_render_method.0, }; + let mut mesh_pipeline_key_bits = MeshPipelineKey::empty(); mesh_pipeline_key_bits.set( MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE, material.reads_view_transmission_texture(), ); + let reads_view_transmission_texture = + mesh_pipeline_key_bits.contains(MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE); + + let render_phase_type = match material.alpha_mode() { + AlphaMode::Blend | AlphaMode::Premultiplied | AlphaMode::Add | AlphaMode::Multiply => { + RenderPhaseType::Transparent + } + _ if reads_view_transmission_texture => RenderPhaseType::Transmissive, + AlphaMode::Opaque | AlphaMode::AlphaToCoverage => RenderPhaseType::Opaque, + AlphaMode::Mask(_) => RenderPhaseType::AlphaMask, + }; + + let draw_function_id = match render_phase_type { + RenderPhaseType::Opaque => draw_opaque_pbr, + RenderPhaseType::AlphaMask => draw_alpha_mask_pbr, + RenderPhaseType::Transmissive => draw_transmissive_pbr, + RenderPhaseType::Transparent => draw_transparent_pbr, + }; + let prepass_draw_function_id = match render_phase_type { + RenderPhaseType::Opaque => draw_opaque_prepass, + RenderPhaseType::AlphaMask => draw_alpha_mask_prepass, + _ => None, + }; + let deferred_draw_function_id = match render_phase_type { + RenderPhaseType::Opaque => draw_opaque_deferred, + RenderPhaseType::AlphaMask => draw_alpha_mask_deferred, + _ => None, + }; + match material.unprepared_bind_group( &pipeline.material_layout, render_device, @@ -1141,10 +1255,13 @@ impl RenderAsset for PreparedMaterial { properties: MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), - reads_view_transmission_texture: mesh_pipeline_key_bits - .contains(MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE), - render_method: method, + 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, }) @@ -1177,10 +1294,13 @@ impl RenderAsset for PreparedMaterial { properties: MaterialProperties { alpha_mode: material.alpha_mode(), depth_bias: material.depth_bias(), - reads_view_transmission_texture: mesh_pipeline_key_bits - .contains(MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE), - render_method: method, + 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, }) @@ -1206,7 +1326,7 @@ impl RenderAsset for PreparedMaterial { _, ref mut bind_group_allocator, ref mut render_material_bindings, - _, + .., ): &mut SystemParamItem, ) { let Some(material_binding_id) = render_material_bindings.remove(&source_asset.untyped()) diff --git a/crates/bevy_pbr/src/mesh_material.rs b/crates/bevy_pbr/src/mesh_material.rs index 84eaf7cffa79a..c3f81943ecbcf 100644 --- a/crates/bevy_pbr/src/mesh_material.rs +++ b/crates/bevy_pbr/src/mesh_material.rs @@ -1,5 +1,5 @@ use crate::Material; -use bevy_asset::{AssetId, Handle}; +use bevy_asset::{AsAssetId, AssetId, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{component::Component, reflect::ReflectComponent}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -57,3 +57,11 @@ impl From<&MeshMaterial3d> for AssetId { material.id() } } + +impl AsAssetId for MeshMaterial3d { + type Asset = M; + + fn as_asset_id(&self) -> AssetId { + self.id() + } +} diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 3bc2dc51e3591..3d6dcf0c12538 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -3,10 +3,11 @@ mod prepass_bindings; use crate::{ alpha_mode_pipeline_key, binding_arrays_are_usable, buffer_layout, material_bind_groups::MaterialBindGroupAllocator, queue_material_meshes, - setup_morph_and_skinning_defs, skin, DrawMesh, Material, MaterialPipeline, MaterialPipelineKey, - MeshLayouts, MeshPipeline, MeshPipelineKey, OpaqueRendererMethod, PreparedMaterial, - RenderLightmaps, RenderMaterialInstances, RenderMeshInstanceFlags, RenderMeshInstances, - SetMaterialBindGroup, SetMeshBindGroup, ShadowView, StandardMaterial, + setup_morph_and_skinning_defs, skin, DrawMesh, EntitySpecializationTicks, Material, + MaterialPipeline, MaterialPipelineKey, MeshLayouts, MeshPipeline, MeshPipelineKey, + OpaqueRendererMethod, PreparedMaterial, RenderLightmaps, RenderMaterialInstances, + RenderMeshInstanceFlags, RenderMeshInstances, RenderPhaseType, SetMaterialBindGroup, + SetMeshBindGroup, ShadowView, StandardMaterial, }; use bevy_app::{App, Plugin, PreUpdate}; use bevy_render::{ @@ -53,7 +54,14 @@ use crate::meshlet::{ MeshletMesh3d, }; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::component::Tick; +use bevy_ecs::entity::EntityHash; +use bevy_ecs::system::SystemChangeTick; +use bevy_platform_support::collections::HashMap; +use bevy_render::sync_world::{MainEntity, MainEntityHashMap}; use bevy_render::view::RenderVisibleEntities; +use bevy_render::RenderSet::PrepareAssets; use core::{hash::Hash, marker::PhantomData}; pub const PREPASS_SHADER_HANDLE: Handle = Handle::weak_from_u128(921124473254008983); @@ -184,17 +192,27 @@ where } render_app + .init_resource::() + .init_resource::() + .init_resource::>() .add_render_command::>() .add_render_command::>() .add_render_command::>() .add_render_command::>() .add_systems( Render, - queue_prepass_material_meshes:: - .in_set(RenderSet::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::), + ( + check_prepass_views_need_specialization.in_set(PrepareAssets), + specialize_prepass_material_meshes:: + .in_set(PrepareAssets) + .after(prepare_assets::>) + .after(prepare_assets::), + queue_prepass_material_meshes:: + .in_set(RenderSet::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::), + ), ); #[cfg(feature = "meshlet")] @@ -771,109 +789,151 @@ pub fn prepare_prepass_view_bind_group( } } -pub fn queue_prepass_material_meshes( - ( - opaque_draw_functions, - alpha_mask_draw_functions, - opaque_deferred_draw_functions, - alpha_mask_deferred_draw_functions, - ): ( - Res>, - Res>, - Res>, - Res>, - ), - prepass_pipeline: Res>, - mut pipelines: ResMut>>, - pipeline_cache: Res, - (render_meshes, render_mesh_instances): ( - Res>, - Res, - ), +#[derive(Resource, Deref, DerefMut)] +pub struct SpecializedPrepassMaterialPipelineCache { + // (view_entity, material_entity) -> (tick, pipeline_id) + #[deref] + map: HashMap<(MainEntity, MainEntity), (Tick, CachedRenderPipelineId), EntityHash>, + marker: PhantomData, +} + +impl Default for SpecializedPrepassMaterialPipelineCache { + fn default() -> Self { + Self { + map: HashMap::default(), + marker: PhantomData, + } + } +} + +#[derive(Resource, Deref, DerefMut, Default, Clone)] +pub struct ViewKeyPrepassCache(MainEntityHashMap); + +#[derive(Resource, Deref, DerefMut, Default, Clone)] +pub struct ViewPrepassSpecializationTicks(MainEntityHashMap); + +pub fn check_prepass_views_need_specialization( + mut view_key_cache: ResMut, + mut view_specialization_ticks: ResMut, + mut views: Query<( + &MainEntity, + &Msaa, + Option<&DepthPrepass>, + Option<&NormalPrepass>, + Option<&MotionVectorPrepass>, + )>, + ticks: SystemChangeTick, +) { + for (view_entity, msaa, depth_prepass, normal_prepass, motion_vector_prepass) in + views.iter_mut() + { + let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()); + if depth_prepass.is_some() { + view_key |= MeshPipelineKey::DEPTH_PREPASS; + } + if normal_prepass.is_some() { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + if motion_vector_prepass.is_some() { + view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; + } + + if let Some(current_key) = view_key_cache.get_mut(view_entity) { + if *current_key != view_key { + view_key_cache.insert(*view_entity, view_key); + view_specialization_ticks.insert(*view_entity, ticks.this_run()); + } + } else { + view_key_cache.insert(*view_entity, view_key); + view_specialization_ticks.insert(*view_entity, ticks.this_run()); + } + } +} + +pub fn specialize_prepass_material_meshes( + render_meshes: Res>, render_materials: Res>>, + render_mesh_instances: Res, render_material_instances: Res>, render_lightmaps: Res, render_visibility_ranges: Res, - (mesh_allocator, material_bind_group_allocator): ( - Res, - Res>, - ), - gpu_preprocessing_support: Res, - mut opaque_prepass_render_phases: ResMut>, - mut alpha_mask_prepass_render_phases: ResMut>, - mut opaque_deferred_render_phases: ResMut>, - mut alpha_mask_deferred_render_phases: ResMut>, + material_bind_group_allocator: Res>, + view_key_cache: Res, views: Query<( + &MainEntity, &ExtractedView, &RenderVisibleEntities, &Msaa, - Option<&DepthPrepass>, - Option<&NormalPrepass>, Option<&MotionVectorPrepass>, Option<&DeferredPrepass>, )>, + ( + opaque_prepass_render_phases, + alpha_mask_prepass_render_phases, + opaque_deferred_render_phases, + alpha_mask_deferred_render_phases, + ): ( + Res>, + Res>, + Res>, + Res>, + ), + ( + mut specialized_material_pipeline_cache, + ticks, + prepass_pipeline, + mut pipelines, + pipeline_cache, + view_specialization_ticks, + entity_specialization_ticks, + ): ( + ResMut>, + SystemChangeTick, + Res>, + ResMut>>, + Res, + Res, + Res>, + ), ) where + M: Material, M::Data: PartialEq + Eq + Hash + Clone, { - let opaque_draw_prepass = opaque_draw_functions - .read() - .get_id::>() - .unwrap(); - let alpha_mask_draw_prepass = alpha_mask_draw_functions - .read() - .get_id::>() - .unwrap(); - let opaque_draw_deferred = opaque_deferred_draw_functions - .read() - .get_id::>() - .unwrap(); - let alpha_mask_draw_deferred = alpha_mask_deferred_draw_functions - .read() - .get_id::>() - .unwrap(); for ( + view_entity, extracted_view, visible_entities, msaa, - depth_prepass, - normal_prepass, motion_vector_prepass, deferred_prepass, ) in &views { - let ( - mut opaque_phase, - mut alpha_mask_phase, - mut opaque_deferred_phase, - mut alpha_mask_deferred_phase, - ) = ( - opaque_prepass_render_phases.get_mut(&extracted_view.retained_view_entity), - alpha_mask_prepass_render_phases.get_mut(&extracted_view.retained_view_entity), - opaque_deferred_render_phases.get_mut(&extracted_view.retained_view_entity), - alpha_mask_deferred_render_phases.get_mut(&extracted_view.retained_view_entity), - ); - - // Skip if there's no place to put the mesh. - if opaque_phase.is_none() - && alpha_mask_phase.is_none() - && opaque_deferred_phase.is_none() - && alpha_mask_deferred_phase.is_none() + if !opaque_deferred_render_phases.contains_key(&extracted_view.retained_view_entity) + && !alpha_mask_deferred_render_phases.contains_key(&extracted_view.retained_view_entity) + && !opaque_prepass_render_phases.contains_key(&extracted_view.retained_view_entity) + && !alpha_mask_prepass_render_phases.contains_key(&extracted_view.retained_view_entity) { continue; } - let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()); - if depth_prepass.is_some() { - view_key |= MeshPipelineKey::DEPTH_PREPASS; - } - if normal_prepass.is_some() { - view_key |= MeshPipelineKey::NORMAL_PREPASS; - } - if motion_vector_prepass.is_some() { - view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; - } + let Some(view_key) = view_key_cache.get(view_entity) else { + continue; + }; + + for (_, visible_entity) in visible_entities.iter::() { + let view_tick = view_specialization_ticks.get(view_entity).unwrap(); + let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap(); + let last_specialized_tick = specialized_material_pipeline_cache + .get(&(*view_entity, *visible_entity)) + .map(|(tick, _)| *tick); + let needs_specialization = last_specialized_tick.is_none_or(|tick| { + view_tick.is_newer_than(tick, ticks.this_run()) + || entity_tick.is_newer_than(tick, ticks.this_run()) + }); + if !needs_specialization { + continue; + } - for (render_entity, visible_entity) in visible_entities.iter::() { let Some(material_asset_id) = render_material_instances.get(visible_entity) else { continue; }; @@ -893,7 +953,7 @@ pub fn queue_prepass_material_meshes( continue; }; - let mut mesh_key = view_key | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits()); + let mut mesh_key = *view_key | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits()); let alpha_mode = material.properties.alpha_mode; match alpha_mode { @@ -976,17 +1036,85 @@ pub fn queue_prepass_material_meshes( } }; + specialized_material_pipeline_cache.insert( + (*view_entity, *visible_entity), + (ticks.this_run(), pipeline_id), + ); + } + } +} + +pub fn queue_prepass_material_meshes( + render_mesh_instances: Res, + render_materials: Res>>, + render_material_instances: Res>, + mesh_allocator: Res, + gpu_preprocessing_support: Res, + mut opaque_prepass_render_phases: ResMut>, + mut alpha_mask_prepass_render_phases: ResMut>, + mut opaque_deferred_render_phases: ResMut>, + mut alpha_mask_deferred_render_phases: ResMut>, + views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>, + specialized_material_pipeline_cache: Res>, +) where + M::Data: PartialEq + Eq + Hash + Clone, +{ + for (view_entity, extracted_view, visible_entities) in &views { + let ( + mut opaque_phase, + mut alpha_mask_phase, + mut opaque_deferred_phase, + mut alpha_mask_deferred_phase, + ) = ( + opaque_prepass_render_phases.get_mut(&extracted_view.retained_view_entity), + alpha_mask_prepass_render_phases.get_mut(&extracted_view.retained_view_entity), + opaque_deferred_render_phases.get_mut(&extracted_view.retained_view_entity), + alpha_mask_deferred_render_phases.get_mut(&extracted_view.retained_view_entity), + ); + + // Skip if there's no place to put the mesh. + if opaque_phase.is_none() + && alpha_mask_phase.is_none() + && opaque_deferred_phase.is_none() + && alpha_mask_deferred_phase.is_none() + { + continue; + } + + for (render_entity, visible_entity) in visible_entities.iter::() { + let Some((_, pipeline_id)) = + specialized_material_pipeline_cache.get(&(*view_entity, *visible_entity)) + else { + continue; + }; + let Some(material_asset_id) = render_material_instances.get(visible_entity) 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 { + continue; + }; let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); - match mesh_key - .intersection(MeshPipelineKey::BLEND_RESERVED_BITS | MeshPipelineKey::MAY_DISCARD) - { - MeshPipelineKey::BLEND_OPAQUE | MeshPipelineKey::BLEND_ALPHA_TO_COVERAGE => { + let deferred = match material.properties.render_method { + OpaqueRendererMethod::Forward => false, + OpaqueRendererMethod::Deferred => true, + OpaqueRendererMethod::Auto => unreachable!(), + }; + + match material.properties.render_phase_type { + RenderPhaseType::Opaque => { if deferred { opaque_deferred_phase.as_mut().unwrap().add( OpaqueNoLightmap3dBatchSetKey { - draw_function: opaque_draw_deferred, - pipeline: pipeline_id, + draw_function: material + .properties + .deferred_draw_function_id + .unwrap(), + pipeline: *pipeline_id, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), index_slab, @@ -1005,8 +1133,11 @@ pub fn queue_prepass_material_meshes( mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); opaque_phase.add( OpaqueNoLightmap3dBatchSetKey { - draw_function: opaque_draw_prepass, - pipeline: pipeline_id, + draw_function: material + .properties + .prepass_draw_function_id + .unwrap(), + pipeline: *pipeline_id, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), index_slab, @@ -1022,14 +1153,13 @@ pub fn queue_prepass_material_meshes( ); } } - // Alpha mask - MeshPipelineKey::MAY_DISCARD => { + RenderPhaseType::AlphaMask => { if deferred { let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); let batch_set_key = OpaqueNoLightmap3dBatchSetKey { - draw_function: alpha_mask_draw_deferred, - pipeline: pipeline_id, + draw_function: material.properties.deferred_draw_function_id.unwrap(), + pipeline: *pipeline_id, material_bind_group_index: Some(material.binding.group.0), vertex_slab: vertex_slab.unwrap_or_default(), index_slab, @@ -1050,8 +1180,8 @@ 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: alpha_mask_draw_prepass, - pipeline: pipeline_id, + draw_function: material.properties.prepass_draw_function_id.unwrap(), + pipeline: *pipeline_id, 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/light.rs b/crates/bevy_pbr/src/render/light.rs index ff4c0b0ac8d48..5cdea0627b7b7 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -5,6 +5,9 @@ use bevy_asset::UntypedAssetId; use bevy_color::ColorToComponents; use bevy_core_pipeline::core_3d::{Camera3d, CORE_3D_DEPTH_FORMAT}; use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::component::Tick; +use bevy_ecs::entity::EntityHash; +use bevy_ecs::system::SystemChangeTick; use bevy_ecs::{ entity::{hash_map::EntityHashMap, hash_set::EntityHashSet}, prelude::*, @@ -37,7 +40,7 @@ use bevy_render::{ }; use bevy_transform::{components::GlobalTransform, prelude::Transform}; use bevy_utils::default; -use core::{hash::Hash, ops::Range}; +use core::{hash::Hash, marker::PhantomData, ops::Range}; #[cfg(feature = "trace")] use tracing::info_span; use tracing::{error, warn}; @@ -1580,25 +1583,99 @@ fn despawn_entities(commands: &mut Commands, entities: Vec) { }); } -/// 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>, +// These will be extracted in the material extraction, which will also clear the needs_specialization +// collection. +pub fn check_light_entities_needing_specialization( + needs_specialization: Query>, Changed)>, + mut entities_needing_specialization: ResMut>, + mut removed_components: RemovedComponents, +) { + for entity in &needs_specialization { + entities_needing_specialization.push(entity); + } + + for removed in removed_components.read() { + entities_needing_specialization.entities.push(removed); + } +} + +#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] +pub struct LightKeyCache(EntityHashMap); + +#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] +pub struct LightSpecializationTicks(EntityHashMap); + +#[derive(Resource, Deref, DerefMut)] +pub struct SpecializedShadowMaterialPipelineCache { + // (view_light_entity, visible_entity) -> (tick, pipeline_id) + #[deref] + map: HashMap<(Entity, MainEntity), (Tick, CachedRenderPipelineId), EntityHash>, + marker: PhantomData, +} + +impl Default for SpecializedShadowMaterialPipelineCache { + fn default() -> Self { + Self { + map: HashMap::default(), + marker: PhantomData, + } + } +} + +pub fn check_views_lights_need_specialization( + view_lights: Query<(Entity, &ViewLightEntities), With>, + view_light_entities: Query<(&LightEntity, &ExtractedView)>, + shadow_render_phases: Res>, + mut light_key_cache: ResMut, + mut light_specialization_ticks: ResMut, + ticks: SystemChangeTick, +) { + for (entity, view_lights) in &view_lights { + for view_light_entity in view_lights.lights.iter().copied() { + let Ok((light_entity, extracted_view_light)) = + view_light_entities.get(view_light_entity) + else { + continue; + }; + if !shadow_render_phases.contains_key(&extracted_view_light.retained_view_entity) { + continue; + } + + let is_directional_light = matches!(light_entity, LightEntity::Directional { .. }); + let mut light_key = MeshPipelineKey::DEPTH_PREPASS; + light_key.set(MeshPipelineKey::UNCLIPPED_DEPTH_ORTHO, is_directional_light); + if let Some(current_key) = light_key_cache.get_mut(&entity) { + if *current_key != light_key { + light_key_cache.insert(view_light_entity, light_key); + light_specialization_ticks.insert(view_light_entity, ticks.this_run()); + } + } else { + light_key_cache.insert(view_light_entity, light_key); + light_specialization_ticks.insert(view_light_entity, ticks.this_run()); + } + } + } +} + +pub fn specialize_shadows( prepass_pipeline: Res>, - (render_meshes, render_mesh_instances, render_materials, render_material_instances): ( + ( + render_meshes, + render_mesh_instances, + render_materials, + render_material_instances, + material_bind_group_allocator, + ): ( Res>, Res, Res>>, Res>, + Res>, ), - material_bind_group_allocator: Res>, - mut shadow_render_phases: ResMut>, + shadow_render_phases: Res>, mut pipelines: ResMut>>, pipeline_cache: Res, render_lightmaps: Res, - gpu_preprocessing_support: Res, - mesh_allocator: Res, view_lights: Query<(Entity, &ViewLightEntities), With>, view_light_entities: Query<(&LightEntity, &ExtractedView)>, point_light_entities: Query<&RenderCubemapVisibleEntities, With>, @@ -1607,24 +1684,28 @@ pub fn queue_shadows( With, >, spot_light_entities: Query<&RenderVisibleMeshEntities, With>, + light_key_cache: Res, + mut specialized_material_pipeline_cache: ResMut>, + light_specialization_ticks: Res, + entity_specialization_ticks: Res>, + ticks: SystemChangeTick, ) where M::Data: PartialEq + Eq + Hash + Clone, { for (entity, view_lights) in &view_lights { - let draw_shadow_mesh = shadow_draw_functions.read().id::>(); for view_light_entity in view_lights.lights.iter().copied() { let Ok((light_entity, extracted_view_light)) = view_light_entities.get(view_light_entity) else { continue; }; - let Some(shadow_phase) = - shadow_render_phases.get_mut(&extracted_view_light.retained_view_entity) - else { + if !shadow_render_phases.contains_key(&extracted_view_light.retained_view_entity) { + continue; + } + let Some(light_key) = light_key_cache.get(&view_light_entity) else { continue; }; - let is_directional_light = matches!(light_entity, LightEntity::Directional { .. }); let visible_entities = match light_entity { LightEntity::Directional { light_entity, @@ -1648,14 +1729,25 @@ pub fn queue_shadows( .get(*light_entity) .expect("Failed to get spot light visible entities"), }; - let mut light_key = MeshPipelineKey::DEPTH_PREPASS; - light_key.set(MeshPipelineKey::UNCLIPPED_DEPTH_ORTHO, is_directional_light); // NOTE: Lights with shadow mapping disabled will have no visible entities // so no meshes will be queued - for (entity, main_entity) in visible_entities.iter().copied() { - let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(main_entity) + for (_, visible_entity) in visible_entities.iter().copied() { + let view_tick = light_specialization_ticks.get(&view_light_entity).unwrap(); + let entity_tick = entity_specialization_ticks.get(&visible_entity).unwrap(); + let last_specialized_tick = specialized_material_pipeline_cache + .get(&(view_light_entity, visible_entity)) + .map(|(tick, _)| *tick); + let needs_specialization = last_specialized_tick.is_none_or(|tick| { + view_tick.is_newer_than(tick, ticks.this_run()) + || entity_tick.is_newer_than(tick, ticks.this_run()) + }); + if !needs_specialization { + continue; + } + let Some(mesh_instance) = + render_mesh_instances.render_mesh_queue_data(visible_entity) else { continue; }; @@ -1665,7 +1757,7 @@ pub fn queue_shadows( { continue; } - let Some(material_asset_id) = render_material_instances.get(&main_entity) else { + let Some(material_asset_id) = render_material_instances.get(&visible_entity) else { continue; }; let Some(material) = render_materials.get(*material_asset_id) else { @@ -1681,14 +1773,17 @@ pub fn queue_shadows( }; let mut mesh_key = - light_key | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits()); + *light_key | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits()); // Even though we don't use the lightmap in the shadow map, the // `SetMeshBindGroup` render command will bind the data for it. So // we need to include the appropriate flag in the mesh pipeline key // to ensure that the necessary bind group layout entries are // present. - if render_lightmaps.render_lightmaps.contains_key(&main_entity) { + if render_lightmaps + .render_lightmaps + .contains_key(&visible_entity) + { mesh_key |= MeshPipelineKey::LIGHTMAPPED; } @@ -1720,11 +1815,96 @@ pub fn queue_shadows( } }; + specialized_material_pipeline_cache.insert( + (view_light_entity, visible_entity), + (ticks.this_run(), pipeline_id), + ); + } + } + } +} + +/// 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>, + render_mesh_instances: Res, + mut shadow_render_phases: ResMut>, + gpu_preprocessing_support: Res, + mesh_allocator: Res, + view_lights: Query<(Entity, &ViewLightEntities), With>, + view_light_entities: Query<(&LightEntity, &ExtractedView)>, + point_light_entities: Query<&RenderCubemapVisibleEntities, With>, + directional_light_entities: Query< + &RenderCascadesVisibleEntities, + 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::>(); + for (entity, view_lights) in &view_lights { + for view_light_entity in view_lights.lights.iter().copied() { + let Ok((light_entity, extracted_view_light)) = + view_light_entities.get(view_light_entity) + else { + continue; + }; + let Some(shadow_phase) = + shadow_render_phases.get_mut(&extracted_view_light.retained_view_entity) + else { + continue; + }; + + let visible_entities = match light_entity { + LightEntity::Directional { + light_entity, + cascade_index, + } => directional_light_entities + .get(*light_entity) + .expect("Failed to get directional light visible entities") + .entities + .get(&entity) + .expect("Failed to get directional light visible entities for view") + .get(*cascade_index) + .expect("Failed to get directional light visible entities for cascade"), + LightEntity::Point { + light_entity, + face_index, + } => point_light_entities + .get(*light_entity) + .expect("Failed to get point light visible entities") + .get(*face_index), + LightEntity::Spot { light_entity } => spot_light_entities + .get(*light_entity) + .expect("Failed to get spot light visible entities"), + }; + + for (entity, main_entity) in visible_entities.iter().copied() { + let Some((_, pipeline_id)) = + specialized_material_pipeline_cache.get(&(view_light_entity, main_entity)) + else { + continue; + }; + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(main_entity) + else { + continue; + }; + if !mesh_instance + .flags + .contains(RenderMeshInstanceFlags::SHADOW_CASTER) + { + continue; + } + let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); let batch_set_key = ShadowBatchSetKey { - pipeline: pipeline_id, + pipeline: *pipeline_id, draw_function: draw_shadow_mesh, 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 33ccbcc9afb03..0b29e73f5b6f6 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1,5 +1,3 @@ -use core::mem::size_of; - use crate::material_bind_groups::{MaterialBindGroupIndex, MaterialBindGroupSlot}; use allocator::MeshAllocator; use bevy_asset::{load_internal_asset, AssetId, UntypedAssetId}; @@ -46,10 +44,14 @@ use bevy_render::{ }; use bevy_transform::components::GlobalTransform; use bevy_utils::{default, Parallel}; +use core::mem::size_of; use material_bind_groups::MaterialBindingId; use render::skin::{self, SkinIndex}; use tracing::{error, warn}; +use self::irradiance_volume::IRRADIANCE_VOLUMES_ARE_USABLE; +use crate::environment_map::EnvironmentMapLight; +use crate::irradiance_volume::IrradianceVolume; use crate::{ render::{ morph::{ @@ -60,14 +62,22 @@ use crate::{ }, *, }; +use bevy_core_pipeline::core_3d::Camera3d; +use bevy_core_pipeline::oit::OrderIndependentTransparencySettings; +use bevy_core_pipeline::prepass::{DeferredPrepass, DepthPrepass, NormalPrepass}; +use bevy_core_pipeline::tonemapping::{DebandDither, Tonemapping}; +use bevy_ecs::component::Tick; +use bevy_ecs::system::SystemChangeTick; +use bevy_render::camera::TemporalJitter; +use bevy_render::prelude::Msaa; use bevy_render::sync_world::{MainEntity, MainEntityHashMap}; +use bevy_render::view::ExtractedView; +use bevy_render::RenderSet::PrepareAssets; use bytemuck::{Pod, Zeroable}; use nonmax::{NonMaxU16, NonMaxU32}; use smallvec::{smallvec, SmallVec}; use static_assertions::const_assert_eq; -use self::irradiance_volume::IRRADIANCE_VOLUMES_ARE_USABLE; - /// Provides support for rendering 3D meshes. #[derive(Default)] pub struct MeshRenderPlugin { @@ -204,8 +214,14 @@ impl Plugin for MeshRenderPlugin { if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app + .init_resource::() + .init_resource::() .init_resource::() - .init_resource::(); + .init_resource::() + .add_systems( + Render, + check_views_need_specialization.in_set(PrepareAssets), + ); let gpu_preprocessing_support = render_app.world().resource::(); @@ -283,6 +299,143 @@ impl Plugin for MeshRenderPlugin { } } +#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] +pub struct ViewKeyCache(MainEntityHashMap); + +#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] +pub struct ViewSpecializationTicks(MainEntityHashMap); + +pub fn check_views_need_specialization( + mut view_key_cache: ResMut, + mut view_specialization_ticks: ResMut, + mut views: Query<( + &MainEntity, + &ExtractedView, + &Msaa, + Option<&Tonemapping>, + Option<&DebandDither>, + Option<&ShadowFilteringMethod>, + Has, + ( + Has, + Has, + Has, + Has, + ), + Option<&Camera3d>, + Has, + Option<&Projection>, + Has, + ( + Has>, + Has>, + ), + Has, + )>, + ticks: SystemChangeTick, +) { + for ( + view_entity, + view, + msaa, + tonemapping, + dither, + shadow_filter_method, + ssao, + (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), + camera_3d, + temporal_jitter, + projection, + distance_fog, + (has_environment_maps, has_irradiance_volumes), + has_oit, + ) in views.iter_mut() + { + let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) + | MeshPipelineKey::from_hdr(view.hdr); + + if normal_prepass { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + + if depth_prepass { + view_key |= MeshPipelineKey::DEPTH_PREPASS; + } + + if motion_vector_prepass { + view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; + } + + if deferred_prepass { + view_key |= MeshPipelineKey::DEFERRED_PREPASS; + } + + if temporal_jitter { + view_key |= MeshPipelineKey::TEMPORAL_JITTER; + } + + if has_environment_maps { + view_key |= MeshPipelineKey::ENVIRONMENT_MAP; + } + + if has_irradiance_volumes { + view_key |= MeshPipelineKey::IRRADIANCE_VOLUME; + } + + if has_oit { + view_key |= MeshPipelineKey::OIT_ENABLED; + } + + if let Some(projection) = projection { + view_key |= match projection { + Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE, + Projection::Orthographic(_) => MeshPipelineKey::VIEW_PROJECTION_ORTHOGRAPHIC, + Projection::Custom(_) => MeshPipelineKey::VIEW_PROJECTION_NONSTANDARD, + }; + } + + match shadow_filter_method.unwrap_or(&ShadowFilteringMethod::default()) { + ShadowFilteringMethod::Hardware2x2 => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2; + } + ShadowFilteringMethod::Gaussian => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN; + } + ShadowFilteringMethod::Temporal => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL; + } + } + + if !view.hdr { + if let Some(tonemapping) = tonemapping { + view_key |= MeshPipelineKey::TONEMAP_IN_SHADER; + view_key |= tonemapping_pipeline_key(*tonemapping); + } + if let Some(DebandDither::Enabled) = dither { + view_key |= MeshPipelineKey::DEBAND_DITHER; + } + } + if ssao { + view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; + } + if distance_fog { + view_key |= MeshPipelineKey::DISTANCE_FOG; + } + if let Some(camera_3d) = camera_3d { + view_key |= screen_space_specular_transmission_pipeline_key( + camera_3d.screen_space_specular_transmission_quality, + ); + } + if !view_key_cache + .get_mut(view_entity) + .is_some_and(|current_key| *current_key == view_key) + { + view_key_cache.insert(*view_entity, view_key); + view_specialization_ticks.insert(*view_entity, ticks.this_run()); + } + } +} + #[derive(Component)] pub struct MeshTransforms { pub world_from_local: Affine3, @@ -568,6 +721,9 @@ pub struct RenderMeshInstanceShared { pub material_bindings_index: MaterialBindingId, /// Various flags. pub flags: RenderMeshInstanceFlags, + /// Index of the slab that the lightmap resides in, if a lightmap is + /// present. + pub lightmap_slab_index: Option, } /// Information that is gathered during the parallel portion of mesh extraction @@ -666,6 +822,7 @@ impl RenderMeshInstanceShared { flags: mesh_instance_flags, // This gets filled in later, during `RenderMeshGpuBuilder::update`. material_bindings_index: default(), + lightmap_slab_index: None, } } @@ -974,6 +1131,11 @@ impl RenderMeshInstanceGpuBuilder { Some(render_lightmap) => u16::from(*render_lightmap.slot_index), None => u16::MAX, }; + let lightmap_slab_index = render_lightmaps + .render_lightmaps + .get(&entity) + .map(|lightmap| lightmap.slab_index); + self.shared.lightmap_slab_index = lightmap_slab_index; // Create the mesh input uniform. let mut mesh_input_uniform = MeshInputUniform { diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index dc977ceb8404f..28362a30e80dc 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -1134,6 +1134,7 @@ pub fn extract_cameras( }) .collect(), }; + let mut commands = commands.entity(render_entity); commands.insert(( ExtractedCamera { diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index 3b04334f5b207..88d7c583ce7b9 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -2,6 +2,7 @@ use core::fmt::Debug; use crate::{primitives::Frustum, view::VisibilitySystems}; use bevy_app::{App, Plugin, PostStartup, PostUpdate}; +use bevy_asset::AssetEvents; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; use bevy_math::{ops, AspectRatio, Mat4, Rect, Vec2, Vec3A, Vec4}; @@ -29,7 +30,9 @@ impl Plugin for CameraProjectionPlugin { .add_systems( PostUpdate, ( - crate::camera::camera_system.in_set(CameraUpdateSystem), + crate::camera::camera_system + .in_set(CameraUpdateSystem) + .before(AssetEvents), crate::view::update_frusta .in_set(VisibilitySystems::UpdateFrusta) .after(crate::camera::camera_system) diff --git a/crates/bevy_render/src/mesh/components.rs b/crates/bevy_render/src/mesh/components.rs index 9bfa05968d733..7c0d87cf51de3 100644 --- a/crates/bevy_render/src/mesh/components.rs +++ b/crates/bevy_render/src/mesh/components.rs @@ -2,7 +2,7 @@ use crate::{ mesh::Mesh, view::{self, Visibility, VisibilityClass}, }; -use bevy_asset::{AssetEvent, AssetId, Handle}; +use bevy_asset::{AsAssetId, AssetEvent, AssetId, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ change_detection::DetectChangesMut, component::Component, event::EventReader, prelude::require, @@ -58,6 +58,14 @@ impl From<&Mesh2d> for AssetId { } } +impl AsAssetId for Mesh2d { + type Asset = Mesh; + + fn as_asset_id(&self) -> AssetId { + self.id() + } +} + /// A component for 3D meshes. Requires a [`MeshMaterial3d`] to be rendered, commonly using a [`StandardMaterial`]. /// /// [`MeshMaterial3d`]: @@ -106,6 +114,14 @@ impl From<&Mesh3d> for AssetId { } } +impl AsAssetId for Mesh3d { + type Asset = Mesh; + + fn as_asset_id(&self) -> AssetId { + self.id() + } +} + /// A system that marks a [`Mesh3d`] as changed if the associated [`Mesh`] asset /// has changed. /// diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index 6f9339bb06198..1667d5019634c 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -13,7 +13,7 @@ use crate::{ }; use allocator::MeshAllocatorPlugin; use bevy_app::{App, Plugin, PostUpdate}; -use bevy_asset::{AssetApp, AssetId, RenderAssetUsages}; +use bevy_asset::{AssetApp, AssetEvents, AssetId, RenderAssetUsages}; use bevy_ecs::{ prelude::*, system::{ @@ -41,7 +41,8 @@ impl Plugin for MeshPlugin { .add_systems( PostUpdate, mark_3d_meshes_as_changed_if_their_assets_changed - .ambiguous_with(VisibilitySystems::CalculateBounds), + .ambiguous_with(VisibilitySystems::CalculateBounds) + .before(AssetEvents), ); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index a3a5118c30cca..f046568dc59ae 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -186,6 +186,16 @@ impl Msaa { pub fn samples(&self) -> u32 { *self as u32 } + + pub fn from_samples(samples: u32) -> Self { + match samples { + 1 => Msaa::Off, + 2 => Msaa::Sample2, + 4 => Msaa::Sample4, + 8 => Msaa::Sample8, + _ => panic!("Unsupported MSAA sample count: {}", samples), + } + } } /// An identifier for a view that is stable across frames. diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 2bd354ac8b263..090a7853c4d85 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -37,7 +37,7 @@ pub use sprite::*; pub use texture_slice::*; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, Assets, Handle}; +use bevy_asset::{load_internal_asset, AssetEvents, Assets, Handle}; use bevy_core_pipeline::core_2d::Transparent2d; use bevy_ecs::prelude::*; use bevy_image::{prelude::*, TextureAtlasPlugin}; @@ -115,7 +115,7 @@ impl Plugin for SpritePlugin { ( calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds), ( - compute_slices_on_asset_event, + compute_slices_on_asset_event.before(AssetEvents), compute_slices_on_sprite_change, ) .in_set(SpriteSystem::ComputeSlices), diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index e44b27a005fa3..745dee2f2ecd1 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -1,22 +1,29 @@ use crate::{ DrawMesh2d, Mesh2d, Mesh2dPipeline, Mesh2dPipelineKey, RenderMesh2dInstances, - SetMesh2dBindGroup, SetMesh2dViewBindGroup, + SetMesh2dBindGroup, SetMesh2dViewBindGroup, ViewKeyCache, ViewSpecializationTicks, }; -use bevy_app::{App, Plugin}; -use bevy_asset::{Asset, AssetApp, AssetId, AssetServer, Handle}; +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_asset::prelude::AssetChanged; +use bevy_asset::{AsAssetId, Asset, AssetApp, AssetEvents, AssetId, AssetServer, Handle}; use bevy_core_pipeline::{ core_2d::{ AlphaMask2d, AlphaMask2dBinKey, BatchSetKey2d, Opaque2d, Opaque2dBinKey, Transparent2d, }, - tonemapping::{DebandDither, Tonemapping}, + tonemapping::Tonemapping, }; use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::component::Tick; +use bevy_ecs::entity::EntityHash; +use bevy_ecs::system::SystemChangeTick; use bevy_ecs::{ prelude::*, system::{lifetimeless::SRes, SystemParamItem}, }; use bevy_math::FloatOrd; +use bevy_platform_support::collections::HashMap; use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_render::render_phase::DrawFunctionId; +use bevy_render::render_resource::CachedRenderPipelineId; use bevy_render::view::RenderVisibleEntities; use bevy_render::{ mesh::{MeshVertexBufferLayoutRef, RenderMesh}, @@ -35,7 +42,7 @@ use bevy_render::{ }, renderer::RenderDevice, sync_world::{MainEntity, MainEntityHashMap}, - view::{ExtractedView, Msaa, ViewVisibility}, + view::{ExtractedView, ViewVisibility}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use core::{hash::Hash, marker::PhantomData}; @@ -202,6 +209,14 @@ impl From<&MeshMaterial2d> for AssetId { } } +impl AsAssetId for MeshMaterial2d { + type Asset = M; + + fn as_asset_id(&self) -> AssetId { + self.id() + } +} + /// Sets how a 2d material's base color alpha channel is used for transparency. /// Currently, this only works with [`Mesh2d`]. Sprites are always transparent. /// @@ -244,22 +259,41 @@ where { fn build(&self, app: &mut App) { app.init_asset::() + .init_resource::>() .register_type::>() - .add_plugins(RenderAssetPlugin::>::default()); + .add_plugins(RenderAssetPlugin::>::default()) + .add_systems( + PostUpdate, + check_entities_needing_specialization::.after(AssetEvents), + ); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app + .init_resource::>() + .init_resource::>() .add_render_command::>() .add_render_command::>() .add_render_command::>() .init_resource::>() .init_resource::>>() - .add_systems(ExtractSchedule, extract_mesh_materials_2d::) + .add_systems( + ExtractSchedule, + ( + extract_entities_needs_specialization::, + extract_mesh_materials_2d::, + ), + ) .add_systems( Render, - queue_material2d_meshes:: - .in_set(RenderSet::QueueMeshes) - .after(prepare_assets::>), + ( + specialize_material2d_meshes:: + .in_set(RenderSet::PrepareAssets) + .after(prepare_assets::>) + .after(prepare_assets::), + queue_material2d_meshes:: + .in_set(RenderSet::QueueMeshes) + .after(prepare_assets::>), + ), ); } } @@ -511,10 +545,89 @@ pub const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> Mesh2dPipelin } } -pub fn queue_material2d_meshes( - opaque_draw_functions: Res>, - alpha_mask_draw_functions: Res>, - transparent_draw_functions: Res>, +pub fn extract_entities_needs_specialization( + entities_needing_specialization: Extract>>, + mut entity_specialization_ticks: ResMut>, + ticks: SystemChangeTick, +) where + M: Material2d, +{ + 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()); + } +} + +#[derive(Clone, Resource, Deref, DerefMut, Debug)] +pub struct EntitiesNeedingSpecialization { + #[deref] + pub entities: Vec, + _marker: PhantomData, +} + +impl Default for EntitiesNeedingSpecialization { + fn default() -> Self { + Self { + entities: Default::default(), + _marker: Default::default(), + } + } +} + +#[derive(Clone, Resource, Deref, DerefMut, Debug)] +pub struct EntitySpecializationTicks { + #[deref] + pub entities: MainEntityHashMap, + _marker: PhantomData, +} + +impl Default for EntitySpecializationTicks { + fn default() -> Self { + Self { + entities: MainEntityHashMap::default(), + _marker: Default::default(), + } + } +} + +#[derive(Resource, Deref, DerefMut)] +pub struct SpecializedMaterial2dPipelineCache { + // (view_entity, material_entity) -> (tick, pipeline_id) + #[deref] + map: HashMap<(MainEntity, MainEntity), (Tick, CachedRenderPipelineId), EntityHash>, + marker: PhantomData, +} + +impl Default for SpecializedMaterial2dPipelineCache { + fn default() -> Self { + Self { + map: HashMap::default(), + marker: PhantomData, + } + } +} + +pub fn check_entities_needing_specialization( + needs_specialization: Query< + Entity, + Or<( + Changed, + AssetChanged, + Changed>, + AssetChanged>, + )>, + >, + mut entities_needing_specialization: ResMut>, +) where + M: Material2d, +{ + entities_needing_specialization.clear(); + for entity in &needs_specialization { + entities_needing_specialization.push(entity); + } +} + +pub fn specialize_material2d_meshes( material2d_pipeline: Res>, mut pipelines: ResMut>>, pipeline_cache: Res, @@ -524,16 +637,15 @@ pub fn queue_material2d_meshes( ), mut render_mesh_instances: ResMut, render_material_instances: Res>, - mut transparent_render_phases: ResMut>, - mut opaque_render_phases: ResMut>, - mut alpha_mask_render_phases: ResMut>, - views: Query<( - &ExtractedView, - &RenderVisibleEntities, - &Msaa, - Option<&Tonemapping>, - Option<&DebandDither>, - )>, + transparent_render_phases: Res>, + opaque_render_phases: Res>, + alpha_mask_render_phases: Res>, + views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>, + view_key_cache: Res, + entity_specialization_ticks: Res>, + view_specialization_ticks: Res, + ticks: SystemChangeTick, + mut specialized_material_pipeline_cache: ResMut>, ) where M::Data: PartialEq + Eq + Hash + Clone, { @@ -541,36 +653,32 @@ pub fn queue_material2d_meshes( return; } - for (view, visible_entities, msaa, tonemapping, dither) in &views { - let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) - else { - continue; - }; - let Some(opaque_phase) = opaque_render_phases.get_mut(&view.retained_view_entity) else { + for (view_entity, view, visible_entities) in &views { + if !transparent_render_phases.contains_key(&view.retained_view_entity) + && !opaque_render_phases.contains_key(&view.retained_view_entity) + && !alpha_mask_render_phases.contains_key(&view.retained_view_entity) + { continue; - }; - let Some(alpha_mask_phase) = alpha_mask_render_phases.get_mut(&view.retained_view_entity) - else { + } + + let Some(view_key) = view_key_cache.get(view_entity) else { continue; }; - let draw_transparent_2d = transparent_draw_functions.read().id::>(); - let draw_opaque_2d = opaque_draw_functions.read().id::>(); - let draw_alpha_mask_2d = alpha_mask_draw_functions.read().id::>(); - - let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) - | Mesh2dPipelineKey::from_hdr(view.hdr); - - if !view.hdr { - if let Some(tonemapping) = tonemapping { - view_key |= Mesh2dPipelineKey::TONEMAP_IN_SHADER; - view_key |= tonemapping_pipeline_key(*tonemapping); - } - if let Some(DebandDither::Enabled) = dither { - view_key |= Mesh2dPipelineKey::DEBAND_DITHER; + for (_, visible_entity) in visible_entities.iter::() { + let view_tick = view_specialization_ticks.get(view_entity).unwrap(); + let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap(); + let last_specialized_tick = specialized_material_pipeline_cache + .get(&(*view_entity, *visible_entity)) + .map(|(tick, _)| *tick); + let needs_specialization = last_specialized_tick.is_none_or(|tick| { + view_tick.is_newer_than(tick, ticks.this_run()) + || entity_tick.is_newer_than(tick, ticks.this_run()) + }); + if !needs_specialization { + continue; } - } - for (render_entity, visible_entity) in visible_entities.iter::() { + let Some(material_asset_id) = render_material_instances.get(visible_entity) else { continue; }; @@ -583,7 +691,7 @@ pub fn queue_material2d_meshes( let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { continue; }; - let mesh_key = view_key + let mesh_key = *view_key | Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology()) | material_2d.properties.mesh_pipeline_key_bits; @@ -605,6 +713,66 @@ pub fn queue_material2d_meshes( } }; + specialized_material_pipeline_cache.insert( + (*view_entity, *visible_entity), + (ticks.this_run(), pipeline_id), + ); + } + } +} + +pub fn queue_material2d_meshes( + (render_meshes, render_materials): ( + Res>, + Res>>, + ), + mut render_mesh_instances: ResMut, + render_material_instances: Res>, + mut transparent_render_phases: ResMut>, + mut opaque_render_phases: ResMut>, + mut alpha_mask_render_phases: ResMut>, + views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>, + specialized_material_pipeline_cache: ResMut>, +) where + M::Data: PartialEq + Eq + Hash + Clone, +{ + if render_material_instances.is_empty() { + return; + } + + for (view_entity, view, visible_entities) in &views { + let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) + else { + continue; + }; + let Some(opaque_phase) = opaque_render_phases.get_mut(&view.retained_view_entity) else { + continue; + }; + let Some(alpha_mask_phase) = alpha_mask_render_phases.get_mut(&view.retained_view_entity) + else { + continue; + }; + + for (render_entity, visible_entity) in visible_entities.iter::() { + let Some(pipeline_id) = specialized_material_pipeline_cache + .get(&(*view_entity, *visible_entity)) + .map(|(_, pipeline_id)| *pipeline_id) + else { + continue; + }; + let Some(material_asset_id) = render_material_instances.get(visible_entity) else { + continue; + }; + let Some(mesh_instance) = render_mesh_instances.get_mut(visible_entity) else { + continue; + }; + let Some(material_2d) = render_materials.get(*material_asset_id) else { + continue; + }; + let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { + continue; + }; + mesh_instance.material_bind_group_id = material_2d.get_bind_group_id(); let mesh_z = mesh_instance.transforms.world_from_local.translation.z; @@ -623,7 +791,7 @@ pub fn queue_material2d_meshes( AlphaMode2d::Opaque => { let bin_key = Opaque2dBinKey { pipeline: pipeline_id, - draw_function: draw_opaque_2d, + draw_function: material_2d.properties.draw_function_id, asset_id: mesh_instance.mesh_asset_id.into(), material_bind_group_id: material_2d.get_bind_group_id().0, }; @@ -639,7 +807,7 @@ pub fn queue_material2d_meshes( AlphaMode2d::Mask(_) => { let bin_key = AlphaMask2dBinKey { pipeline: pipeline_id, - draw_function: draw_alpha_mask_2d, + draw_function: material_2d.properties.draw_function_id, asset_id: mesh_instance.mesh_asset_id.into(), material_bind_group_id: material_2d.get_bind_group_id().0, }; @@ -655,7 +823,7 @@ pub fn queue_material2d_meshes( AlphaMode2d::Blend => { transparent_phase.add(Transparent2d { entity: (*render_entity, *visible_entity), - draw_function: draw_transparent_2d, + draw_function: material_2d.properties.draw_function_id, pipeline: pipeline_id, // NOTE: Back-to-front ordering for transparent with ascending sort means far should have the // lowest sort key and getting closer should increase. As we have @@ -689,6 +857,7 @@ pub struct Material2dProperties { /// These are precalculated so that we can just "or" them together in /// [`queue_material2d_meshes`]. pub mesh_pipeline_key_bits: Mesh2dPipelineKey, + pub draw_function_id: DrawFunctionId, } /// Data prepared for a [`Material2d`] instance. @@ -708,17 +877,42 @@ impl PreparedMaterial2d { impl RenderAsset for PreparedMaterial2d { type SourceAsset = M; - type Param = (SRes, SRes>, M::Param); + type Param = ( + SRes, + SRes>, + SRes>, + SRes>, + SRes>, + M::Param, + ); fn prepare_asset( material: Self::SourceAsset, _: AssetId, - (render_device, pipeline, material_param): &mut SystemParamItem, + ( + render_device, + pipeline, + opaque_draw_functions, + alpha_mask_draw_functions, + transparent_draw_functions, + material_param, + ): &mut SystemParamItem, ) -> Result> { match material.as_bind_group(&pipeline.material2d_layout, render_device, material_param) { Ok(prepared) => { let mut mesh_pipeline_key_bits = Mesh2dPipelineKey::empty(); mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key(material.alpha_mode())); + + let draw_function_id = match material.alpha_mode() { + AlphaMode2d::Opaque => opaque_draw_functions.read().id::>(), + AlphaMode2d::Mask(_) => { + alpha_mask_draw_functions.read().id::>() + } + AlphaMode2d::Blend => { + transparent_draw_functions.read().id::>() + } + }; + Ok(PreparedMaterial2d { bindings: prepared.bindings, bind_group: prepared.bind_group, @@ -727,6 +921,7 @@ impl RenderAsset for PreparedMaterial2d { depth_bias: material.depth_bias(), alpha_mode: material.alpha_mode(), mesh_pipeline_key_bits, + draw_function_id, }, }) } diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 86d02d5680b8b..42a249a86abb1 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -1,7 +1,8 @@ use bevy_app::Plugin; use bevy_asset::{load_internal_asset, AssetId, Handle}; -use crate::Material2dBindGroupId; +use crate::{tonemapping_pipeline_key, Material2dBindGroupId}; +use bevy_core_pipeline::tonemapping::DebandDither; use bevy_core_pipeline::{ core_2d::{AlphaMask2d, Camera2d, Opaque2d, Transparent2d, CORE_2D_DEPTH_FORMAT}, tonemapping::{ @@ -9,6 +10,8 @@ use bevy_core_pipeline::{ }, }; use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::component::Tick; +use bevy_ecs::system::SystemChangeTick; use bevy_ecs::{ prelude::*, query::ROQueryItem, @@ -16,6 +19,8 @@ use bevy_ecs::{ }; use bevy_image::{BevyDefault, Image, ImageSampler, TextureFormatPixelInfo}; use bevy_math::{Affine3, Vec4}; +use bevy_render::prelude::Msaa; +use bevy_render::RenderSet::PrepareAssets; use bevy_render::{ batching::{ gpu_preprocessing::IndirectParametersMetadata, @@ -94,6 +99,7 @@ impl Plugin for Mesh2dRenderPlugin { if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app + .init_resource::() .init_resource::() .init_resource::>() .add_systems(ExtractSchedule, extract_mesh2d) @@ -137,7 +143,13 @@ impl Plugin for Mesh2dRenderPlugin { render_app .insert_resource(batched_instance_buffer) - .init_resource::(); + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems( + Render, + check_views_need_specialization.in_set(PrepareAssets), + ); } // Load the mesh_bindings shader module here as it depends on runtime information about @@ -152,6 +164,48 @@ impl Plugin for Mesh2dRenderPlugin { } } +#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] +pub struct ViewKeyCache(MainEntityHashMap); + +#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] +pub struct ViewSpecializationTicks(MainEntityHashMap); + +pub fn check_views_need_specialization( + mut view_key_cache: ResMut, + mut view_specialization_ticks: ResMut, + views: Query<( + &MainEntity, + &ExtractedView, + &Msaa, + Option<&Tonemapping>, + Option<&DebandDither>, + )>, + ticks: SystemChangeTick, +) { + for (view_entity, view, msaa, tonemapping, dither) in &views { + let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) + | Mesh2dPipelineKey::from_hdr(view.hdr); + + if !view.hdr { + if let Some(tonemapping) = tonemapping { + view_key |= Mesh2dPipelineKey::TONEMAP_IN_SHADER; + view_key |= tonemapping_pipeline_key(*tonemapping); + } + if let Some(DebandDither::Enabled) = dither { + view_key |= Mesh2dPipelineKey::DEBAND_DITHER; + } + } + + if !view_key_cache + .get_mut(view_entity) + .is_some_and(|current_key| *current_key == view_key) + { + view_key_cache.insert(*view_entity, view_key); + view_specialization_ticks.insert(*view_entity, ticks.this_run()); + } + } +} + #[derive(Component)] pub struct Mesh2dTransforms { pub world_from_local: Affine3, diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 7f71f486e4eb6..670f793c31c15 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -67,9 +67,9 @@ pub mod prelude { } use bevy_app::{prelude::*, Animation}; -use bevy_asset::AssetApp; #[cfg(feature = "default_font")] use bevy_asset::{load_internal_binary_asset, Handle}; +use bevy_asset::{AssetApp, AssetEvents}; use bevy_ecs::prelude::*; use bevy_render::{ camera::CameraUpdateSystem, view::VisibilitySystems, ExtractSchedule, RenderApp, @@ -124,7 +124,7 @@ impl Plugin for TextPlugin { .add_systems( PostUpdate, ( - remove_dropped_font_atlas_sets, + remove_dropped_font_atlas_sets.before(AssetEvents), detect_text_needs_rerender::, update_text2d_layout // Potential conflict: `Assets`