From 5e569af2d0efdac71beaa3e107c3a999717d8119 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Mon, 17 Feb 2025 23:23:33 -0800 Subject: [PATCH] Make the specialized pipeline cache two-level. (#17915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, the specialized pipeline cache maps a (view entity, mesh entity) tuple to the retained pipeline for that entity. This causes two problems: 1. Using the view entity is incorrect, because the view entity isn't stable from frame to frame. 2. Switching the view entity to a `RetainedViewEntity`, which is necessary for correctness, significantly regresses performance of `specialize_material_meshes` and `specialize_shadows` because of the loss of the fast `EntityHash`. This patch fixes both problems by switching to a *two-level* hash table. The outer level of the table maps each `RetainedViewEntity` to an inner table, which maps each `MainEntity` to its pipeline ID and change tick. Because we loop over views first and, within that loop, loop over entities visible from that view, we hoist the slow lookup of the view entity out of the inner entity loop. Additionally, this patch fixes a bug whereby pipeline IDs were leaked when removing the view. We still have a problem with leaking pipeline IDs for deleted entities, but that won't be fixed until the specialized pipeline cache is retained. This patch improves performance of the [Caldera benchmark] from 7.8× faster than 0.14 to 9.0× faster than 0.14, when applied on top of the global binding arrays PR, #17898. [Caldera benchmark]: https://github.com/DGriffin91/bevy_caldera_scene --- crates/bevy_pbr/src/material.rs | 78 ++++++++++++++----- crates/bevy_pbr/src/prepass/mod.rs | 93 ++++++++++++++--------- crates/bevy_pbr/src/render/light.rs | 62 +++++++++++---- crates/bevy_pbr/src/render/mesh.rs | 16 ++-- crates/bevy_sprite/src/mesh2d/material.rs | 51 ++++++++++--- 5 files changed, 210 insertions(+), 90 deletions(-) diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 7c6d93ec32..944bd764e4 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -19,7 +19,6 @@ use bevy_core_pipeline::{ }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::component::Tick; -use bevy_ecs::entity::EntityHash; use bevy_ecs::system::SystemChangeTick; use bevy_ecs::{ prelude::*, @@ -28,7 +27,8 @@ use bevy_ecs::{ SystemParamItem, }, }; -use bevy_platform_support::collections::HashMap; +use bevy_platform_support::collections::{HashMap, HashSet}; +use bevy_platform_support::hash::FixedHasher; use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; use bevy_render::mesh::mark_3d_meshes_as_changed_if_their_assets_changed; @@ -41,7 +41,7 @@ use bevy_render::{ render_resource::*, renderer::RenderDevice, sync_world::MainEntity, - view::{ExtractedView, Msaa, RenderVisibilityRanges, ViewVisibility}, + view::{ExtractedView, Msaa, RenderVisibilityRanges, RetainedViewEntity, ViewVisibility}, Extract, }; use bevy_render::{mesh::allocator::MeshAllocator, sync_world::MainEntityHashMap}; @@ -735,11 +735,22 @@ impl Default for EntitySpecializationTicks { } } +/// Stores the [`SpecializedMaterialViewPipelineCache`] for each view. #[derive(Resource, Deref, DerefMut)] pub struct SpecializedMaterialPipelineCache { - // (view_entity, material_entity) -> (tick, pipeline_id) + // view entity -> view pipeline cache #[deref] - map: HashMap<(MainEntity, MainEntity), (Tick, CachedRenderPipelineId), EntityHash>, + map: HashMap>, + marker: PhantomData, +} + +/// Stores the cached render pipeline ID for each entity in a single view, as +/// well as the last time it was changed. +#[derive(Deref, DerefMut)] +pub struct SpecializedMaterialViewPipelineCache { + // material entity -> (tick, pipeline_id) + #[deref] + map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, marker: PhantomData, } @@ -752,6 +763,15 @@ impl Default for SpecializedMaterialPipelineCache { } } +impl Default for SpecializedMaterialViewPipelineCache { + fn default() -> Self { + Self { + map: MainEntityHashMap::default(), + marker: PhantomData, + } + } +} + pub fn check_entities_needing_specialization( needs_specialization: Query< Entity, @@ -792,7 +812,7 @@ pub fn specialize_material_meshes( Res>, Res>, ), - views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>, + views: Query<(&ExtractedView, &RenderVisibleEntities)>, view_key_cache: Res, entity_specialization_ticks: Res>, view_specialization_ticks: Res, @@ -804,7 +824,13 @@ pub fn specialize_material_meshes( ) where M::Data: PartialEq + Eq + Hash + Clone, { - for (view_entity, view, visible_entities) in &views { + // Record the retained IDs of all shadow views so that we can expire old + // pipeline IDs. + let mut all_views: HashSet = HashSet::default(); + + for (view, visible_entities) in &views { + all_views.insert(view.retained_view_entity); + 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) @@ -813,15 +839,21 @@ pub fn specialize_material_meshes( continue; } - let Some(view_key) = view_key_cache.get(view_entity) else { + let Some(view_key) = view_key_cache.get(&view.retained_view_entity) else { continue; }; + let view_tick = view_specialization_ticks + .get(&view.retained_view_entity) + .unwrap(); + let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache + .entry(view.retained_view_entity) + .or_default(); + 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)) + let last_specialized_tick = view_specialized_material_pipeline_cache + .get(visible_entity) .map(|(tick, _)| *tick); let needs_specialization = last_specialized_tick.is_none_or(|tick| { view_tick.is_newer_than(tick, ticks.this_run()) @@ -901,12 +933,14 @@ pub fn specialize_material_meshes( } }; - specialized_material_pipeline_cache.insert( - (*view_entity, *visible_entity), - (ticks.this_run(), pipeline_id), - ); + view_specialized_material_pipeline_cache + .insert(*visible_entity, (ticks.this_run(), pipeline_id)); } } + + // Delete specialized pipelines belonging to views that have expired. + specialized_material_pipeline_cache + .retain(|retained_view_entity, _| all_views.contains(retained_view_entity)); } /// For each view, iterates over all the meshes visible from that view and adds @@ -921,12 +955,12 @@ pub fn queue_material_meshes( mut alpha_mask_render_phases: ResMut>, mut transmissive_render_phases: ResMut>, mut transparent_render_phases: ResMut>, - views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>, + views: Query<(&ExtractedView, &RenderVisibleEntities)>, specialized_material_pipeline_cache: ResMut>, ) where M::Data: PartialEq + Eq + Hash + Clone, { - for (view_entity, view, visible_entities) in &views { + for (view, visible_entities) in &views { let ( Some(opaque_phase), Some(alpha_mask_phase), @@ -942,10 +976,16 @@ pub fn queue_material_meshes( continue; }; + let Some(view_specialized_material_pipeline_cache) = + specialized_material_pipeline_cache.get(&view.retained_view_entity) + else { + continue; + }; + let rangefinder = view.rangefinder3d(); for (render_entity, visible_entity) in visible_entities.iter::() { - let Some((current_change_tick, pipeline_id)) = specialized_material_pipeline_cache - .get(&(*view_entity, *visible_entity)) + let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache + .get(visible_entity) .map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id)) else { continue; diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 4885238e10..c8902145c8 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -18,7 +18,7 @@ use bevy_render::{ render_resource::binding_types::uniform_buffer, renderer::RenderAdapter, sync_world::RenderEntity, - view::{RenderVisibilityRanges, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT}, + view::{RenderVisibilityRanges, RetainedViewEntity, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT}, ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSet, }; pub use prepass_bindings::*; @@ -56,10 +56,9 @@ use crate::meshlet::{ 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::sync_world::MainEntityHashMap; use bevy_render::view::RenderVisibleEntities; use bevy_render::RenderSet::{PrepareAssets, PrepareResources}; use core::{hash::Hash, marker::PhantomData}; @@ -807,11 +806,22 @@ pub fn prepare_prepass_view_bind_group( } } +/// Stores the [`SpecializedPrepassMaterialViewPipelineCache`] for each view. #[derive(Resource, Deref, DerefMut)] pub struct SpecializedPrepassMaterialPipelineCache { - // (view_entity, material_entity) -> (tick, pipeline_id) + // view_entity -> view pipeline cache #[deref] - map: HashMap<(MainEntity, MainEntity), (Tick, CachedRenderPipelineId), EntityHash>, + map: HashMap>, + marker: PhantomData, +} + +/// Stores the cached render pipeline ID for each entity in a single view, as +/// well as the last time it was changed. +#[derive(Deref, DerefMut)] +pub struct SpecializedPrepassMaterialViewPipelineCache { + // material entity -> (tick, pipeline_id) + #[deref] + map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, marker: PhantomData, } @@ -824,17 +834,26 @@ impl Default for SpecializedPrepassMaterialPipelineCache { } } -#[derive(Resource, Deref, DerefMut, Default, Clone)] -pub struct ViewKeyPrepassCache(MainEntityHashMap); +impl Default for SpecializedPrepassMaterialViewPipelineCache { + fn default() -> Self { + Self { + map: HashMap::default(), + marker: PhantomData, + } + } +} #[derive(Resource, Deref, DerefMut, Default, Clone)] -pub struct ViewPrepassSpecializationTicks(MainEntityHashMap); +pub struct ViewKeyPrepassCache(HashMap); + +#[derive(Resource, Deref, DerefMut, Default, Clone)] +pub struct ViewPrepassSpecializationTicks(HashMap); pub fn check_prepass_views_need_specialization( mut view_key_cache: ResMut, mut view_specialization_ticks: ResMut, mut views: Query<( - &MainEntity, + &ExtractedView, &Msaa, Option<&DepthPrepass>, Option<&NormalPrepass>, @@ -842,9 +861,7 @@ pub fn check_prepass_views_need_specialization( )>, ticks: SystemChangeTick, ) { - for (view_entity, msaa, depth_prepass, normal_prepass, motion_vector_prepass) in - views.iter_mut() - { + for (view, 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; @@ -856,14 +873,14 @@ pub fn check_prepass_views_need_specialization( view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; } - if let Some(current_key) = view_key_cache.get_mut(view_entity) { + if let Some(current_key) = view_key_cache.get_mut(&view.retained_view_entity) { if *current_key != view_key { - view_key_cache.insert(*view_entity, view_key); - view_specialization_ticks.insert(*view_entity, ticks.this_run()); + view_key_cache.insert(view.retained_view_entity, view_key); + view_specialization_ticks.insert(view.retained_view_entity, ticks.this_run()); } } else { - view_key_cache.insert(*view_entity, view_key); - view_specialization_ticks.insert(*view_entity, ticks.this_run()); + view_key_cache.insert(view.retained_view_entity, view_key); + view_specialization_ticks.insert(view.retained_view_entity, ticks.this_run()); } } } @@ -878,7 +895,6 @@ pub fn specialize_prepass_material_meshes( material_bind_group_allocator: Res>, view_key_cache: Res, views: Query<( - &MainEntity, &ExtractedView, &RenderVisibleEntities, &Msaa, @@ -917,14 +933,7 @@ pub fn specialize_prepass_material_meshes( M: Material, M::Data: PartialEq + Eq + Hash + Clone, { - for ( - view_entity, - extracted_view, - visible_entities, - msaa, - motion_vector_prepass, - deferred_prepass, - ) in &views + for (extracted_view, visible_entities, msaa, motion_vector_prepass, deferred_prepass) in &views { if !opaque_deferred_render_phases.contains_key(&extracted_view.retained_view_entity) && !alpha_mask_deferred_render_phases.contains_key(&extracted_view.retained_view_entity) @@ -934,15 +943,21 @@ pub fn specialize_prepass_material_meshes( continue; } - let Some(view_key) = view_key_cache.get(view_entity) else { + let Some(view_key) = view_key_cache.get(&extracted_view.retained_view_entity) else { continue; }; + let view_tick = view_specialization_ticks + .get(&extracted_view.retained_view_entity) + .unwrap(); + let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache + .entry(extracted_view.retained_view_entity) + .or_default(); + 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)) + let last_specialized_tick = view_specialized_material_pipeline_cache + .get(visible_entity) .map(|(tick, _)| *tick); let needs_specialization = last_specialized_tick.is_none_or(|tick| { view_tick.is_newer_than(tick, ticks.this_run()) @@ -1054,10 +1069,8 @@ pub fn specialize_prepass_material_meshes( } }; - specialized_material_pipeline_cache.insert( - (*view_entity, *visible_entity), - (ticks.this_run(), pipeline_id), - ); + view_specialized_material_pipeline_cache + .insert(*visible_entity, (ticks.this_run(), pipeline_id)); } } } @@ -1072,12 +1085,12 @@ pub fn queue_prepass_material_meshes( 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)>, + views: Query<(&ExtractedView, &RenderVisibleEntities)>, specialized_material_pipeline_cache: Res>, ) where M::Data: PartialEq + Eq + Hash + Clone, { - for (view_entity, extracted_view, visible_entities) in &views { + for (extracted_view, visible_entities) in &views { let ( mut opaque_phase, mut alpha_mask_phase, @@ -1090,6 +1103,12 @@ pub fn queue_prepass_material_meshes( alpha_mask_deferred_render_phases.get_mut(&extracted_view.retained_view_entity), ); + let Some(view_specialized_material_pipeline_cache) = + specialized_material_pipeline_cache.get(&extracted_view.retained_view_entity) + else { + continue; + }; + // Skip if there's no place to put the mesh. if opaque_phase.is_none() && alpha_mask_phase.is_none() @@ -1101,7 +1120,7 @@ pub fn queue_prepass_material_meshes( for (render_entity, visible_entity) in visible_entities.iter::() { let Some((current_change_tick, pipeline_id)) = - specialized_material_pipeline_cache.get(&(*view_entity, *visible_entity)) + view_specialized_material_pipeline_cache.get(visible_entity) else { continue; }; diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 80347f0d1b..6b00636523 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -14,6 +14,8 @@ use bevy_ecs::{ }; use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_platform_support::collections::{HashMap, HashSet}; +use bevy_platform_support::hash::FixedHasher; +use bevy_render::sync_world::MainEntityHashMap; use bevy_render::{ batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, camera::SortedCameras, @@ -1613,9 +1615,16 @@ pub struct LightSpecializationTicks(HashMap); #[derive(Resource, Deref, DerefMut)] pub struct SpecializedShadowMaterialPipelineCache { - // (view_light_entity, visible_entity) -> (tick, pipeline_id) + // view light entity -> view pipeline cache #[deref] - map: HashMap<(RetainedViewEntity, MainEntity), (Tick, CachedRenderPipelineId)>, + map: HashMap>, + marker: PhantomData, +} + +#[derive(Deref, DerefMut)] +pub struct SpecializedShadowMaterialViewPipelineCache { + #[deref] + map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, marker: PhantomData, } @@ -1628,6 +1637,15 @@ impl Default for SpecializedShadowMaterialPipelineCache { } } +impl Default for SpecializedShadowMaterialViewPipelineCache { + fn default() -> Self { + Self { + map: MainEntityHashMap::default(), + marker: PhantomData, + } + } +} + pub fn check_views_lights_need_specialization( view_lights: Query<&ViewLightEntities, With>, view_light_entities: Query<(&LightEntity, &ExtractedView)>, @@ -1702,6 +1720,10 @@ pub fn specialize_shadows( ) where M::Data: PartialEq + Eq + Hash + Clone, { + // Record the retained IDs of all shadow views so that we can expire old + // pipeline IDs. + let mut all_shadow_views: HashSet = HashSet::default(); + for (entity, view_lights) in &view_lights { for view_light_entity in view_lights.lights.iter().copied() { let Ok((light_entity, extracted_view_light)) = @@ -1709,6 +1731,9 @@ pub fn specialize_shadows( else { continue; }; + + all_shadow_views.insert(extracted_view_light.retained_view_entity); + if !shadow_render_phases.contains_key(&extracted_view_light.retained_view_entity) { continue; } @@ -1744,13 +1769,17 @@ pub fn specialize_shadows( // NOTE: Lights with shadow mapping disabled will have no visible entities // so no meshes will be queued + let view_tick = light_specialization_ticks + .get(&extracted_view_light.retained_view_entity) + .unwrap(); + let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache + .entry(extracted_view_light.retained_view_entity) + .or_default(); + for (_, visible_entity) in visible_entities.iter().copied() { - let view_tick = light_specialization_ticks - .get(&extracted_view_light.retained_view_entity) - .unwrap(); let entity_tick = entity_specialization_ticks.get(&visible_entity).unwrap(); - let last_specialized_tick = specialized_material_pipeline_cache - .get(&(extracted_view_light.retained_view_entity, visible_entity)) + let last_specialized_tick = view_specialized_material_pipeline_cache + .get(&visible_entity) .map(|(tick, _)| *tick); let needs_specialization = last_specialized_tick.is_none_or(|tick| { view_tick.is_newer_than(tick, ticks.this_run()) @@ -1829,13 +1858,14 @@ pub fn specialize_shadows( } }; - specialized_material_pipeline_cache.insert( - (extracted_view_light.retained_view_entity, visible_entity), - (ticks.this_run(), pipeline_id), - ); + view_specialized_material_pipeline_cache + .insert(visible_entity, (ticks.this_run(), pipeline_id)); } } } + + // Delete specialized pipelines belonging to views that have expired. + specialized_material_pipeline_cache.retain(|view, _| all_shadow_views.contains(view)); } /// For each shadow cascade, iterates over all the meshes "visible" from it and @@ -1875,6 +1905,12 @@ pub fn queue_shadows( continue; }; + let Some(view_specialized_material_pipeline_cache) = + specialized_material_pipeline_cache.get(&extracted_view_light.retained_view_entity) + else { + continue; + }; + let visible_entities = match light_entity { LightEntity::Directional { light_entity, @@ -1900,8 +1936,8 @@ pub fn queue_shadows( }; for (entity, main_entity) in visible_entities.iter().copied() { - let Some((current_change_tick, pipeline_id)) = specialized_material_pipeline_cache - .get(&(extracted_view_light.retained_view_entity, main_entity)) + let Some((current_change_tick, pipeline_id)) = + view_specialized_material_pipeline_cache.get(&main_entity) else { continue; }; diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 781fb3f908..5acdbbc3d8 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -37,8 +37,8 @@ use bevy_render::{ renderer::{RenderAdapter, RenderDevice, RenderQueue}, texture::DefaultImageSampler, view::{ - self, NoFrustumCulling, NoIndirectDrawing, RenderVisibilityRanges, ViewTarget, - ViewUniformOffset, ViewVisibility, VisibilityRange, + self, NoFrustumCulling, NoIndirectDrawing, RenderVisibilityRanges, RetainedViewEntity, + ViewTarget, ViewUniformOffset, ViewVisibility, VisibilityRange, }, Extract, }; @@ -317,16 +317,15 @@ impl Plugin for MeshRenderPlugin { } #[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] -pub struct ViewKeyCache(MainEntityHashMap); +pub struct ViewKeyCache(HashMap); #[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] -pub struct ViewSpecializationTicks(MainEntityHashMap); +pub struct ViewSpecializationTicks(HashMap); pub fn check_views_need_specialization( mut view_key_cache: ResMut, mut view_specialization_ticks: ResMut, mut views: Query<( - &MainEntity, &ExtractedView, &Msaa, Option<&Tonemapping>, @@ -352,7 +351,6 @@ pub fn check_views_need_specialization( ticks: SystemChangeTick, ) { for ( - view_entity, view, msaa, tonemapping, @@ -444,11 +442,11 @@ pub fn check_views_need_specialization( ); } if !view_key_cache - .get_mut(view_entity) + .get_mut(&view.retained_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()); + view_key_cache.insert(view.retained_view_entity, view_key); + view_specialization_ticks.insert(view.retained_view_entity, ticks.this_run()); } } } diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index d0c4a1fea6..fbe2c2132c 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -13,7 +13,6 @@ use bevy_core_pipeline::{ }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::component::Tick; -use bevy_ecs::entity::EntityHash; use bevy_ecs::system::SystemChangeTick; use bevy_ecs::{ prelude::*, @@ -590,11 +589,22 @@ impl Default for EntitySpecializationTicks { } } +/// Stores the [`SpecializedMaterial2dViewPipelineCache`] for each view. #[derive(Resource, Deref, DerefMut)] pub struct SpecializedMaterial2dPipelineCache { - // (view_entity, material_entity) -> (tick, pipeline_id) + // view_entity -> view pipeline cache #[deref] - map: HashMap<(MainEntity, MainEntity), (Tick, CachedRenderPipelineId), EntityHash>, + map: MainEntityHashMap>, + marker: PhantomData, +} + +/// Stores the cached render pipeline ID for each entity in a single view, as +/// well as the last time it was changed. +#[derive(Deref, DerefMut)] +pub struct SpecializedMaterial2dViewPipelineCache { + // material entity -> (tick, pipeline_id) + #[deref] + map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, marker: PhantomData, } @@ -607,6 +617,15 @@ impl Default for SpecializedMaterial2dPipelineCache { } } +impl Default for SpecializedMaterial2dViewPipelineCache { + fn default() -> Self { + Self { + map: HashMap::default(), + marker: PhantomData, + } + } +} + pub fn check_entities_needing_specialization( needs_specialization: Query< Entity, @@ -665,11 +684,15 @@ pub fn specialize_material2d_meshes( continue; }; + let view_tick = view_specialization_ticks.get(view_entity).unwrap(); + let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache + .entry(*view_entity) + .or_default(); + 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)) + let last_specialized_tick = view_specialized_material_pipeline_cache + .get(visible_entity) .map(|(tick, _)| *tick); let needs_specialization = last_specialized_tick.is_none_or(|tick| { view_tick.is_newer_than(tick, ticks.this_run()) @@ -713,10 +736,8 @@ pub fn specialize_material2d_meshes( } }; - specialized_material_pipeline_cache.insert( - (*view_entity, *visible_entity), - (ticks.this_run(), pipeline_id), - ); + view_specialized_material_pipeline_cache + .insert(*visible_entity, (ticks.this_run(), pipeline_id)); } } } @@ -741,6 +762,12 @@ pub fn queue_material2d_meshes( } for (view_entity, view, visible_entities) in &views { + let Some(view_specialized_material_pipeline_cache) = + specialized_material_pipeline_cache.get(view_entity) + else { + continue; + }; + let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) else { continue; @@ -754,8 +781,8 @@ pub fn queue_material2d_meshes( }; for (render_entity, visible_entity) in visible_entities.iter::() { - let Some((current_change_tick, pipeline_id)) = specialized_material_pipeline_cache - .get(&(*view_entity, *visible_entity)) + let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache + .get(visible_entity) .map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id)) else { continue;