Divide the single VisibleEntities list into separate lists for 2D meshes, 3D meshes, lights, and UI elements, for performance. (#12582)

This commit splits `VisibleEntities::entities` into four separate lists:
one for lights, one for 2D meshes, one for 3D meshes, and one for UI
elements. This allows `queue_material_meshes` and similar methods to
avoid examining entities that are obviously irrelevant. In particular,
this separation helps scenes with many skinned meshes, as the individual
bones are considered visible entities but have no rendered appearance.

Internally, `VisibleEntities::entities` is a `HashMap` from the `TypeId`
representing a `QueryFilter` to the appropriate `Entity` list. I had to
do this because `VisibleEntities` is located within an upstream crate
from the crates that provide lights (`bevy_pbr`) and 2D meshes
(`bevy_sprite`). As an added benefit, this setup allows apps to provide
their own types of renderable components, by simply adding a specialized
`check_visibility` to the schedule.

This provides a 16.23% end-to-end speedup on `many_foxes` with 10,000
foxes (24.06 ms/frame to 20.70 ms/frame).

## Migration guide

* `check_visibility` and `VisibleEntities` now store the four types of
renderable entities--2D meshes, 3D meshes, lights, and UI
elements--separately. If your custom rendering code examines
`VisibleEntities`, it will now need to specify which type of entity it's
interested in using the `WithMesh2d`, `WithMesh`, `WithLight`, and
`WithNode` types respectively. If your app introduces a new type of
renderable entity, you'll need to add an explicit call to
`check_visibility` to the schedule to accommodate your new component or
components.

## Analysis

`many_foxes`, 10,000 foxes: `main`:
![Screenshot 2024-03-31
114444](https://github.com/bevyengine/bevy/assets/157897/16ecb2ff-6e04-46c0-a4b0-b2fde2084bad)

`many_foxes`, 10,000 foxes, this branch:
![Screenshot 2024-03-31
114256](https://github.com/bevyengine/bevy/assets/157897/94dedae4-bd00-45b2-9aaf-dfc237004ddb)

`queue_material_meshes` (yellow = this branch, red = `main`):
![Screenshot 2024-03-31
114637](https://github.com/bevyengine/bevy/assets/157897/f90912bd-45bd-42c4-bd74-57d98a0f036e)

`queue_shadows` (yellow = this branch, red = `main`):
![Screenshot 2024-03-31
114607](https://github.com/bevyengine/bevy/assets/157897/6ce693e3-20c0-4234-8ec9-a6f191299e2d)
This commit is contained in:
Patrick Walton 2024-04-11 15:33:20 -05:00 committed by GitHub
parent 5c3ae32ab1
commit 5caf085dac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 159 additions and 54 deletions

View file

@ -98,7 +98,7 @@ use bevy_render::{
render_graph::RenderGraph,
render_resource::Shader,
texture::{GpuImage, Image},
view::VisibilitySystems,
view::{check_visibility, VisibilitySystems},
ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_transform::TransformSystem;
@ -349,6 +349,14 @@ impl Plugin for PbrPlugin {
.in_set(SimulationLightSystems::UpdateLightFrusta)
.after(TransformSystem::TransformPropagate)
.after(SimulationLightSystems::AssignLightsToClusters),
check_visibility::<WithLight>
.in_set(VisibilitySystems::CheckVisibility)
.after(VisibilitySystems::CalculateBounds)
.after(VisibilitySystems::UpdateOrthographicFrusta)
.after(VisibilitySystems::UpdatePerspectiveFrusta)
.after(VisibilitySystems::UpdateProjectionFrusta)
.after(VisibilitySystems::VisibilityPropagate)
.after(TransformSystem::TransformPropagate),
check_light_mesh_visibility
.in_set(SimulationLightSystems::CheckLightVisibility)
.after(VisibilitySystems::CalculateBounds)

View file

@ -10,10 +10,11 @@ use bevy_render::{
camera::{Camera, CameraProjection},
extract_component::ExtractComponent,
extract_resource::ExtractResource,
mesh::Mesh,
primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, HalfSpace, Sphere},
render_resource::BufferBindingType,
renderer::RenderDevice,
view::{InheritedVisibility, RenderLayers, ViewVisibility, VisibleEntities},
view::{InheritedVisibility, RenderLayers, ViewVisibility, VisibleEntities, WithMesh},
};
use bevy_transform::components::{GlobalTransform, Transform};
use bevy_utils::tracing::warn;
@ -98,6 +99,10 @@ impl Default for PointLightShadowMap {
}
}
/// A convenient alias for `Or<(With<PointLight>, With<SpotLight>,
/// With<DirectionalLight>)>`, for use with [`VisibleEntities`].
pub type WithLight = Or<(With<PointLight>, With<SpotLight>, With<DirectionalLight>)>;
/// Controls the resolution of [`DirectionalLight`] shadow maps.
#[derive(Resource, Clone, Debug, Reflect)]
#[reflect(Resource)]
@ -432,11 +437,11 @@ fn calculate_cascade(
texel_size: cascade_texel_size,
}
}
/// Add this component to make a [`Mesh`](bevy_render::mesh::Mesh) not cast shadows.
/// Add this component to make a [`Mesh`] not cast shadows.
#[derive(Component, Reflect, Default)]
#[reflect(Component, Default)]
pub struct NotShadowCaster;
/// Add this component to make a [`Mesh`](bevy_render::mesh::Mesh) not receive shadows.
/// Add this component to make a [`Mesh`] not receive shadows.
///
/// **Note:** If you're using diffuse transmission, setting [`NotShadowReceiver`] will
/// cause both “regular” shadows as well as diffusely transmitted shadows to be disabled,
@ -444,7 +449,7 @@ pub struct NotShadowCaster;
#[derive(Component, Reflect, Default)]
#[reflect(Component, Default)]
pub struct NotShadowReceiver;
/// Add this component to make a [`Mesh`](bevy_render::mesh::Mesh) using a PBR material with [`diffuse_transmission`](crate::pbr_material::StandardMaterial::diffuse_transmission)`> 0.0`
/// Add this component to make a [`Mesh`] using a PBR material with [`diffuse_transmission`](crate::pbr_material::StandardMaterial::diffuse_transmission)`> 0.0`
/// receive shadows on its diffuse transmission lobe. (i.e. its “backside”)
///
/// Not enabled by default, as it requires carefully setting up [`thickness`](crate::pbr_material::StandardMaterial::thickness)
@ -1859,7 +1864,11 @@ pub fn check_light_mesh_visibility(
Option<&Aabb>,
Option<&GlobalTransform>,
),
(Without<NotShadowCaster>, Without<DirectionalLight>),
(
Without<NotShadowCaster>,
Without<DirectionalLight>,
With<Handle<Mesh>>,
),
>,
) {
fn shrink_entities(visible_entities: &mut VisibleEntities) {
@ -1947,7 +1956,7 @@ pub fn check_light_mesh_visibility(
}
view_visibility.set();
frustum_visible_entities.entities.push(entity);
frustum_visible_entities.get_mut::<WithMesh>().push(entity);
}
}
} else {
@ -1959,7 +1968,7 @@ pub fn check_light_mesh_visibility(
.expect("Per-view visible entities should have been inserted already");
for frustum_visible_entities in view_visible_entities {
frustum_visible_entities.entities.push(entity);
frustum_visible_entities.get_mut::<WithMesh>().push(entity);
}
}
}
@ -2028,13 +2037,13 @@ pub fn check_light_mesh_visibility(
{
if frustum.intersects_obb(aabb, &model_to_world, true, true) {
view_visibility.set();
visible_entities.entities.push(entity);
visible_entities.push::<WithMesh>(entity);
}
}
} else {
view_visibility.set();
for visible_entities in cubemap_visible_entities.iter_mut() {
visible_entities.entities.push(entity);
visible_entities.push::<WithMesh>(entity);
}
}
}
@ -2089,11 +2098,11 @@ pub fn check_light_mesh_visibility(
if frustum.intersects_obb(aabb, &model_to_world, true, true) {
view_visibility.set();
visible_entities.entities.push(entity);
visible_entities.push::<WithMesh>(entity);
}
} else {
view_visibility.set();
visible_entities.entities.push(entity);
visible_entities.push::<WithMesh>(entity);
}
}

View file

@ -31,7 +31,7 @@ use bevy_render::{
render_resource::*,
renderer::RenderDevice,
texture::FallbackImage,
view::{ExtractedView, Msaa, VisibleEntities},
view::{ExtractedView, Msaa, VisibleEntities, WithMesh},
};
use bevy_utils::tracing::error;
use std::marker::PhantomData;
@ -645,7 +645,7 @@ pub fn queue_material_meshes<M: Material>(
}
let rangefinder = view.rangefinder3d();
for visible_entity in &visible_entities.entities {
for visible_entity in visible_entities.iter::<WithMesh>() {
let Some(material_asset_id) = render_material_instances.get(visible_entity) else {
continue;
};

View file

@ -2,6 +2,7 @@ mod prepass_bindings;
use bevy_render::mesh::{GpuMesh, MeshVertexBufferLayoutRef};
use bevy_render::render_resource::binding_types::uniform_buffer;
use bevy_render::view::WithMesh;
pub use prepass_bindings::*;
use bevy_asset::{load_internal_asset, AssetServer};
@ -774,7 +775,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS;
}
for visible_entity in &visible_entities.entities {
for visible_entity in visible_entities.iter::<WithMesh>() {
let Some(material_asset_id) = render_material_instances.get(visible_entity) else {
continue;
};

View file

@ -15,7 +15,7 @@ use bevy_render::{
render_resource::*,
renderer::{RenderContext, RenderDevice, RenderQueue},
texture::*,
view::{ExtractedView, RenderLayers, ViewVisibility, VisibleEntities},
view::{ExtractedView, RenderLayers, ViewVisibility, VisibleEntities, WithMesh},
Extract,
};
use bevy_transform::{components::GlobalTransform, prelude::Transform};
@ -1647,13 +1647,13 @@ pub fn queue_shadows<M: Material>(
.get(*light_entity)
.expect("Failed to get spot light visible entities"),
};
// NOTE: Lights with shadow mapping disabled will have no visible entities
// so no meshes will be queued
let mut light_key = MeshPipelineKey::DEPTH_PREPASS;
light_key.set(MeshPipelineKey::DEPTH_CLAMP_ORTHO, is_directional_light);
for entity in visible_entities.iter().copied() {
// NOTE: Lights with shadow mapping disabled will have no visible entities
// so no meshes will be queued
for entity in visible_entities.iter::<WithMesh>().copied() {
let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(entity)
else {
continue;

View file

@ -1,6 +1,9 @@
mod render_layers;
use std::any::TypeId;
use bevy_derive::Deref;
use bevy_ecs::query::QueryFilter;
pub use render_layers::*;
use bevy_app::{Plugin, PostUpdate};
@ -9,7 +12,7 @@ use bevy_ecs::prelude::*;
use bevy_hierarchy::{Children, Parent};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_transform::{components::GlobalTransform, TransformSystem};
use bevy_utils::Parallel;
use bevy_utils::{Parallel, TypeIdMap};
use crate::{
camera::{
@ -170,23 +173,67 @@ pub struct NoFrustumCulling;
#[reflect(Component, Default)]
pub struct VisibleEntities {
#[reflect(ignore)]
pub entities: Vec<Entity>,
pub entities: TypeIdMap<Vec<Entity>>,
}
impl VisibleEntities {
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Entity> {
self.entities.iter()
pub fn get<QF>(&self) -> &[Entity]
where
QF: 'static,
{
match self.entities.get(&TypeId::of::<QF>()) {
Some(entities) => &entities[..],
None => &[],
}
}
pub fn len(&self) -> usize {
self.entities.len()
pub fn get_mut<QF>(&mut self) -> &mut Vec<Entity>
where
QF: 'static,
{
self.entities.entry(TypeId::of::<QF>()).or_default()
}
pub fn is_empty(&self) -> bool {
self.entities.is_empty()
pub fn iter<QF>(&self) -> impl DoubleEndedIterator<Item = &Entity>
where
QF: 'static,
{
self.get::<QF>().iter()
}
pub fn len<QF>(&self) -> usize
where
QF: 'static,
{
self.get::<QF>().len()
}
pub fn is_empty<QF>(&self) -> bool
where
QF: 'static,
{
self.get::<QF>().is_empty()
}
pub fn clear<QF>(&mut self)
where
QF: 'static,
{
self.get_mut::<QF>().clear();
}
pub fn push<QF>(&mut self, entity: Entity)
where
QF: 'static,
{
self.get_mut::<QF>().push(entity);
}
}
/// A convenient alias for `With<Handle<Mesh>>`, for use with
/// [`VisibleEntities`].
pub type WithMesh = With<Handle<Mesh>>;
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub enum VisibilitySystems {
/// Label for the [`calculate_bounds`], `calculate_bounds_2d` and `calculate_bounds_text2d` systems,
@ -238,7 +285,7 @@ impl Plugin for VisibilityPlugin {
.after(camera_system::<Projection>)
.after(TransformSystem::TransformPropagate),
(visibility_propagate_system, reset_view_visibility).in_set(VisibilityPropagate),
check_visibility
check_visibility::<WithMesh>
.in_set(CheckVisibility)
.after(CalculateBounds)
.after(UpdateOrthographicFrusta)
@ -366,10 +413,15 @@ fn reset_view_visibility(mut query: Query<&mut ViewVisibility>) {
/// System updating the visibility of entities each frame.
///
/// The system is part of the [`VisibilitySystems::CheckVisibility`] set. Each frame, it updates the
/// [`ViewVisibility`] of all entities, and for each view also compute the [`VisibleEntities`]
/// for that view.
pub fn check_visibility(
/// The system is part of the [`VisibilitySystems::CheckVisibility`] set. Each
/// frame, it updates the [`ViewVisibility`] of all entities, and for each view
/// also compute the [`VisibleEntities`] for that view.
///
/// This system needs to be run for each type of renderable entity. If you add a
/// new type of renderable entity, you'll need to add an instantiation of this
/// system to the [`VisibilitySystems::CheckVisibility`] set so that Bevy will
/// detect visibility properly for those entities.
pub fn check_visibility<QF>(
mut thread_queues: Local<Parallel<Vec<Entity>>>,
mut view_query: Query<(
&mut VisibleEntities,
@ -377,16 +429,21 @@ pub fn check_visibility(
Option<&RenderLayers>,
&Camera,
)>,
mut visible_aabb_query: Query<(
Entity,
&InheritedVisibility,
&mut ViewVisibility,
Option<&RenderLayers>,
Option<&Aabb>,
&GlobalTransform,
Has<NoFrustumCulling>,
)>,
) {
mut visible_aabb_query: Query<
(
Entity,
&InheritedVisibility,
&mut ViewVisibility,
Option<&RenderLayers>,
Option<&Aabb>,
&GlobalTransform,
Has<NoFrustumCulling>,
),
QF,
>,
) where
QF: QueryFilter + 'static,
{
for (mut visible_entities, frustum, maybe_view_mask, camera) in &mut view_query {
if !camera.is_active {
continue;
@ -394,7 +451,6 @@ pub fn check_visibility(
let view_mask = maybe_view_mask.copied().unwrap_or_default();
visible_entities.entities.clear();
visible_aabb_query.par_iter_mut().for_each(|query_item| {
let (
entity,
@ -442,8 +498,8 @@ pub fn check_visibility(
});
});
visible_entities.entities.clear();
thread_queues.drain_into(&mut visible_entities.entities);
visible_entities.clear::<QF>();
thread_queues.drain_into(visible_entities.get_mut::<QF>());
}
}

View file

@ -32,6 +32,7 @@ pub mod prelude {
};
}
use bevy_transform::TransformSystem;
pub use bundle::*;
pub use dynamic_texture_atlas_builder::*;
pub use mesh2d::*;
@ -51,7 +52,7 @@ use bevy_render::{
render_phase::AddRenderCommand,
render_resource::{Shader, SpecializedRenderPipelines},
texture::Image,
view::{NoFrustumCulling, VisibilitySystems},
view::{check_visibility, NoFrustumCulling, VisibilitySystems},
ExtractSchedule, Render, RenderApp, RenderSet,
};
@ -94,6 +95,14 @@ impl Plugin for SpritePlugin {
compute_slices_on_sprite_change,
)
.in_set(SpriteSystem::ComputeSlices),
check_visibility::<WithMesh2d>
.in_set(VisibilitySystems::CheckVisibility)
.after(VisibilitySystems::CalculateBounds)
.after(VisibilitySystems::UpdateOrthographicFrusta)
.after(VisibilitySystems::UpdatePerspectiveFrusta)
.after(VisibilitySystems::UpdateProjectionFrusta)
.after(VisibilitySystems::VisibilityPropagate)
.after(TransformSystem::TransformPropagate),
),
);

View file

@ -37,7 +37,7 @@ use std::marker::PhantomData;
use crate::{
DrawMesh2d, Mesh2dHandle, Mesh2dPipeline, Mesh2dPipelineKey, RenderMesh2dInstances,
SetMesh2dBindGroup, SetMesh2dViewBindGroup,
SetMesh2dBindGroup, SetMesh2dViewBindGroup, WithMesh2d,
};
/// Materials are used alongside [`Material2dPlugin`] and [`MaterialMesh2dBundle`]
@ -403,7 +403,7 @@ pub fn queue_material2d_meshes<M: Material2d>(
view_key |= Mesh2dPipelineKey::DEBAND_DITHER;
}
}
for visible_entity in &visible_entities.entities {
for visible_entity in visible_entities.iter::<WithMesh2d>() {
let Some(material_asset_id) = render_material_instances.get(visible_entity) else {
continue;
};

View file

@ -48,6 +48,10 @@ impl From<Handle<Mesh>> for Mesh2dHandle {
}
}
/// A convenient alias for `With<Mesh2dHandle>`, for use with
/// [`bevy_render::view::VisibleEntities`].
pub type WithMesh2d = With<Mesh2dHandle>;
#[derive(Default)]
pub struct Mesh2dRenderPlugin;

View file

@ -2,7 +2,7 @@ use std::ops::Range;
use crate::{
texture_atlas::{TextureAtlas, TextureAtlasLayout},
ComputedTextureSlices, Sprite, SPRITE_SHADER_HANDLE,
ComputedTextureSlices, Sprite, WithMesh2d, SPRITE_SHADER_HANDLE,
};
use bevy_asset::{AssetEvent, AssetId, Assets, Handle};
use bevy_color::LinearRgba;
@ -488,7 +488,11 @@ pub fn queue_sprites(
let pipeline = pipelines.specialize(&pipeline_cache, &sprite_pipeline, view_key);
view_entities.clear();
view_entities.extend(visible_entities.entities.iter().map(|e| e.index() as usize));
view_entities.extend(
visible_entities
.iter::<WithMesh2d>()
.map(|e| e.index() as usize),
);
transparent_phase
.items

View file

@ -53,7 +53,10 @@ pub mod prelude {
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_input::InputSystem;
use bevy_render::RenderApp;
use bevy_render::{
view::{check_visibility, VisibilitySystems},
RenderApp,
};
use bevy_transform::TransformSystem;
use layout::ui_surface::UiSurface;
use stack::ui_stack_system;
@ -98,6 +101,10 @@ struct AmbiguousWithTextSystem;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
struct AmbiguousWithUpdateText2DLayout;
/// A convenient alias for `With<Node>`, for use with
/// [`bevy_render::view::VisibleEntities`].
pub type WithNode = With<Node>;
impl Plugin for UiPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<UiSurface>()
@ -130,6 +137,13 @@ impl Plugin for UiPlugin {
app.add_systems(
PostUpdate,
(
check_visibility::<WithNode>
.in_set(VisibilitySystems::CheckVisibility)
.after(VisibilitySystems::CalculateBounds)
.after(VisibilitySystems::UpdateOrthographicFrusta)
.after(VisibilitySystems::UpdatePerspectiveFrusta)
.after(VisibilitySystems::UpdateProjectionFrusta)
.after(VisibilitySystems::VisibilityPropagate),
update_target_camera_system.before(UiSystem::Layout),
apply_deferred
.after(update_target_camera_system)

View file

@ -28,7 +28,7 @@ use bevy::{
sprite::{
extract_mesh2d, DrawMesh2d, Material2dBindGroupId, Mesh2dHandle, Mesh2dPipeline,
Mesh2dPipelineKey, Mesh2dTransforms, MeshFlags, RenderMesh2dInstance,
RenderMesh2dInstances, SetMesh2dBindGroup, SetMesh2dViewBindGroup,
RenderMesh2dInstances, SetMesh2dBindGroup, SetMesh2dViewBindGroup, WithMesh2d,
},
};
use std::f32::consts::PI;
@ -375,7 +375,7 @@ pub fn queue_colored_mesh2d(
| Mesh2dPipelineKey::from_hdr(view.hdr);
// Queue all entities visible to that view
for visible_entity in &visible_entities.entities {
for visible_entity in visible_entities.iter::<WithMesh2d>() {
if let Some(mesh_instance) = render_mesh_instances.get(visible_entity) {
let mesh2d_handle = mesh_instance.mesh_asset_id;
let mesh2d_transforms = &mesh_instance.transforms;