Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

shadow performance #11647

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open

shadow performance #11647

wants to merge 7 commits into from

Conversation

robtfm
Copy link
Contributor

@robtfm robtfm commented Feb 1, 2024

Objective

some minor optimisations for shadow rendering performance

Solution

  • added --shadows option to many_cubes for testing
  • added mesh-id sorting for shadow phase items.
  • when rendering shadows, added material batching for materials that have a common depth-prepass output (such as opaque StandardMaterials). the material must support the batching via Material::shadow_material_key
  • added shadow batching for extended materials, using the base material's key no batching by default. i am not sure if this is the right thing to do, it will cause some existing ExtendedMaterials to render incorrectly (e.g. if they move vertices based on material data). the alternative is to by default disable shadow batching for extended materials (it could still be enabled by authors)

perf impacts:

command line current fps new fps
cargo run --example many_cubes --release -- --benchmark --shadows 27 27
cargo run --example many_cubes --release -- --benchmark --shadows --vary-per-instance 2.5 12

when sharing a material handle there is no impact as expected. when using distinct materials the frame rate increases ~5x.

this is obviously an exaggeration of real world impacts, but on a random real world scene i had

scenario fps
without shadows 110
with shadows current 50
with shadows new 64
with shadows just sorting 57
with shadows just batching 60

a second test scene (thanks @Elabajaba) showed a smaller change from 56fps to 60fps.

@robtfm robtfm added A-Rendering Drawing game state to the screen C-Performance A change motivated by improving speed, memory usage or compile times labels Feb 1, 2024
@robtfm
Copy link
Contributor Author

robtfm commented Feb 1, 2024

note this also fixes the issues from #11645 but is maybe more contentious

@robtfm
Copy link
Contributor Author

robtfm commented Feb 3, 2024

modified to sort shadows by mesh id, since we only batch entities with the same mesh (we still sort by pipeline first)

@Elabajaba
Copy link
Contributor

This is saving ~7000 draw calls in my castle stress test view (total went from ~19.8k -> ~12.6k, which is still insane, but less insane).

I wonder if sorting the opaque pass by mesh id would net us a similar decrease?

@robtfm
Copy link
Contributor Author

robtfm commented Feb 3, 2024

I wonder if sorting the opaque pass by mesh id would net us a similar decrease?

I guess that would depend on frag shader cost (more overdraw / less drawcalls or vice versa) … might be nice to make it an option somehow.

@@ -801,6 +801,11 @@ impl Material for StandardMaterial {
PBR_SHADER_HANDLE.into()
}

fn shadow_material_key(&self) -> Option<u64> {
// we can batch all pure opaque materials together for shadow rendering
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it just meshes that break our batching for shadows then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for standard materials after this pr, yes. part of this pr is to make all opaque standard materials share a material instance for shadow casting

commands.spawn(DirectionalLightBundle { ..default() });
commands.spawn(DirectionalLightBundle {
directional_light: DirectionalLight {
shadows_enabled: args.shadows,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this default to shadows on or off?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaults to off

/// the same depth-prepass output (such as opaque [`StandardMaterial`]s with different textures), rendering with the first
/// material of the batch for all meshes.
fn shadow_material_key(&self) -> Option<u64> {
None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So why is this None by default?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is in the Material trait. it's overridden in the StandardMaterial impl

@@ -1635,7 +1636,7 @@ pub fn queue_shadows<M: Material>(
// NOTE: Lights with shadow mapping disabled will have no visible entities
// so no meshes will be queued
for entity in visible_entities.iter().copied() {
let Some(mesh_instance) = render_mesh_instances.get(&entity) else {
let Some(mesh_instance) = render_mesh_instances.get_mut(&entity) else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look like it needs to be mut?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's modified @1689 to add the shadow_bind_group_id

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, not sure how I missed that.

@@ -1699,7 +1702,7 @@ pub fn queue_shadows<M: Material>(
}

pub struct Shadow {
pub distance: f32,
pub asset_id: AssetId<Mesh>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future we probably want to be able to represent AssetID as a u64 or something so we can go back to using radsort, as I noticed decreases in sort performance in the opaque sorting by pipeline PR (that were more than made up for by the better batching).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

definitely, should be easy to do.

crates/bevy_pbr/src/render/light.rs Outdated Show resolved Hide resolved
entity: Entity,
) -> Option<(Self::BufferData, Option<Self::CompareData>)> {
let mesh_instance = mesh_instances.get(&entity)?;
let maybe_lightmap = lightmaps.render_lightmaps.get(&entity);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would there be any issues with removing lightmaps in CompareData for shadow pass?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For standard materials, no… in general, possibly I guess? But probably not.

If it did matter for a custom material then the author could add some data into the material and use it to differentiate in the shadow_material_key fn. I’ll remove it from here.

@JMS55 JMS55 added this to the 0.13 milestone Feb 10, 2024
/// Specify a batch key to use for shadows. Returning Some(value) allows batching of distinct materials which will produce
/// the same depth-prepass output (such as opaque [`StandardMaterial`]s with different textures), rendering with the first
/// material of the batch for all meshes.
fn shadow_material_key(&self) -> Option<u64> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels pretty hacky :/. I'd rather we just have a separate set of bindings for shadow passes, or split bindings between vertex/fragment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered how to do this without requiring large user changes, I couldn’t see a way.

Ideally you want this to work for standard materials without extra user input, so

  • a separate component (ShadowMaterial(Handle<M>)) doesn't really work unless we also add a system to add the component automatically where appropriate, and that system would need to do lookups into the assets resource to make the determination, which sounds expensive and still messy.
  • have the key function return a handle/asset id, this requires either adding the handle(s) to every material or using some resource to store them and giving access in the key function. This gets at least as awkward, particularly if you have more than one groupable set (I have this case in my work code where some opaque materials can be grouped but not all, based on the “plot” they belong to in the world). Also adds effort around extracting/preparing the new handles, basically seems like a lot of extra complexity for limited reward.

If you can see a clean way to do this I’d be happy to change it, but I don’t see it right now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose for now this is the best we can do. I think for 0.14 we might want to look into redoing the Material API so we can support batching, bindless, etc easier.

@@ -180,6 +180,13 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized {
) -> Result<(), SpecializedMeshPipelineError> {
Ok(())
}

/// Specify a batch key to use for shadows. Returning Some(value) allows batching of distinct materials which will produce
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wording isn't very clear. It took me some time to realize that this would basically force-batch different materials together by grouping them by this arbitrary value, and then using the first material of the batch for all meshes in the batch.

@alice-i-cecile alice-i-cecile modified the milestones: 0.13, 0.14 Feb 14, 2024
@alice-i-cecile
Copy link
Member

This is nice but not vital: bumping from the milestone.

@JMS55 JMS55 removed this from the 0.14 milestone May 14, 2024
@janhohenheim janhohenheim added S-Needs-Review Needs reviewer attention (from anyone!) to move forward D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes labels Sep 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Rendering Drawing game state to the screen C-Performance A change motivated by improving speed, memory usage or compile times D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward
Projects
Status: In Review
Development

Successfully merging this pull request may close these issues.

6 participants