Enhance GpuComponentArrayBuffer so that it can be used to store arbitrary data on mesh instances.#24922
Enhance GpuComponentArrayBuffer so that it can be used to store arbitrary data on mesh instances.#24922pcwalton wants to merge 1 commit into
GpuComponentArrayBuffer so that it can be used to store arbitrary data on mesh instances.#24922Conversation
arbitrary data on mesh instances. Currently, the only way to store arbitrary data that can be accessed in a shader on the mesh is to store it in the material. However, this has overhead, because it duplicates the material for each mesh instance, and updating that material can cause performance problems, as material updates go through the bind group allocator and `AsBindGroup` machinery. Recognizing this problem, other game engines have developed lightweight mechanisms to store per-instance data, such as Unity's [Material Property Blocks] and Godot's [instance uniforms]. As it turns out, Bevy has a feature called `GpuComponentArrayBuffer` which was added in 2023 and at a glance looks like it's supposed to be a solution to this problem. However, there are a number of problems with it: 1. `GpuComponentArrayBuffer` successfully gathers up the components and transfers them to the GPU, but it doesn't actually provide any mechanism to associate an instance ID with the data, making it difficult and/or impossible to use in practice unless there's only one object in the scene that uses the shader (and if there is only one such object, there's little reason to use `GpuComponentArrayBuffer` in the first place, since a UBO on the material suffices). 2. `GpuComponentArrayBuffer` requires that the component be `encase`-compatible, which essentially requires that the component be POD. There's no mechanism to convert a CPU-side representation of a component to a suitable GPU representation. 3. `GpuComponentArrayBuffer` extracts all components from scratch every frame. This isn't considered a best practice anymore in modern Bevy; modern Bevy rendering prefers to retain objects between frames and use fine-grained change detection. This PR fixes all of these issues: 1. The `GpuComponentArrayBuffer` now uses the `MeshTag` to associate each mesh instance with its extracted component. The `MeshTag` is per-instance data that's accessible in the shader, so it's easy for a shader to look up its associated data. 2. `GpuComponentArrayBuffer` is now a trait (following the established Rust naming convention, which prefers to avoid suffixes like *-able*). By implementing this trait for a component, the application can customize the mapping of the CPU-side component to its GPU-side representation. The application registers a `GpuComponentArrayBuffer` implementation by adding the `GpuComponentArrayBufferPlugin`, following the existing pattern established by `ExtractComponentPlugin` and so forth. 3. The `GpuComponentArrayBuffer` now caches data from frame to frame. It uses a `Changed` query filter to avoid re-extracting data that hasn't changed. The buffer remains tightly packed even when data is removed. (Thus tags aren't stable from frame to frame. As `GpuComponentArrayBuffer` is a general-purpose mechanism for the application to use, and I can't assume what the allocation and deallocation patterns are for a specific allocation, I figured avoiding fragmentation in the buffer was more important than maintaining stable tags.) The intended use case for `GpuComponentArrayBuffer` is to move frequently updated `SpriteMaterial` data from the material to an array buffer. Updating a material is a relatively expensive operation because it requires going through the asset re-extraction and bind group allocation process. Moreover, having to allocate a different material for every frame of a sprite's flipbook animation is inelegant; this would remain true even if we made updating materials much faster. So `GpuComponentArrayBuffer` is a desirable abstraction not only in terms of performance, but also conceptually. A new example, `gpu_component_array_buffer`, has been added. This example demonstrates how to associate arbitrary data with mesh instances and to access it in a shader. The custom material in that example can operate in bindless or non-bindless mode. The example spawns and despawns objects randomly in order to test that the buffer properly maintains the indices into the data buffer. [Material Property Blocks]: https://docs.unity3d.com/ScriptReference/MaterialPropertyBlock.html [instance uniforms]: https://godotengine.org/article/godot-40-gets-global-and-instance-shader-uniforms/
|
The generated |
beicause
left a comment
There was a problem hiding this comment.
Doesn't work on webgpu:
WGPU_SETTINGS_PRIO="webgpu" cargo r --example gpu_component_array_buffer
2026-07-09T07:05:23.376130Z ERROR naga::proc::typifier: Access sub-type Struct { members: [StructMember { name: Some("data_array"), ty: [42], binding: None, offset: 0 }], span: 16 }
2026-07-09T07:05:23.439397Z ERROR bevy_render::render_resource::pipeline_cache: failed to process shader error:
error: Invalid sub-access into type [43], indexed: false
= Invalid sub-access into type [43], indexed: false
Might be an issue with bindless, but theoretically this should be supported because it just needs storage buffers.
| /// An optional filter to apply to the query. | ||
| type QueryFilter: QueryFilter; | ||
| /// The packed GPU data. | ||
| type Out: Pod + ShaderType + Default; |
There was a problem hiding this comment.
| type Out: Pod + ShaderType + Default; | |
| type Out: NoUninit + AnyBitPattern; |
Relaxs the trait requirements.
| /// This is copied byte-by-byte to the GPU, not processed through | ||
| /// [`ShaderType`]. Consequently, we must insert all padding ourselves. We pad | ||
| /// the value out to 16 bytes, which is a good conservative practice. | ||
| #[derive(Clone, Copy, Default, ShaderType, Pod, Zeroable)] |
There was a problem hiding this comment.
| #[derive(Clone, Copy, Default, ShaderType, Pod, Zeroable)] | |
| #[derive(Clone, Copy, Pod, Zeroable)] |
| pub enum ShaderBufferData { | ||
| /// The buffer will be uninitialized when created and has the given size in | ||
| /// bytes. | ||
| Uninitialized(usize), |
There was a problem hiding this comment.
| Uninitialized(usize), | |
| Uninitialized(wgpu_types::BufferAddress), |
I think this should match the type of wgpu::BufferDescriptor::size
| pub fn resize_in_place(&mut self, new_size: u64) { | ||
| match self.data { | ||
| ShaderBufferData::Initialized(ref mut data) => { | ||
| data.resize(new_size as usize, 0); | ||
| } | ||
| ShaderBufferData::Uninitialized(ref mut size) => { | ||
| *size = new_size as usize; | ||
| self.copy_on_resize = true; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This is the same as resize and the behavior doesn't match the comment, and implicitly changing copy_on_resize is confusing.
Should we only keep one resize and make it not change copy_on_resize?
| created with `ShaderBufferData::Initialized`" | ||
| ); | ||
| }; | ||
| &bytemuck::cast_slice(data_buffer.as_slice())[tag as usize] |
There was a problem hiding this comment.
I think this is problematic. ShaderBuffer's data is Vec<u8> aligned to 1 byte, but C::Out may have larger alignment, thus this can fail.
Same for the other cast_slice/cast_slice_mut
There was a problem hiding this comment.
You can use https://docs.rs/bytemuck/latest/bytemuck/fn.pod_read_unaligned.html to read and copy_from_slice(bytes_of(&val)) to write, which don't require alignment.
There was a problem hiding this comment.
I included some changes of ShaderBuffer in #24938 and attempted to improve this.
There was a problem hiding this comment.
OK, let's wait for a decision on #24938 before landing my PR.
|
Blocked on #24938 |
changed. At the moment, whenever a material changes, we free and recreate the bind group in the material bind group allocator. This is necessary in general when one or more GPU resources (buffers, textures, samplers) change, because it's possible for changing one of those to cause a material to overflow the slab. However, when only POD (plain old data) changes, for example a color, this constitutes needless overhead. This patch improves the performance in this case by adding an early check and fast path to material asset preparation for bindless materials. With this PR, before we free and reallocate a material, we walk through the list of all GPU resources, checking to see if the new version of the material has the same bindings. If so, we keep the material as-is in the bind group allocator and only update the data buffers. Additionally, this PR removes the allocations in the `unprepared_bind_group` function by introducing an `BindGroupBuilder` abstraction that allows reusing allocations between frames. The `BindGroupBuilder::binding_resources` vector and the `BindGroupBuilder::data_buffer` are now temporary staging buffers that can be reused from bind group to bind group. Note that we currently still perform allocations for non-bindless materials. This can be improved, but because non-bindless materials upload their data later than bindless materials do, reusing the data buffers isn't as straightforward. Importantly, this PR opens the door to parallel material preparation in the future. This is because neither the fast path nor the check to make sure we can take the fast path mutate the `MaterialBindGroupAllocator`, outside of the data buffer itself. Therefore, in the future, we can migrate the material data buffer to `AtomicSparseBufferVec` and update it in parallel just as we do for mesh instance uniforms. Note however that parallel material preparation needs some more work in the assets infrastructure, and I suspect that work will collide with assets-as-entities, so I opted to leave that for a follow-up (and also to prevent this patch from growing too large). Future work could involve adding a special fast path for materials that are *only* POD to avoid most of the material bind group allocator machinery entirely. Note that, if updating shader-accessible instance data is performance-critical, applications may find [`GpuComponentArrayBuffer`] to be a better fit than the material system. On `many_cubes --vary-material-data-per-instance --animate-materials --instance-count 16000`, this PR improves the performance of `prepare_erased_assets` on `StandardMaterial` from 33.0 ms/frame to 21.7 ms/frame, a 1.51× speedup. [`GpuComponentArrayBuffer`]: bevyengine#24922
| data: Some(data.to_vec()), | ||
| pub fn new(data: Vec<u8>, asset_usage: RenderAssetUsages) -> Self { | ||
| ShaderBuffer { | ||
| data: ShaderBufferData::Initialized(data.to_vec()), |
There was a problem hiding this comment.
Is to_vec() still necessary since we're consuming data ?
| data: ShaderBufferData::Initialized(data.to_vec()), | |
| data: ShaderBufferData::Initialized(data), |
Currently, the only way to store arbitrary data that can be accessed in a shader on the mesh is to store it in the material. However, this has overhead, because it duplicates the material for each mesh instance, and updating that material can cause performance problems, as material updates go through the bind group allocator and
AsBindGroupmachinery. Recognizing this problem, other game engines have developed lightweight mechanisms to store per-instance data, such as Unity's Material Property Blocks and Godot's instance uniforms.As it turns out, Bevy has a feature called
GpuComponentArrayBufferwhich was added in 2023 and at a glance looks like it's supposed to be a solution to this problem. However, there are a number of problems with it:GpuComponentArrayBuffersuccessfully gathers up the components and transfers them to the GPU, but it doesn't actually provide any mechanism to associate an instance ID with the data, making it difficult and/or impossible to use in practice unless there's only one object in the scene that uses the shader (and if there is only one such object, there's little reason to useGpuComponentArrayBufferin the first place, since a UBO on the material suffices).GpuComponentArrayBufferrequires that the component beencase-compatible, which essentially requires that the component be POD. There's no mechanism to convert a CPU-side representation of a component to a suitable GPU representation.GpuComponentArrayBufferextracts all components from scratch every frame. This isn't considered a best practice anymore in modern Bevy; modern Bevy rendering prefers to retain objects between frames and use fine-grained change detection.This PR fixes all of these issues:
The
GpuComponentArrayBuffernow uses theMeshTagto associate each mesh instance with its extracted component. TheMeshTagis per-instance data that's accessible in the shader, so it's easy for a shader to look up its associated data.GpuComponentArrayBufferis now a trait (following the established Rust naming convention, which prefers to avoid suffixes like -able). By implementing this trait for a component, the application can customize the mapping of the CPU-side component to its GPU-side representation. The application registers aGpuComponentArrayBufferimplementation by adding theGpuComponentArrayBufferPlugin, following the existing pattern established byExtractComponentPluginand so forth.The
GpuComponentArrayBuffernow caches data from frame to frame. It uses aChangedquery filter to avoid re-extracting data that hasn't changed. The buffer remains tightly packed even when data is removed. (Thus tags aren't stable from frame to frame. AsGpuComponentArrayBufferis a general-purpose mechanism for the application to use, and I can't assume what the allocation and deallocation patterns are for a specific allocation, I figured avoiding fragmentation in the buffer was more important than maintaining stable tags.)The intended use case for
GpuComponentArrayBufferis to move frequently updatedSpriteMaterialdata from the material to an array buffer. Updating a material is a relatively expensive operation because it requires going through the asset re-extraction and bind group allocation process. Moreover, having to allocate a different material for every frame of a sprite's flipbook animation is inelegant; this would remain true even if we made updating materials much faster. SoGpuComponentArrayBufferis a desirable abstraction not only in terms of performance, but also conceptually.A new example,
gpu_component_array_buffer, has been added. This example demonstrates how to associate arbitrary data with mesh instances and to access it in a shader. The custom material in that example can operate in bindless or non-bindless mode. The example spawns and despawns objects randomly in order to test that the buffer properly maintains the indices into the data buffer.