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 {
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>) {
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 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 {
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);
}
@ -59,7 +67,5 @@ fn update(@builtin(global_invocation_id) invocation_id: vec3<u32>) {
}
let color = vec4<f32>(f32(alive));
storageBarrier();
textureStore(texture, location, color);
}
textureStore(output, location, color);
}

View file

@ -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<Assets<Image>>) {
);
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<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;
@ -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::<GameOfLifeImage>::default());
app.add_plugins(ExtractResourcePlugin::<GameOfLifeImages>::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<Image>,
#[derive(Resource, Clone, ExtractResource)]
struct GameOfLifeImages {
texture_a: Handle<Image>,
texture_b: Handle<Image>,
}
#[derive(Resource)]
struct GameOfLifeImageBindGroup(BindGroup);
struct GameOfLifeImageBindGroups([BindGroup; 2]);
fn prepare_bind_group(
mut commands: Commands,
pipeline: Res<GameOfLifePipeline>,
gpu_images: Res<RenderAssets<Image>>,
game_of_life_image: Res<GameOfLifeImage>,
game_of_life_images: Res<GameOfLifeImages>,
render_device: Res<RenderDevice>,
) {
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::<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 pipeline_cache = world.resource::<PipelineCache>();
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::<GameOfLifeImageBindGroup>().0;
let bind_groups = &world.resource::<GameOfLifeImageBindGroups>().0;
let pipeline_cache = world.resource::<PipelineCache>();
let pipeline = world.resource::<GameOfLifePipeline>();
@ -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);
}