Skip to content

Commit 5e569af

Browse files
authored
Make the specialized pipeline cache two-level. (#17915)
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
1 parent 8976a45 commit 5e569af

File tree

5 files changed

+209
-89
lines changed

5 files changed

+209
-89
lines changed

crates/bevy_pbr/src/material.rs

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ use bevy_core_pipeline::{
1919
};
2020
use bevy_derive::{Deref, DerefMut};
2121
use bevy_ecs::component::Tick;
22-
use bevy_ecs::entity::EntityHash;
2322
use bevy_ecs::system::SystemChangeTick;
2423
use bevy_ecs::{
2524
prelude::*,
@@ -28,7 +27,8 @@ use bevy_ecs::{
2827
SystemParamItem,
2928
},
3029
};
31-
use bevy_platform_support::collections::HashMap;
30+
use bevy_platform_support::collections::{HashMap, HashSet};
31+
use bevy_platform_support::hash::FixedHasher;
3232
use bevy_reflect::std_traits::ReflectDefault;
3333
use bevy_reflect::Reflect;
3434
use bevy_render::mesh::mark_3d_meshes_as_changed_if_their_assets_changed;
@@ -41,7 +41,7 @@ use bevy_render::{
4141
render_resource::*,
4242
renderer::RenderDevice,
4343
sync_world::MainEntity,
44-
view::{ExtractedView, Msaa, RenderVisibilityRanges, ViewVisibility},
44+
view::{ExtractedView, Msaa, RenderVisibilityRanges, RetainedViewEntity, ViewVisibility},
4545
Extract,
4646
};
4747
use bevy_render::{mesh::allocator::MeshAllocator, sync_world::MainEntityHashMap};
@@ -735,11 +735,22 @@ impl<M> Default for EntitySpecializationTicks<M> {
735735
}
736736
}
737737

738+
/// Stores the [`SpecializedMaterialViewPipelineCache`] for each view.
738739
#[derive(Resource, Deref, DerefMut)]
739740
pub struct SpecializedMaterialPipelineCache<M> {
740-
// (view_entity, material_entity) -> (tick, pipeline_id)
741+
// view entity -> view pipeline cache
741742
#[deref]
742-
map: HashMap<(MainEntity, MainEntity), (Tick, CachedRenderPipelineId), EntityHash>,
743+
map: HashMap<RetainedViewEntity, SpecializedMaterialViewPipelineCache<M>>,
744+
marker: PhantomData<M>,
745+
}
746+
747+
/// Stores the cached render pipeline ID for each entity in a single view, as
748+
/// well as the last time it was changed.
749+
#[derive(Deref, DerefMut)]
750+
pub struct SpecializedMaterialViewPipelineCache<M> {
751+
// material entity -> (tick, pipeline_id)
752+
#[deref]
753+
map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>,
743754
marker: PhantomData<M>,
744755
}
745756

@@ -752,6 +763,15 @@ impl<M> Default for SpecializedMaterialPipelineCache<M> {
752763
}
753764
}
754765

766+
impl<M> Default for SpecializedMaterialViewPipelineCache<M> {
767+
fn default() -> Self {
768+
Self {
769+
map: MainEntityHashMap::default(),
770+
marker: PhantomData,
771+
}
772+
}
773+
}
774+
755775
pub fn check_entities_needing_specialization<M>(
756776
needs_specialization: Query<
757777
Entity,
@@ -792,7 +812,7 @@ pub fn specialize_material_meshes<M: Material>(
792812
Res<ViewSortedRenderPhases<Transmissive3d>>,
793813
Res<ViewSortedRenderPhases<Transparent3d>>,
794814
),
795-
views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>,
815+
views: Query<(&ExtractedView, &RenderVisibleEntities)>,
796816
view_key_cache: Res<ViewKeyCache>,
797817
entity_specialization_ticks: Res<EntitySpecializationTicks<M>>,
798818
view_specialization_ticks: Res<ViewSpecializationTicks>,
@@ -804,7 +824,13 @@ pub fn specialize_material_meshes<M: Material>(
804824
) where
805825
M::Data: PartialEq + Eq + Hash + Clone,
806826
{
807-
for (view_entity, view, visible_entities) in &views {
827+
// Record the retained IDs of all shadow views so that we can expire old
828+
// pipeline IDs.
829+
let mut all_views: HashSet<RetainedViewEntity, FixedHasher> = HashSet::default();
830+
831+
for (view, visible_entities) in &views {
832+
all_views.insert(view.retained_view_entity);
833+
808834
if !transparent_render_phases.contains_key(&view.retained_view_entity)
809835
&& !opaque_render_phases.contains_key(&view.retained_view_entity)
810836
&& !alpha_mask_render_phases.contains_key(&view.retained_view_entity)
@@ -813,15 +839,21 @@ pub fn specialize_material_meshes<M: Material>(
813839
continue;
814840
}
815841

816-
let Some(view_key) = view_key_cache.get(view_entity) else {
842+
let Some(view_key) = view_key_cache.get(&view.retained_view_entity) else {
817843
continue;
818844
};
819845

846+
let view_tick = view_specialization_ticks
847+
.get(&view.retained_view_entity)
848+
.unwrap();
849+
let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache
850+
.entry(view.retained_view_entity)
851+
.or_default();
852+
820853
for (_, visible_entity) in visible_entities.iter::<Mesh3d>() {
821-
let view_tick = view_specialization_ticks.get(view_entity).unwrap();
822854
let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap();
823-
let last_specialized_tick = specialized_material_pipeline_cache
824-
.get(&(*view_entity, *visible_entity))
855+
let last_specialized_tick = view_specialized_material_pipeline_cache
856+
.get(visible_entity)
825857
.map(|(tick, _)| *tick);
826858
let needs_specialization = last_specialized_tick.is_none_or(|tick| {
827859
view_tick.is_newer_than(tick, ticks.this_run())
@@ -901,12 +933,14 @@ pub fn specialize_material_meshes<M: Material>(
901933
}
902934
};
903935

904-
specialized_material_pipeline_cache.insert(
905-
(*view_entity, *visible_entity),
906-
(ticks.this_run(), pipeline_id),
907-
);
936+
view_specialized_material_pipeline_cache
937+
.insert(*visible_entity, (ticks.this_run(), pipeline_id));
908938
}
909939
}
940+
941+
// Delete specialized pipelines belonging to views that have expired.
942+
specialized_material_pipeline_cache
943+
.retain(|retained_view_entity, _| all_views.contains(retained_view_entity));
910944
}
911945

912946
/// For each view, iterates over all the meshes visible from that view and adds
@@ -921,12 +955,12 @@ pub fn queue_material_meshes<M: Material>(
921955
mut alpha_mask_render_phases: ResMut<ViewBinnedRenderPhases<AlphaMask3d>>,
922956
mut transmissive_render_phases: ResMut<ViewSortedRenderPhases<Transmissive3d>>,
923957
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<Transparent3d>>,
924-
views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>,
958+
views: Query<(&ExtractedView, &RenderVisibleEntities)>,
925959
specialized_material_pipeline_cache: ResMut<SpecializedMaterialPipelineCache<M>>,
926960
) where
927961
M::Data: PartialEq + Eq + Hash + Clone,
928962
{
929-
for (view_entity, view, visible_entities) in &views {
963+
for (view, visible_entities) in &views {
930964
let (
931965
Some(opaque_phase),
932966
Some(alpha_mask_phase),
@@ -942,10 +976,16 @@ pub fn queue_material_meshes<M: Material>(
942976
continue;
943977
};
944978

979+
let Some(view_specialized_material_pipeline_cache) =
980+
specialized_material_pipeline_cache.get(&view.retained_view_entity)
981+
else {
982+
continue;
983+
};
984+
945985
let rangefinder = view.rangefinder3d();
946986
for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {
947-
let Some((current_change_tick, pipeline_id)) = specialized_material_pipeline_cache
948-
.get(&(*view_entity, *visible_entity))
987+
let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache
988+
.get(visible_entity)
949989
.map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id))
950990
else {
951991
continue;

crates/bevy_pbr/src/prepass/mod.rs

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use bevy_render::{
1818
render_resource::binding_types::uniform_buffer,
1919
renderer::RenderAdapter,
2020
sync_world::RenderEntity,
21-
view::{RenderVisibilityRanges, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT},
21+
view::{RenderVisibilityRanges, RetainedViewEntity, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT},
2222
ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSet,
2323
};
2424
pub use prepass_bindings::*;
@@ -56,10 +56,9 @@ use crate::meshlet::{
5656

5757
use bevy_derive::{Deref, DerefMut};
5858
use bevy_ecs::component::Tick;
59-
use bevy_ecs::entity::EntityHash;
6059
use bevy_ecs::system::SystemChangeTick;
6160
use bevy_platform_support::collections::HashMap;
62-
use bevy_render::sync_world::{MainEntity, MainEntityHashMap};
61+
use bevy_render::sync_world::MainEntityHashMap;
6362
use bevy_render::view::RenderVisibleEntities;
6463
use bevy_render::RenderSet::{PrepareAssets, PrepareResources};
6564
use core::{hash::Hash, marker::PhantomData};
@@ -807,11 +806,22 @@ pub fn prepare_prepass_view_bind_group<M: Material>(
807806
}
808807
}
809808

809+
/// Stores the [`SpecializedPrepassMaterialViewPipelineCache`] for each view.
810810
#[derive(Resource, Deref, DerefMut)]
811811
pub struct SpecializedPrepassMaterialPipelineCache<M> {
812-
// (view_entity, material_entity) -> (tick, pipeline_id)
812+
// view_entity -> view pipeline cache
813813
#[deref]
814-
map: HashMap<(MainEntity, MainEntity), (Tick, CachedRenderPipelineId), EntityHash>,
814+
map: HashMap<RetainedViewEntity, SpecializedPrepassMaterialViewPipelineCache<M>>,
815+
marker: PhantomData<M>,
816+
}
817+
818+
/// Stores the cached render pipeline ID for each entity in a single view, as
819+
/// well as the last time it was changed.
820+
#[derive(Deref, DerefMut)]
821+
pub struct SpecializedPrepassMaterialViewPipelineCache<M> {
822+
// material entity -> (tick, pipeline_id)
823+
#[deref]
824+
map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>,
815825
marker: PhantomData<M>,
816826
}
817827

@@ -824,27 +834,34 @@ impl<M> Default for SpecializedPrepassMaterialPipelineCache<M> {
824834
}
825835
}
826836

837+
impl<M> Default for SpecializedPrepassMaterialViewPipelineCache<M> {
838+
fn default() -> Self {
839+
Self {
840+
map: HashMap::default(),
841+
marker: PhantomData,
842+
}
843+
}
844+
}
845+
827846
#[derive(Resource, Deref, DerefMut, Default, Clone)]
828-
pub struct ViewKeyPrepassCache(MainEntityHashMap<MeshPipelineKey>);
847+
pub struct ViewKeyPrepassCache(HashMap<RetainedViewEntity, MeshPipelineKey>);
829848

830849
#[derive(Resource, Deref, DerefMut, Default, Clone)]
831-
pub struct ViewPrepassSpecializationTicks(MainEntityHashMap<Tick>);
850+
pub struct ViewPrepassSpecializationTicks(HashMap<RetainedViewEntity, Tick>);
832851

833852
pub fn check_prepass_views_need_specialization(
834853
mut view_key_cache: ResMut<ViewKeyPrepassCache>,
835854
mut view_specialization_ticks: ResMut<ViewPrepassSpecializationTicks>,
836855
mut views: Query<(
837-
&MainEntity,
856+
&ExtractedView,
838857
&Msaa,
839858
Option<&DepthPrepass>,
840859
Option<&NormalPrepass>,
841860
Option<&MotionVectorPrepass>,
842861
)>,
843862
ticks: SystemChangeTick,
844863
) {
845-
for (view_entity, msaa, depth_prepass, normal_prepass, motion_vector_prepass) in
846-
views.iter_mut()
847-
{
864+
for (view, msaa, depth_prepass, normal_prepass, motion_vector_prepass) in views.iter_mut() {
848865
let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples());
849866
if depth_prepass.is_some() {
850867
view_key |= MeshPipelineKey::DEPTH_PREPASS;
@@ -856,14 +873,14 @@ pub fn check_prepass_views_need_specialization(
856873
view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS;
857874
}
858875

859-
if let Some(current_key) = view_key_cache.get_mut(view_entity) {
876+
if let Some(current_key) = view_key_cache.get_mut(&view.retained_view_entity) {
860877
if *current_key != view_key {
861-
view_key_cache.insert(*view_entity, view_key);
862-
view_specialization_ticks.insert(*view_entity, ticks.this_run());
878+
view_key_cache.insert(view.retained_view_entity, view_key);
879+
view_specialization_ticks.insert(view.retained_view_entity, ticks.this_run());
863880
}
864881
} else {
865-
view_key_cache.insert(*view_entity, view_key);
866-
view_specialization_ticks.insert(*view_entity, ticks.this_run());
882+
view_key_cache.insert(view.retained_view_entity, view_key);
883+
view_specialization_ticks.insert(view.retained_view_entity, ticks.this_run());
867884
}
868885
}
869886
}
@@ -878,7 +895,6 @@ pub fn specialize_prepass_material_meshes<M>(
878895
material_bind_group_allocator: Res<MaterialBindGroupAllocator<M>>,
879896
view_key_cache: Res<ViewKeyPrepassCache>,
880897
views: Query<(
881-
&MainEntity,
882898
&ExtractedView,
883899
&RenderVisibleEntities,
884900
&Msaa,
@@ -917,14 +933,7 @@ pub fn specialize_prepass_material_meshes<M>(
917933
M: Material,
918934
M::Data: PartialEq + Eq + Hash + Clone,
919935
{
920-
for (
921-
view_entity,
922-
extracted_view,
923-
visible_entities,
924-
msaa,
925-
motion_vector_prepass,
926-
deferred_prepass,
927-
) in &views
936+
for (extracted_view, visible_entities, msaa, motion_vector_prepass, deferred_prepass) in &views
928937
{
929938
if !opaque_deferred_render_phases.contains_key(&extracted_view.retained_view_entity)
930939
&& !alpha_mask_deferred_render_phases.contains_key(&extracted_view.retained_view_entity)
@@ -934,15 +943,21 @@ pub fn specialize_prepass_material_meshes<M>(
934943
continue;
935944
}
936945

937-
let Some(view_key) = view_key_cache.get(view_entity) else {
946+
let Some(view_key) = view_key_cache.get(&extracted_view.retained_view_entity) else {
938947
continue;
939948
};
940949

950+
let view_tick = view_specialization_ticks
951+
.get(&extracted_view.retained_view_entity)
952+
.unwrap();
953+
let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache
954+
.entry(extracted_view.retained_view_entity)
955+
.or_default();
956+
941957
for (_, visible_entity) in visible_entities.iter::<Mesh3d>() {
942-
let view_tick = view_specialization_ticks.get(view_entity).unwrap();
943958
let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap();
944-
let last_specialized_tick = specialized_material_pipeline_cache
945-
.get(&(*view_entity, *visible_entity))
959+
let last_specialized_tick = view_specialized_material_pipeline_cache
960+
.get(visible_entity)
946961
.map(|(tick, _)| *tick);
947962
let needs_specialization = last_specialized_tick.is_none_or(|tick| {
948963
view_tick.is_newer_than(tick, ticks.this_run())
@@ -1054,10 +1069,8 @@ pub fn specialize_prepass_material_meshes<M>(
10541069
}
10551070
};
10561071

1057-
specialized_material_pipeline_cache.insert(
1058-
(*view_entity, *visible_entity),
1059-
(ticks.this_run(), pipeline_id),
1060-
);
1072+
view_specialized_material_pipeline_cache
1073+
.insert(*visible_entity, (ticks.this_run(), pipeline_id));
10611074
}
10621075
}
10631076
}
@@ -1072,12 +1085,12 @@ pub fn queue_prepass_material_meshes<M: Material>(
10721085
mut alpha_mask_prepass_render_phases: ResMut<ViewBinnedRenderPhases<AlphaMask3dPrepass>>,
10731086
mut opaque_deferred_render_phases: ResMut<ViewBinnedRenderPhases<Opaque3dDeferred>>,
10741087
mut alpha_mask_deferred_render_phases: ResMut<ViewBinnedRenderPhases<AlphaMask3dDeferred>>,
1075-
views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>,
1088+
views: Query<(&ExtractedView, &RenderVisibleEntities)>,
10761089
specialized_material_pipeline_cache: Res<SpecializedPrepassMaterialPipelineCache<M>>,
10771090
) where
10781091
M::Data: PartialEq + Eq + Hash + Clone,
10791092
{
1080-
for (view_entity, extracted_view, visible_entities) in &views {
1093+
for (extracted_view, visible_entities) in &views {
10811094
let (
10821095
mut opaque_phase,
10831096
mut alpha_mask_phase,
@@ -1090,6 +1103,12 @@ pub fn queue_prepass_material_meshes<M: Material>(
10901103
alpha_mask_deferred_render_phases.get_mut(&extracted_view.retained_view_entity),
10911104
);
10921105

1106+
let Some(view_specialized_material_pipeline_cache) =
1107+
specialized_material_pipeline_cache.get(&extracted_view.retained_view_entity)
1108+
else {
1109+
continue;
1110+
};
1111+
10931112
// Skip if there's no place to put the mesh.
10941113
if opaque_phase.is_none()
10951114
&& alpha_mask_phase.is_none()
@@ -1101,7 +1120,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
11011120

11021121
for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {
11031122
let Some((current_change_tick, pipeline_id)) =
1104-
specialized_material_pipeline_cache.get(&(*view_entity, *visible_entity))
1123+
view_specialized_material_pipeline_cache.get(visible_entity)
11051124
else {
11061125
continue;
11071126
};

0 commit comments

Comments
 (0)