//! A compute shader that simulates Conway's Game of Life. //! //! Compute shaders use the GPU for computing arbitrary information, that may be independent of what //! is rendered to the screen. use bevy::{ prelude::*, render::{ extract_resource::{ExtractResource, ExtractResourcePlugin}, render_asset::RenderAssetUsages, render_asset::RenderAssets, render_graph::{self, RenderGraph, RenderLabel}, render_resource::*, renderer::{RenderContext, RenderDevice}, Render, RenderApp, RenderSet, }, window::WindowPlugin, }; use std::borrow::Cow; const SIZE: (u32, u32) = (1280, 720); 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, ..default() }), ..default() }), GameOfLifeComputePlugin, )) .add_systems(Startup, setup) .run(); } fn setup(mut commands: Commands, mut images: ResMut>) { let mut image = Image::new_fill( Extent3d { width: SIZE.0, height: SIZE.1, depth_or_array_layers: 1, }, TextureDimension::D2, &[0, 0, 0, 255], TextureFormat::Rgba8Unorm, RenderAssetUsages::RENDER_WORLD, ); image.texture_descriptor.usage = TextureUsages::COPY_DST | TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING; let image = 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(), ..default() }); commands.spawn(Camera2dBundle::default()); commands.insert_resource(GameOfLifeImage { texture: image }); } struct GameOfLifeComputePlugin; #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] struct GameOfLifeLabel; 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()); let render_app = app.sub_app_mut(RenderApp); render_app.add_systems( Render, prepare_bind_group.in_set(RenderSet::PrepareBindGroups), ); let mut render_graph = render_app.world.resource_mut::(); render_graph.add_node(GameOfLifeLabel, GameOfLifeNode::default()); render_graph.add_node_edge(GameOfLifeLabel, bevy::render::graph::CameraDriverLabel); } fn finish(&self, app: &mut App) { let render_app = app.sub_app_mut(RenderApp); render_app.init_resource::(); } } #[derive(Resource, Clone, Deref, ExtractResource, AsBindGroup)] struct GameOfLifeImage { #[storage_texture(0, image_format = Rgba8Unorm, access = ReadWrite)] texture: Handle, } #[derive(Resource)] struct GameOfLifeImageBindGroup(BindGroup); fn prepare_bind_group( mut commands: Commands, pipeline: Res, gpu_images: Res>, game_of_life_image: Res, render_device: Res, ) { let view = gpu_images.get(&game_of_life_image.texture).unwrap(); let bind_group = render_device.create_bind_group( None, &pipeline.texture_bind_group_layout, &BindGroupEntries::single(&view.texture_view), ); commands.insert_resource(GameOfLifeImageBindGroup(bind_group)); } #[derive(Resource)] struct GameOfLifePipeline { texture_bind_group_layout: BindGroupLayout, init_pipeline: CachedComputePipelineId, update_pipeline: CachedComputePipelineId, } 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 shader = world .resource::() .load("shaders/game_of_life.wgsl"); let pipeline_cache = world.resource::(); let init_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { label: None, layout: vec![texture_bind_group_layout.clone()], push_constant_ranges: Vec::new(), shader: shader.clone(), shader_defs: vec![], entry_point: Cow::from("init"), }); let update_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { label: None, layout: vec![texture_bind_group_layout.clone()], push_constant_ranges: Vec::new(), shader, shader_defs: vec![], entry_point: Cow::from("update"), }); GameOfLifePipeline { texture_bind_group_layout, init_pipeline, update_pipeline, } } } enum GameOfLifeState { Loading, Init, Update, } struct GameOfLifeNode { state: GameOfLifeState, } impl Default for GameOfLifeNode { fn default() -> Self { Self { state: GameOfLifeState::Loading, } } } impl render_graph::Node for GameOfLifeNode { fn update(&mut self, world: &mut World) { let pipeline = world.resource::(); let pipeline_cache = world.resource::(); // if the corresponding pipeline has loaded, transition to the next stage match self.state { GameOfLifeState::Loading => { if let CachedPipelineState::Ok(_) = pipeline_cache.get_compute_pipeline_state(pipeline.init_pipeline) { self.state = GameOfLifeState::Init; } } GameOfLifeState::Init => { if let CachedPipelineState::Ok(_) = pipeline_cache.get_compute_pipeline_state(pipeline.update_pipeline) { self.state = GameOfLifeState::Update; } } GameOfLifeState::Update => {} } } fn run( &self, _graph: &mut render_graph::RenderGraphContext, render_context: &mut RenderContext, world: &World, ) -> Result<(), render_graph::NodeRunError> { let texture_bind_group = &world.resource::().0; let pipeline_cache = world.resource::(); let pipeline = world.resource::(); let mut pass = render_context .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 => {} GameOfLifeState::Init => { let init_pipeline = pipeline_cache .get_compute_pipeline(pipeline.init_pipeline) .unwrap(); pass.set_pipeline(init_pipeline); pass.dispatch_workgroups(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1); } GameOfLifeState::Update => { let update_pipeline = pipeline_cache .get_compute_pipeline(pipeline.update_pipeline) .unwrap(); pass.set_pipeline(update_pipeline); pass.dispatch_workgroups(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1); } } Ok(()) } }