Skip to content

Commit 5bc5a13

Browse files
dontgetfoundoutdontgetfoundout
andauthored
Update Game of Life compute example to include a uniform buffer variable (#20466)
# Objective It is currently a little unclear how to use uniform buffers in compute shaders. The other examples of uniform buffers in the Bevy examples and codebase either are built on Materials or use `DynamicUniformBuffer`s created from a `ViewNode`. Neither of these are a great fit for use in a compute shader. ## Solution Update the compute shader example to pass a uniform buffer to the shader that determines the color for alive cells. ## Discussion Topics - Is this the right way to pass this data to the shader? - Should we be encouraging use of uniform buffers in compute shaders at all? Some in the community prefer the ergonomics of storage buffers in most (all?) compute shader cases. Do we want to push users to use storage buffers instead? - I took the idea to use color as the input from IceSentry on Discord, but this did require me to change the texture format to support non-red colors. Does this undermine the goals of the shader example? Is this the wrong texture format? ## Testing - Did you test these changes? If so, how? - The changes were manually validated with a number of different `LinearRgba` values for `alive_color` - Are there any parts that need more testing? - How can other people (reviewers) test your changes? Is there anything specific they need to know? - ` cargo run --example compute_shader_game_of_life` - Color can be set using `alive_color` property on `GameOfLifeUniforms` - If relevant, what platforms did you test these changes on, and are there any important ones you can't test? - Manually validated on Windows and WASM (WebGPU) targets - WASM WebGL2 doesn't appear to support textures in compute shaders --- ## Showcase <img width="1602" height="939" alt="image" src="https://github.com/user-attachments/assets/9a535617-a179-4f20-b686-596899f11d18" /> --------- Co-authored-by: dontgetfoundout <[email protected]>
1 parent d8df506 commit 5bc5a13

File tree

2 files changed

+52
-13
lines changed

2 files changed

+52
-13
lines changed

assets/shaders/game_of_life.wgsl

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@
44
// Two textures are needed for the game of life as each pixel of step N depends on the state of its
55
// neighbors at step N-1.
66

7-
@group(0) @binding(0) var input: texture_storage_2d<r32float, read>;
7+
@group(0) @binding(0) var input: texture_storage_2d<rgba32float, read>;
88

9-
@group(0) @binding(1) var output: texture_storage_2d<r32float, write>;
9+
@group(0) @binding(1) var output: texture_storage_2d<rgba32float, write>;
10+
11+
@group(0) @binding(2) var<uniform> config: GameOfLifeUniforms;
12+
13+
struct GameOfLifeUniforms {
14+
alive_color: vec4<f32>,
15+
}
1016

1117
fn hash(value: u32) -> u32 {
1218
var state = value;
@@ -29,14 +35,15 @@ fn init(@builtin(global_invocation_id) invocation_id: vec3<u32>, @builtin(num_wo
2935

3036
let randomNumber = randomFloat((invocation_id.y << 16u) | invocation_id.x);
3137
let alive = randomNumber > 0.9;
32-
let color = vec4<f32>(f32(alive));
38+
// Use alpha channel to keep track of cell's state
39+
let color = vec4(config.alive_color.rgb, f32(alive));
3340

3441
textureStore(output, location, color);
3542
}
3643

3744
fn is_alive(location: vec2<i32>, offset_x: i32, offset_y: i32) -> i32 {
3845
let value: vec4<f32> = textureLoad(input, location + vec2<i32>(offset_x, offset_y));
39-
return i32(value.x);
46+
return i32(value.a);
4047
}
4148

4249
fn count_alive(location: vec2<i32>) -> i32 {
@@ -65,7 +72,7 @@ fn update(@builtin(global_invocation_id) invocation_id: vec3<u32>) {
6572
} else {
6673
alive = false;
6774
}
68-
let color = vec4<f32>(f32(alive));
75+
let color = vec4(config.alive_color.rgb, f32(alive));
6976

7077
textureStore(output, location, color);
7178
}

examples/shader/compute_shader_game_of_life.rs

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ use bevy::{
1010
extract_resource::{ExtractResource, ExtractResourcePlugin},
1111
render_asset::RenderAssets,
1212
render_graph::{self, RenderGraph, RenderLabel},
13-
render_resource::{binding_types::texture_storage_2d, *},
14-
renderer::{RenderContext, RenderDevice},
13+
render_resource::{
14+
binding_types::{texture_storage_2d, uniform_buffer},
15+
*,
16+
},
17+
renderer::{RenderContext, RenderDevice, RenderQueue},
1518
texture::GpuImage,
1619
Render, RenderApp, RenderStartup, RenderSystems,
1720
},
@@ -53,7 +56,7 @@ fn main() {
5356
}
5457

5558
fn setup(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
56-
let mut image = Image::new_target_texture(SIZE.0, SIZE.1, TextureFormat::R32Float);
59+
let mut image = Image::new_target_texture(SIZE.0, SIZE.1, TextureFormat::Rgba32Float);
5760
image.asset_usage = RenderAssetUsages::RENDER_WORLD;
5861
image.texture_descriptor.usage =
5962
TextureUsages::COPY_DST | TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING;
@@ -74,6 +77,10 @@ fn setup(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
7477
texture_a: image0,
7578
texture_b: image1,
7679
});
80+
81+
commands.insert_resource(GameOfLifeUniforms {
82+
alive_color: LinearRgba::RED,
83+
});
7784
}
7885

7986
// Switch texture to display every frame to show the one that was written to most recently.
@@ -94,7 +101,10 @@ impl Plugin for GameOfLifeComputePlugin {
94101
fn build(&self, app: &mut App) {
95102
// Extract the game of life image resource from the main world into the render world
96103
// for operation on by the compute shader and display on the sprite.
97-
app.add_plugins(ExtractResourcePlugin::<GameOfLifeImages>::default());
104+
app.add_plugins((
105+
ExtractResourcePlugin::<GameOfLifeImages>::default(),
106+
ExtractResourcePlugin::<GameOfLifeUniforms>::default(),
107+
));
98108
let render_app = app.sub_app_mut(RenderApp);
99109
render_app
100110
.add_systems(RenderStartup, init_game_of_life_pipeline)
@@ -115,6 +125,11 @@ struct GameOfLifeImages {
115125
texture_b: Handle<Image>,
116126
}
117127

128+
#[derive(Resource, Clone, ExtractResource, ShaderType)]
129+
struct GameOfLifeUniforms {
130+
alive_color: LinearRgba,
131+
}
132+
118133
#[derive(Resource)]
119134
struct GameOfLifeImageBindGroups([BindGroup; 2]);
120135

@@ -123,19 +138,35 @@ fn prepare_bind_group(
123138
pipeline: Res<GameOfLifePipeline>,
124139
gpu_images: Res<RenderAssets<GpuImage>>,
125140
game_of_life_images: Res<GameOfLifeImages>,
141+
game_of_life_uniforms: Res<GameOfLifeUniforms>,
126142
render_device: Res<RenderDevice>,
143+
queue: Res<RenderQueue>,
127144
) {
128145
let view_a = gpu_images.get(&game_of_life_images.texture_a).unwrap();
129146
let view_b = gpu_images.get(&game_of_life_images.texture_b).unwrap();
147+
148+
// Uniform buffer is used here to demonstrate how to set up a uniform in a compute shader
149+
// Alternatives such as storage buffers or push constants may be more suitable for your use case
150+
let mut uniform_buffer = UniformBuffer::from(game_of_life_uniforms.into_inner());
151+
uniform_buffer.write_buffer(&render_device, &queue);
152+
130153
let bind_group_0 = render_device.create_bind_group(
131154
None,
132155
&pipeline.texture_bind_group_layout,
133-
&BindGroupEntries::sequential((&view_a.texture_view, &view_b.texture_view)),
156+
&BindGroupEntries::sequential((
157+
&view_a.texture_view,
158+
&view_b.texture_view,
159+
&uniform_buffer,
160+
)),
134161
);
135162
let bind_group_1 = render_device.create_bind_group(
136163
None,
137164
&pipeline.texture_bind_group_layout,
138-
&BindGroupEntries::sequential((&view_b.texture_view, &view_a.texture_view)),
165+
&BindGroupEntries::sequential((
166+
&view_b.texture_view,
167+
&view_a.texture_view,
168+
&uniform_buffer,
169+
)),
139170
);
140171
commands.insert_resource(GameOfLifeImageBindGroups([bind_group_0, bind_group_1]));
141172
}
@@ -158,8 +189,9 @@ fn init_game_of_life_pipeline(
158189
&BindGroupLayoutEntries::sequential(
159190
ShaderStages::COMPUTE,
160191
(
161-
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::ReadOnly),
162-
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
192+
texture_storage_2d(TextureFormat::Rgba32Float, StorageTextureAccess::ReadOnly),
193+
texture_storage_2d(TextureFormat::Rgba32Float, StorageTextureAccess::WriteOnly),
194+
uniform_buffer::<GameOfLifeUniforms>(false),
163195
),
164196
),
165197
);

0 commit comments

Comments
 (0)