From 915fa69b666eae1d04976995534ffae969973dde Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Tue, 14 Jun 2022 02:07:40 +0000 Subject: [PATCH] Parallel Frustum Culling (#4489) # Objective Working with a large number of entities with `Aabbs`, rendered with an instanced shader, I found the bottleneck became the frustum culling system. The goal of this PR is to significantly improve culling performance without any major changes. We should consider constructing a BVH for more substantial improvements. ## Solution - Convert the inner entity query to a parallel iterator with `par_for_each_mut` using a batch size of 1,024. - This outperforms single threaded culling when there are more than 1,000 entities. - Below this they are approximately equal, with <= 10 microseconds of multithreading overhead. - Above this, the multithreaded version is significantly faster, scaling linearly with core count. - In my million-entity-workload, this PR improves my framerate by 200% - 300%. ## log-log of `check_visibility` time vs. entities for single/multithreaded ![image](https://user-images.githubusercontent.com/2632925/163709007-7eab4437-e9f9-4c06-bac0-250073885110.png) --- ## Changelog Frustum culling is now run with a parallel query. When culling more than a thousand entities, this is faster than the previous method, scaling proportionally with the number of available cores. --- crates/bevy_render/Cargo.toml | 1 + crates/bevy_render/src/view/visibility/mod.rs | 106 +++++++++--------- 2 files changed, 51 insertions(+), 56 deletions(-) diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index ef15ffac44..fc9796e83f 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -56,6 +56,7 @@ once_cell = "1.4.1" # TODO: replace once_cell with std equivalent if/when this l downcast-rs = "1.2.0" thiserror = "1.0" futures-lite = "1.4.0" +crossbeam-channel = "0.5.0" anyhow = "1.0" hex = "0.4.2" hexasphere = "7.0.0" diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 104c72757d..4f8a456486 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -149,71 +149,65 @@ pub fn update_frusta( pub fn check_visibility( mut view_query: Query<(&mut VisibleEntities, &Frustum, Option<&RenderLayers>), With>, - mut visible_entity_query: ParamSet<( - Query<&mut ComputedVisibility>, - Query<( - Entity, - &Visibility, - &mut ComputedVisibility, - Option<&RenderLayers>, - Option<&Aabb>, - Option<&NoFrustumCulling>, - Option<&GlobalTransform>, - )>, + mut visible_entity_query: Query<( + Entity, + &Visibility, + &mut ComputedVisibility, + Option<&RenderLayers>, + Option<&Aabb>, + Option<&NoFrustumCulling>, + Option<&GlobalTransform>, )>, ) { - // Reset the computed visibility to false - for mut computed_visibility in visible_entity_query.p0().iter_mut() { - computed_visibility.is_visible = false; - } - for (mut visible_entities, frustum, maybe_view_mask) in view_query.iter_mut() { - visible_entities.entities.clear(); let view_mask = maybe_view_mask.copied().unwrap_or_default(); + let (visible_entity_sender, visible_entity_receiver) = crossbeam_channel::unbounded(); - for ( - entity, - visibility, - mut computed_visibility, - maybe_entity_mask, - maybe_aabb, - maybe_no_frustum_culling, - maybe_transform, - ) in visible_entity_query.p1().iter_mut() - { - if !visibility.is_visible { - continue; - } + visible_entity_query.par_for_each_mut( + 1024, + |( + entity, + visibility, + mut computed_visibility, + maybe_entity_mask, + maybe_aabb, + maybe_no_frustum_culling, + maybe_transform, + )| { + // Reset visibility + computed_visibility.is_visible = false; - let entity_mask = maybe_entity_mask.copied().unwrap_or_default(); - if !view_mask.intersects(&entity_mask) { - continue; - } - - // If we have an aabb and transform, do frustum culling - if let (Some(model_aabb), None, Some(transform)) = - (maybe_aabb, maybe_no_frustum_culling, maybe_transform) - { - let model = transform.compute_matrix(); - let model_sphere = Sphere { - center: model.transform_point3a(model_aabb.center), - radius: (Vec3A::from(transform.scale) * model_aabb.half_extents).length(), - }; - // Do quick sphere-based frustum culling - if !frustum.intersects_sphere(&model_sphere, false) { - continue; + if !visibility.is_visible { + return; } - // If we have an aabb, do aabb-based frustum culling - if !frustum.intersects_obb(model_aabb, &model, false) { - continue; + let entity_mask = maybe_entity_mask.copied().unwrap_or_default(); + if !view_mask.intersects(&entity_mask) { + return; } - } - computed_visibility.is_visible = true; - visible_entities.entities.push(entity); - } + // If we have an aabb and transform, do frustum culling + if let (Some(model_aabb), None, Some(transform)) = + (maybe_aabb, maybe_no_frustum_culling, maybe_transform) + { + let model = transform.compute_matrix(); + let model_sphere = Sphere { + center: model.transform_point3a(model_aabb.center), + radius: (Vec3A::from(transform.scale) * model_aabb.half_extents).length(), + }; + // Do quick sphere-based frustum culling + if !frustum.intersects_sphere(&model_sphere, false) { + return; + } + // If we have an aabb, do aabb-based frustum culling + if !frustum.intersects_obb(model_aabb, &model, false) { + return; + } + } - // TODO: check for big changes in visible entities len() vs capacity() (ex: 2x) and resize - // to prevent holding unneeded memory + computed_visibility.is_visible = true; + visible_entity_sender.send(entity).ok(); + }, + ); + visible_entities.entities = visible_entity_receiver.try_iter().collect(); } }