Skip to content

Commit a9a8523

Browse files
pcwaltonmrchantey
authored andcommitted
Retain bins from frame to frame. (bevyengine#17698)
This PR makes Bevy keep entities in bins from frame to frame if they haven't changed. This reduces the time spent in `queue_material_meshes` and related functions to near zero for static geometry. This patch uses the same change tick technique that bevyengine#17567 uses to detect when meshes have changed in such a way as to require re-binning. In order to quickly find the relevant bin for an entity when that entity has changed, we introduce a new type of cache, the *bin key cache*. This cache stores a mapping from main world entity ID to cached bin key, as well as the tick of the most recent change to the entity. As we iterate through the visible entities in `queue_material_meshes`, we check the cache to see whether the entity needs to be re-binned. If it doesn't, then we mark it as clean in the `valid_cached_entity_bin_keys` bit set. If it does, then we insert it into the correct bin, and then mark the entity as clean. At the end, all entities not marked as clean are removed from the bins. This patch has a dramatic effect on the rendering performance of most benchmarks, as it effectively eliminates `queue_material_meshes` from the profile. Note, however, that it generally simultaneously regresses `batch_and_prepare_binned_render_phase` by a bit (not by enough to outweigh the win, however). I believe that's because, before this patch, `queue_material_meshes` put the bins in the CPU cache for `batch_and_prepare_binned_render_phase` to use, while with this patch, `batch_and_prepare_binned_render_phase` must load the bins into the CPU cache itself. On Caldera, this reduces the time spent in `queue_material_meshes` from 5+ ms to 0.2ms-0.3ms. Note that benchmarking on that scene is very noisy right now because of bevyengine#17535. ![Screenshot 2025-02-05 153458](https://github.com/user-attachments/assets/e55f8134-b7e3-4b78-a5af-8d83e1e213b7)
1 parent a91ba46 commit a9a8523

File tree

15 files changed

+583
-86
lines changed

15 files changed

+583
-86
lines changed

crates/bevy_core_pipeline/src/core_2d/mod.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -436,8 +436,9 @@ pub fn extract_core_2d_camera_phases(
436436
let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0);
437437

438438
transparent_2d_phases.insert_or_clear(retained_view_entity);
439-
opaque_2d_phases.insert_or_clear(retained_view_entity, GpuPreprocessingMode::None);
440-
alpha_mask_2d_phases.insert_or_clear(retained_view_entity, GpuPreprocessingMode::None);
439+
opaque_2d_phases.prepare_for_new_frame(retained_view_entity, GpuPreprocessingMode::None);
440+
alpha_mask_2d_phases
441+
.prepare_for_new_frame(retained_view_entity, GpuPreprocessingMode::None);
441442

442443
live_entities.insert(retained_view_entity);
443444
}

crates/bevy_core_pipeline/src/core_3d/mod.rs

+8-6
Original file line numberDiff line numberDiff line change
@@ -629,8 +629,8 @@ pub fn extract_core_3d_camera_phases(
629629
// This is the main 3D camera, so use the first subview index (0).
630630
let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0);
631631

632-
opaque_3d_phases.insert_or_clear(retained_view_entity, gpu_preprocessing_mode);
633-
alpha_mask_3d_phases.insert_or_clear(retained_view_entity, gpu_preprocessing_mode);
632+
opaque_3d_phases.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode);
633+
alpha_mask_3d_phases.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode);
634634
transmissive_3d_phases.insert_or_clear(retained_view_entity);
635635
transparent_3d_phases.insert_or_clear(retained_view_entity);
636636

@@ -698,18 +698,20 @@ pub fn extract_camera_prepass_phase(
698698
let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0);
699699

700700
if depth_prepass || normal_prepass || motion_vector_prepass {
701-
opaque_3d_prepass_phases.insert_or_clear(retained_view_entity, gpu_preprocessing_mode);
701+
opaque_3d_prepass_phases
702+
.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode);
702703
alpha_mask_3d_prepass_phases
703-
.insert_or_clear(retained_view_entity, gpu_preprocessing_mode);
704+
.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode);
704705
} else {
705706
opaque_3d_prepass_phases.remove(&retained_view_entity);
706707
alpha_mask_3d_prepass_phases.remove(&retained_view_entity);
707708
}
708709

709710
if deferred_prepass {
710-
opaque_3d_deferred_phases.insert_or_clear(retained_view_entity, gpu_preprocessing_mode);
711+
opaque_3d_deferred_phases
712+
.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode);
711713
alpha_mask_3d_deferred_phases
712-
.insert_or_clear(retained_view_entity, gpu_preprocessing_mode);
714+
.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode);
713715
} else {
714716
opaque_3d_deferred_phases.remove(&retained_view_entity);
715717
alpha_mask_3d_deferred_phases.remove(&retained_view_entity);

crates/bevy_pbr/src/material.rs

+16-2
Original file line numberDiff line numberDiff line change
@@ -940,12 +940,20 @@ pub fn queue_material_meshes<M: Material>(
940940

941941
let rangefinder = view.rangefinder3d();
942942
for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {
943-
let Some(pipeline_id) = specialized_material_pipeline_cache
943+
let Some((current_change_tick, pipeline_id)) = specialized_material_pipeline_cache
944944
.get(&(*view_entity, *visible_entity))
945-
.map(|(_, pipeline_id)| *pipeline_id)
945+
.map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id))
946946
else {
947947
continue;
948948
};
949+
950+
// Skip the entity if it's cached in a bin and up to date.
951+
if opaque_phase.validate_cached_entity(*visible_entity, current_change_tick)
952+
|| alpha_mask_phase.validate_cached_entity(*visible_entity, current_change_tick)
953+
{
954+
continue;
955+
}
956+
949957
let Some(material_asset_id) = render_material_instances.get(visible_entity) else {
950958
continue;
951959
};
@@ -997,6 +1005,7 @@ pub fn queue_material_meshes<M: Material>(
9971005
mesh_instance.should_batch(),
9981006
&gpu_preprocessing_support,
9991007
),
1008+
current_change_tick,
10001009
);
10011010
}
10021011
// Alpha mask
@@ -1019,6 +1028,7 @@ pub fn queue_material_meshes<M: Material>(
10191028
mesh_instance.should_batch(),
10201029
&gpu_preprocessing_support,
10211030
),
1031+
current_change_tick,
10221032
);
10231033
}
10241034
RenderPhaseType::Transparent => {
@@ -1036,6 +1046,10 @@ pub fn queue_material_meshes<M: Material>(
10361046
}
10371047
}
10381048
}
1049+
1050+
// Remove invalid entities from the bins.
1051+
opaque_phase.sweep_old_entities();
1052+
alpha_mask_phase.sweep_old_entities();
10391053
}
10401054
}
10411055

crates/bevy_pbr/src/prepass/mod.rs

+33-1
Original file line numberDiff line numberDiff line change
@@ -1089,11 +1089,25 @@ pub fn queue_prepass_material_meshes<M: Material>(
10891089
}
10901090

10911091
for (render_entity, visible_entity) in visible_entities.iter::<Mesh3d>() {
1092-
let Some((_, pipeline_id)) =
1092+
let Some((current_change_tick, pipeline_id)) =
10931093
specialized_material_pipeline_cache.get(&(*view_entity, *visible_entity))
10941094
else {
10951095
continue;
10961096
};
1097+
1098+
// Skip the entity if it's cached in a bin and up to date.
1099+
if opaque_phase.as_mut().is_some_and(|phase| {
1100+
phase.validate_cached_entity(*visible_entity, *current_change_tick)
1101+
}) || alpha_mask_phase.as_mut().is_some_and(|phase| {
1102+
phase.validate_cached_entity(*visible_entity, *current_change_tick)
1103+
}) || opaque_deferred_phase.as_mut().is_some_and(|phase| {
1104+
phase.validate_cached_entity(*visible_entity, *current_change_tick)
1105+
}) || alpha_mask_deferred_phase.as_mut().is_some_and(|phase| {
1106+
phase.validate_cached_entity(*visible_entity, *current_change_tick)
1107+
}) {
1108+
continue;
1109+
}
1110+
10971111
let Some(material_asset_id) = render_material_instances.get(visible_entity) else {
10981112
continue;
10991113
};
@@ -1134,6 +1148,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
11341148
mesh_instance.should_batch(),
11351149
&gpu_preprocessing_support,
11361150
),
1151+
*current_change_tick,
11371152
);
11381153
} else if let Some(opaque_phase) = opaque_phase.as_mut() {
11391154
let (vertex_slab, index_slab) =
@@ -1157,6 +1172,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
11571172
mesh_instance.should_batch(),
11581173
&gpu_preprocessing_support,
11591174
),
1175+
*current_change_tick,
11601176
);
11611177
}
11621178
}
@@ -1182,6 +1198,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
11821198
mesh_instance.should_batch(),
11831199
&gpu_preprocessing_support,
11841200
),
1201+
*current_change_tick,
11851202
);
11861203
} else if let Some(alpha_mask_phase) = alpha_mask_phase.as_mut() {
11871204
let (vertex_slab, index_slab) =
@@ -1204,12 +1221,27 @@ pub fn queue_prepass_material_meshes<M: Material>(
12041221
mesh_instance.should_batch(),
12051222
&gpu_preprocessing_support,
12061223
),
1224+
*current_change_tick,
12071225
);
12081226
}
12091227
}
12101228
_ => {}
12111229
}
12121230
}
1231+
1232+
// Remove invalid entities from the bins.
1233+
if let Some(phase) = opaque_phase {
1234+
phase.sweep_old_entities();
1235+
}
1236+
if let Some(phase) = alpha_mask_phase {
1237+
phase.sweep_old_entities();
1238+
}
1239+
if let Some(phase) = opaque_deferred_phase {
1240+
phase.sweep_old_entities();
1241+
}
1242+
if let Some(phase) = alpha_mask_deferred_phase {
1243+
phase.sweep_old_entities();
1244+
}
12131245
}
12141246
}
12151247

crates/bevy_pbr/src/render/light.rs

+16-4
Original file line numberDiff line numberDiff line change
@@ -1299,7 +1299,7 @@ pub fn prepare_lights(
12991299
if first {
13001300
// Subsequent views with the same light entity will reuse the same shadow map
13011301
shadow_render_phases
1302-
.insert_or_clear(retained_view_entity, gpu_preprocessing_mode);
1302+
.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode);
13031303
live_shadow_mapping_lights.insert(retained_view_entity);
13041304
}
13051305
}
@@ -1396,7 +1396,8 @@ pub fn prepare_lights(
13961396

13971397
if first {
13981398
// Subsequent views with the same light entity will reuse the same shadow map
1399-
shadow_render_phases.insert_or_clear(retained_view_entity, gpu_preprocessing_mode);
1399+
shadow_render_phases
1400+
.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode);
14001401
live_shadow_mapping_lights.insert(retained_view_entity);
14011402
}
14021403
}
@@ -1539,7 +1540,8 @@ pub fn prepare_lights(
15391540
// Subsequent views with the same light entity will **NOT** reuse the same shadow map
15401541
// (Because the cascades are unique to each view)
15411542
// TODO: Implement GPU culling for shadow passes.
1542-
shadow_render_phases.insert_or_clear(retained_view_entity, gpu_preprocessing_mode);
1543+
shadow_render_phases
1544+
.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode);
15431545
live_shadow_mapping_lights.insert(retained_view_entity);
15441546
}
15451547
}
@@ -1886,11 +1888,17 @@ pub fn queue_shadows<M: Material>(
18861888
};
18871889

18881890
for (entity, main_entity) in visible_entities.iter().copied() {
1889-
let Some((_, pipeline_id)) =
1891+
let Some((current_change_tick, pipeline_id)) =
18901892
specialized_material_pipeline_cache.get(&(view_light_entity, main_entity))
18911893
else {
18921894
continue;
18931895
};
1896+
1897+
// Skip the entity if it's cached in a bin and up to date.
1898+
if shadow_phase.validate_cached_entity(main_entity, *current_change_tick) {
1899+
continue;
1900+
}
1901+
18941902
let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(main_entity)
18951903
else {
18961904
continue;
@@ -1930,8 +1938,12 @@ pub fn queue_shadows<M: Material>(
19301938
mesh_instance.should_batch(),
19311939
&gpu_preprocessing_support,
19321940
),
1941+
*current_change_tick,
19331942
);
19341943
}
1944+
1945+
// Remove invalid entities from the bins.
1946+
shadow_phase.sweep_old_entities();
19351947
}
19361948
}
19371949
}

crates/bevy_render/Cargo.toml

+5
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,18 @@ smallvec = { version = "1.11", features = ["const_new"] }
9696
offset-allocator = "0.2"
9797
variadics_please = "1.1"
9898
tracing = { version = "0.1", default-features = false, features = ["std"] }
99+
indexmap = { version = "2" }
100+
fixedbitset = { version = "0.5" }
99101

100102
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
101103
# Omit the `glsl` feature in non-WebAssembly by default.
102104
naga_oil = { version = "0.16", default-features = false, features = [
103105
"test_shader",
104106
] }
105107

108+
[dev-dependencies]
109+
proptest = "1"
110+
106111
[target.'cfg(target_arch = "wasm32")'.dependencies]
107112
naga_oil = "0.16"
108113
js-sys = "0.3"

crates/bevy_render/src/batching/gpu_preprocessing.rs

+11-9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use core::any::TypeId;
44

55
use bevy_app::{App, Plugin};
66
use bevy_ecs::{
7+
prelude::Entity,
78
query::{Has, With},
89
resource::Resource,
910
schedule::IntoSystemConfigs as _,
@@ -1326,8 +1327,9 @@ pub fn batch_and_prepare_binned_render_phase<BPI, GFBD>(
13261327
let first_output_index = data_buffer.len() as u32;
13271328
let mut batch: Option<BinnedRenderPhaseBatch> = None;
13281329

1329-
for &(entity, main_entity) in &bin.entities {
1330-
let Some(input_index) = GFBD::get_binned_index(&system_param_item, main_entity)
1330+
for main_entity in bin.entities() {
1331+
let Some(input_index) =
1332+
GFBD::get_binned_index(&system_param_item, *main_entity)
13311333
else {
13321334
continue;
13331335
};
@@ -1378,7 +1380,7 @@ pub fn batch_and_prepare_binned_render_phase<BPI, GFBD>(
13781380
},
13791381
);
13801382
batch = Some(BinnedRenderPhaseBatch {
1381-
representative_entity: (entity, main_entity),
1383+
representative_entity: (Entity::PLACEHOLDER, *main_entity),
13821384
instance_range: output_index..output_index + 1,
13831385
extra_index: PhaseItemExtraIndex::maybe_indirect_parameters_index(
13841386
NonMaxU32::new(indirect_parameters_index),
@@ -1424,8 +1426,8 @@ pub fn batch_and_prepare_binned_render_phase<BPI, GFBD>(
14241426
let first_output_index = data_buffer.len() as u32;
14251427

14261428
let mut batch: Option<BinnedRenderPhaseBatch> = None;
1427-
for &(entity, main_entity) in &phase.batchable_mesh_values[key].entities {
1428-
let Some(input_index) = GFBD::get_binned_index(&system_param_item, main_entity)
1429+
for main_entity in phase.batchable_mesh_values[key].entities() {
1430+
let Some(input_index) = GFBD::get_binned_index(&system_param_item, *main_entity)
14291431
else {
14301432
continue;
14311433
};
@@ -1487,7 +1489,7 @@ pub fn batch_and_prepare_binned_render_phase<BPI, GFBD>(
14871489
},
14881490
);
14891491
batch = Some(BinnedRenderPhaseBatch {
1490-
representative_entity: (entity, main_entity),
1492+
representative_entity: (Entity::PLACEHOLDER, *main_entity),
14911493
instance_range: output_index..output_index + 1,
14921494
extra_index: PhaseItemExtraIndex::IndirectParametersIndex {
14931495
range: indirect_parameters_index..(indirect_parameters_index + 1),
@@ -1507,7 +1509,7 @@ pub fn batch_and_prepare_binned_render_phase<BPI, GFBD>(
15071509
},
15081510
);
15091511
batch = Some(BinnedRenderPhaseBatch {
1510-
representative_entity: (entity, main_entity),
1512+
representative_entity: (Entity::PLACEHOLDER, *main_entity),
15111513
instance_range: output_index..output_index + 1,
15121514
extra_index: PhaseItemExtraIndex::None,
15131515
});
@@ -1559,8 +1561,8 @@ pub fn batch_and_prepare_binned_render_phase<BPI, GFBD>(
15591561
)
15601562
};
15611563

1562-
for &(_, main_entity) in &unbatchables.entities {
1563-
let Some(input_index) = GFBD::get_binned_index(&system_param_item, main_entity)
1564+
for main_entity in unbatchables.entities.keys() {
1565+
let Some(input_index) = GFBD::get_binned_index(&system_param_item, *main_entity)
15641566
else {
15651567
continue;
15661568
};

crates/bevy_render/src/batching/mod.rs

+14
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,22 @@ where
182182
BPI: BinnedPhaseItem,
183183
{
184184
for phase in phases.values_mut() {
185+
phase.multidrawable_mesh_keys.clear();
186+
phase
187+
.multidrawable_mesh_keys
188+
.extend(phase.multidrawable_mesh_values.keys().cloned());
185189
phase.multidrawable_mesh_keys.sort_unstable();
190+
191+
phase.batchable_mesh_keys.clear();
192+
phase
193+
.batchable_mesh_keys
194+
.extend(phase.batchable_mesh_values.keys().cloned());
186195
phase.batchable_mesh_keys.sort_unstable();
196+
197+
phase.unbatchable_mesh_keys.clear();
198+
phase
199+
.unbatchable_mesh_keys
200+
.extend(phase.unbatchable_mesh_values.keys().cloned());
187201
phase.unbatchable_mesh_keys.sort_unstable();
188202
}
189203
}

crates/bevy_render/src/batching/no_gpu_preprocessing.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Batching functionality when GPU preprocessing isn't in use.
22
33
use bevy_derive::{Deref, DerefMut};
4+
use bevy_ecs::entity::Entity;
45
use bevy_ecs::resource::Resource;
56
use bevy_ecs::system::{Res, ResMut, StaticSystemParam};
67
use smallvec::{smallvec, SmallVec};
@@ -109,9 +110,9 @@ pub fn batch_and_prepare_binned_render_phase<BPI, GFBD>(
109110

110111
for key in &phase.batchable_mesh_keys {
111112
let mut batch_set: SmallVec<[BinnedRenderPhaseBatch; 1]> = smallvec![];
112-
for &(entity, main_entity) in &phase.batchable_mesh_values[key].entities {
113+
for main_entity in phase.batchable_mesh_values[key].entities() {
113114
let Some(buffer_data) =
114-
GFBD::get_binned_batch_data(&system_param_item, main_entity)
115+
GFBD::get_binned_batch_data(&system_param_item, *main_entity)
115116
else {
116117
continue;
117118
};
@@ -128,7 +129,7 @@ pub fn batch_and_prepare_binned_render_phase<BPI, GFBD>(
128129
== PhaseItemExtraIndex::maybe_dynamic_offset(instance.dynamic_offset)
129130
}) {
130131
batch_set.push(BinnedRenderPhaseBatch {
131-
representative_entity: (entity, main_entity),
132+
representative_entity: (Entity::PLACEHOLDER, *main_entity),
132133
instance_range: instance.index..instance.index,
133134
extra_index: PhaseItemExtraIndex::maybe_dynamic_offset(
134135
instance.dynamic_offset,
@@ -157,9 +158,9 @@ pub fn batch_and_prepare_binned_render_phase<BPI, GFBD>(
157158
// Prepare unbatchables.
158159
for key in &phase.unbatchable_mesh_keys {
159160
let unbatchables = phase.unbatchable_mesh_values.get_mut(key).unwrap();
160-
for &(_, main_entity) in &unbatchables.entities {
161+
for main_entity in unbatchables.entities.keys() {
161162
let Some(buffer_data) =
162-
GFBD::get_binned_batch_data(&system_param_item, main_entity)
163+
GFBD::get_binned_batch_data(&system_param_item, *main_entity)
163164
else {
164165
continue;
165166
};

0 commit comments

Comments
 (0)