Skip to content

Commit 5558f7b

Browse files
authored
For each mesh instance, only store the previous frame's transform, not the entire MeshInputUniform. (#23892)
Currently, we copy the entire `MeshInputUniform`, which is 24 words, to the previous mesh instance buffer. This is wasteful, as we only ever use the transform for motion blur. This patch fixes the issue by introducing a new type, `PreviousMeshInputUniform`, that contains only the transform. Not only does this improve performance, but it will also simplify future work to sparsely update only the `MeshInputUniform` fields that changed. On `bevy_city --no-cpu-culling`, this commit reduces `write_previous_input_buffers` from a median time of 240.1 μs to 65.33 μs, a 3.68× speedup. <img width="2756" height="1800" alt="Screenshot 2026-04-20 003301" src="https://github.com/user-attachments/assets/35fa099d-4935-4af5-9b9c-99a3db570148" />
1 parent d227783 commit 5558f7b

6 files changed

Lines changed: 83 additions & 29 deletions

File tree

crates/bevy_pbr/src/render/gpu_preprocess.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ use bitflags::bitflags;
7272
use smallvec::{smallvec, SmallVec};
7373
use tracing::warn;
7474

75-
use crate::{LightEntity, MeshCullingData, MeshCullingDataBuffer, MeshInputUniform, MeshUniform};
75+
use crate::{
76+
LightEntity, MeshCullingData, MeshCullingDataBuffer, MeshInputUniform, MeshUniform,
77+
PreviousMeshInputUniform,
78+
};
7679

7780
use super::{ShadowView, ViewLightEntities};
7881

@@ -1418,7 +1421,10 @@ fn preprocess_direct_bind_group_layout_entries() -> DynamicBindGroupLayoutEntrie
14181421
// `current_input`
14191422
(3, storage_buffer_read_only::<MeshInputUniform>(false)),
14201423
// `previous_input`
1421-
(4, storage_buffer_read_only::<MeshInputUniform>(false)),
1424+
(
1425+
4,
1426+
storage_buffer_read_only::<PreviousMeshInputUniform>(false),
1427+
),
14221428
// `indices`
14231429
(5, storage_buffer_read_only::<PreprocessWorkItem>(false)),
14241430
// `output`

crates/bevy_pbr/src/render/mesh.rs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ use bevy_mesh::{
4040
MeshTag, MeshVertexBufferLayoutRef, VertexAttributeDescriptor,
4141
};
4242
use bevy_platform::collections::{hash_map::Entry, HashMap};
43-
use bevy_render::batching::gpu_preprocessing::PreviousInstanceInputUniformBuffer;
43+
use bevy_render::batching::gpu_preprocessing::{
44+
BufferDataInput, PreviousInstanceInputUniformBuffer,
45+
};
4446
use bevy_render::impl_atomic_pod;
4547
use bevy_render::mesh::allocator::{MeshSlabId, MeshSlabs};
4648
use bevy_render::mesh::morph::{
@@ -632,8 +634,22 @@ pub struct MeshInputUniform {
632634
pub pad3: u32,
633635
}
634636

637+
/// Per-mesh-instance data that we retain from the previous frame.
638+
#[derive(ShaderType, Pod, Zeroable, Clone, Copy, Default, Debug)]
639+
#[repr(C)]
640+
pub struct PreviousMeshInputUniform {
641+
/// The model transform, an affine 4×3 matrix transposed to 3×4.
642+
pub world_from_local: [Vec4; 3],
643+
}
644+
645+
impl BufferDataInput for MeshInputUniform {
646+
type Previous = PreviousMeshInputUniform;
647+
}
648+
635649
impl_atomic_pod!(MeshInputUniform, MeshInputUniformBlob);
636650

651+
impl_atomic_pod!(PreviousMeshInputUniform, PreviousMeshInputUniformBlob);
652+
637653
/// Information about each mesh instance needed to cull it on GPU.
638654
///
639655
/// This consists of its axis-aligned bounding box (AABB).
@@ -1550,7 +1566,7 @@ impl RenderMeshInstanceGpuPrepared {
15501566
entity: MainEntity,
15511567
render_mesh_instances: &mut MainEntityHashMap<RenderMeshInstanceGpu>,
15521568
current_input_buffer: &mut InstanceInputUniformBuffer<MeshInputUniform>,
1553-
previous_input_buffer: &PreviousInstanceInputUniformBuffer<MeshInputUniform>,
1569+
previous_input_buffer: &PreviousInstanceInputUniformBuffer<PreviousMeshInputUniform>,
15541570
) -> u32 {
15551571
// Did the last frame contain this entity as well?
15561572
let current_uniform_index;
@@ -1564,11 +1580,14 @@ impl RenderMeshInstanceGpuPrepared {
15641580
.gpu_specific
15651581
.current_uniform_index();
15661582

1567-
// Save the old mesh input uniform. The mesh preprocessing
1568-
// shader will need it to compute motion vectors.
1569-
let previous_mesh_input_uniform =
1570-
current_input_buffer.get_unchecked(current_uniform_index);
1571-
let previous_input_index = previous_input_buffer.push(previous_mesh_input_uniform);
1583+
// Save the old mesh transform. The mesh preprocessing shader
1584+
// will need it to compute motion vectors.
1585+
let previous_world_from_local = current_input_buffer
1586+
.get_unchecked(current_uniform_index)
1587+
.world_from_local;
1588+
let previous_input_index = previous_input_buffer.push(PreviousMeshInputUniform {
1589+
world_from_local: previous_world_from_local,
1590+
});
15721591
self.mesh_input_uniform.previous_input_index = previous_input_index;
15731592

15741593
// Write in the new mesh input uniform.
@@ -2606,10 +2625,13 @@ pub fn collect_meshes_for_gpu_building(
26062625
let current_uniform_index =
26072626
render_mesh_instance.gpu_specific.current_uniform_index();
26082627

2609-
let previous_mesh_input_uniform =
2610-
current_input_buffer.get_unchecked(current_uniform_index);
2628+
let previous_world_from_local = current_input_buffer
2629+
.get_unchecked(current_uniform_index)
2630+
.world_from_local;
26112631
let previous_input_index =
2612-
previous_input_buffer.push(previous_mesh_input_uniform);
2632+
previous_input_buffer.push(PreviousMeshInputUniform {
2633+
world_from_local: previous_world_from_local,
2634+
});
26132635
prepared.mesh_input_uniform.previous_input_index =
26142636
previous_input_index;
26152637

crates/bevy_pbr/src/render/mesh_preprocess.wgsl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
// respectively.
1616

1717
#import bevy_pbr::mesh_preprocess_types::{
18-
IndirectParametersCpuMetadata, IndirectParametersGpuMetadata, MeshInput, PreprocessWorkItem
18+
IndirectParametersCpuMetadata, IndirectParametersGpuMetadata, MeshInput, PreprocessWorkItem,
19+
PreviousMeshInput
1920
}
2021
#import bevy_pbr::mesh_types::{
2122
Mesh, MESH_FLAGS_AABB_BASED_VISIBILITY_RANGE_BIT, MESH_FLAGS_NO_FRUSTUM_CULLING_BIT,
@@ -87,7 +88,7 @@ struct Immediates {
8788
// The current frame's `MeshInput`.
8889
@group(0) @binding(3) var<storage> current_input: array<MeshInput>;
8990
// The `MeshInput` values from the previous frame.
90-
@group(0) @binding(4) var<storage> previous_input: array<MeshInput>;
91+
@group(0) @binding(4) var<storage> previous_input: array<PreviousMeshInput>;
9192
// Indices into the `MeshInput` buffer.
9293
//
9394
// There may be many indices that map to the same `MeshInput`.

crates/bevy_render/src/batching/gpu_preprocessing.rs

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ pub enum GpuPreprocessingMode {
167167
pub struct BatchedInstanceBuffers<BD, BDI>
168168
where
169169
BD: GpuArrayBufferable + Sync + Send + 'static,
170-
BDI: AtomicPod,
170+
BDI: BufferDataInput,
171171
{
172172
/// The uniform data inputs for the current frame.
173173
///
@@ -181,7 +181,7 @@ where
181181
/// can spawn or despawn between frames. Instead, each current buffer
182182
/// data input uniform is expected to contain the index of the
183183
/// corresponding buffer data input uniform in this list.
184-
pub previous_input_buffer: PreviousInstanceInputUniformBuffer<BDI>,
184+
pub previous_input_buffer: PreviousInstanceInputUniformBuffer<BDI::Previous>,
185185

186186
/// The data needed to render buffers for each phase.
187187
///
@@ -193,7 +193,7 @@ where
193193
impl<BD, BDI> Default for BatchedInstanceBuffers<BD, BDI>
194194
where
195195
BD: GpuArrayBufferable + Sync + Send + 'static,
196-
BDI: AtomicPod,
196+
BDI: BufferDataInput,
197197
{
198198
fn default() -> Self {
199199
BatchedInstanceBuffers {
@@ -204,6 +204,22 @@ where
204204
}
205205
}
206206

207+
/// A trait that defines the data that we supply to the GPU for a single mesh
208+
/// instance (2D or 3D).
209+
///
210+
/// A compute shader is expected to expand this data to the full *buffer data*
211+
/// type. See [`BatchedInstanceBuffers`] for more detail.
212+
pub trait BufferDataInput: AtomicPod {
213+
/// The type of the data that we supply for the GPU for the previous
214+
type Previous: AtomicPod;
215+
}
216+
217+
// We don't use GPU mesh preprocessing for the 2D pipeline, so we implement
218+
// `BufferDataInput` for the unit type here.
219+
impl BufferDataInput for () {
220+
type Previous = ();
221+
}
222+
207223
/// The GPU buffers holding the data needed to render batches for a single
208224
/// phase.
209225
///
@@ -420,23 +436,23 @@ where
420436
/// large size, enough to hold all push operations that could possibly occur on
421437
/// the worker threads, and only synchronize the changed portion of the buffer
422438
/// to the GPU on each frame.
423-
pub struct PreviousInstanceInputUniformBuffer<BDI>
439+
pub struct PreviousInstanceInputUniformBuffer<BPDI>
424440
where
425-
BDI: AtomicPod,
441+
BPDI: AtomicPod,
426442
{
427443
/// The buffer containing the data that will be uploaded to the GPU.
428-
buffer: AtomicRawBufferVec<BDI>,
444+
buffer: AtomicRawBufferVec<BPDI>,
429445

430446
/// The number of elements pushed since the last [`Self::reserve`].
431447
atomic_len: AtomicU32,
432448
}
433449

434-
impl<BDI> PreviousInstanceInputUniformBuffer<BDI>
450+
impl<BPDI> PreviousInstanceInputUniformBuffer<BPDI>
435451
where
436-
BDI: AtomicPod,
452+
BPDI: AtomicPod,
437453
{
438454
/// Creates a new, empty buffer.
439-
pub fn new() -> PreviousInstanceInputUniformBuffer<BDI> {
455+
pub fn new() -> PreviousInstanceInputUniformBuffer<BPDI> {
440456
PreviousInstanceInputUniformBuffer {
441457
buffer: AtomicRawBufferVec::with_label(
442458
BufferUsages::STORAGE,
@@ -475,7 +491,7 @@ where
475491
/// Appends a value and returns its index. Thread-safe.
476492
///
477493
/// [`Self::reserve`] must have been called first with sufficient capacity.
478-
pub fn push(&self, value: BDI) -> u32 {
494+
pub fn push(&self, value: BPDI) -> u32 {
479495
let index = self.atomic_len.fetch_add(1, Ordering::Relaxed);
480496
debug_assert!(
481497
(index as usize) < self.buffer.len() as usize,
@@ -499,9 +515,9 @@ where
499515
}
500516
}
501517

502-
impl<BDI> Default for PreviousInstanceInputUniformBuffer<BDI>
518+
impl<BPDI> Default for PreviousInstanceInputUniformBuffer<BPDI>
503519
where
504-
BDI: AtomicPod,
520+
BPDI: AtomicPod,
505521
{
506522
fn default() -> Self {
507523
Self::new()
@@ -1383,7 +1399,7 @@ impl FromWorld for GpuPreprocessingSupport {
13831399
impl<BD, BDI> BatchedInstanceBuffers<BD, BDI>
13841400
where
13851401
BD: GpuArrayBufferable + Sync + Send + 'static,
1386-
BDI: AtomicPod,
1402+
BDI: BufferDataInput,
13871403
{
13881404
/// Creates new buffers.
13891405
pub fn new() -> Self {

crates/bevy_render/src/batching/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ use nonmax::NonMaxU32;
99
use bevy_material::{descriptor::CachedRenderPipelineId, labels::DrawFunctionId};
1010

1111
use crate::{
12+
batching::gpu_preprocessing::BufferDataInput,
1213
render_phase::{
1314
BinnedPhaseItem, CachedRenderPipelinePhaseItem, PhaseItemExtraIndex, SortedPhaseItem,
1415
SortedRenderPhase, ViewBinnedRenderPhases,
1516
},
16-
render_resource::{AtomicPod, GpuArrayBufferable},
17+
render_resource::GpuArrayBufferable,
1718
sync_world::MainEntity,
1819
};
1920

@@ -119,7 +120,7 @@ pub trait GetBatchData {
119120
pub trait GetFullBatchData: GetBatchData {
120121
/// The per-instance data that was inserted into the
121122
/// [`crate::render_resource::BufferVec`] during extraction.
122-
type BufferInputData: AtomicPod;
123+
type BufferInputData: BufferDataInput;
123124

124125
/// Get the per-instance data to be inserted into the
125126
/// [`crate::render_resource::GpuArrayBuffer`].

crates/bevy_render/src/occlusion_culling/mesh_preprocess_types.wgsl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ struct MeshInput {
1010
lightmap_uv_rect: vec2<u32>,
1111
// Various flags.
1212
flags: u32,
13+
// The index of the `PreviousMeshInput` uniform in the buffer, or `u32::MAX`
14+
// if there's no such uniform.
1315
previous_input_index: u32,
1416
first_vertex_index: u32,
1517
first_index_index: u32,
@@ -29,6 +31,12 @@ struct MeshInput {
2931
metadata_index: u32
3032
}
3133

34+
// Per-mesh-instance data that we retain from the previous frame.
35+
struct PreviousMeshInput {
36+
// The model transform.
37+
world_from_local: mat3x4<f32>,
38+
};
39+
3240
// The `wgpu` indirect parameters structure. This is a union of two structures.
3341
// For more information, see the corresponding comment in
3442
// `gpu_preprocessing.rs`.

0 commit comments

Comments
 (0)