Skip to content

Commit e4eb916

Browse files
authored
Optimize Visibility Systems (#22226)
# Objective - Make visibility systems less slow. - Saves as much as 1.2ms of CPU time on a (GPU bound) 6.7ms frame, rendering caldera hotel. - Fixes #22256 ## Solution - Replace the `EntityHashSet` with a component added directly to entities. This still allows for correct change detection triggers on visibility, but avoids hashing. This also enables parallel updates. ## Summary - This PR lead to a rabbit hole that uncovered #22256 - This PR resolves the regression introduced since 0.17 released - A more fair comparison is not to main (which has a regression), but to 0.17 - Compared to 0.17.3, this is a 27% speedup to the `PostUpdate` schedule for caldera hotel on an M4 Max <img width="558" height="251" alt="image" src="https://github.com/user-attachments/assets/2d711e3c-b65e-4c3a-9d2a-a587f8f36aae" /> ## Testing - Many cubes, many foxes, caldera - Yellow is new, red is old. - Note the bimodality in the old traces, this was consistently repeatable, and seems to have something to do with `EntityHashSet` and `EntityHashMap`. Worth investigating that further, as I've seen that bimodal behavior before, and blamed it on pcores vs ecores, but I verified that is not happening. - Summary: caldera hotel no longer exhibits bimodal performance with a pathological mode that runs very slowly. Roughly comparing the ~90th percentile performance, the optimized code is about 1.2ms faster. This is particularly significant when you consider this is running on a device that is already hitting 150fps (GPU bound), so it only has a frame budget of 6.7ms. - Notably, visibility checking was the last egregiously performing set of systems in the common hot path of most bevy apps, and no longer stick out as obviously slow in a frame profile: A high level comparison of the change in CPU time by looking at just PostUpdate between old(red) and new(yellow) <img width="1738" height="512" alt="image" src="https://github.com/user-attachments/assets/5252ff0f-d9f3-49d1-a3ce-309bd84d5485" /> ### caldera hotel - all entities in view For this test, I didn't touch the camera, so all entities were in view at all times. `check_visibility` <img width="1262" height="397" alt="image" src="https://github.com/user-attachments/assets/c80c470a-1548-42d2-838c-4e74525a6cb8" /> `reset_view_visibility` <img width="1262" height="407" alt="image" src="https://github.com/user-attachments/assets/7b550823-f832-41f1-9e42-20cab342b0f2" /> `mark_newly_hidden_entities_invisible` This is 23us slower than the "fast mode" of the existing code on main, but 180-250us *faster* than the weird "slow mode" on main. The new code is significantly more stable, and does not exhibit the super slow mode. <img width="1256" height="418" alt="image" src="https://github.com/user-attachments/assets/b7cbf57d-a221-468a-9af8-3381981874b2" /> ### caldera hotel - no entities in view For this test, I immediately rotated the camera so the hotel was not in view. `check_visibility` This is largely a wash. The old code is 2us faster, but this is likely in the realm of noise. <img width="1255" height="372" alt="image" src="https://github.com/user-attachments/assets/2c795f72-ebea-4b35-ba19-a8eccd4c56fc" /> `reset_view_visibility` This is a big win. The main peak is about 30us faster now, but the major win is the worst case, which is nearly 500us faster. <img width="1252" height="398" alt="image" src="https://github.com/user-attachments/assets/f8a9d99f-a6e1-48db-8639-d52276dba9ab" /> `mark_newly_hidden_entities_invisible` Same story as the other caldera comparison with everything in view, this is a bit slower than the fast mode, but way faster than the slow mode on main. <img width="1248" height="400" alt="image" src="https://github.com/user-attachments/assets/4564d94e-911a-4b3f-a584-c6e102ef2dd0" />
1 parent 730b002 commit e4eb916

File tree

2 files changed

+96
-126
lines changed

2 files changed

+96
-126
lines changed

crates/bevy_camera/src/visibility/mod.rs

Lines changed: 77 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ mod render_layers;
33

44
use core::any::TypeId;
55

6-
use bevy_ecs::entity::{EntityHashMap, EntityHashSet};
6+
use bevy_ecs::entity::EntityHashMap;
77
use bevy_ecs::lifecycle::HookContext;
88
use bevy_ecs::world::DeferredWorld;
99
use derive_more::derive::{Deref, DerefMut};
@@ -163,46 +163,85 @@ impl InheritedVisibility {
163163
#[component(clone_behavior=Ignore)]
164164
pub struct VisibilityClass(pub SmallVec<[TypeId; 1]>);
165165

166-
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering.
166+
/// Algorithmically computed indication of whether an entity is visible and should be extracted for
167+
/// rendering.
167168
///
168-
/// Each frame, this will be reset to `false` during [`VisibilityPropagate`] systems in [`PostUpdate`].
169-
/// Later in the frame, systems in [`CheckVisibility`] will mark any visible entities using [`ViewVisibility::set`].
170-
/// Because of this, values of this type will be marked as changed every frame, even when they do not change.
169+
/// Each frame, this will be reset to `false` during [`VisibilityPropagate`] systems in
170+
/// [`PostUpdate`]. Later in the frame, systems in [`CheckVisibility`] will mark any visible
171+
/// entities using [`ViewVisibility::set`]. Because of this, values of this type will be marked as
172+
/// changed every frame, even when they do not change.
171173
///
172-
/// If you wish to add custom visibility system that sets this value, make sure you add it to the [`CheckVisibility`] set.
174+
/// If you wish to add a custom visibility system that sets this value, be sure to add it to the
175+
/// [`CheckVisibility`] set.
173176
///
174177
/// [`VisibilityPropagate`]: VisibilitySystems::VisibilityPropagate
175178
/// [`CheckVisibility`]: VisibilitySystems::CheckVisibility
176-
#[derive(Component, Deref, Debug, Default, Clone, Copy, Reflect, PartialEq, Eq)]
179+
#[derive(Component, Debug, Default, Clone, Copy, Reflect, PartialEq, Eq)]
177180
#[reflect(Component, Default, Debug, PartialEq, Clone)]
178-
pub struct ViewVisibility(bool);
181+
pub struct ViewVisibility(
182+
/// Bit packed booleans to track current and previous view visibility state.
183+
///
184+
/// Previous visibility is used as a scratch space to ensure that [`ViewVisibility`] is only
185+
/// mutated (triggering change detection) when necessary.
186+
///
187+
/// This is needed because an entity might be seen by many views (cameras, lights that cast
188+
/// shadows, etc.), so it is easy to know if an entity is visible to something, but hard to know
189+
/// if it is *globally* non-visible to any view. To solve this, we track the visibility from the
190+
/// previous frame. Then, during the [`VisibilitySystems::CheckVisibility`] system set, systems
191+
/// call [`SetViewVisibility::set_visible`] to mark entities as visible.
192+
///
193+
/// Finally, we can look for entities that were previously visible but are no longer visible
194+
/// and set their current state to hidden, ensuring that we have only triggered change detection
195+
/// when necessary.
196+
u8,
197+
);
179198

180199
impl ViewVisibility {
181200
/// An entity that cannot be seen from any views.
182-
pub const HIDDEN: Self = Self(false);
201+
pub const HIDDEN: Self = Self(0);
183202

184203
/// Returns `true` if the entity is visible in any view.
185204
/// Otherwise, returns `false`.
186205
#[inline]
187206
pub fn get(self) -> bool {
188-
self.0
207+
self.0 & 1 != 0
189208
}
190209

191-
/// Sets the visibility to `true`. This should not be considered reversible for a given frame,
192-
/// as this component tracks whether or not the entity visible in _any_ view.
193-
///
194-
/// This will be automatically reset to `false` every frame in [`VisibilityPropagate`] and then set
195-
/// to the proper value in [`CheckVisibility`].
210+
/// Returns `true` if this entity was visible in the previous frame but is now hidden.
211+
#[inline]
212+
fn was_visible_now_hidden(self) -> bool {
213+
// The first bit is false (current), and the second bit is true (previous).
214+
self.0 == 0b10
215+
}
216+
217+
#[inline]
218+
fn update(&mut self) {
219+
// Copy the first bit (current) to the second bit position (previous)
220+
// Clear the second bit, then set it based on the first bit
221+
self.0 = (self.0 & !2) | ((self.0 & 1) << 1);
222+
}
223+
}
224+
225+
pub trait SetViewVisibility {
226+
/// Sets the visibility to `true` if not already visible, triggering change detection only when
227+
/// needed. This should not be considered reversible for a given frame, as this component tracks
228+
/// if the entity is visible in _any_ view.
196229
///
197230
/// You should only manually set this if you are defining a custom visibility system,
198231
/// in which case the system should be placed in the [`CheckVisibility`] set.
199232
/// For normal user-defined entity visibility, see [`Visibility`].
200233
///
201-
/// [`VisibilityPropagate`]: VisibilitySystems::VisibilityPropagate
202234
/// [`CheckVisibility`]: VisibilitySystems::CheckVisibility
235+
fn set_visible(&mut self);
236+
}
237+
238+
impl<'a> SetViewVisibility for Mut<'a, ViewVisibility> {
203239
#[inline]
204-
pub fn set(&mut self) {
205-
self.0 = true;
240+
fn set_visible(&mut self) {
241+
if !self.as_ref().get() {
242+
// Set the first bit (current vis) to true
243+
self.0 |= 1;
244+
}
206245
}
207246
}
208247

@@ -369,7 +408,6 @@ impl Plugin for VisibilityPlugin {
369408
.ambiguous_with(CalculateBounds)
370409
.ambiguous_with(mark_3d_meshes_as_changed_if_their_assets_changed),
371410
)
372-
.init_resource::<PreviousVisibleEntities>()
373411
.add_systems(
374412
PostUpdate,
375413
(
@@ -522,28 +560,11 @@ fn propagate_recursive(
522560
Ok(())
523561
}
524562

525-
/// Stores all entities that were visible in the previous frame.
526-
///
527-
/// As systems that check visibility judge entities visible, they remove them
528-
/// from this set. Afterward, the `mark_newly_hidden_entities_invisible` system
529-
/// runs and marks every mesh still remaining in this set as hidden.
530-
#[derive(Resource, Default, Deref, DerefMut)]
531-
pub struct PreviousVisibleEntities(EntityHashSet);
532-
533-
/// Resets the view visibility of every entity.
534-
/// Entities that are visible will be marked as such later this frame
535-
/// by a [`VisibilitySystems::CheckVisibility`] system.
536-
fn reset_view_visibility(
537-
mut query: Query<(Entity, &ViewVisibility)>,
538-
mut previous_visible_entities: ResMut<PreviousVisibleEntities>,
539-
) {
540-
previous_visible_entities.clear();
541-
542-
query.iter_mut().for_each(|(entity, view_visibility)| {
543-
// Record the entities that were previously visible.
544-
if view_visibility.get() {
545-
previous_visible_entities.insert(entity);
546-
}
563+
/// Track entities that were visible last frame, used to granularly update [`ViewVisibility`] this
564+
/// frame without spurious `Change` detecation.
565+
fn reset_view_visibility(mut query: Query<&mut ViewVisibility>) {
566+
query.par_iter_mut().for_each(|mut view_visibility| {
567+
view_visibility.bypass_change_detection().update();
547568
});
548569
}
549570

@@ -577,7 +598,6 @@ pub fn check_visibility(
577598
Has<VisibilityRange>,
578599
)>,
579600
visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
580-
mut previous_visible_entities: ResMut<PreviousVisibleEntities>,
581601
) {
582602
let visible_entity_ranges = visible_entity_ranges.as_deref();
583603

@@ -645,17 +665,12 @@ pub fn check_visibility(
645665
}
646666
}
647667

648-
// Make sure we don't trigger changed notifications
649-
// unnecessarily by checking whether the flag is set before
650-
// setting it.
651-
if !**view_visibility {
652-
view_visibility.set();
653-
}
668+
view_visibility.set_visible();
654669

655670
// The visibility class may be None here because AABB gizmos can be enabled via
656671
// config without a renderable component being added to the entity. This workaround
657-
// allows view visibility to be set for entities without a renderable component but
658-
// that still need to render gizmos.
672+
// allows view visibility to be set for entities without a renderable component, but
673+
// still need to render gizmos.
659674
if let Some(visibility_class) = visibility_class {
660675
// Add the entity to the queue for all visibility classes the entity is in.
661676
for visibility_class_id in visibility_class.iter() {
@@ -670,40 +685,23 @@ pub fn check_visibility(
670685
// Drain all the thread queues into the `visible_entities` list.
671686
for class_queues in thread_queues.iter_mut() {
672687
for (class, entities) in class_queues {
673-
let visible_entities_for_class = visible_entities.get_mut(*class);
674-
for entity in entities.drain(..) {
675-
// As we mark entities as visible, we remove them from the
676-
// `previous_visible_entities` list. At the end, all of the
677-
// entities remaining in `previous_visible_entities` will be
678-
// entities that were visible last frame but are no longer
679-
// visible this frame.
680-
previous_visible_entities.remove(&entity);
681-
682-
visible_entities_for_class.push(entity);
683-
}
688+
visible_entities.get_mut(*class).append(entities);
684689
}
685690
}
686691
}
687692
}
688693

689-
/// Marks any entities that weren't judged visible this frame as invisible.
690-
///
691-
/// As visibility-determining systems run, they remove entities that they judge
692-
/// visible from [`PreviousVisibleEntities`]. At the end of visibility
693-
/// determination, all entities that remain in [`PreviousVisibleEntities`] must
694-
/// be invisible. This system goes through those entities and marks them newly
695-
/// invisible (which sets the change flag for them).
696-
fn mark_newly_hidden_entities_invisible(
697-
mut view_visibilities: Query<&mut ViewVisibility>,
698-
mut previous_visible_entities: ResMut<PreviousVisibleEntities>,
699-
) {
700-
// Whatever previous visible entities are left are entities that were
701-
// visible last frame but just became invisible.
702-
for entity in previous_visible_entities.drain() {
703-
if let Ok(mut view_visibility) = view_visibilities.get_mut(entity) {
704-
*view_visibility = ViewVisibility::HIDDEN;
705-
}
706-
}
694+
/// The last step in the visibility pipeline. Looks at entities that were visible last frame but not
695+
/// marked as visible this frame and marks them as hidden by setting the [`ViewVisibility`]. This
696+
/// process is needed to ensure we only trigger change detection on [`ViewVisibility`] when needed.
697+
fn mark_newly_hidden_entities_invisible(mut view_visibilities: Query<&mut ViewVisibility>) {
698+
view_visibilities
699+
.par_iter_mut()
700+
.for_each(|mut view_visibility| {
701+
if view_visibility.as_ref().was_visible_now_hidden() {
702+
*view_visibility = ViewVisibility::HIDDEN;
703+
}
704+
});
707705
}
708706

709707
/// A generic component add hook that automatically adds the appropriate

crates/bevy_light/src/lib.rs

Lines changed: 19 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use bevy_camera::{
55
primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere},
66
visibility::{
77
CascadesVisibleEntities, CubemapVisibleEntities, InheritedVisibility, NoFrustumCulling,
8-
PreviousVisibleEntities, RenderLayers, ViewVisibility, VisibilityRange, VisibilitySystems,
9-
VisibleEntityRanges, VisibleMeshEntities,
8+
RenderLayers, ViewVisibility, VisibilityRange, VisibilitySystems, VisibleEntityRanges,
9+
VisibleMeshEntities,
1010
},
1111
CameraUpdateSystems,
1212
};
@@ -26,6 +26,8 @@ use cluster::{
2626
};
2727
mod ambient_light;
2828
pub use ambient_light::{AmbientLight, GlobalAmbientLight};
29+
use bevy_camera::visibility::SetViewVisibility;
30+
2931
mod probe;
3032
pub use probe::{
3133
AtmosphereEnvironmentMapLight, EnvironmentMapLight, GeneratedEnvironmentMapLight,
@@ -188,9 +190,10 @@ impl Plugin for LightPlugin {
188190
.after(VisibilitySystems::CalculateBounds)
189191
.after(TransformSystems::Propagate)
190192
.after(SimulationLightSystems::UpdateLightFrusta)
191-
// NOTE: This MUST be scheduled AFTER the core renderer visibility check
192-
// because that resets entity `ViewVisibility` for the first view
193-
// which would override any results from this otherwise
193+
// Lights can "see" entities and mark them as visible. This is done to
194+
// correctly render shadows for entities that are not in view of a camera,
195+
// but must be renderable to cast shadows. Because of this, we need to check
196+
// entity visibility and mark as visible before they can be hidden.
194197
.after(VisibilitySystems::CheckVisibility)
195198
.before(VisibilitySystems::MarkNewlyHiddenEntitiesInvisible),
196199
build_directional_light_cascades
@@ -450,23 +453,13 @@ pub fn check_dir_light_mesh_visibility(
450453
// TODO: use resource to avoid unnecessary memory alloc
451454
let mut defer_queue = core::mem::take(defer_visible_entities_queue.deref_mut());
452455
commands.queue(move |world: &mut World| {
453-
world.resource_scope::<PreviousVisibleEntities, _>(
454-
|world, mut previous_visible_entities| {
455-
let mut query = world.query::<(Entity, &mut ViewVisibility)>();
456-
for entities in defer_queue.iter_mut() {
457-
let mut iter = query.iter_many_mut(world, entities.iter());
458-
while let Some((entity, mut view_visibility)) = iter.fetch_next() {
459-
if !**view_visibility {
460-
view_visibility.set();
461-
}
462-
463-
// Remove any entities that were discovered to be
464-
// visible from the `PreviousVisibleEntities` resource.
465-
previous_visible_entities.remove(&entity);
466-
}
467-
}
468-
},
469-
);
456+
let mut query = world.query::<&mut ViewVisibility>();
457+
for entities in defer_queue.iter_mut() {
458+
let mut iter = query.iter_many_mut(world, entities.iter());
459+
while let Some(mut view_visibility) = iter.fetch_next() {
460+
view_visibility.set_visible();
461+
}
462+
}
470463
});
471464
}
472465

@@ -504,7 +497,6 @@ pub fn check_point_light_mesh_visibility(
504497
),
505498
>,
506499
visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
507-
mut previous_visible_entities: ResMut<PreviousVisibleEntities>,
508500
mut cubemap_visible_entities_queue: Local<Parallel<[Vec<Entity>; 6]>>,
509501
mut spot_visible_entities_queue: Local<Parallel<Vec<Entity>>>,
510502
mut checked_lights: Local<EntityHashSet>,
@@ -587,16 +579,12 @@ pub fn check_point_light_mesh_visibility(
587579
if has_no_frustum_culling
588580
|| frustum.intersects_obb(aabb, &model_to_world, true, true)
589581
{
590-
if !**view_visibility {
591-
view_visibility.set();
592-
}
582+
view_visibility.set_visible();
593583
visible_entities.push(entity);
594584
}
595585
}
596586
} else {
597-
if !**view_visibility {
598-
view_visibility.set();
599-
}
587+
view_visibility.set_visible();
600588
for visible_entities in cubemap_visible_entities_local_queue.iter_mut()
601589
{
602590
visible_entities.push(entity);
@@ -609,12 +597,6 @@ pub fn check_point_light_mesh_visibility(
609597
for (dst, source) in
610598
cubemap_visible_entities.iter_mut().zip(entities.iter_mut())
611599
{
612-
// Remove any entities that were discovered to be
613-
// visible from the `PreviousVisibleEntities` resource.
614-
for entity in source.iter() {
615-
previous_visible_entities.remove(entity);
616-
}
617-
618600
dst.entities.append(source);
619601
}
620602
}
@@ -683,28 +665,18 @@ pub fn check_point_light_mesh_visibility(
683665
if has_no_frustum_culling
684666
|| frustum.intersects_obb(aabb, &model_to_world, true, true)
685667
{
686-
if !**view_visibility {
687-
view_visibility.set();
688-
}
668+
view_visibility.set_visible();
689669
spot_visible_entities_local_queue.push(entity);
690670
}
691671
} else {
692-
if !**view_visibility {
693-
view_visibility.set();
694-
}
672+
view_visibility.set_visible();
695673
spot_visible_entities_local_queue.push(entity);
696674
}
697675
},
698676
);
699677

700678
for entities in spot_visible_entities_queue.iter_mut() {
701679
visible_entities.append(entities);
702-
703-
// Remove any entities that were discovered to be visible
704-
// from the `PreviousVisibleEntities` resource.
705-
for entity in entities {
706-
previous_visible_entities.remove(entity);
707-
}
708680
}
709681

710682
shrink_entities(visible_entities.deref_mut());

0 commit comments

Comments
 (0)