From de3ec47f3f2b291d8c22018dcab8dd18fa900440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Mon, 8 Apr 2024 19:19:07 +0200 Subject: [PATCH] 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 --- assets/shaders/game_of_life.wgsl | 22 ++-- .../shader/compute_shader_game_of_life.rs | 110 ++++++++++++------ 2 files changed, 91 insertions(+), 41 deletions(-) diff --git a/assets/shaders/game_of_life.wgsl b/assets/shaders/game_of_life.wgsl index d47b618a0c..c5b94533e5 100644 --- a/assets/shaders/game_of_life.wgsl +++ b/assets/shaders/game_of_life.wgsl @@ -1,4 +1,12 @@ -@group(0) @binding(0) var texture: texture_storage_2d; +// 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; + +@group(0) @binding(1) var output: texture_storage_2d; fn hash(value: u32) -> u32 { var state = value; @@ -19,15 +27,15 @@ fn randomFloat(value: u32) -> f32 { fn init(@builtin(global_invocation_id) invocation_id: vec3, @builtin(num_workgroups) num_workgroups: vec3) { let location = vec2(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 color = vec4(f32(alive)); - textureStore(texture, location, color); + textureStore(output, location, color); } fn is_alive(location: vec2, offset_x: i32, offset_y: i32) -> i32 { - let value: vec4 = textureLoad(texture, location + vec2(offset_x, offset_y)); + let value: vec4 = textureLoad(input, location + vec2(offset_x, offset_y)); return i32(value.x); } @@ -59,7 +67,5 @@ fn update(@builtin(global_invocation_id) invocation_id: vec3) { } let color = vec4(f32(alive)); - storageBarrier(); - - textureStore(texture, location, color); -} \ No newline at end of file + textureStore(output, location, color); +} diff --git a/examples/shader/compute_shader_game_of_life.rs b/examples/shader/compute_shader_game_of_life.rs index 43f8a0b448..a26bb6c308 100644 --- a/examples/shader/compute_shader_game_of_life.rs +++ b/examples/shader/compute_shader_game_of_life.rs @@ -7,34 +7,42 @@ use bevy::{ prelude::*, render::{ extract_resource::{ExtractResource, ExtractResourcePlugin}, - render_asset::RenderAssetUsages, - render_asset::RenderAssets, + render_asset::{RenderAssetUsages, RenderAssets}, render_graph::{self, RenderGraph, RenderLabel}, - render_resource::*, + render_resource::{binding_types::texture_storage_2d, *}, renderer::{RenderContext, RenderDevice}, Render, RenderApp, RenderSet, }, }; 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; fn main() { App::new() .insert_resource(ClearColor(Color::BLACK)) .add_plugins(( - DefaultPlugins.set(WindowPlugin { - primary_window: Some(Window { - // uncomment for unthrottled FPS - // present_mode: bevy::window::PresentMode::AutoNoVsync, + DefaultPlugins + .set(WindowPlugin { + primary_window: Some(Window { + 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() - }), + }) + .set(ImagePlugin::default_nearest()), GameOfLifeComputePlugin, )) .add_systems(Startup, setup) + .add_systems(Update, switch_textures) .run(); } @@ -52,19 +60,34 @@ fn setup(mut commands: Commands, mut images: ResMut>) { ); image.texture_descriptor.usage = 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 { sprite: Sprite { custom_size: Some(Vec2::new(SIZE.0 as f32, SIZE.1 as f32)), ..default() }, - texture: image.clone(), + texture: image0.clone(), + transform: Transform::from_scale(Vec3::splat(DISPLAY_FACTOR as f32)), ..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, mut displayed: Query<&mut Handle>) { + 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; @@ -76,7 +99,7 @@ impl Plugin for GameOfLifeComputePlugin { fn build(&self, app: &mut App) { // 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. - app.add_plugins(ExtractResourcePlugin::::default()); + app.add_plugins(ExtractResourcePlugin::::default()); let render_app = app.sub_app_mut(RenderApp); render_app.add_systems( Render, @@ -94,29 +117,35 @@ impl Plugin for GameOfLifeComputePlugin { } } -#[derive(Resource, Clone, Deref, ExtractResource, AsBindGroup)] -struct GameOfLifeImage { - #[storage_texture(0, image_format = R32Float, access = ReadWrite)] - texture: Handle, +#[derive(Resource, Clone, ExtractResource)] +struct GameOfLifeImages { + texture_a: Handle, + texture_b: Handle, } #[derive(Resource)] -struct GameOfLifeImageBindGroup(BindGroup); +struct GameOfLifeImageBindGroups([BindGroup; 2]); fn prepare_bind_group( mut commands: Commands, pipeline: Res, gpu_images: Res>, - game_of_life_image: Res, + game_of_life_images: Res, render_device: Res, ) { - let view = gpu_images.get(&game_of_life_image.texture).unwrap(); - let bind_group = render_device.create_bind_group( + let view_a = gpu_images.get(&game_of_life_images.texture_a).unwrap(); + let view_b = gpu_images.get(&game_of_life_images.texture_b).unwrap(); + let bind_group_0 = render_device.create_bind_group( None, &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)] @@ -129,7 +158,16 @@ struct GameOfLifePipeline { impl FromWorld for GameOfLifePipeline { fn from_world(world: &mut World) -> Self { let render_device = world.resource::(); - 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 pipeline_cache = world.resource::(); let init_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { @@ -160,7 +198,7 @@ impl FromWorld for GameOfLifePipeline { enum GameOfLifeState { Loading, Init, - Update, + Update(usize), } struct GameOfLifeNode { @@ -193,10 +231,16 @@ impl render_graph::Node for GameOfLifeNode { if let CachedPipelineState::Ok(_) = 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, world: &World, ) -> Result<(), render_graph::NodeRunError> { - let texture_bind_group = &world.resource::().0; + let bind_groups = &world.resource::().0; let pipeline_cache = world.resource::(); let pipeline = world.resource::(); @@ -214,8 +258,6 @@ impl render_graph::Node for GameOfLifeNode { .command_encoder() .begin_compute_pass(&ComputePassDescriptor::default()); - pass.set_bind_group(0, texture_bind_group, &[]); - // select the pipeline based on the current state match self.state { GameOfLifeState::Loading => {} @@ -223,13 +265,15 @@ impl render_graph::Node for GameOfLifeNode { let init_pipeline = pipeline_cache .get_compute_pipeline(pipeline.init_pipeline) .unwrap(); + pass.set_bind_group(0, &bind_groups[0], &[]); pass.set_pipeline(init_pipeline); pass.dispatch_workgroups(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1); } - GameOfLifeState::Update => { + GameOfLifeState::Update(index) => { let update_pipeline = pipeline_cache .get_compute_pipeline(pipeline.update_pipeline) .unwrap(); + pass.set_bind_group(0, &bind_groups[index], &[]); pass.set_pipeline(update_pipeline); pass.dispatch_workgroups(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1); }