Skip to content

Commit

Permalink
Use storage buffers for clustered forward point lights (#3989)
Browse files Browse the repository at this point in the history
# 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 <mcanders1@gmail.com>
  • Loading branch information
superdump and cart committed Apr 6, 2022
1 parent f907d67 commit 6840eca
Show file tree
Hide file tree
Showing 16 changed files with 623 additions and 133 deletions.
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,10 @@ min_sdk_version = 16
target_sdk_version = 29

# Stress Tests
[[example]]
name = "many_lights"
path = "examples/stress_tests/many_lights.rs"

[[example]]
name = "transform_hierarchy"
path = "examples/stress_tests/transform_hierarchy.rs"

10 changes: 4 additions & 6 deletions crates/bevy_pbr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,10 @@ impl Plugin for PbrPlugin {
)
.add_system_to_stage(
RenderStage::Prepare,
// this is added as an exclusive system because it contributes new views. it must run (and have Commands applied)
// _before_ the `prepare_views()` system is run. ideally this becomes a normal system when "stageless" features come out
render::prepare_clusters
.exclusive_system()
.label(RenderLightSystems::PrepareClusters)
.after(RenderLightSystems::PrepareLights),
// NOTE: This needs to run after prepare_lights. As prepare_lights is an exclusive system,
// just adding it to the non-exclusive systems in the Prepare stage means it runs after
// prepare_lights.
render::prepare_clusters.label(RenderLightSystems::PrepareClusters),
)
.add_system_to_stage(
RenderStage::Queue,
Expand Down
25 changes: 19 additions & 6 deletions crates/bevy_pbr/src/light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use bevy_render::{
color::Color,
prelude::Image,
primitives::{Aabb, CubemapFrusta, Frustum, Sphere},
render_resource::BufferBindingType,
renderer::RenderDevice,
view::{ComputedVisibility, RenderLayers, Visibility, VisibleEntities},
};
use bevy_transform::components::GlobalTransform;
Expand All @@ -17,7 +19,8 @@ use bevy_window::Windows;

use crate::{
calculate_cluster_factors, CubeMapFace, CubemapVisibleEntities, ViewClusterBindings,
CUBE_MAP_FACES, MAX_POINT_LIGHTS, POINT_LIGHT_NEAR_Z,
CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, CUBE_MAP_FACES, MAX_UNIFORM_BUFFER_POINT_LIGHTS,
POINT_LIGHT_NEAR_Z,
};

/// A light that emits light in all directions from a central point.
Expand Down Expand Up @@ -709,6 +712,7 @@ pub(crate) fn assign_lights_to_clusters(
lights_query: Query<(Entity, &GlobalTransform, &PointLight, &Visibility)>,
mut lights: Local<Vec<PointLightAssignmentData>>,
mut max_point_lights_warning_emitted: Local<bool>,
render_device: Res<RenderDevice>,
) {
global_lights.entities.clear();
lights.clear();
Expand All @@ -727,7 +731,13 @@ pub(crate) fn assign_lights_to_clusters(
),
);

if lights.len() > MAX_POINT_LIGHTS {
let clustered_forward_buffer_binding_type =
render_device.get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT);
let supports_storage_buffers = matches!(
clustered_forward_buffer_binding_type,
BufferBindingType::Storage { .. }
);
if lights.len() > MAX_UNIFORM_BUFFER_POINT_LIGHTS && !supports_storage_buffers {
lights.sort_by(|light_1, light_2| {
point_light_order(
(&light_1.entity, &light_1.shadows_enabled),
Expand All @@ -743,7 +753,7 @@ pub(crate) fn assign_lights_to_clusters(
let mut lights_in_view_count = 0;
lights.retain(|light| {
// take one extra light to check if we should emit the warning
if lights_in_view_count == MAX_POINT_LIGHTS + 1 {
if lights_in_view_count == MAX_UNIFORM_BUFFER_POINT_LIGHTS + 1 {
false
} else {
let light_sphere = Sphere {
Expand All @@ -763,12 +773,15 @@ pub(crate) fn assign_lights_to_clusters(
}
});

if lights.len() > MAX_POINT_LIGHTS && !*max_point_lights_warning_emitted {
warn!("MAX_POINT_LIGHTS ({}) exceeded", MAX_POINT_LIGHTS);
if lights.len() > MAX_UNIFORM_BUFFER_POINT_LIGHTS && !*max_point_lights_warning_emitted {
warn!(
"MAX_UNIFORM_BUFFER_POINT_LIGHTS ({}) exceeded",
MAX_UNIFORM_BUFFER_POINT_LIGHTS
);
*max_point_lights_warning_emitted = true;
}

lights.truncate(MAX_POINT_LIGHTS);
lights.truncate(MAX_UNIFORM_BUFFER_POINT_LIGHTS);
}

for (view_entity, camera_transform, camera, frustum, config, clusters, mut visible_lights) in
Expand Down
11 changes: 7 additions & 4 deletions crates/bevy_pbr/src/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ use std::marker::PhantomData;
/// way to render [`Mesh`] entities with custom shader logic. For materials that can specialize their [`RenderPipelineDescriptor`]
/// based on specific material values, see [`SpecializedMaterial`]. [`Material`] automatically implements [`SpecializedMaterial`]
/// and can be used anywhere that type is used (such as [`MaterialPlugin`]).
pub trait Material: Asset + RenderAsset {
pub trait Material: Asset + RenderAsset + Sized {
/// Returns this material's [`BindGroup`]. This should match the layout returned by [`Material::bind_group_layout`].
fn bind_group(material: &<Self as RenderAsset>::PreparedAsset) -> &BindGroup;

Expand Down Expand Up @@ -78,6 +78,7 @@ pub trait Material: Asset + RenderAsset {
#[allow(unused_variables)]
#[inline]
fn specialize(
pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor,
layout: &MeshVertexBufferLayout,
) -> Result<(), SpecializedMeshPipelineError> {
Expand All @@ -93,11 +94,12 @@ impl<M: Material> SpecializedMaterial for M {

#[inline]
fn specialize(
pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor,
_key: Self::Key,
layout: &MeshVertexBufferLayout,
) -> Result<(), SpecializedMeshPipelineError> {
<M as Material>::specialize(descriptor, layout)
<M as Material>::specialize(pipeline, descriptor, layout)
}

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

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

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

M::specialize(&mut descriptor, key.material_key, layout)?;
M::specialize(self, &mut descriptor, key.material_key, layout)?;
Ok(descriptor)
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_pbr/src/pbr_material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ impl SpecializedMaterial for StandardMaterial {
}

fn specialize(
_pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor,
key: Self::Key,
_layout: &MeshVertexBufferLayout,
Expand Down
Loading

0 comments on commit 6840eca

Please sign in to comment.