From 5c884c5a151e178f6ad98a74edab473f12c344dd Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Fri, 22 Sep 2023 00:12:34 +0200 Subject: [PATCH] Automatic batching/instancing of draw commands (#9685) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective - Implement the foundations of automatic batching/instancing of draw commands as the next step from #89 - NOTE: More performance improvements will come when more data is managed and bound in ways that do not require rebinding such as mesh, material, and texture data. ## Solution - The core idea for batching of draw commands is to check whether any of the information that has to be passed when encoding a draw command changes between two things that are being drawn according to the sorted render phase order. These should be things like the pipeline, bind groups and their dynamic offsets, index/vertex buffers, and so on. - The following assumptions have been made: - Only entities with prepared assets (pipelines, materials, meshes) are queued to phases - View bindings are constant across a phase for a given draw function as phases are per-view - `batch_and_prepare_render_phase` is the only system that performs this batching and has sole responsibility for preparing the per-object data. As such the mesh binding and dynamic offsets are assumed to only vary as a result of the `batch_and_prepare_render_phase` system, e.g. due to having to split data across separate uniform bindings within the same buffer due to the maximum uniform buffer binding size. - Implement `GpuArrayBuffer` for `Mesh2dUniform` to store Mesh2dUniform in arrays in GPU buffers rather than each one being at a dynamic offset in a uniform buffer. This is the same optimisation that was made for 3D not long ago. - Change batch size for a range in `PhaseItem`, adding API for getting or mutating the range. This is more flexible than a size as the length of the range can be used in place of the size, but the start and end can be otherwise whatever is needed. - Add an optional mesh bind group dynamic offset to `PhaseItem`. This avoids having to do a massive table move just to insert `GpuArrayBufferIndex` components. ## Benchmarks All tests have been run on an M1 Max on AC power. `bevymark` and `many_cubes` were modified to use 1920x1080 with a scale factor of 1. I run a script that runs a separate Tracy capture process, and then runs the bevy example with `--features bevy_ci_testing,trace_tracy` and `CI_TESTING_CONFIG=../benchmark.ron` with the contents of `../benchmark.ron`: ```rust ( exit_after: Some(1500) ) ``` ...in order to run each test for 1500 frames. The recent changes to `many_cubes` and `bevymark` added reproducible random number generation so that with the same settings, the same rng will occur. They also added benchmark modes that use a fixed delta time for animations. Combined this means that the same frames should be rendered both on main and on the branch. The graphs compare main (yellow) to this PR (red). ### 3D Mesh `many_cubes --benchmark` Screenshot 2023-09-03 at 23 42 10 The mesh and material are the same for all instances. This is basically the best case for the initial batching implementation as it results in 1 draw for the ~11.7k visible meshes. It gives a ~30% reduction in median frame time. The 1000th frame is identical using the flip tool: ![flip many_cubes-main-mesh3d many_cubes-batching-mesh3d 67ppd ldr](https://github.com/bevyengine/bevy/assets/302146/2511f37a-6df8-481a-932f-706ca4de7643) ``` Mean: 0.000000 Weighted median: 0.000000 1st weighted quartile: 0.000000 3rd weighted quartile: 0.000000 Min: 0.000000 Max: 0.000000 Evaluation time: 0.4615 seconds ``` ### 3D Mesh `many_cubes --benchmark --material-texture-count 10` Screenshot 2023-09-03 at 23 45 18 This run uses 10 different materials by varying their textures. The materials are randomly selected, and there is no sorting by material bind group for opaque 3D so any batching is 'random'. The PR produces a ~5% reduction in median frame time. If we were to sort the opaque phase by the material bind group, then this should be a lot faster. This produces about 10.5k draws for the 11.7k visible entities. This makes sense as randomly selecting from 10 materials gives a chance that two adjacent entities randomly select the same material and can be batched. The 1000th frame is identical in flip: ![flip many_cubes-main-mesh3d-mtc10 many_cubes-batching-mesh3d-mtc10 67ppd ldr](https://github.com/bevyengine/bevy/assets/302146/2b3a8614-9466-4ed8-b50c-d4aa71615dbb) ``` Mean: 0.000000 Weighted median: 0.000000 1st weighted quartile: 0.000000 3rd weighted quartile: 0.000000 Min: 0.000000 Max: 0.000000 Evaluation time: 0.4537 seconds ``` ### 3D Mesh `many_cubes --benchmark --vary-per-instance` Screenshot 2023-09-03 at 23 48 44 This run varies the material data per instance by randomly-generating its colour. This is the worst case for batching and that it performs about the same as `main` is a good thing as it demonstrates that the batching has minimal overhead when dealing with ~11k visible mesh entities. The 1000th frame is identical according to flip: ![flip many_cubes-main-mesh3d-vpi many_cubes-batching-mesh3d-vpi 67ppd ldr](https://github.com/bevyengine/bevy/assets/302146/ac5f5c14-9bda-4d1a-8219-7577d4aac68c) ``` Mean: 0.000000 Weighted median: 0.000000 1st weighted quartile: 0.000000 3rd weighted quartile: 0.000000 Min: 0.000000 Max: 0.000000 Evaluation time: 0.4568 seconds ``` ### 2D Mesh `bevymark --benchmark --waves 160 --per-wave 1000 --mode mesh2d` Screenshot 2023-09-03 at 23 59 56 This spawns 160 waves of 1000 quad meshes that are shaded with ColorMaterial. Each wave has a different material so 160 waves currently should result in 160 batches. This results in a 50% reduction in median frame time. Capturing a screenshot of the 1000th frame main vs PR gives: ![flip bevymark-main-mesh2d bevymark-batching-mesh2d 67ppd ldr](https://github.com/bevyengine/bevy/assets/302146/80102728-1217-4059-87af-14d05044df40) ``` Mean: 0.001222 Weighted median: 0.750432 1st weighted quartile: 0.453494 3rd weighted quartile: 0.969758 Min: 0.000000 Max: 0.990296 Evaluation time: 0.4255 seconds ``` So they seem to produce the same results. I also double-checked the number of draws. `main` does 160000 draws, and the PR does 160, as expected. ### 2D Mesh `bevymark --benchmark --waves 160 --per-wave 1000 --mode mesh2d --material-texture-count 10` Screenshot 2023-09-04 at 00 09 22 This generates 10 textures and generates materials for each of those and then selects one material per wave. The median frame time is reduced by 50%. Similar to the plain run above, this produces 160 draws on the PR and 160000 on `main` and the 1000th frame is identical (ignoring the fps counter text overlay). ![flip bevymark-main-mesh2d-mtc10 bevymark-batching-mesh2d-mtc10 67ppd ldr](https://github.com/bevyengine/bevy/assets/302146/ebed2822-dce7-426a-858b-b77dc45b986f) ``` Mean: 0.002877 Weighted median: 0.964980 1st weighted quartile: 0.668871 3rd weighted quartile: 0.982749 Min: 0.000000 Max: 0.992377 Evaluation time: 0.4301 seconds ``` ### 2D Mesh `bevymark --benchmark --waves 160 --per-wave 1000 --mode mesh2d --vary-per-instance` Screenshot 2023-09-04 at 00 13 53 This creates unique materials per instance by randomly-generating the material's colour. This is the worst case for 2D batching. Somehow, this PR manages a 7% reduction in median frame time. Both main and this PR issue 160000 draws. The 1000th frame is the same: ![flip bevymark-main-mesh2d-vpi bevymark-batching-mesh2d-vpi 67ppd ldr](https://github.com/bevyengine/bevy/assets/302146/a2ec471c-f576-4a36-a23b-b24b22578b97) ``` Mean: 0.001214 Weighted median: 0.937499 1st weighted quartile: 0.635467 3rd weighted quartile: 0.979085 Min: 0.000000 Max: 0.988971 Evaluation time: 0.4462 seconds ``` ### 2D Sprite `bevymark --benchmark --waves 160 --per-wave 1000 --mode sprite` Screenshot 2023-09-04 at 12 21 12 This just spawns 160 waves of 1000 sprites. There should be and is no notable difference between main and the PR. ### 2D Sprite `bevymark --benchmark --waves 160 --per-wave 1000 --mode sprite --material-texture-count 10` Screenshot 2023-09-04 at 12 36 08 This spawns the sprites selecting a texture at random per instance from the 10 generated textures. This has no significant change vs main and shouldn't. ### 2D Sprite `bevymark --benchmark --waves 160 --per-wave 1000 --mode sprite --vary-per-instance` Screenshot 2023-09-04 at 12 29 52 This sets the sprite colour as being unique per instance. This can still all be drawn using one batch. There should be no difference but the PR produces median frame times that are 4% higher. Investigation showed no clear sources of cost, rather a mix of give and take that should not happen. It seems like noise in the results. ### Summary | Benchmark | % change in median frame time | | ------------- | ------------- | | many_cubes | 🟩 -30% | | many_cubes 10 materials | 🟩 -5% | | many_cubes unique materials | 🟩 ~0% | | bevymark mesh2d | 🟩 -50% | | bevymark mesh2d 10 materials | 🟩 -50% | | bevymark mesh2d unique materials | 🟩 -7% | | bevymark sprite | 🟥 2% | | bevymark sprite 10 materials | 🟥 0.6% | | bevymark sprite unique materials | 🟥 4.1% | --- ## Changelog - Added: 2D and 3D mesh entities that share the same mesh and material (same textures, same data) are now batched into the same draw command for better performance. --------- Co-authored-by: robtfm <50659922+robtfm@users.noreply.github.com> Co-authored-by: Nicola Papale --- assets/shaders/custom_gltf_2d.wgsl | 6 +- crates/bevy_core_pipeline/src/core_2d/mod.rs | 26 ++- crates/bevy_core_pipeline/src/core_3d/mod.rs | 70 +++++- crates/bevy_core_pipeline/src/prepass/mod.rs | 48 ++++- crates/bevy_gizmos/src/pipeline_2d.rs | 3 +- crates/bevy_gizmos/src/pipeline_3d.rs | 3 +- crates/bevy_math/src/affine3.rs | 32 ++- crates/bevy_pbr/src/material.rs | 68 ++++-- crates/bevy_pbr/src/prepass/mod.rs | 12 +- crates/bevy_pbr/src/render/light.rs | 27 ++- crates/bevy_pbr/src/render/mesh.rs | 199 ++++++------------ crates/bevy_pbr/src/render/morph.rs | 5 +- crates/bevy_pbr/src/wireframe.rs | 3 +- crates/bevy_render/src/batching/mod.rs | 123 +++++++++++ crates/bevy_render/src/lib.rs | 1 + crates/bevy_render/src/render_phase/mod.rs | 54 ++--- .../render_resource/batched_uniform_buffer.rs | 5 +- .../src/render_resource/gpu_array_buffer.rs | 9 +- crates/bevy_sprite/src/mesh2d/material.rs | 71 +++++-- crates/bevy_sprite/src/mesh2d/mesh.rs | 170 +++++++++++---- crates/bevy_sprite/src/mesh2d/mesh2d.wgsl | 8 +- .../src/mesh2d/mesh2d_bindings.wgsl | 20 +- .../src/mesh2d/mesh2d_functions.wgsl | 15 +- .../bevy_sprite/src/mesh2d/mesh2d_types.wgsl | 12 +- crates/bevy_sprite/src/render/mod.rs | 14 +- crates/bevy_ui/src/render/mod.rs | 8 +- crates/bevy_ui/src/render/render_pass.rs | 26 ++- crates/bevy_utils/Cargo.toml | 1 + crates/bevy_utils/src/lib.rs | 5 + examples/2d/mesh2d_manual.rs | 28 ++- examples/shader/shader_instancing.rs | 3 +- 31 files changed, 772 insertions(+), 303 deletions(-) create mode 100644 crates/bevy_render/src/batching/mod.rs diff --git a/assets/shaders/custom_gltf_2d.wgsl b/assets/shaders/custom_gltf_2d.wgsl index 58058d9501033..5e0a908c875d4 100644 --- a/assets/shaders/custom_gltf_2d.wgsl +++ b/assets/shaders/custom_gltf_2d.wgsl @@ -1,8 +1,9 @@ #import bevy_sprite::mesh2d_view_bindings globals #import bevy_sprite::mesh2d_bindings mesh -#import bevy_sprite::mesh2d_functions mesh2d_position_local_to_clip +#import bevy_sprite::mesh2d_functions get_model_matrix, mesh2d_position_local_to_clip struct Vertex { + @builtin(instance_index) instance_index: u32, @location(0) position: vec3, @location(1) color: vec4, @location(2) barycentric: vec3, @@ -17,7 +18,8 @@ struct VertexOutput { @vertex fn vertex(vertex: Vertex) -> VertexOutput { var out: VertexOutput; - out.clip_position = mesh2d_position_local_to_clip(mesh.model, vec4(vertex.position, 1.0)); + let model = get_model_matrix(vertex.instance_index); + out.clip_position = mesh2d_position_local_to_clip(model, vec4(vertex.position, 1.0)); out.color = vertex.color; out.barycentric = vertex.barycentric; return out; diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 084205ee24752..530d48cde38a5 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -19,6 +19,8 @@ pub mod graph { } pub const CORE_2D: &str = graph::NAME; +use std::ops::Range; + pub use camera_2d::*; pub use main_pass_2d_node::*; @@ -35,7 +37,7 @@ use bevy_render::{ render_resource::CachedRenderPipelineId, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_utils::FloatOrd; +use bevy_utils::{nonmax::NonMaxU32, FloatOrd}; use crate::{tonemapping::TonemappingNode, upscaling::UpscalingNode}; @@ -83,7 +85,8 @@ pub struct Transparent2d { pub entity: Entity, pub pipeline: CachedRenderPipelineId, pub draw_function: DrawFunctionId, - pub batch_size: usize, + pub batch_range: Range, + pub dynamic_offset: Option, } impl PhaseItem for Transparent2d { @@ -111,8 +114,23 @@ impl PhaseItem for Transparent2d { } #[inline] - fn batch_size(&self) -> usize { - self.batch_size + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn dynamic_offset(&self) -> Option { + self.dynamic_offset + } + + #[inline] + fn dynamic_offset_mut(&mut self) -> &mut Option { + &mut self.dynamic_offset } } diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 04f4f0973998f..e30b20b8d5e49 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -24,7 +24,7 @@ pub mod graph { } pub const CORE_3D: &str = graph::NAME; -use std::cmp::Reverse; +use std::{cmp::Reverse, ops::Range}; pub use camera_3d::*; pub use main_opaque_pass_3d_node::*; @@ -50,7 +50,7 @@ use bevy_render::{ view::ViewDepthTexture, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_utils::{FloatOrd, HashMap}; +use bevy_utils::{nonmax::NonMaxU32, FloatOrd, HashMap}; use crate::{ prepass::{ @@ -135,7 +135,8 @@ pub struct Opaque3d { pub pipeline: CachedRenderPipelineId, pub entity: Entity, pub draw_function: DrawFunctionId, - pub batch_size: usize, + pub batch_range: Range, + pub dynamic_offset: Option, } impl PhaseItem for Opaque3d { @@ -164,8 +165,23 @@ impl PhaseItem for Opaque3d { } #[inline] - fn batch_size(&self) -> usize { - self.batch_size + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn dynamic_offset(&self) -> Option { + self.dynamic_offset + } + + #[inline] + fn dynamic_offset_mut(&mut self) -> &mut Option { + &mut self.dynamic_offset } } @@ -181,7 +197,8 @@ pub struct AlphaMask3d { pub pipeline: CachedRenderPipelineId, pub entity: Entity, pub draw_function: DrawFunctionId, - pub batch_size: usize, + pub batch_range: Range, + pub dynamic_offset: Option, } impl PhaseItem for AlphaMask3d { @@ -210,8 +227,23 @@ impl PhaseItem for AlphaMask3d { } #[inline] - fn batch_size(&self) -> usize { - self.batch_size + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn dynamic_offset(&self) -> Option { + self.dynamic_offset + } + + #[inline] + fn dynamic_offset_mut(&mut self) -> &mut Option { + &mut self.dynamic_offset } } @@ -227,7 +259,8 @@ pub struct Transparent3d { pub pipeline: CachedRenderPipelineId, pub entity: Entity, pub draw_function: DrawFunctionId, - pub batch_size: usize, + pub batch_range: Range, + pub dynamic_offset: Option, } impl PhaseItem for Transparent3d { @@ -255,8 +288,23 @@ impl PhaseItem for Transparent3d { } #[inline] - fn batch_size(&self) -> usize { - self.batch_size + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn dynamic_offset(&self) -> Option { + self.dynamic_offset + } + + #[inline] + fn dynamic_offset_mut(&mut self) -> &mut Option { + &mut self.dynamic_offset } } diff --git a/crates/bevy_core_pipeline/src/prepass/mod.rs b/crates/bevy_core_pipeline/src/prepass/mod.rs index 38c71050a194b..f408a168e7c7c 100644 --- a/crates/bevy_core_pipeline/src/prepass/mod.rs +++ b/crates/bevy_core_pipeline/src/prepass/mod.rs @@ -27,7 +27,7 @@ pub mod node; -use std::cmp::Reverse; +use std::{cmp::Reverse, ops::Range}; use bevy_ecs::prelude::*; use bevy_reflect::Reflect; @@ -36,7 +36,7 @@ use bevy_render::{ render_resource::{CachedRenderPipelineId, Extent3d, TextureFormat}, texture::CachedTexture, }; -use bevy_utils::FloatOrd; +use bevy_utils::{nonmax::NonMaxU32, FloatOrd}; pub const DEPTH_PREPASS_FORMAT: TextureFormat = TextureFormat::Depth32Float; pub const NORMAL_PREPASS_FORMAT: TextureFormat = TextureFormat::Rgb10a2Unorm; @@ -83,6 +83,8 @@ pub struct Opaque3dPrepass { pub entity: Entity, pub pipeline_id: CachedRenderPipelineId, pub draw_function: DrawFunctionId, + pub batch_range: Range, + pub dynamic_offset: Option, } impl PhaseItem for Opaque3dPrepass { @@ -109,6 +111,26 @@ impl PhaseItem for Opaque3dPrepass { // Key negated to match reversed SortKey ordering radsort::sort_by_key(items, |item| -item.distance); } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn dynamic_offset(&self) -> Option { + self.dynamic_offset + } + + #[inline] + fn dynamic_offset_mut(&mut self) -> &mut Option { + &mut self.dynamic_offset + } } impl CachedRenderPipelinePhaseItem for Opaque3dPrepass { @@ -128,6 +150,8 @@ pub struct AlphaMask3dPrepass { pub entity: Entity, pub pipeline_id: CachedRenderPipelineId, pub draw_function: DrawFunctionId, + pub batch_range: Range, + pub dynamic_offset: Option, } impl PhaseItem for AlphaMask3dPrepass { @@ -154,6 +178,26 @@ impl PhaseItem for AlphaMask3dPrepass { // Key negated to match reversed SortKey ordering radsort::sort_by_key(items, |item| -item.distance); } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn dynamic_offset(&self) -> Option { + self.dynamic_offset + } + + #[inline] + fn dynamic_offset_mut(&mut self) -> &mut Option { + &mut self.dynamic_offset + } } impl CachedRenderPipelinePhaseItem for AlphaMask3dPrepass { diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index 2ec777e8ca52b..5b64598eb403f 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -178,7 +178,8 @@ fn queue_line_gizmos_2d( draw_function, pipeline, sort_key: FloatOrd(f32::INFINITY), - batch_size: 1, + batch_range: 0..1, + dynamic_offset: None, }); } } diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index acb827dff3346..c9a465f595549 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -192,7 +192,8 @@ fn queue_line_gizmos_3d( draw_function, pipeline, distance: 0., - batch_size: 1, + batch_range: 0..1, + dynamic_offset: None, }); } } diff --git a/crates/bevy_math/src/affine3.rs b/crates/bevy_math/src/affine3.rs index 51598e4beae12..a03f12dd57e65 100644 --- a/crates/bevy_math/src/affine3.rs +++ b/crates/bevy_math/src/affine3.rs @@ -1,4 +1,4 @@ -use glam::{Affine3A, Mat3, Vec3}; +use glam::{Affine3A, Mat3, Vec3, Vec3Swizzles, Vec4}; /// Reduced-size version of `glam::Affine3A` for use when storage has /// significant performance impact. Convert to `glam::Affine3A` to do @@ -10,6 +10,36 @@ pub struct Affine3 { pub translation: Vec3, } +impl Affine3 { + /// Calculates the transpose of the affine 4x3 matrix to a 3x4 and formats it for packing into GPU buffers + #[inline] + pub fn to_transpose(&self) -> [Vec4; 3] { + let transpose_3x3 = self.matrix3.transpose(); + [ + transpose_3x3.x_axis.extend(self.translation.x), + transpose_3x3.y_axis.extend(self.translation.y), + transpose_3x3.z_axis.extend(self.translation.z), + ] + } + + /// Calculates the inverse transpose of the 3x3 matrix and formats it for packing into GPU buffers + #[inline] + pub fn inverse_transpose_3x3(&self) -> ([Vec4; 2], f32) { + let inverse_transpose_3x3 = Affine3A::from(self).inverse().matrix3.transpose(); + ( + [ + (inverse_transpose_3x3.x_axis, inverse_transpose_3x3.y_axis.x).into(), + ( + inverse_transpose_3x3.y_axis.yz(), + inverse_transpose_3x3.z_axis.xy(), + ) + .into(), + ], + inverse_transpose_3x3.z_axis.z, + ) + } +} + impl From<&Affine3A> for Affine3 { fn from(affine: &Affine3A) -> Self { Self { diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 20873ed0f341d..d9c835abcaffa 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -20,7 +20,6 @@ use bevy_ecs::{ }, }; use bevy_render::{ - extract_component::ExtractComponentPlugin, mesh::{Mesh, MeshVertexBufferLayout}, prelude::Image, render_asset::{prepare_assets, RenderAssets}, @@ -29,13 +28,13 @@ use bevy_render::{ RenderPhase, SetItemPipeline, TrackedRenderPass, }, render_resource::{ - AsBindGroup, AsBindGroupError, BindGroup, BindGroupLayout, OwnedBindingResource, - PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipeline, - SpecializedMeshPipelineError, SpecializedMeshPipelines, + AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout, + OwnedBindingResource, PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef, + SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines, }, renderer::RenderDevice, texture::FallbackImage, - view::{ExtractedView, Msaa, VisibleEntities}, + view::{ExtractedView, Msaa, ViewVisibility, VisibleEntities}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_utils::{tracing::error, HashMap, HashSet}; @@ -180,8 +179,7 @@ where M::Data: PartialEq + Eq + Hash + Clone, { fn build(&self, app: &mut App) { - app.init_asset::() - .add_plugins(ExtractComponentPlugin::>::extract_visible()); + app.init_asset::(); if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app @@ -193,7 +191,10 @@ where .init_resource::>() .init_resource::>() .init_resource::>>() - .add_systems(ExtractSchedule, extract_materials::) + .add_systems( + ExtractSchedule, + (extract_materials::, extract_material_meshes::), + ) .add_systems( Render, ( @@ -225,6 +226,26 @@ where } } +fn extract_material_meshes( + mut commands: Commands, + mut previous_len: Local, + query: Extract)>>, +) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, view_visibility, material) in &query { + if view_visibility.get() { + // NOTE: MaterialBindGroupId is inserted here to avoid a table move. Upcoming changes + // to use SparseSet for render world entity storage will do this automatically. + values.push(( + entity, + (material.clone_weak(), MaterialBindGroupId::default()), + )); + } + } + *previous_len = values.len(); + commands.insert_or_spawn_batch(values); +} + /// A key uniquely identifying a specialized [`MaterialPipeline`]. pub struct MaterialPipelineKey { pub mesh_key: MeshPipelineKey, @@ -403,7 +424,12 @@ pub fn queue_material_meshes( msaa: Res, render_meshes: Res>, render_materials: Res>, - material_meshes: Query<(&Handle, &Handle, &MeshTransforms)>, + mut material_meshes: Query<( + &Handle, + &mut MaterialBindGroupId, + &Handle, + &MeshTransforms, + )>, images: Res>, mut views: Query<( &ExtractedView, @@ -467,8 +493,8 @@ pub fn queue_material_meshes( } let rangefinder = view.rangefinder3d(); for visible_entity in &visible_entities.entities { - let Ok((material_handle, mesh_handle, mesh_transforms)) = - material_meshes.get(*visible_entity) + let Ok((material_handle, mut material_bind_group_id, mesh_handle, mesh_transforms)) = + material_meshes.get_mut(*visible_entity) else { continue; }; @@ -504,6 +530,8 @@ pub fn queue_material_meshes( } }; + *material_bind_group_id = material.get_bind_group_id(); + let distance = rangefinder.distance_translation(&mesh_transforms.transform.translation) + material.properties.depth_bias; match material.properties.alpha_mode { @@ -513,7 +541,8 @@ pub fn queue_material_meshes( draw_function: draw_opaque_pbr, pipeline: pipeline_id, distance, - batch_size: 1, + batch_range: 0..1, + dynamic_offset: None, }); } AlphaMode::Mask(_) => { @@ -522,7 +551,8 @@ pub fn queue_material_meshes( draw_function: draw_alpha_mask_pbr, pipeline: pipeline_id, distance, - batch_size: 1, + batch_range: 0..1, + dynamic_offset: None, }); } AlphaMode::Blend @@ -534,7 +564,8 @@ pub fn queue_material_meshes( draw_function: draw_transparent_pbr, pipeline: pipeline_id, distance, - batch_size: 1, + batch_range: 0..1, + dynamic_offset: None, }); } } @@ -560,6 +591,15 @@ pub struct PreparedMaterial { pub properties: MaterialProperties, } +#[derive(Component, Clone, Copy, Default, PartialEq, Eq, Deref, DerefMut)] +pub struct MaterialBindGroupId(Option); + +impl PreparedMaterial { + pub fn get_bind_group_id(&self) -> MaterialBindGroupId { + MaterialBindGroupId(Some(self.bind_group.id())) + } +} + #[derive(Resource)] pub struct ExtractedMaterials { extracted: Vec<(AssetId, M)>, diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 3c9bdd7ddb4ec..ca1caf5c44720 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -17,6 +17,7 @@ use bevy_ecs::{ }; use bevy_math::{Affine3A, Mat4}; use bevy_render::{ + batching::batch_and_prepare_render_phase, globals::{GlobalsBuffer, GlobalsUniform}, mesh::MeshVertexBufferLayout, prelude::{Camera, Mesh}, @@ -158,7 +159,12 @@ where .add_systems(ExtractSchedule, extract_camera_previous_view_projection) .add_systems( Render, - prepare_previous_view_projection_uniforms.in_set(RenderSet::PrepareResources), + ( + prepare_previous_view_projection_uniforms, + batch_and_prepare_render_phase::, + batch_and_prepare_render_phase::, + ) + .in_set(RenderSet::PrepareResources), ); } @@ -849,6 +855,8 @@ pub fn queue_prepass_material_meshes( draw_function: opaque_draw_prepass, pipeline_id, distance, + batch_range: 0..1, + dynamic_offset: None, }); } AlphaMode::Mask(_) => { @@ -857,6 +865,8 @@ pub fn queue_prepass_material_meshes( draw_function: alpha_mask_draw_prepass, pipeline_id, distance, + batch_range: 0..1, + dynamic_offset: None, }); } AlphaMode::Blend diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 6c5087e721b9a..bad686b92f2f9 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -27,10 +27,11 @@ use bevy_render::{ }; use bevy_transform::{components::GlobalTransform, prelude::Transform}; use bevy_utils::{ + nonmax::NonMaxU32, tracing::{error, warn}, HashMap, }; -use std::{hash::Hash, num::NonZeroU64}; +use std::{hash::Hash, num::NonZeroU64, ops::Range}; #[derive(Component)] pub struct ExtractedPointLight { @@ -1641,6 +1642,8 @@ pub fn queue_shadows( pipeline: pipeline_id, entity, distance: 0.0, // TODO: sort front-to-back + batch_range: 0..1, + dynamic_offset: None, }); } } @@ -1652,6 +1655,8 @@ pub struct Shadow { pub entity: Entity, pub pipeline: CachedRenderPipelineId, pub draw_function: DrawFunctionId, + pub batch_range: Range, + pub dynamic_offset: Option, } impl PhaseItem for Shadow { @@ -1679,6 +1684,26 @@ impl PhaseItem for Shadow { // better than rebinding everything at a high rate. radsort::sort_by_key(items, |item| item.sort_key()); } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn dynamic_offset(&self) -> Option { + self.dynamic_offset + } + + #[inline] + fn dynamic_offset_mut(&mut self) -> &mut Option { + &mut self.dynamic_offset + } } impl CachedRenderPipelinePhaseItem for Shadow { diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 9661016e9a35f..995c8bfa59f2c 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1,8 +1,8 @@ use crate::{ environment_map, prepass, EnvironmentMapLight, FogMeta, GlobalLightMeta, GpuFog, GpuLights, - GpuPointLights, LightMeta, NotShadowCaster, NotShadowReceiver, PreviousGlobalTransform, - ScreenSpaceAmbientOcclusionTextures, Shadow, ShadowSamplers, ViewClusterBindings, - ViewFogUniformOffset, ViewLightsUniformOffset, ViewShadowBindings, + GpuPointLights, LightMeta, MaterialBindGroupId, NotShadowCaster, NotShadowReceiver, + PreviousGlobalTransform, ScreenSpaceAmbientOcclusionTextures, Shadow, ShadowSamplers, + ViewClusterBindings, ViewFogUniformOffset, ViewLightsUniformOffset, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, MAX_CASCADES_PER_LIGHT, MAX_DIRECTIONAL_LIGHTS, }; use bevy_app::Plugin; @@ -16,11 +16,15 @@ use bevy_core_pipeline::{ }; use bevy_ecs::{ prelude::*, - query::ROQueryItem, + query::{QueryItem, ROQueryItem}, system::{lifetimeless::*, SystemParamItem, SystemState}, }; -use bevy_math::{Affine3, Affine3A, Mat4, Vec2, Vec3Swizzles, Vec4}; +use bevy_math::{Affine3, Mat4, Vec2, Vec4}; use bevy_render::{ + batching::{ + batch_and_prepare_render_phase, write_batched_instance_buffer, GetBatchData, + NoAutomaticBatching, + }, globals::{GlobalsBuffer, GlobalsUniform}, mesh::{ skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, @@ -29,7 +33,7 @@ use bevy_render::{ }, prelude::Msaa, render_asset::RenderAssets, - render_phase::{PhaseItem, RenderCommand, RenderCommandResult, RenderPhase, TrackedRenderPass}, + render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass}, render_resource::*, renderer::{RenderDevice, RenderQueue}, texture::{ @@ -41,7 +45,6 @@ use bevy_render::{ }; use bevy_transform::components::GlobalTransform; use bevy_utils::{tracing::error, HashMap, Hashed}; -use fixedbitset::FixedBitSet; use crate::render::{ morph::{extract_morphs, prepare_morphs, MorphIndex, MorphUniform}, @@ -119,7 +122,15 @@ impl Plugin for MeshRenderPlugin { .add_systems( Render, ( - prepare_mesh_uniforms.in_set(RenderSet::PrepareResources), + ( + batch_and_prepare_render_phase::, + batch_and_prepare_render_phase::, + batch_and_prepare_render_phase::, + batch_and_prepare_render_phase::, + ) + .in_set(RenderSet::PrepareResources), + write_batched_instance_buffer:: + .in_set(RenderSet::PrepareResourcesFlush), prepare_skinned_meshes.in_set(RenderSet::PrepareResources), prepare_morphs.in_set(RenderSet::PrepareResources), prepare_mesh_bind_group.in_set(RenderSet::PrepareBindGroups), @@ -184,48 +195,13 @@ pub struct MeshUniform { impl From<&MeshTransforms> for MeshUniform { fn from(mesh_transforms: &MeshTransforms) -> Self { - let transpose_model_3x3 = mesh_transforms.transform.matrix3.transpose(); - let transpose_previous_model_3x3 = mesh_transforms.previous_transform.matrix3.transpose(); - let inverse_transpose_model_3x3 = Affine3A::from(&mesh_transforms.transform) - .inverse() - .matrix3 - .transpose(); + let (inverse_transpose_model_a, inverse_transpose_model_b) = + mesh_transforms.transform.inverse_transpose_3x3(); Self { - transform: [ - transpose_model_3x3 - .x_axis - .extend(mesh_transforms.transform.translation.x), - transpose_model_3x3 - .y_axis - .extend(mesh_transforms.transform.translation.y), - transpose_model_3x3 - .z_axis - .extend(mesh_transforms.transform.translation.z), - ], - previous_transform: [ - transpose_previous_model_3x3 - .x_axis - .extend(mesh_transforms.previous_transform.translation.x), - transpose_previous_model_3x3 - .y_axis - .extend(mesh_transforms.previous_transform.translation.y), - transpose_previous_model_3x3 - .z_axis - .extend(mesh_transforms.previous_transform.translation.z), - ], - inverse_transpose_model_a: [ - ( - inverse_transpose_model_3x3.x_axis, - inverse_transpose_model_3x3.y_axis.x, - ) - .into(), - ( - inverse_transpose_model_3x3.y_axis.yz(), - inverse_transpose_model_3x3.z_axis.xy(), - ) - .into(), - ], - inverse_transpose_model_b: inverse_transpose_model_3x3.z_axis.z, + transform: mesh_transforms.transform.to_transpose(), + previous_transform: mesh_transforms.previous_transform.to_transpose(), + inverse_transpose_model_a, + inverse_transpose_model_b, flags: mesh_transforms.flags, } } @@ -234,7 +210,7 @@ impl From<&MeshTransforms> for MeshUniform { // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_types.wgsl! bitflags::bitflags! { #[repr(transparent)] - struct MeshFlags: u32 { + pub struct MeshFlags: u32 { const SHADOW_RECEIVER = (1 << 0); // Indicates the sign of the determinant of the 3x3 model matrix. If the sign is positive, // then the flag should be set, else it should not be set. @@ -361,7 +337,12 @@ pub fn extract_skinned_meshes( SkinnedMeshJoints::build(skin, &inverse_bindposes, &joint_query, &mut uniform.buffer) { last_start = last_start.max(skinned_joints.index as usize); - values.push((entity, skinned_joints.to_buffer_index())); + // NOTE: The skinned joints uniform buffer has to be bound at a dynamic offset per + // entity and so cannot currently be batched. + values.push(( + entity, + (skinned_joints.to_buffer_index(), NoAutomaticBatching), + )); } } @@ -374,63 +355,6 @@ pub fn extract_skinned_meshes( commands.insert_or_spawn_batch(values); } -#[allow(clippy::too_many_arguments)] -pub fn prepare_mesh_uniforms( - mut seen: Local, - mut commands: Commands, - mut previous_len: Local, - render_device: Res, - render_queue: Res, - mut gpu_array_buffer: ResMut>, - views: Query<( - &RenderPhase, - &RenderPhase, - &RenderPhase, - )>, - shadow_views: Query<&RenderPhase>, - meshes: Query<(Entity, &MeshTransforms)>, -) { - gpu_array_buffer.clear(); - seen.clear(); - - let mut indices = Vec::with_capacity(*previous_len); - let mut push_indices = |(mesh, mesh_uniform): (Entity, &MeshTransforms)| { - let index = mesh.index() as usize; - if !seen.contains(index) { - if index >= seen.len() { - seen.grow(index + 1); - } - seen.insert(index); - indices.push((mesh, gpu_array_buffer.push(mesh_uniform.into()))); - } - }; - - for (opaque_phase, transparent_phase, alpha_phase) in &views { - meshes - .iter_many(opaque_phase.iter_entities()) - .for_each(&mut push_indices); - - meshes - .iter_many(transparent_phase.iter_entities()) - .for_each(&mut push_indices); - - meshes - .iter_many(alpha_phase.iter_entities()) - .for_each(&mut push_indices); - } - - for shadow_phase in &shadow_views { - meshes - .iter_many(shadow_phase.iter_entities()) - .for_each(&mut push_indices); - } - - *previous_len = indices.len(); - commands.insert_or_spawn_batch(indices); - - gpu_array_buffer.write_buffer(&render_device, &render_queue); -} - #[derive(Resource, Clone)] pub struct MeshPipeline { pub view_layout: BindGroupLayout, @@ -713,6 +637,26 @@ impl MeshPipeline { } } +impl GetBatchData for MeshPipeline { + type Query = ( + Option<&'static MaterialBindGroupId>, + &'static Handle, + &'static MeshTransforms, + ); + type CompareData = (Option, AssetId); + type BufferData = MeshUniform; + + fn get_buffer_data(&(.., mesh_transforms): &QueryItem) -> Self::BufferData { + mesh_transforms.into() + } + + fn get_compare_data( + &(material_bind_group_id, mesh_handle, ..): &QueryItem, + ) -> Self::CompareData { + (material_bind_group_id.copied(), mesh_handle.id()) + } +} + bitflags::bitflags! { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[repr(transparent)] @@ -1364,16 +1308,15 @@ impl RenderCommand

for SetMeshBindGroup { type ViewWorldQuery = (); type ItemWorldQuery = ( Read>, - Read>, Option>, Option>, ); #[inline] fn render<'w>( - _item: &P, + item: &P, _view: (), - (mesh, batch_indices, skin_index, morph_index): ROQueryItem, + (mesh, skin_index, morph_index): ROQueryItem, bind_groups: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { @@ -1391,20 +1334,20 @@ impl RenderCommand

for SetMeshBindGroup { }; let mut dynamic_offsets: [u32; 3] = Default::default(); - let mut index_count = 0; - if let Some(mesh_index) = batch_indices.dynamic_offset { - dynamic_offsets[index_count] = mesh_index; - index_count += 1; + let mut offset_count = 0; + if let Some(dynamic_offset) = item.dynamic_offset() { + dynamic_offsets[offset_count] = dynamic_offset.get(); + offset_count += 1; } if let Some(skin_index) = skin_index { - dynamic_offsets[index_count] = skin_index.index; - index_count += 1; + dynamic_offsets[offset_count] = skin_index.index; + offset_count += 1; } if let Some(morph_index) = morph_index { - dynamic_offsets[index_count] = morph_index.index; - index_count += 1; + dynamic_offsets[offset_count] = morph_index.index; + offset_count += 1; } - pass.set_bind_group(I, bind_group, &dynamic_offsets[0..index_count]); + pass.set_bind_group(I, bind_group, &dynamic_offsets[0..offset_count]); RenderCommandResult::Success } @@ -1414,22 +1357,23 @@ pub struct DrawMesh; impl RenderCommand

for DrawMesh { type Param = SRes>; type ViewWorldQuery = (); - type ItemWorldQuery = (Read>, Read>); + type ItemWorldQuery = Read>; #[inline] fn render<'w>( - _item: &P, + item: &P, _view: (), - (batch_indices, mesh_handle): ROQueryItem<'_, Self::ItemWorldQuery>, + mesh_handle: ROQueryItem<'_, Self::ItemWorldQuery>, meshes: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { if let Some(gpu_mesh) = meshes.into_inner().get(mesh_handle) { + let batch_range = item.batch_range(); pass.set_vertex_buffer(0, gpu_mesh.vertex_buffer.slice(..)); #[cfg(all(feature = "webgl", target_arch = "wasm32"))] pass.set_push_constants( ShaderStages::VERTEX, 0, - &(batch_indices.index as i32).to_le_bytes(), + &(batch_range.start as i32).to_le_bytes(), ); match &gpu_mesh.buffer_info { GpuBufferInfo::Indexed { @@ -1438,13 +1382,10 @@ impl RenderCommand

for DrawMesh { count, } => { pass.set_index_buffer(buffer.slice(..), 0, *index_format); - pass.draw_indexed(0..*count, 0, batch_indices.index..batch_indices.index + 1); + pass.draw_indexed(0..*count, 0, batch_range.clone()); } GpuBufferInfo::NonIndexed => { - pass.draw( - 0..gpu_mesh.vertex_count, - batch_indices.index..batch_indices.index + 1, - ); + pass.draw(0..gpu_mesh.vertex_count, batch_range.clone()); } } RenderCommandResult::Success diff --git a/crates/bevy_pbr/src/render/morph.rs b/crates/bevy_pbr/src/render/morph.rs index 753c366726934..5b98de2ad84d9 100644 --- a/crates/bevy_pbr/src/render/morph.rs +++ b/crates/bevy_pbr/src/render/morph.rs @@ -2,6 +2,7 @@ use std::{iter, mem}; use bevy_ecs::prelude::*; use bevy_render::{ + batching::NoAutomaticBatching, mesh::morph::{MeshMorphWeights, MAX_MORPH_WEIGHTS}, render_resource::{BufferUsages, BufferVec}, renderer::{RenderDevice, RenderQueue}, @@ -89,7 +90,9 @@ pub fn extract_morphs( add_to_alignment::(&mut uniform.buffer); let index = (start * mem::size_of::()) as u32; - values.push((entity, MorphIndex { index })); + // NOTE: Because morph targets require per-morph target texture bindings, they cannot + // currently be batched. + values.push((entity, (MorphIndex { index }, NoAutomaticBatching))); } *previous_len = values.len(); commands.insert_or_spawn_batch(values); diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index 128609071a398..b1be7a2ef5ccb 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -152,7 +152,8 @@ fn queue_wireframes( pipeline: pipeline_id, draw_function: draw_custom, distance: rangefinder.distance_translation(&mesh_transforms.transform.translation), - batch_size: 1, + batch_range: 0..1, + dynamic_offset: None, }); }; diff --git a/crates/bevy_render/src/batching/mod.rs b/crates/bevy_render/src/batching/mod.rs new file mode 100644 index 0000000000000..715402b2b4b16 --- /dev/null +++ b/crates/bevy_render/src/batching/mod.rs @@ -0,0 +1,123 @@ +use bevy_ecs::{ + component::Component, + prelude::Res, + query::{Has, QueryItem, ReadOnlyWorldQuery}, + system::{Query, ResMut}, +}; +use bevy_utils::nonmax::NonMaxU32; + +use crate::{ + render_phase::{CachedRenderPipelinePhaseItem, DrawFunctionId, RenderPhase}, + render_resource::{CachedRenderPipelineId, GpuArrayBuffer, GpuArrayBufferable}, + renderer::{RenderDevice, RenderQueue}, +}; + +/// Add this component to mesh entities to disable automatic batching +#[derive(Component)] +pub struct NoAutomaticBatching; + +/// Data necessary to be equal for two draw commands to be mergeable +/// +/// This is based on the following assumptions: +/// - Only entities with prepared assets (pipelines, materials, meshes) are +/// queued to phases +/// - View bindings are constant across a phase for a given draw function as +/// phases are per-view +/// - `batch_and_prepare_render_phase` is the only system that performs this +/// batching and has sole responsibility for preparing the per-object data. +/// As such the mesh binding and dynamic offsets are assumed to only be +/// variable as a result of the `batch_and_prepare_render_phase` system, e.g. +/// due to having to split data across separate uniform bindings within the +/// same buffer due to the maximum uniform buffer binding size. +#[derive(PartialEq)] +struct BatchMeta { + /// The pipeline id encompasses all pipeline configuration including vertex + /// buffers and layouts, shaders and their specializations, bind group + /// layouts, etc. + pipeline_id: CachedRenderPipelineId, + /// The draw function id defines the RenderCommands that are called to + /// set the pipeline and bindings, and make the draw command + draw_function_id: DrawFunctionId, + dynamic_offset: Option, + user_data: T, +} + +impl BatchMeta { + fn new(item: &impl CachedRenderPipelinePhaseItem, user_data: T) -> Self { + BatchMeta { + pipeline_id: item.cached_pipeline(), + draw_function_id: item.draw_function(), + dynamic_offset: item.dynamic_offset(), + user_data, + } + } +} + +/// A trait to support getting data used for batching draw commands via phase +/// items. +pub trait GetBatchData { + type Query: ReadOnlyWorldQuery; + /// Data used for comparison between phase items. If the pipeline id, draw + /// function id, per-instance data buffer dynamic offset and this data + /// matches, the draws can be batched. + type CompareData: PartialEq; + /// The per-instance data to be inserted into the [`GpuArrayBuffer`] + /// containing these data for all instances. + type BufferData: GpuArrayBufferable + Sync + Send + 'static; + /// Get the per-instance data to be inserted into the [`GpuArrayBuffer`]. + fn get_buffer_data(query_item: &QueryItem) -> Self::BufferData; + /// Get the data used for comparison when deciding whether draws can be + /// batched. + fn get_compare_data(query_item: &QueryItem) -> Self::CompareData; +} + +/// Batch the items in a render phase. This means comparing metadata needed to draw each phase item +/// and trying to combine the draws into a batch. +pub fn batch_and_prepare_render_phase( + gpu_array_buffer: ResMut>, + mut views: Query<&mut RenderPhase>, + query: Query<(Has, F::Query)>, +) { + let gpu_array_buffer = gpu_array_buffer.into_inner(); + + let mut process_item = |item: &mut I| { + let (no_auto_batching, batch_query_item) = query.get(item.entity()).ok()?; + + let buffer_data = F::get_buffer_data(&batch_query_item); + let buffer_index = gpu_array_buffer.push(buffer_data); + + let index = buffer_index.index.get(); + *item.batch_range_mut() = index..index + 1; + *item.dynamic_offset_mut() = buffer_index.dynamic_offset; + + (!no_auto_batching).then(|| { + let compare_data = F::get_compare_data(&batch_query_item); + BatchMeta::new(item, compare_data) + }) + }; + + for mut phase in &mut views { + let items = phase.items.iter_mut().map(|item| { + let batch_data = process_item(item); + (item.batch_range_mut(), batch_data) + }); + items.reduce(|(start_range, prev_batch_meta), (range, batch_meta)| { + if batch_meta.is_some() && prev_batch_meta == batch_meta { + start_range.end = range.end; + (start_range, prev_batch_meta) + } else { + (range, batch_meta) + } + }); + } +} + +pub fn write_batched_instance_buffer( + render_device: Res, + render_queue: Res, + gpu_array_buffer: ResMut>, +) { + let gpu_array_buffer = gpu_array_buffer.into_inner(); + gpu_array_buffer.write_buffer(&render_device, &render_queue); + gpu_array_buffer.clear(); +} diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index cc62f3fcead84..c72353df47fd6 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -5,6 +5,7 @@ compile_error!("bevy_render cannot compile for a 16-bit platform."); extern crate core; +pub mod batching; pub mod camera; pub mod color; pub mod extract_component; diff --git a/crates/bevy_render/src/render_phase/mod.rs b/crates/bevy_render/src/render_phase/mod.rs index 54870cfc260b7..6230d2e1d9cfa 100644 --- a/crates/bevy_render/src/render_phase/mod.rs +++ b/crates/bevy_render/src/render_phase/mod.rs @@ -29,6 +29,7 @@ mod draw; mod draw_state; mod rangefinder; +use bevy_utils::nonmax::NonMaxU32; pub use draw::*; pub use draw_state::*; pub use rangefinder::*; @@ -38,7 +39,7 @@ use bevy_ecs::{ prelude::*, system::{lifetimeless::SRes, SystemParamItem}, }; -use std::ops::Range; +use std::{ops::Range, slice::SliceIndex}; /// A collection of all rendering instructions, that will be executed by the GPU, for a /// single render phase for a single view. @@ -86,22 +87,7 @@ impl RenderPhase { world: &'w World, view: Entity, ) { - let draw_functions = world.resource::>(); - let mut draw_functions = draw_functions.write(); - draw_functions.prepare(world); - - let mut index = 0; - while index < self.items.len() { - let item = &self.items[index]; - let batch_size = item.batch_size(); - if batch_size > 0 { - let draw_function = draw_functions.get_mut(item.draw_function()).unwrap(); - draw_function.draw(world, render_pass, view, item); - index += batch_size; - } else { - index += 1; - } - } + self.render_range(render_pass, world, view, ..); } /// Renders all [`PhaseItem`]s in the provided `range` (based on their index in `self.items`) using their corresponding draw functions. @@ -110,27 +96,27 @@ impl RenderPhase { render_pass: &mut TrackedRenderPass<'w>, world: &'w World, view: Entity, - range: Range, + range: impl SliceIndex<[I], Output = [I]>, ) { - let draw_functions = world.resource::>(); - let mut draw_functions = draw_functions.write(); - draw_functions.prepare(world); - let items = self .items .get(range) .expect("`Range` provided to `render_range()` is out of bounds"); + let draw_functions = world.resource::>(); + let mut draw_functions = draw_functions.write(); + draw_functions.prepare(world); + let mut index = 0; while index < items.len() { let item = &items[index]; - let batch_size = item.batch_size(); - if batch_size > 0 { + let batch_range = item.batch_range(); + if batch_range.is_empty() { + index += 1; + } else { let draw_function = draw_functions.get_mut(item.draw_function()).unwrap(); draw_function.draw(world, render_pass, view, item); - index += batch_size; - } else { - index += 1; + index += batch_range.len(); } } } @@ -182,12 +168,14 @@ pub trait PhaseItem: Sized + Send + Sync + 'static { items.sort_unstable_by_key(|item| item.sort_key()); } - /// The number of items to skip after rendering this [`PhaseItem`]. - /// - /// Items with a `batch_size` of 0 will not be rendered. - fn batch_size(&self) -> usize { - 1 - } + /// The range of instances that the batch covers. After doing a batched draw, batch range + /// length phase items will be skipped. This design is to avoid having to restructure the + /// render phase unnecessarily. + fn batch_range(&self) -> &Range; + fn batch_range_mut(&mut self) -> &mut Range; + + fn dynamic_offset(&self) -> Option; + fn dynamic_offset_mut(&mut self) -> &mut Option; } /// A [`PhaseItem`] item, that automatically sets the appropriate render pipeline, diff --git a/crates/bevy_render/src/render_resource/batched_uniform_buffer.rs b/crates/bevy_render/src/render_resource/batched_uniform_buffer.rs index a9fba2ac7fb42..08c29a8664856 100644 --- a/crates/bevy_render/src/render_resource/batched_uniform_buffer.rs +++ b/crates/bevy_render/src/render_resource/batched_uniform_buffer.rs @@ -3,6 +3,7 @@ use crate::{ render_resource::DynamicUniformBuffer, renderer::{RenderDevice, RenderQueue}, }; +use bevy_utils::nonmax::NonMaxU32; use encase::{ private::{ArrayMetadata, BufferMut, Metadata, RuntimeSizedArray, WriteInto, Writer}, ShaderType, @@ -76,8 +77,8 @@ impl BatchedUniformBuffer { pub fn push(&mut self, component: T) -> GpuArrayBufferIndex { let result = GpuArrayBufferIndex { - index: self.temp.0.len() as u32, - dynamic_offset: Some(self.current_offset), + index: NonMaxU32::new(self.temp.0.len() as u32).unwrap(), + dynamic_offset: NonMaxU32::new(self.current_offset), element_type: PhantomData, }; self.temp.0.push(component); diff --git a/crates/bevy_render/src/render_resource/gpu_array_buffer.rs b/crates/bevy_render/src/render_resource/gpu_array_buffer.rs index 45eaba4f73246..13694439ba5dc 100644 --- a/crates/bevy_render/src/render_resource/gpu_array_buffer.rs +++ b/crates/bevy_render/src/render_resource/gpu_array_buffer.rs @@ -4,6 +4,7 @@ use crate::{ renderer::{RenderDevice, RenderQueue}, }; use bevy_ecs::{prelude::Component, system::Resource}; +use bevy_utils::nonmax::NonMaxU32; use encase::{private::WriteInto, ShaderSize, ShaderType}; use std::{marker::PhantomData, mem}; use wgpu::{BindGroupLayoutEntry, BindingResource, BindingType, BufferBindingType, ShaderStages}; @@ -52,7 +53,7 @@ impl GpuArrayBuffer { match self { GpuArrayBuffer::Uniform(buffer) => buffer.push(value), GpuArrayBuffer::Storage((_, buffer)) => { - let index = buffer.len() as u32; + let index = NonMaxU32::new(buffer.len() as u32).unwrap(); buffer.push(value); GpuArrayBufferIndex { index, @@ -118,12 +119,12 @@ impl GpuArrayBuffer { } /// An index into a [`GpuArrayBuffer`] for a given element. -#[derive(Component)] +#[derive(Component, Clone)] pub struct GpuArrayBufferIndex { /// The index to use in a shader into the array. - pub index: u32, + pub index: NonMaxU32, /// The dynamic offset to use when setting the bind group in a pass. /// Only used on platforms that don't support storage buffers. - pub dynamic_offset: Option, + pub dynamic_offset: Option, pub element_type: PhantomData, } diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 605115d2403f8..4b496c7242ac4 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -15,7 +15,6 @@ use bevy_ecs::{ }; use bevy_log::error; use bevy_render::{ - extract_component::ExtractComponentPlugin, mesh::{Mesh, MeshVertexBufferLayout}, prelude::Image, render_asset::{prepare_assets, RenderAssets}, @@ -24,9 +23,9 @@ use bevy_render::{ RenderPhase, SetItemPipeline, TrackedRenderPass, }, render_resource::{ - AsBindGroup, AsBindGroupError, BindGroup, BindGroupLayout, OwnedBindingResource, - PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipeline, - SpecializedMeshPipelineError, SpecializedMeshPipelines, + AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout, + OwnedBindingResource, PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef, + SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines, }, renderer::RenderDevice, texture::FallbackImage, @@ -39,8 +38,8 @@ use std::hash::Hash; use std::marker::PhantomData; use crate::{ - DrawMesh2d, Mesh2dHandle, Mesh2dPipeline, Mesh2dPipelineKey, Mesh2dUniform, SetMesh2dBindGroup, - SetMesh2dViewBindGroup, + DrawMesh2d, Mesh2dHandle, Mesh2dPipeline, Mesh2dPipelineKey, Mesh2dTransforms, + SetMesh2dBindGroup, SetMesh2dViewBindGroup, }; /// Materials are used alongside [`Material2dPlugin`] and [`MaterialMesh2dBundle`] @@ -144,8 +143,7 @@ where M::Data: PartialEq + Eq + Hash + Clone, { fn build(&self, app: &mut App) { - app.init_asset::() - .add_plugins(ExtractComponentPlugin::>::extract_visible()); + app.init_asset::(); if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app @@ -153,7 +151,10 @@ where .init_resource::>() .init_resource::>() .init_resource::>>() - .add_systems(ExtractSchedule, extract_materials_2d::) + .add_systems( + ExtractSchedule, + (extract_materials_2d::, extract_material_meshes_2d::), + ) .add_systems( Render, ( @@ -175,6 +176,26 @@ where } } +fn extract_material_meshes_2d( + mut commands: Commands, + mut previous_len: Local, + query: Extract)>>, +) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, view_visibility, material) in &query { + if view_visibility.get() { + // NOTE: Material2dBindGroupId is inserted here to avoid a table move. Upcoming changes + // to use SparseSet for render world entity storage will do this automatically. + values.push(( + entity, + (material.clone_weak(), Material2dBindGroupId::default()), + )); + } + } + *previous_len = values.len(); + commands.insert_or_spawn_batch(values); +} + /// Render pipeline data for a given [`Material2d`] #[derive(Resource)] pub struct Material2dPipeline { @@ -343,7 +364,12 @@ pub fn queue_material2d_meshes( msaa: Res, render_meshes: Res>, render_materials: Res>, - material2d_meshes: Query<(&Handle, &Mesh2dHandle, &Mesh2dUniform)>, + mut material2d_meshes: Query<( + &Handle, + &mut Material2dBindGroupId, + &Mesh2dHandle, + &Mesh2dTransforms, + )>, mut views: Query<( &ExtractedView, &VisibleEntities, @@ -374,8 +400,12 @@ pub fn queue_material2d_meshes( } } for visible_entity in &visible_entities.entities { - let Ok((material2d_handle, mesh2d_handle, mesh2d_uniform)) = - material2d_meshes.get(*visible_entity) + let Ok(( + material2d_handle, + mut material2d_bind_group_id, + mesh2d_handle, + mesh2d_uniform, + )) = material2d_meshes.get_mut(*visible_entity) else { continue; }; @@ -406,7 +436,8 @@ pub fn queue_material2d_meshes( } }; - let mesh_z = mesh2d_uniform.transform.w_axis.z; + *material2d_bind_group_id = material2d.get_bind_group_id(); + let mesh_z = mesh2d_uniform.transform.translation.z; transparent_phase.add(Transparent2d { entity: *visible_entity, draw_function: draw_transparent_pbr, @@ -416,13 +447,17 @@ pub fn queue_material2d_meshes( // -z in front of the camera, the largest distance is -far with values increasing toward the // camera. As such we can just use mesh_z as the distance sort_key: FloatOrd(mesh_z), - // This material is not batched - batch_size: 1, + // Batching is done in batch_and_prepare_render_phase + batch_range: 0..1, + dynamic_offset: None, }); } } } +#[derive(Component, Clone, Copy, Default, PartialEq, Eq, Deref, DerefMut)] +pub struct Material2dBindGroupId(Option); + /// Data prepared for a [`Material2d`] instance. pub struct PreparedMaterial2d { pub bindings: Vec, @@ -430,6 +465,12 @@ pub struct PreparedMaterial2d { pub key: T::Data, } +impl PreparedMaterial2d { + pub fn get_bind_group_id(&self) -> Material2dBindGroupId { + Material2dBindGroupId(Some(self.bind_group.id())) + } +} + #[derive(Resource)] pub struct ExtractedMaterials2d { extracted: Vec<(AssetId, M)>, diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 09b69b296a664..2717acd394d4e 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -1,15 +1,16 @@ use bevy_app::Plugin; -use bevy_asset::{load_internal_asset, Handle}; +use bevy_asset::{load_internal_asset, AssetId, Handle}; +use bevy_core_pipeline::core_2d::Transparent2d; use bevy_ecs::{ prelude::*, - query::ROQueryItem, + query::{QueryItem, ROQueryItem}, system::{lifetimeless::*, SystemParamItem, SystemState}, }; -use bevy_math::{Mat4, Vec2}; +use bevy_math::{Affine3, Vec2, Vec4}; use bevy_reflect::Reflect; use bevy_render::{ - extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin}, + batching::{batch_and_prepare_render_phase, write_batched_instance_buffer, GetBatchData}, globals::{GlobalsBuffer, GlobalsUniform}, mesh::{GpuBufferInfo, Mesh, MeshVertexBufferLayout}, render_asset::RenderAssets, @@ -26,10 +27,12 @@ use bevy_render::{ }; use bevy_transform::components::GlobalTransform; +use crate::Material2dBindGroupId; + /// Component for rendering with meshes in the 2d pipeline, usually with a [2d material](crate::Material2d) such as [`ColorMaterial`](crate::ColorMaterial). /// /// It wraps a [`Handle`] to differentiate from the 3d pipelines which use the handles directly as components -#[derive(Default, Clone, Component, Debug, Reflect)] +#[derive(Default, Clone, Component, Debug, Reflect, PartialEq, Eq)] #[reflect(Component)] pub struct Mesh2dHandle(pub Handle); @@ -76,12 +79,6 @@ impl Plugin for Mesh2dRenderPlugin { "mesh2d_types.wgsl", Shader::from_wgsl ); - load_internal_asset!( - app, - MESH2D_BINDINGS_HANDLE, - "mesh2d_bindings.wgsl", - Shader::from_wgsl - ); load_internal_asset!( app, MESH2D_FUNCTIONS_HANDLE, @@ -90,8 +87,6 @@ impl Plugin for Mesh2dRenderPlugin { ); load_internal_asset!(app, MESH2D_SHADER_HANDLE, "mesh2d.wgsl", Shader::from_wgsl); - app.add_plugins(UniformComponentPlugin::::default()); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::>() @@ -99,6 +94,10 @@ impl Plugin for Mesh2dRenderPlugin { .add_systems( Render, ( + batch_and_prepare_render_phase:: + .in_set(RenderSet::PrepareResources), + write_batched_instance_buffer:: + .in_set(RenderSet::PrepareResourcesFlush), prepare_mesh2d_bind_group.in_set(RenderSet::PrepareBindGroups), prepare_mesh2d_view_bind_groups.in_set(RenderSet::PrepareBindGroups), ), @@ -107,19 +106,69 @@ impl Plugin for Mesh2dRenderPlugin { } fn finish(&self, app: &mut bevy_app::App) { + let mut mesh_bindings_shader_defs = Vec::with_capacity(1); + if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.init_resource::(); + if let Some(per_object_buffer_batch_size) = GpuArrayBuffer::::batch_size( + render_app.world.resource::(), + ) { + mesh_bindings_shader_defs.push(ShaderDefVal::UInt( + "PER_OBJECT_BUFFER_BATCH_SIZE".into(), + per_object_buffer_batch_size, + )); + } + + render_app + .insert_resource(GpuArrayBuffer::::new( + render_app.world.resource::(), + )) + .init_resource::(); } + + // Load the mesh_bindings shader module here as it depends on runtime information about + // whether storage buffers are supported, or the maximum uniform buffer binding size. + load_internal_asset!( + app, + MESH2D_BINDINGS_HANDLE, + "mesh2d_bindings.wgsl", + Shader::from_wgsl_with_defs, + mesh_bindings_shader_defs + ); } } -#[derive(Component, ShaderType, Clone)] +#[derive(Component)] +pub struct Mesh2dTransforms { + pub transform: Affine3, + pub flags: u32, +} + +#[derive(ShaderType, Clone)] pub struct Mesh2dUniform { - pub transform: Mat4, - pub inverse_transpose_model: Mat4, + // Affine 4x3 matrix transposed to 3x4 + pub transform: [Vec4; 3], + // 3x3 matrix packed in mat2x4 and f32 as: + // [0].xyz, [1].x, + // [1].yz, [2].xy + // [2].z + pub inverse_transpose_model_a: [Vec4; 2], + pub inverse_transpose_model_b: f32, pub flags: u32, } +impl From<&Mesh2dTransforms> for Mesh2dUniform { + fn from(mesh_transforms: &Mesh2dTransforms) -> Self { + let (inverse_transpose_model_a, inverse_transpose_model_b) = + mesh_transforms.transform.inverse_transpose_3x3(); + Self { + transform: mesh_transforms.transform.to_transpose(), + inverse_transpose_model_a, + inverse_transpose_model_b, + flags: mesh_transforms.flags, + } + } +} + // NOTE: These must match the bit flags in bevy_sprite/src/mesh2d/mesh2d.wgsl! bitflags::bitflags! { #[repr(transparent)] @@ -139,15 +188,13 @@ pub fn extract_mesh2d( if !view_visibility.get() { continue; } - let transform = transform.compute_matrix(); values.push(( entity, ( Mesh2dHandle(handle.0.clone_weak()), - Mesh2dUniform { + Mesh2dTransforms { + transform: (&transform.affine()).into(), flags: MeshFlags::empty().bits(), - transform, - inverse_transpose_model: transform.inverse().transpose(), }, ), )); @@ -162,13 +209,18 @@ pub struct Mesh2dPipeline { pub mesh_layout: BindGroupLayout, // This dummy white texture is to be used in place of optional textures pub dummy_white_gpu_image: GpuImage, + pub per_object_buffer_batch_size: Option, } impl FromWorld for Mesh2dPipeline { fn from_world(world: &mut World) -> Self { - let mut system_state: SystemState<(Res, Res)> = - SystemState::new(world); - let (render_device, default_sampler) = system_state.get_mut(world); + let mut system_state: SystemState<( + Res, + Res, + Res, + )> = SystemState::new(world); + let (render_device, render_queue, default_sampler) = system_state.get_mut(world); + let render_device = render_device.into_inner(); let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { entries: &[ // View @@ -197,16 +249,11 @@ impl FromWorld for Mesh2dPipeline { }); let mesh_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, - ty: BindingType::Buffer { - ty: BufferBindingType::Uniform, - has_dynamic_offset: true, - min_binding_size: Some(Mesh2dUniform::min_size()), - }, - count: None, - }], + entries: &[GpuArrayBuffer::::binding_layout( + 0, + ShaderStages::VERTEX_FRAGMENT, + render_device, + )], label: Some("mesh2d_layout"), }); // A 1x1x1 'all 1.0' texture to use as a dummy texture to use in place of optional StandardMaterial textures @@ -219,7 +266,6 @@ impl FromWorld for Mesh2dPipeline { }; let format_size = image.texture_descriptor.format.pixel_size(); - let render_queue = world.resource_mut::(); render_queue.write_texture( ImageCopyTexture { texture: &texture, @@ -253,6 +299,9 @@ impl FromWorld for Mesh2dPipeline { view_layout, mesh_layout, dummy_white_gpu_image, + per_object_buffer_batch_size: GpuArrayBuffer::::batch_size( + render_device, + ), } } } @@ -275,6 +324,26 @@ impl Mesh2dPipeline { } } +impl GetBatchData for Mesh2dPipeline { + type Query = ( + Option<&'static Material2dBindGroupId>, + &'static Mesh2dHandle, + &'static Mesh2dTransforms, + ); + type CompareData = (Option, AssetId); + type BufferData = Mesh2dUniform; + + fn get_buffer_data(&(.., mesh_transforms): &QueryItem) -> Self::BufferData { + mesh_transforms.into() + } + + fn get_compare_data( + &(material_bind_group_id, mesh_handle, ..): &QueryItem, + ) -> Self::CompareData { + (material_bind_group_id.copied(), mesh_handle.0.id()) + } +} + bitflags::bitflags! { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[repr(transparent)] @@ -477,9 +546,9 @@ pub fn prepare_mesh2d_bind_group( mut commands: Commands, mesh2d_pipeline: Res, render_device: Res, - mesh2d_uniforms: Res>, + mesh2d_uniforms: Res>, ) { - if let Some(binding) = mesh2d_uniforms.uniforms().binding() { + if let Some(binding) = mesh2d_uniforms.binding() { commands.insert_resource(Mesh2dBindGroup { value: render_device.create_bind_group(&BindGroupDescriptor { entries: &[BindGroupEntry { @@ -557,20 +626,26 @@ pub struct SetMesh2dBindGroup; impl RenderCommand

for SetMesh2dBindGroup { type Param = SRes; type ViewWorldQuery = (); - type ItemWorldQuery = Read>; + type ItemWorldQuery = (); #[inline] fn render<'w>( - _item: &P, + item: &P, _view: (), - mesh2d_index: &'_ DynamicUniformIndex, + _item_query: (), mesh2d_bind_group: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { + let mut dynamic_offsets: [u32; 1] = Default::default(); + let mut offset_count = 0; + if let Some(dynamic_offset) = item.dynamic_offset() { + dynamic_offsets[offset_count] = dynamic_offset.get(); + offset_count += 1; + } pass.set_bind_group( I, &mesh2d_bind_group.into_inner().value, - &[mesh2d_index.index()], + &dynamic_offsets[..offset_count], ); RenderCommandResult::Success } @@ -584,14 +659,21 @@ impl RenderCommand

for DrawMesh2d { #[inline] fn render<'w>( - _item: &P, + item: &P, _view: (), mesh_handle: ROQueryItem<'w, Self::ItemWorldQuery>, meshes: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { + let batch_range = item.batch_range(); if let Some(gpu_mesh) = meshes.into_inner().get(&mesh_handle.0) { pass.set_vertex_buffer(0, gpu_mesh.vertex_buffer.slice(..)); + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + pass.set_push_constants( + ShaderStages::VERTEX, + 0, + &(batch_range.start as i32).to_le_bytes(), + ); match &gpu_mesh.buffer_info { GpuBufferInfo::Indexed { buffer, @@ -599,10 +681,10 @@ impl RenderCommand

for DrawMesh2d { count, } => { pass.set_index_buffer(buffer.slice(..), 0, *index_format); - pass.draw_indexed(0..*count, 0, 0..1); + pass.draw_indexed(0..*count, 0, batch_range.clone()); } GpuBufferInfo::NonIndexed => { - pass.draw(0..gpu_mesh.vertex_count, 0..1); + pass.draw(0..gpu_mesh.vertex_count, batch_range.clone()); } } RenderCommandResult::Success diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d.wgsl b/crates/bevy_sprite/src/mesh2d/mesh2d.wgsl index 2b99639836d31..003f7dda13af9 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh2d.wgsl +++ b/crates/bevy_sprite/src/mesh2d/mesh2d.wgsl @@ -8,6 +8,7 @@ #endif struct Vertex { + @builtin(instance_index) instance_index: u32, #ifdef VERTEX_POSITIONS @location(0) position: vec3, #endif @@ -33,20 +34,21 @@ fn vertex(vertex: Vertex) -> MeshVertexOutput { #endif #ifdef VERTEX_POSITIONS + var model = mesh_functions::get_model_matrix(vertex.instance_index); out.world_position = mesh_functions::mesh2d_position_local_to_world( - mesh.model, + model, vec4(vertex.position, 1.0) ); out.position = mesh_functions::mesh2d_position_world_to_clip(out.world_position); #endif #ifdef VERTEX_NORMALS - out.world_normal = mesh_functions::mesh2d_normal_local_to_world(vertex.normal); + out.world_normal = mesh_functions::mesh2d_normal_local_to_world(vertex.normal, vertex.instance_index); #endif #ifdef VERTEX_TANGENTS out.world_tangent = mesh_functions::mesh2d_tangent_local_to_world( - mesh.model, + model, vertex.tangent ); #endif diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_bindings.wgsl b/crates/bevy_sprite/src/mesh2d/mesh2d_bindings.wgsl index 521ccfa846e43..e673ef23f06b6 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh2d_bindings.wgsl +++ b/crates/bevy_sprite/src/mesh2d/mesh2d_bindings.wgsl @@ -1,5 +1,21 @@ #define_import_path bevy_sprite::mesh2d_bindings -#import bevy_sprite::mesh2d_types +#import bevy_sprite::mesh2d_types Mesh2d -@group(2) @binding(0) var mesh: bevy_sprite::mesh2d_types::Mesh2d; +#ifdef MESH_BINDGROUP_1 + +#ifdef PER_OBJECT_BUFFER_BATCH_SIZE +@group(1) @binding(0) var mesh: array; +#else +@group(1) @binding(0) var mesh: array; +#endif // PER_OBJECT_BUFFER_BATCH_SIZE + +#else // MESH_BINDGROUP_1 + +#ifdef PER_OBJECT_BUFFER_BATCH_SIZE +@group(2) @binding(0) var mesh: array; +#else +@group(2) @binding(0) var mesh: array; +#endif // PER_OBJECT_BUFFER_BATCH_SIZE + +#endif // MESH_BINDGROUP_1 diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_functions.wgsl b/crates/bevy_sprite/src/mesh2d/mesh2d_functions.wgsl index cf8d6e2522068..b936cad10f66f 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh2d_functions.wgsl +++ b/crates/bevy_sprite/src/mesh2d/mesh2d_functions.wgsl @@ -2,6 +2,12 @@ #import bevy_sprite::mesh2d_view_bindings view #import bevy_sprite::mesh2d_bindings mesh +#import bevy_render::instance_index get_instance_index +#import bevy_render::maths affine_to_square, mat2x4_f32_to_mat3x3_unpack + +fn get_model_matrix(instance_index: u32) -> mat4x4 { + return affine_to_square(mesh[get_instance_index(instance_index)].model); +} fn mesh2d_position_local_to_world(model: mat4x4, vertex_position: vec4) -> vec4 { return model * vertex_position; @@ -19,11 +25,10 @@ fn mesh2d_position_local_to_clip(model: mat4x4, vertex_position: vec4) return mesh2d_position_world_to_clip(world_position); } -fn mesh2d_normal_local_to_world(vertex_normal: vec3) -> vec3 { - return mat3x3( - mesh.inverse_transpose_model[0].xyz, - mesh.inverse_transpose_model[1].xyz, - mesh.inverse_transpose_model[2].xyz +fn mesh2d_normal_local_to_world(vertex_normal: vec3, instance_index: u32) -> vec3 { + return mat2x4_f32_to_mat3x3_unpack( + mesh[instance_index].inverse_transpose_model_a, + mesh[instance_index].inverse_transpose_model_b, ) * vertex_normal; } diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_types.wgsl b/crates/bevy_sprite/src/mesh2d/mesh2d_types.wgsl index 1de0218112a47..f855707790001 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh2d_types.wgsl +++ b/crates/bevy_sprite/src/mesh2d/mesh2d_types.wgsl @@ -1,8 +1,16 @@ #define_import_path bevy_sprite::mesh2d_types struct Mesh2d { - model: mat4x4, - inverse_transpose_model: mat4x4, + // Affine 4x3 matrix transposed to 3x4 + // Use bevy_render::maths::affine_to_square to unpack + model: mat3x4, + // 3x3 matrix packed in mat2x4 and f32 as: + // [0].xyz, [1].x, + // [1].yz, [2].xy + // [2].z + // Use bevy_render::maths::mat2x4_f32_to_mat3x3_unpack to unpack + inverse_transpose_model_a: mat2x4, + inverse_transpose_model_b: f32, // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. flags: u32, }; diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index a0a16ea612796..2d5343a867adc 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -565,8 +565,9 @@ pub fn queue_sprites( pipeline: colored_pipeline, entity: *entity, sort_key, - // batch_size will be calculated in prepare_sprites - batch_size: 0, + // batch_range and dynamic_offset will be calculated in prepare_sprites + batch_range: 0..0, + dynamic_offset: None, }); } else { transparent_phase.add(Transparent2d { @@ -574,8 +575,9 @@ pub fn queue_sprites( pipeline, entity: *entity, sort_key, - // batch_size will be calculated in prepare_sprites - batch_size: 0, + // batch_range and dynamic_offset will be calculated in prepare_sprites + batch_range: 0..0, + dynamic_offset: None, }); } } @@ -739,7 +741,9 @@ pub fn prepare_sprites( )); } - transparent_phase.items[batch_item_index].batch_size += 1; + transparent_phase.items[batch_item_index] + .batch_range_mut() + .end += 1; batches.last_mut().unwrap().1.range.end += 1; index += 1; } diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index c4195a6f41200..d146e53beb79d 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -4,6 +4,7 @@ mod render_pass; use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; use bevy_ecs::storage::SparseSet; use bevy_hierarchy::Parent; +use bevy_render::render_phase::PhaseItem; use bevy_render::view::ViewVisibility; use bevy_render::{ExtractSchedule, Render}; use bevy_window::{PrimaryWindow, Window}; @@ -665,8 +666,9 @@ pub fn queue_uinodes( pipeline, entity: *entity, sort_key: FloatOrd(extracted_uinode.stack_index as f32), - // batch_size will be calculated in prepare_uinodes - batch_size: 0, + // batch_range will be calculated in prepare_uinodes + batch_range: 0..0, + dynamic_offset: None, }); } } @@ -892,7 +894,7 @@ pub fn prepare_uinodes( } index += QUAD_INDICES.len() as u32; existing_batch.unwrap().1.range.end = index; - ui_phase.items[batch_item_index].batch_size += 1; + ui_phase.items[batch_item_index].batch_range_mut().end += 1; } else { batch_image_handle = AssetId::invalid(); } diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index 1eb3836b8c634..f483c8cf0bd90 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -1,3 +1,5 @@ +use std::ops::Range; + use super::{UiBatch, UiImageBindGroups, UiMeta}; use crate::{prelude::UiCameraConfig, DefaultCameraView}; use bevy_ecs::{ @@ -11,7 +13,7 @@ use bevy_render::{ renderer::*, view::*, }; -use bevy_utils::FloatOrd; +use bevy_utils::{nonmax::NonMaxU32, FloatOrd}; pub struct UiPassNode { ui_view_query: QueryState< @@ -90,7 +92,8 @@ pub struct TransparentUi { pub entity: Entity, pub pipeline: CachedRenderPipelineId, pub draw_function: DrawFunctionId, - pub batch_size: usize, + pub batch_range: Range, + pub dynamic_offset: Option, } impl PhaseItem for TransparentUi { @@ -117,8 +120,23 @@ impl PhaseItem for TransparentUi { } #[inline] - fn batch_size(&self) -> usize { - self.batch_size + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn dynamic_offset(&self) -> Option { + self.dynamic_offset + } + + #[inline] + fn dynamic_offset_mut(&mut self) -> &mut Option { + &mut self.dynamic_offset } } diff --git a/crates/bevy_utils/Cargo.toml b/crates/bevy_utils/Cargo.toml index ba887e8057220..20a4cb32bb99c 100644 --- a/crates/bevy_utils/Cargo.toml +++ b/crates/bevy_utils/Cargo.toml @@ -20,6 +20,7 @@ hashbrown = { version = "0.14", features = ["serde"] } bevy_utils_proc_macros = {version = "0.12.0-dev", path = "macros"} petgraph = "0.6" thiserror = "1.0" +nonmax = "0.5" [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = {version = "0.2.0", features = ["js"]} diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index 7916caf769476..52f33f31d7dc6 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -34,6 +34,11 @@ pub use thiserror; pub use tracing; pub use uuid::Uuid; +#[allow(missing_docs)] +pub mod nonmax { + pub use nonmax::*; +} + use hashbrown::hash_map::RawEntryMut; use std::{ fmt::Debug, diff --git a/examples/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index 780b56f9920fe..f1047a9fb88d4 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -21,7 +21,7 @@ use bevy::{ Extract, Render, RenderApp, RenderSet, }, sprite::{ - DrawMesh2d, Mesh2dHandle, Mesh2dPipeline, Mesh2dPipelineKey, Mesh2dUniform, + DrawMesh2d, Mesh2dHandle, Mesh2dPipeline, Mesh2dPipelineKey, Mesh2dTransforms, SetMesh2dBindGroup, SetMesh2dViewBindGroup, }, utils::FloatOrd, @@ -148,19 +148,24 @@ impl SpecializedRenderPipeline for ColoredMesh2dPipeline { false => TextureFormat::bevy_default(), }; + // Meshes typically live in bind group 2. Because we are using bind group 1 + // we need to add the MESH_BINDGROUP_1 shader def so that the bindings are correctly + // linked in the shader. + let shader_defs = vec!["MESH_BINDGROUP_1".into()]; + RenderPipelineDescriptor { vertex: VertexState { // Use our custom shader shader: COLORED_MESH2D_SHADER_HANDLE, entry_point: "vertex".into(), - shader_defs: Vec::new(), + shader_defs: shader_defs.clone(), // Use our custom vertex buffer buffers: vec![vertex_layout], }, fragment: Some(FragmentState { // Use our custom shader shader: COLORED_MESH2D_SHADER_HANDLE, - shader_defs: Vec::new(), + shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format, @@ -212,13 +217,12 @@ type DrawColoredMesh2d = ( // using `include_str!()`, or loaded like any other asset with `asset_server.load()`. const COLORED_MESH2D_SHADER: &str = r" // Import the standard 2d mesh uniforms and set their bind groups -#import bevy_sprite::mesh2d_types as MeshTypes +#import bevy_sprite::mesh2d_bindings mesh #import bevy_sprite::mesh2d_functions as MeshFunctions -@group(1) @binding(0) var mesh: MeshTypes::Mesh2d; - // The structure of the vertex buffer is as specified in `specialize()` struct Vertex { + @builtin(instance_index) instance_index: u32, @location(0) position: vec3, @location(1) color: u32, }; @@ -235,7 +239,8 @@ struct VertexOutput { fn vertex(vertex: Vertex) -> VertexOutput { var out: VertexOutput; // Project the world position of the mesh into screen position - out.clip_position = MeshFunctions::mesh2d_position_local_to_clip(mesh.model, vec4(vertex.position, 1.0)); + let model = MeshFunctions::get_model_matrix(vertex.instance_index); + out.clip_position = MeshFunctions::mesh2d_position_local_to_clip(model, vec4(vertex.position, 1.0)); // Unpack the `u32` from the vertex buffer into the `vec4` used by the fragment shader out.color = vec4((vec4(vertex.color) >> vec4(0u, 8u, 16u, 24u)) & vec4(255u)) / 255.0; return out; @@ -315,7 +320,7 @@ pub fn queue_colored_mesh2d( pipeline_cache: Res, msaa: Res, render_meshes: Res>, - colored_mesh2d: Query<(&Mesh2dHandle, &Mesh2dUniform), With>, + colored_mesh2d: Query<(&Mesh2dHandle, &Mesh2dTransforms), With>, mut views: Query<( &VisibleEntities, &mut RenderPhase, @@ -334,7 +339,7 @@ pub fn queue_colored_mesh2d( // Queue all entities visible to that view for visible_entity in &visible_entities.entities { - if let Ok((mesh2d_handle, mesh2d_uniform)) = colored_mesh2d.get(*visible_entity) { + if let Ok((mesh2d_handle, mesh2d_transforms)) = colored_mesh2d.get(*visible_entity) { // Get our specialized pipeline let mut mesh2d_key = mesh_key; if let Some(mesh) = render_meshes.get(&mesh2d_handle.0) { @@ -345,7 +350,7 @@ pub fn queue_colored_mesh2d( let pipeline_id = pipelines.specialize(&pipeline_cache, &colored_mesh2d_pipeline, mesh2d_key); - let mesh_z = mesh2d_uniform.transform.w_axis.z; + let mesh_z = mesh2d_transforms.transform.translation.z; transparent_phase.add(Transparent2d { entity: *visible_entity, draw_function: draw_colored_mesh2d, @@ -354,7 +359,8 @@ pub fn queue_colored_mesh2d( // in order to get correct transparency sort_key: FloatOrd(mesh_z), // This material is not batched - batch_size: 1, + batch_range: 0..1, + dynamic_offset: None, }); } } diff --git a/examples/shader/shader_instancing.rs b/examples/shader/shader_instancing.rs index 326183d917a0b..d5e751ae0fa1d 100644 --- a/examples/shader/shader_instancing.rs +++ b/examples/shader/shader_instancing.rs @@ -136,7 +136,8 @@ fn queue_custom( draw_function: draw_custom, distance: rangefinder .distance_translation(&mesh_transforms.transform.translation), - batch_size: 1, + batch_range: 0..1, + dynamic_offset: None, }); } }