Skip to content

Commit a464004

Browse files
Adds ShaderStorageBuffer asset (#14663)
Adds a new `Handle<Storage>` asset type that can be used as a render asset, particularly for use with `AsBindGroup`. Closes: #13658 # Objective Allow users to create storage buffers in the main world without having to access the `RenderDevice`. While this resource is technically available, it's bad form to use in the main world and requires mixing rendering details with main world code. Additionally, this makes storage buffers easier to use with `AsBindGroup`, particularly in the following scenarios: - Sharing the same buffers between a compute stage and material shader. We already have examples of this for storage textures (see game of life example) and these changes allow a similar pattern to be used with storage buffers. - Preventing repeated gpu upload (see the previous easier to use `Vec` `AsBindGroup` option). - Allow initializing custom materials using `Default`. Previously, the lack of a `Default` implement for the raw `wgpu::Buffer` type made implementing a `AsBindGroup + Default` bound difficult in the presence of buffers. ## Solution Adds a new `Handle<Storage>` asset type that is prepared into a `GpuStorageBuffer` render asset. This asset can either be initialized with a `Vec<u8>` of properly aligned data or with a size hint. Users can modify the underlying `wgpu::BufferDescriptor` to provide additional usage flags. ## Migration Guide The `AsBindGroup` `storage` attribute has been modified to reference the new `Handle<Storage>` asset instead. Usages of Vec` should be converted into assets instead. --------- Co-authored-by: IceSentry <[email protected]>
1 parent 3a8d559 commit a464004

File tree

8 files changed

+297
-29
lines changed

8 files changed

+297
-29
lines changed

Cargo.toml

+11
Original file line numberDiff line numberDiff line change
@@ -2417,6 +2417,17 @@ description = "A shader that shows how to bind and sample multiple textures as a
24172417
category = "Shaders"
24182418
wasm = false
24192419

2420+
[[example]]
2421+
name = "storage_buffer"
2422+
path = "examples/shader/storage_buffer.rs"
2423+
doc-scrape-examples = true
2424+
2425+
[package.metadata.example.storage_buffer]
2426+
name = "Storage Buffer"
2427+
description = "A shader that shows how to bind a storage buffer using a custom material."
2428+
category = "Shaders"
2429+
wasm = true
2430+
24202431
[[example]]
24212432
name = "specialized_mesh_pipeline"
24222433
path = "examples/shader/specialized_mesh_pipeline.rs"

assets/shaders/storage_buffer.wgsl

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#import bevy_pbr::{
2+
mesh_functions,
3+
view_transformations::position_world_to_clip
4+
}
5+
6+
@group(2) @binding(0) var<storage, read> colors: array<vec4<f32>, 5>;
7+
8+
struct Vertex {
9+
@builtin(instance_index) instance_index: u32,
10+
@location(0) position: vec3<f32>,
11+
};
12+
13+
struct VertexOutput {
14+
@builtin(position) clip_position: vec4<f32>,
15+
@location(0) world_position: vec4<f32>,
16+
@location(1) color: vec4<f32>,
17+
};
18+
19+
@vertex
20+
fn vertex(vertex: Vertex) -> VertexOutput {
21+
var out: VertexOutput;
22+
var world_from_local = mesh_functions::get_world_from_local(vertex.instance_index);
23+
out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0));
24+
out.clip_position = position_world_to_clip(out.world_position.xyz);
25+
26+
// We have 5 colors in the storage buffer, but potentially many instances of the mesh, so
27+
// we use the instance index to select a color from the storage buffer.
28+
out.color = colors[vertex.instance_index % 5];
29+
30+
return out;
31+
}
32+
33+
@fragment
34+
fn fragment(
35+
mesh: VertexOutput,
36+
) -> @location(0) vec4<f32> {
37+
return mesh.color;
38+
}

crates/bevy_render/macros/src/as_bind_group.rs

+12-24
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,6 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
212212
visibility.hygienic_quote(&quote! { #render_path::render_resource });
213213

214214
let field_name = field.ident.as_ref().unwrap();
215-
let field_ty = &field.ty;
216-
217-
let min_binding_size = if buffer {
218-
quote! {None}
219-
} else {
220-
quote! {Some(<#field_ty as #render_path::render_resource::ShaderType>::min_size())}
221-
};
222215

223216
if buffer {
224217
binding_impls.push(quote! {
@@ -230,21 +223,15 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
230223
)
231224
});
232225
} else {
233-
binding_impls.push(quote! {{
234-
use #render_path::render_resource::AsBindGroupShaderType;
235-
let mut buffer = #render_path::render_resource::encase::StorageBuffer::new(Vec::new());
236-
buffer.write(&self.#field_name).unwrap();
237-
(
238-
#binding_index,
239-
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
240-
&#render_path::render_resource::BufferInitDescriptor {
241-
label: None,
242-
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::STORAGE,
243-
contents: buffer.as_ref(),
244-
},
245-
))
246-
)
247-
}});
226+
binding_impls.push(quote! {
227+
(
228+
#binding_index,
229+
#render_path::render_resource::OwnedBindingResource::Buffer({
230+
let handle: &#asset_path::Handle<#render_path::storage::ShaderStorageBuffer> = (&self.#field_name);
231+
storage_buffers.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.buffer.clone()
232+
})
233+
)
234+
});
248235
}
249236

250237
binding_layouts.push(quote! {
@@ -254,7 +241,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
254241
ty: #render_path::render_resource::BindingType::Buffer {
255242
ty: #render_path::render_resource::BufferBindingType::Storage { read_only: #read_only },
256243
has_dynamic_offset: false,
257-
min_binding_size: #min_binding_size,
244+
min_binding_size: None,
258245
},
259246
count: None,
260247
}
@@ -527,6 +514,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
527514
type Param = (
528515
#ecs_path::system::lifetimeless::SRes<#render_path::render_asset::RenderAssets<#render_path::texture::GpuImage>>,
529516
#ecs_path::system::lifetimeless::SRes<#render_path::texture::FallbackImage>,
517+
#ecs_path::system::lifetimeless::SRes<#render_path::render_asset::RenderAssets<#render_path::storage::GpuShaderStorageBuffer>>,
530518
);
531519

532520
fn label() -> Option<&'static str> {
@@ -537,7 +525,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
537525
&self,
538526
layout: &#render_path::render_resource::BindGroupLayout,
539527
render_device: &#render_path::renderer::RenderDevice,
540-
(images, fallback_image): &mut #ecs_path::system::SystemParamItem<'_, '_, Self::Param>,
528+
(images, fallback_image, storage_buffers): &mut #ecs_path::system::SystemParamItem<'_, '_, Self::Param>,
541529
) -> Result<#render_path::render_resource::UnpreparedBindGroup<Self::Data>, #render_path::render_resource::AsBindGroupError> {
542530
let bindings = vec![#(#binding_impls,)*];
543531

crates/bevy_render/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub mod render_resource;
3535
pub mod renderer;
3636
pub mod settings;
3737
mod spatial_bundle;
38+
pub mod storage;
3839
pub mod texture;
3940
pub mod view;
4041
pub mod prelude {
@@ -75,6 +76,7 @@ use crate::{
7576
render_resource::{PipelineCache, Shader, ShaderLoader},
7677
renderer::{render_system, RenderInstance},
7778
settings::RenderCreation,
79+
storage::StoragePlugin,
7880
view::{ViewPlugin, WindowRenderPlugin},
7981
};
8082
use bevy_app::{App, AppLabel, Plugin, SubApp};
@@ -356,6 +358,7 @@ impl Plugin for RenderPlugin {
356358
GlobalsPlugin,
357359
MorphPlugin,
358360
BatchingPlugin,
361+
StoragePlugin,
359362
));
360363

361364
app.init_resource::<RenderAssetBytesPerFrame>()

crates/bevy_render/src/render_resource/bind_group.rs

+10-5
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ impl Deref for BindGroup {
7777
/// # use bevy_render::{render_resource::*, texture::Image};
7878
/// # use bevy_color::LinearRgba;
7979
/// # use bevy_asset::Handle;
80+
/// # use bevy_render::storage::ShaderStorageBuffer;
81+
///
8082
/// #[derive(AsBindGroup)]
8183
/// struct CoolMaterial {
8284
/// #[uniform(0)]
@@ -85,9 +87,9 @@ impl Deref for BindGroup {
8587
/// #[sampler(2)]
8688
/// color_texture: Handle<Image>,
8789
/// #[storage(3, read_only)]
88-
/// values: Vec<f32>,
90+
/// storage_buffer: Handle<ShaderStorageBuffer>,
8991
/// #[storage(4, read_only, buffer)]
90-
/// buffer: Buffer,
92+
/// raw_buffer: Buffer,
9193
/// #[storage_texture(5)]
9294
/// storage_texture: Handle<Image>,
9395
/// }
@@ -99,7 +101,8 @@ impl Deref for BindGroup {
99101
/// @group(2) @binding(0) var<uniform> color: vec4<f32>;
100102
/// @group(2) @binding(1) var color_texture: texture_2d<f32>;
101103
/// @group(2) @binding(2) var color_sampler: sampler;
102-
/// @group(2) @binding(3) var<storage> values: array<f32>;
104+
/// @group(2) @binding(3) var<storage> storage_buffer: array<f32>;
105+
/// @group(2) @binding(4) var<storage> raw_buffer: array<f32>;
103106
/// @group(2) @binding(5) var storage_texture: texture_storage_2d<rgba8unorm, read_write>;
104107
/// ```
105108
/// Note that the "group" index is determined by the usage context. It is not defined in [`AsBindGroup`]. For example, in Bevy material bind groups
@@ -151,15 +154,17 @@ impl Deref for BindGroup {
151154
/// |------------------------|-------------------------------------------------------------------------|------------------------|
152155
/// | `sampler_type` = "..." | `"filtering"`, `"non_filtering"`, `"comparison"`. | `"filtering"` |
153156
/// | `visibility(...)` | `all`, `none`, or a list-combination of `vertex`, `fragment`, `compute` | `vertex`, `fragment` |
154-
///
155157
/// * `storage(BINDING_INDEX, arguments)`
156-
/// * The field will be converted to a shader-compatible type using the [`ShaderType`] trait, written to a [`Buffer`], and bound as a storage buffer.
158+
/// * The field's [`Handle<Storage>`](bevy_asset::Handle) will be used to look up the matching [`Buffer`] GPU resource, which
159+
/// will be bound as a storage buffer in shaders. If the `storage` attribute is used, the field is expected a raw
160+
/// buffer, and the buffer will be bound as a storage buffer in shaders.
157161
/// * It supports and optional `read_only` parameter. Defaults to false if not present.
158162
///
159163
/// | Arguments | Values | Default |
160164
/// |------------------------|-------------------------------------------------------------------------|----------------------|
161165
/// | `visibility(...)` | `all`, `none`, or a list-combination of `vertex`, `fragment`, `compute` | `vertex`, `fragment` |
162166
/// | `read_only` | if present then value is true, otherwise false | `false` |
167+
/// | `buffer` | if present then the field will be assumed to be a raw wgpu buffer | |
163168
///
164169
/// Note that fields without field-level binding attributes will be ignored.
165170
/// ```

crates/bevy_render/src/storage.rs

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use crate::render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssetUsages};
2+
use crate::render_resource::{Buffer, BufferUsages};
3+
use crate::renderer::RenderDevice;
4+
use bevy_app::{App, Plugin};
5+
use bevy_asset::{Asset, AssetApp};
6+
use bevy_ecs::system::lifetimeless::SRes;
7+
use bevy_ecs::system::SystemParamItem;
8+
use bevy_reflect::prelude::ReflectDefault;
9+
use bevy_reflect::Reflect;
10+
use bevy_utils::default;
11+
use wgpu::util::BufferInitDescriptor;
12+
13+
/// Adds [`ShaderStorageBuffer`] as an asset that is extracted and uploaded to the GPU.
14+
#[derive(Default)]
15+
pub struct StoragePlugin;
16+
17+
impl Plugin for StoragePlugin {
18+
fn build(&self, app: &mut App) {
19+
app.add_plugins(RenderAssetPlugin::<GpuShaderStorageBuffer>::default())
20+
.register_type::<ShaderStorageBuffer>()
21+
.init_asset::<ShaderStorageBuffer>()
22+
.register_asset_reflect::<ShaderStorageBuffer>();
23+
}
24+
}
25+
26+
/// A storage buffer that is prepared as a [`RenderAsset`] and uploaded to the GPU.
27+
#[derive(Asset, Reflect, Debug, Clone)]
28+
#[reflect_value(Default)]
29+
pub struct ShaderStorageBuffer {
30+
/// Optional data used to initialize the buffer.
31+
pub data: Option<Vec<u8>>,
32+
/// The buffer description used to create the buffer.
33+
pub buffer_description: wgpu::BufferDescriptor<'static>,
34+
/// The asset usage of the storage buffer.
35+
pub asset_usage: RenderAssetUsages,
36+
}
37+
38+
impl Default for ShaderStorageBuffer {
39+
fn default() -> Self {
40+
Self {
41+
data: None,
42+
buffer_description: wgpu::BufferDescriptor {
43+
label: None,
44+
size: 0,
45+
usage: BufferUsages::STORAGE,
46+
mapped_at_creation: false,
47+
},
48+
asset_usage: RenderAssetUsages::default(),
49+
}
50+
}
51+
}
52+
53+
impl ShaderStorageBuffer {
54+
/// Creates a new storage buffer with the given data and asset usage.
55+
pub fn new(data: &[u8], asset_usage: RenderAssetUsages) -> Self {
56+
let mut storage = ShaderStorageBuffer {
57+
data: Some(data.to_vec()),
58+
..default()
59+
};
60+
storage.asset_usage = asset_usage;
61+
storage
62+
}
63+
64+
/// Creates a new storage buffer with the given size and asset usage.
65+
pub fn with_size(size: usize, asset_usage: RenderAssetUsages) -> Self {
66+
let mut storage = ShaderStorageBuffer {
67+
data: None,
68+
..default()
69+
};
70+
storage.buffer_description.size = size as u64;
71+
storage.buffer_description.mapped_at_creation = false;
72+
storage.asset_usage = asset_usage;
73+
storage
74+
}
75+
}
76+
77+
/// A storage buffer that is prepared as a [`RenderAsset`] and uploaded to the GPU.
78+
pub struct GpuShaderStorageBuffer {
79+
pub buffer: Buffer,
80+
}
81+
82+
impl RenderAsset for GpuShaderStorageBuffer {
83+
type SourceAsset = ShaderStorageBuffer;
84+
type Param = SRes<RenderDevice>;
85+
86+
fn asset_usage(source_asset: &Self::SourceAsset) -> RenderAssetUsages {
87+
source_asset.asset_usage
88+
}
89+
90+
fn prepare_asset(
91+
source_asset: Self::SourceAsset,
92+
render_device: &mut SystemParamItem<Self::Param>,
93+
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
94+
match source_asset.data {
95+
Some(data) => {
96+
let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
97+
label: source_asset.buffer_description.label,
98+
contents: &data,
99+
usage: source_asset.buffer_description.usage,
100+
});
101+
Ok(GpuShaderStorageBuffer { buffer })
102+
}
103+
None => {
104+
let buffer = render_device.create_buffer(&source_asset.buffer_description);
105+
Ok(GpuShaderStorageBuffer { buffer })
106+
}
107+
}
108+
}
109+
}

examples/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ Example | Description
404404
[Post Processing - Custom Render Pass](../examples/shader/custom_post_processing.rs) | A custom post processing effect, using a custom render pass that runs after the main pass
405405
[Shader Defs](../examples/shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader)
406406
[Specialized Mesh Pipeline](../examples/shader/specialized_mesh_pipeline.rs) | Demonstrates how to write a specialized mesh pipeline
407+
[Storage Buffer](../examples/shader/storage_buffer.rs) | A shader that shows how to bind a storage buffer using a custom material.
407408
[Texture Binding Array (Bindless Textures)](../examples/shader/texture_binding_array.rs) | A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures).
408409

409410
## State

0 commit comments

Comments
 (0)