Skip to content

Commit 6840eca

Browse files
superdumpcart
andcommitted
Use storage buffers for clustered forward point lights (#3989)
# Objective - Make use of storage buffers, where they are available, for clustered forward bindings to support far more point lights in a scene - Fixes #3605 - Based on top of #4079 This branch on an M1 Max can keep 60fps with about 2150 point lights of radius 1m in the Sponza scene where I've been testing. The bottleneck is mostly assigning lights to clusters which grows faster than linearly (I think 1000 lights was about 1.5ms and 5000 was 7.5ms). I have seen papers and presentations leveraging compute shaders that can get this up to over 1 million. That said, I think any further optimisations should probably be done in a separate PR. ## Solution - Add `RenderDevice` to the `Material` and `SpecializedMaterial` trait `::key()` functions to allow setting flags on the keys depending on feature/limit availability - Make `GpuPointLights` and `ViewClusterBuffers` into enums containing `UniformVec` and `StorageBuffer` variants. Implement the necessary API on them to make usage the same for both cases, and the only difference is at initialisation time. - Appropriate shader defs in the shader code to handle the two cases ## Context on some decisions / open questions - I'm using `max_storage_buffers_per_shader_stage >= 3` as a check to see if storage buffers are supported. I was thinking about diving into 'binding resource management' but it feels like we don't have enough use cases to understand the problem yet, and it is mostly a separate concern to this PR, so I think it should be handled separately. - Should `ViewClusterBuffers` and `ViewClusterBindings` be merged, duplicating the count variables into the enum variants? Co-authored-by: Carter Anderson <[email protected]>
1 parent f907d67 commit 6840eca

File tree

16 files changed

+623
-133
lines changed

16 files changed

+623
-133
lines changed

Cargo.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,10 @@ min_sdk_version = 16
612612
target_sdk_version = 29
613613

614614
# Stress Tests
615+
[[example]]
616+
name = "many_lights"
617+
path = "examples/stress_tests/many_lights.rs"
618+
615619
[[example]]
616620
name = "transform_hierarchy"
617621
path = "examples/stress_tests/transform_hierarchy.rs"
618-

crates/bevy_pbr/src/lib.rs

+4-6
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,10 @@ impl Plugin for PbrPlugin {
150150
)
151151
.add_system_to_stage(
152152
RenderStage::Prepare,
153-
// this is added as an exclusive system because it contributes new views. it must run (and have Commands applied)
154-
// _before_ the `prepare_views()` system is run. ideally this becomes a normal system when "stageless" features come out
155-
render::prepare_clusters
156-
.exclusive_system()
157-
.label(RenderLightSystems::PrepareClusters)
158-
.after(RenderLightSystems::PrepareLights),
153+
// NOTE: This needs to run after prepare_lights. As prepare_lights is an exclusive system,
154+
// just adding it to the non-exclusive systems in the Prepare stage means it runs after
155+
// prepare_lights.
156+
render::prepare_clusters.label(RenderLightSystems::PrepareClusters),
159157
)
160158
.add_system_to_stage(
161159
RenderStage::Queue,

crates/bevy_pbr/src/light.rs

+19-6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use bevy_render::{
99
color::Color,
1010
prelude::Image,
1111
primitives::{Aabb, CubemapFrusta, Frustum, Sphere},
12+
render_resource::BufferBindingType,
13+
renderer::RenderDevice,
1214
view::{ComputedVisibility, RenderLayers, Visibility, VisibleEntities},
1315
};
1416
use bevy_transform::components::GlobalTransform;
@@ -17,7 +19,8 @@ use bevy_window::Windows;
1719

1820
use crate::{
1921
calculate_cluster_factors, CubeMapFace, CubemapVisibleEntities, ViewClusterBindings,
20-
CUBE_MAP_FACES, MAX_POINT_LIGHTS, POINT_LIGHT_NEAR_Z,
22+
CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, CUBE_MAP_FACES, MAX_UNIFORM_BUFFER_POINT_LIGHTS,
23+
POINT_LIGHT_NEAR_Z,
2124
};
2225

2326
/// A light that emits light in all directions from a central point.
@@ -709,6 +712,7 @@ pub(crate) fn assign_lights_to_clusters(
709712
lights_query: Query<(Entity, &GlobalTransform, &PointLight, &Visibility)>,
710713
mut lights: Local<Vec<PointLightAssignmentData>>,
711714
mut max_point_lights_warning_emitted: Local<bool>,
715+
render_device: Res<RenderDevice>,
712716
) {
713717
global_lights.entities.clear();
714718
lights.clear();
@@ -727,7 +731,13 @@ pub(crate) fn assign_lights_to_clusters(
727731
),
728732
);
729733

730-
if lights.len() > MAX_POINT_LIGHTS {
734+
let clustered_forward_buffer_binding_type =
735+
render_device.get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT);
736+
let supports_storage_buffers = matches!(
737+
clustered_forward_buffer_binding_type,
738+
BufferBindingType::Storage { .. }
739+
);
740+
if lights.len() > MAX_UNIFORM_BUFFER_POINT_LIGHTS && !supports_storage_buffers {
731741
lights.sort_by(|light_1, light_2| {
732742
point_light_order(
733743
(&light_1.entity, &light_1.shadows_enabled),
@@ -743,7 +753,7 @@ pub(crate) fn assign_lights_to_clusters(
743753
let mut lights_in_view_count = 0;
744754
lights.retain(|light| {
745755
// take one extra light to check if we should emit the warning
746-
if lights_in_view_count == MAX_POINT_LIGHTS + 1 {
756+
if lights_in_view_count == MAX_UNIFORM_BUFFER_POINT_LIGHTS + 1 {
747757
false
748758
} else {
749759
let light_sphere = Sphere {
@@ -763,12 +773,15 @@ pub(crate) fn assign_lights_to_clusters(
763773
}
764774
});
765775

766-
if lights.len() > MAX_POINT_LIGHTS && !*max_point_lights_warning_emitted {
767-
warn!("MAX_POINT_LIGHTS ({}) exceeded", MAX_POINT_LIGHTS);
776+
if lights.len() > MAX_UNIFORM_BUFFER_POINT_LIGHTS && !*max_point_lights_warning_emitted {
777+
warn!(
778+
"MAX_UNIFORM_BUFFER_POINT_LIGHTS ({}) exceeded",
779+
MAX_UNIFORM_BUFFER_POINT_LIGHTS
780+
);
768781
*max_point_lights_warning_emitted = true;
769782
}
770783

771-
lights.truncate(MAX_POINT_LIGHTS);
784+
lights.truncate(MAX_UNIFORM_BUFFER_POINT_LIGHTS);
772785
}
773786

774787
for (view_entity, camera_transform, camera, frustum, config, clusters, mut visible_lights) in

crates/bevy_pbr/src/material.rs

+7-4
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ use std::marker::PhantomData;
3939
/// way to render [`Mesh`] entities with custom shader logic. For materials that can specialize their [`RenderPipelineDescriptor`]
4040
/// based on specific material values, see [`SpecializedMaterial`]. [`Material`] automatically implements [`SpecializedMaterial`]
4141
/// and can be used anywhere that type is used (such as [`MaterialPlugin`]).
42-
pub trait Material: Asset + RenderAsset {
42+
pub trait Material: Asset + RenderAsset + Sized {
4343
/// Returns this material's [`BindGroup`]. This should match the layout returned by [`Material::bind_group_layout`].
4444
fn bind_group(material: &<Self as RenderAsset>::PreparedAsset) -> &BindGroup;
4545

@@ -78,6 +78,7 @@ pub trait Material: Asset + RenderAsset {
7878
#[allow(unused_variables)]
7979
#[inline]
8080
fn specialize(
81+
pipeline: &MaterialPipeline<Self>,
8182
descriptor: &mut RenderPipelineDescriptor,
8283
layout: &MeshVertexBufferLayout,
8384
) -> Result<(), SpecializedMeshPipelineError> {
@@ -93,11 +94,12 @@ impl<M: Material> SpecializedMaterial for M {
9394

9495
#[inline]
9596
fn specialize(
97+
pipeline: &MaterialPipeline<Self>,
9698
descriptor: &mut RenderPipelineDescriptor,
9799
_key: Self::Key,
98100
layout: &MeshVertexBufferLayout,
99101
) -> Result<(), SpecializedMeshPipelineError> {
100-
<M as Material>::specialize(descriptor, layout)
102+
<M as Material>::specialize(pipeline, descriptor, layout)
101103
}
102104

103105
#[inline]
@@ -137,7 +139,7 @@ impl<M: Material> SpecializedMaterial for M {
137139
/// way to render [`Mesh`] entities with custom shader logic. [`SpecializedMaterials`](SpecializedMaterial) use their [`SpecializedMaterial::Key`]
138140
/// to customize their [`RenderPipelineDescriptor`] based on specific material values. The slightly simpler [`Material`] trait
139141
/// should be used for materials that do not need specialization. [`Material`] types automatically implement [`SpecializedMaterial`].
140-
pub trait SpecializedMaterial: Asset + RenderAsset {
142+
pub trait SpecializedMaterial: Asset + RenderAsset + Sized {
141143
/// The key used to specialize this material's [`RenderPipelineDescriptor`].
142144
type Key: PartialEq + Eq + Hash + Clone + Send + Sync;
143145

@@ -148,6 +150,7 @@ pub trait SpecializedMaterial: Asset + RenderAsset {
148150

149151
/// Specializes the given `descriptor` according to the given `key`.
150152
fn specialize(
153+
pipeline: &MaterialPipeline<Self>,
151154
descriptor: &mut RenderPipelineDescriptor,
152155
key: Self::Key,
153156
layout: &MeshVertexBufferLayout,
@@ -251,7 +254,7 @@ impl<M: SpecializedMaterial> SpecializedMeshPipeline for MaterialPipeline<M> {
251254
let descriptor_layout = descriptor.layout.as_mut().unwrap();
252255
descriptor_layout.insert(1, self.material_layout.clone());
253256

254-
M::specialize(&mut descriptor, key.material_key, layout)?;
257+
M::specialize(self, &mut descriptor, key.material_key, layout)?;
255258
Ok(descriptor)
256259
}
257260
}

crates/bevy_pbr/src/pbr_material.rs

+1
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ impl SpecializedMaterial for StandardMaterial {
370370
}
371371

372372
fn specialize(
373+
_pipeline: &MaterialPipeline<Self>,
373374
descriptor: &mut RenderPipelineDescriptor,
374375
key: Self::Key,
375376
_layout: &MeshVertexBufferLayout,

0 commit comments

Comments
 (0)