Fix example game of life (#12897)

# Objective

- Example `compute_shader_game_of_life` is random and not following the
rules of the game of life: at each steps, it randomly reads some pixel
of the current step and some of the previous step instead of only from
the previous step
- Fixes #9353 

## Solution

- Adopted from #9678 
- Added a switch of the texture displayed every frame otherwise the game
of life looks wrong
- Added a way to display the texture bigger so that I could manually
check everything was right

---------

Co-authored-by: Sludge <96552222+SludgePhD@users.noreply.github.com>
Co-authored-by: IceSentry <IceSentry@users.noreply.github.com>
This commit is contained in:
François Mockers 2024-04-08 19:19:07 +02:00 committed by GitHub
parent b9a232966b
commit de3ec47f3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 91 additions and 41 deletions

View file

@ -1,4 +1,12 @@
@group(0) @binding(0) var texture: texture_storage_2d<r32float, read_write>; // The shader reads the previous frame's state from the `input` texture, and writes the new state of
// each pixel to the `output` texture. The textures are flipped each step to progress the
// simulation.
// Two textures are needed for the game of life as each pixel of step N depends on the state of its
// neighbors at step N-1.
@group(0) @binding(0) var input: texture_storage_2d<r32float, read>;
@group(0) @binding(1) var output: texture_storage_2d<r32float, write>;
fn hash(value: u32) -> u32 { fn hash(value: u32) -> u32 {
var state = value; var state = value;
@ -19,15 +27,15 @@ fn randomFloat(value: u32) -> f32 {
fn init(@builtin(global_invocation_id) invocation_id: vec3<u32>, @builtin(num_workgroups) num_workgroups: vec3<u32>) { fn init(@builtin(global_invocation_id) invocation_id: vec3<u32>, @builtin(num_workgroups) num_workgroups: vec3<u32>) {
let location = vec2<i32>(i32(invocation_id.x), i32(invocation_id.y)); let location = vec2<i32>(i32(invocation_id.x), i32(invocation_id.y));
let randomNumber = randomFloat(invocation_id.y * num_workgroups.x + invocation_id.x); let randomNumber = randomFloat(invocation_id.y << 16u | invocation_id.x);
let alive = randomNumber > 0.9; let alive = randomNumber > 0.9;
let color = vec4<f32>(f32(alive)); let color = vec4<f32>(f32(alive));
textureStore(texture, location, color); textureStore(output, location, color);
} }
fn is_alive(location: vec2<i32>, offset_x: i32, offset_y: i32) -> i32 { fn is_alive(location: vec2<i32>, offset_x: i32, offset_y: i32) -> i32 {
let value: vec4<f32> = textureLoad(texture, location + vec2<i32>(offset_x, offset_y)); let value: vec4<f32> = textureLoad(input, location + vec2<i32>(offset_x, offset_y));
return i32(value.x); return i32(value.x);
} }
@ -59,7 +67,5 @@ fn update(@builtin(global_invocation_id) invocation_id: vec3<u32>) {
} }
let color = vec4<f32>(f32(alive)); let color = vec4<f32>(f32(alive));
storageBarrier(); textureStore(output, location, color);
}
textureStore(texture, location, color);
}

View file

@ -7,34 +7,42 @@ use bevy::{
prelude::*, prelude::*,
render::{ render::{
extract_resource::{ExtractResource, ExtractResourcePlugin}, extract_resource::{ExtractResource, ExtractResourcePlugin},
render_asset::RenderAssetUsages, render_asset::{RenderAssetUsages, RenderAssets},
render_asset::RenderAssets,
render_graph::{self, RenderGraph, RenderLabel}, render_graph::{self, RenderGraph, RenderLabel},
render_resource::*, render_resource::{binding_types::texture_storage_2d, *},
renderer::{RenderContext, RenderDevice}, renderer::{RenderContext, RenderDevice},
Render, RenderApp, RenderSet, Render, RenderApp, RenderSet,
}, },
}; };
use std::borrow::Cow; use std::borrow::Cow;
const SIZE: (u32, u32) = (1280, 720); const DISPLAY_FACTOR: u32 = 4;
const SIZE: (u32, u32) = (1280 / DISPLAY_FACTOR, 720 / DISPLAY_FACTOR);
const WORKGROUP_SIZE: u32 = 8; const WORKGROUP_SIZE: u32 = 8;
fn main() { fn main() {
App::new() App::new()
.insert_resource(ClearColor(Color::BLACK)) .insert_resource(ClearColor(Color::BLACK))
.add_plugins(( .add_plugins((
DefaultPlugins.set(WindowPlugin { DefaultPlugins
primary_window: Some(Window { .set(WindowPlugin {
// uncomment for unthrottled FPS primary_window: Some(Window {
// present_mode: bevy::window::PresentMode::AutoNoVsync, resolution: (
(SIZE.0 * DISPLAY_FACTOR) as f32,
(SIZE.1 * DISPLAY_FACTOR) as f32,
)
.into(),
// uncomment for unthrottled FPS
// present_mode: bevy::window::PresentMode::AutoNoVsync,
..default()
}),
..default() ..default()
}), })
..default() .set(ImagePlugin::default_nearest()),
}),
GameOfLifeComputePlugin, GameOfLifeComputePlugin,
)) ))
.add_systems(Startup, setup) .add_systems(Startup, setup)
.add_systems(Update, switch_textures)
.run(); .run();
} }
@ -52,19 +60,34 @@ fn setup(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
); );
image.texture_descriptor.usage = image.texture_descriptor.usage =
TextureUsages::COPY_DST | TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING; TextureUsages::COPY_DST | TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING;
let image = images.add(image); let image0 = images.add(image.clone());
let image1 = images.add(image);
commands.spawn(SpriteBundle { commands.spawn(SpriteBundle {
sprite: Sprite { sprite: Sprite {
custom_size: Some(Vec2::new(SIZE.0 as f32, SIZE.1 as f32)), custom_size: Some(Vec2::new(SIZE.0 as f32, SIZE.1 as f32)),
..default() ..default()
}, },
texture: image.clone(), texture: image0.clone(),
transform: Transform::from_scale(Vec3::splat(DISPLAY_FACTOR as f32)),
..default() ..default()
}); });
commands.spawn(Camera2dBundle::default()); commands.spawn(Camera2dBundle::default());
commands.insert_resource(GameOfLifeImage { texture: image }); commands.insert_resource(GameOfLifeImages {
texture_a: image0,
texture_b: image1,
});
}
// Switch texture to display every frame to show the one that was written to most recently.
fn switch_textures(images: Res<GameOfLifeImages>, mut displayed: Query<&mut Handle<Image>>) {
let mut displayed = displayed.single_mut();
if *displayed == images.texture_a {
*displayed = images.texture_b.clone_weak();
} else {
*displayed = images.texture_a.clone_weak();
}
} }
struct GameOfLifeComputePlugin; struct GameOfLifeComputePlugin;
@ -76,7 +99,7 @@ impl Plugin for GameOfLifeComputePlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
// Extract the game of life image resource from the main world into the render world // Extract the game of life image resource from the main world into the render world
// for operation on by the compute shader and display on the sprite. // for operation on by the compute shader and display on the sprite.
app.add_plugins(ExtractResourcePlugin::<GameOfLifeImage>::default()); app.add_plugins(ExtractResourcePlugin::<GameOfLifeImages>::default());
let render_app = app.sub_app_mut(RenderApp); let render_app = app.sub_app_mut(RenderApp);
render_app.add_systems( render_app.add_systems(
Render, Render,
@ -94,29 +117,35 @@ impl Plugin for GameOfLifeComputePlugin {
} }
} }
#[derive(Resource, Clone, Deref, ExtractResource, AsBindGroup)] #[derive(Resource, Clone, ExtractResource)]
struct GameOfLifeImage { struct GameOfLifeImages {
#[storage_texture(0, image_format = R32Float, access = ReadWrite)] texture_a: Handle<Image>,
texture: Handle<Image>, texture_b: Handle<Image>,
} }
#[derive(Resource)] #[derive(Resource)]
struct GameOfLifeImageBindGroup(BindGroup); struct GameOfLifeImageBindGroups([BindGroup; 2]);
fn prepare_bind_group( fn prepare_bind_group(
mut commands: Commands, mut commands: Commands,
pipeline: Res<GameOfLifePipeline>, pipeline: Res<GameOfLifePipeline>,
gpu_images: Res<RenderAssets<Image>>, gpu_images: Res<RenderAssets<Image>>,
game_of_life_image: Res<GameOfLifeImage>, game_of_life_images: Res<GameOfLifeImages>,
render_device: Res<RenderDevice>, render_device: Res<RenderDevice>,
) { ) {
let view = gpu_images.get(&game_of_life_image.texture).unwrap(); let view_a = gpu_images.get(&game_of_life_images.texture_a).unwrap();
let bind_group = render_device.create_bind_group( let view_b = gpu_images.get(&game_of_life_images.texture_b).unwrap();
let bind_group_0 = render_device.create_bind_group(
None, None,
&pipeline.texture_bind_group_layout, &pipeline.texture_bind_group_layout,
&BindGroupEntries::single(&view.texture_view), &BindGroupEntries::sequential((&view_a.texture_view, &view_b.texture_view)),
); );
commands.insert_resource(GameOfLifeImageBindGroup(bind_group)); let bind_group_1 = render_device.create_bind_group(
None,
&pipeline.texture_bind_group_layout,
&BindGroupEntries::sequential((&view_b.texture_view, &view_a.texture_view)),
);
commands.insert_resource(GameOfLifeImageBindGroups([bind_group_0, bind_group_1]));
} }
#[derive(Resource)] #[derive(Resource)]
@ -129,7 +158,16 @@ struct GameOfLifePipeline {
impl FromWorld for GameOfLifePipeline { impl FromWorld for GameOfLifePipeline {
fn from_world(world: &mut World) -> Self { fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>(); let render_device = world.resource::<RenderDevice>();
let texture_bind_group_layout = GameOfLifeImage::bind_group_layout(render_device); let texture_bind_group_layout = render_device.create_bind_group_layout(
"GameOfLifeImages",
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::ReadOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
),
),
);
let shader = world.load_asset("shaders/game_of_life.wgsl"); let shader = world.load_asset("shaders/game_of_life.wgsl");
let pipeline_cache = world.resource::<PipelineCache>(); let pipeline_cache = world.resource::<PipelineCache>();
let init_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { let init_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
@ -160,7 +198,7 @@ impl FromWorld for GameOfLifePipeline {
enum GameOfLifeState { enum GameOfLifeState {
Loading, Loading,
Init, Init,
Update, Update(usize),
} }
struct GameOfLifeNode { struct GameOfLifeNode {
@ -193,10 +231,16 @@ impl render_graph::Node for GameOfLifeNode {
if let CachedPipelineState::Ok(_) = if let CachedPipelineState::Ok(_) =
pipeline_cache.get_compute_pipeline_state(pipeline.update_pipeline) pipeline_cache.get_compute_pipeline_state(pipeline.update_pipeline)
{ {
self.state = GameOfLifeState::Update; self.state = GameOfLifeState::Update(1);
} }
} }
GameOfLifeState::Update => {} GameOfLifeState::Update(0) => {
self.state = GameOfLifeState::Update(1);
}
GameOfLifeState::Update(1) => {
self.state = GameOfLifeState::Update(0);
}
GameOfLifeState::Update(_) => unreachable!(),
} }
} }
@ -206,7 +250,7 @@ impl render_graph::Node for GameOfLifeNode {
render_context: &mut RenderContext, render_context: &mut RenderContext,
world: &World, world: &World,
) -> Result<(), render_graph::NodeRunError> { ) -> Result<(), render_graph::NodeRunError> {
let texture_bind_group = &world.resource::<GameOfLifeImageBindGroup>().0; let bind_groups = &world.resource::<GameOfLifeImageBindGroups>().0;
let pipeline_cache = world.resource::<PipelineCache>(); let pipeline_cache = world.resource::<PipelineCache>();
let pipeline = world.resource::<GameOfLifePipeline>(); let pipeline = world.resource::<GameOfLifePipeline>();
@ -214,8 +258,6 @@ impl render_graph::Node for GameOfLifeNode {
.command_encoder() .command_encoder()
.begin_compute_pass(&ComputePassDescriptor::default()); .begin_compute_pass(&ComputePassDescriptor::default());
pass.set_bind_group(0, texture_bind_group, &[]);
// select the pipeline based on the current state // select the pipeline based on the current state
match self.state { match self.state {
GameOfLifeState::Loading => {} GameOfLifeState::Loading => {}
@ -223,13 +265,15 @@ impl render_graph::Node for GameOfLifeNode {
let init_pipeline = pipeline_cache let init_pipeline = pipeline_cache
.get_compute_pipeline(pipeline.init_pipeline) .get_compute_pipeline(pipeline.init_pipeline)
.unwrap(); .unwrap();
pass.set_bind_group(0, &bind_groups[0], &[]);
pass.set_pipeline(init_pipeline); pass.set_pipeline(init_pipeline);
pass.dispatch_workgroups(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1); pass.dispatch_workgroups(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1);
} }
GameOfLifeState::Update => { GameOfLifeState::Update(index) => {
let update_pipeline = pipeline_cache let update_pipeline = pipeline_cache
.get_compute_pipeline(pipeline.update_pipeline) .get_compute_pipeline(pipeline.update_pipeline)
.unwrap(); .unwrap();
pass.set_bind_group(0, &bind_groups[index], &[]);
pass.set_pipeline(update_pipeline); pass.set_pipeline(update_pipeline);
pass.dispatch_workgroups(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1); pass.dispatch_workgroups(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1);
} }