Path: blob/main/examples/shader/compute_shader_game_of_life.rs
6595 views
//! A compute shader that simulates Conway's Game of Life.1//!2//! Compute shaders use the GPU for computing arbitrary information, that may be independent of what3//! is rendered to the screen.45use bevy::{6asset::RenderAssetUsages,7prelude::*,8render::{9extract_resource::{ExtractResource, ExtractResourcePlugin},10render_asset::RenderAssets,11render_graph::{self, RenderGraph, RenderLabel},12render_resource::{13binding_types::{texture_storage_2d, uniform_buffer},14*,15},16renderer::{RenderContext, RenderDevice, RenderQueue},17texture::GpuImage,18Render, RenderApp, RenderStartup, RenderSystems,19},20shader::PipelineCacheError,21};22use std::borrow::Cow;2324/// This example uses a shader source file from the assets subdirectory25const SHADER_ASSET_PATH: &str = "shaders/game_of_life.wgsl";2627const DISPLAY_FACTOR: u32 = 4;28const SIZE: UVec2 = UVec2::new(1280 / DISPLAY_FACTOR, 720 / DISPLAY_FACTOR);29const WORKGROUP_SIZE: u32 = 8;3031fn main() {32App::new()33.insert_resource(ClearColor(Color::BLACK))34.add_plugins((35DefaultPlugins36.set(WindowPlugin {37primary_window: Some(Window {38resolution: (SIZE * DISPLAY_FACTOR).into(),39// uncomment for unthrottled FPS40// present_mode: bevy::window::PresentMode::AutoNoVsync,41..default()42}),43..default()44})45.set(ImagePlugin::default_nearest()),46GameOfLifeComputePlugin,47))48.add_systems(Startup, setup)49.add_systems(Update, switch_textures)50.run();51}5253fn setup(mut commands: Commands, mut images: ResMut<Assets<Image>>) {54let mut image = Image::new_target_texture(SIZE.x, SIZE.y, TextureFormat::Rgba32Float);55image.asset_usage = RenderAssetUsages::RENDER_WORLD;56image.texture_descriptor.usage =57TextureUsages::COPY_DST | TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING;58let image0 = images.add(image.clone());59let image1 = images.add(image);6061commands.spawn((62Sprite {63image: image0.clone(),64custom_size: Some(SIZE.as_vec2()),65..default()66},67Transform::from_scale(Vec3::splat(DISPLAY_FACTOR as f32)),68));69commands.spawn(Camera2d);7071commands.insert_resource(GameOfLifeImages {72texture_a: image0,73texture_b: image1,74});7576commands.insert_resource(GameOfLifeUniforms {77alive_color: LinearRgba::RED,78});79}8081// Switch texture to display every frame to show the one that was written to most recently.82fn switch_textures(images: Res<GameOfLifeImages>, mut sprite: Single<&mut Sprite>) {83if sprite.image == images.texture_a {84sprite.image = images.texture_b.clone();85} else {86sprite.image = images.texture_a.clone();87}88}8990struct GameOfLifeComputePlugin;9192#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]93struct GameOfLifeLabel;9495impl Plugin for GameOfLifeComputePlugin {96fn build(&self, app: &mut App) {97// Extract the game of life image resource from the main world into the render world98// for operation on by the compute shader and display on the sprite.99app.add_plugins((100ExtractResourcePlugin::<GameOfLifeImages>::default(),101ExtractResourcePlugin::<GameOfLifeUniforms>::default(),102));103let render_app = app.sub_app_mut(RenderApp);104render_app105.add_systems(RenderStartup, init_game_of_life_pipeline)106.add_systems(107Render,108prepare_bind_group.in_set(RenderSystems::PrepareBindGroups),109);110111let mut render_graph = render_app.world_mut().resource_mut::<RenderGraph>();112render_graph.add_node(GameOfLifeLabel, GameOfLifeNode::default());113render_graph.add_node_edge(GameOfLifeLabel, bevy::render::graph::CameraDriverLabel);114}115}116117#[derive(Resource, Clone, ExtractResource)]118struct GameOfLifeImages {119texture_a: Handle<Image>,120texture_b: Handle<Image>,121}122123#[derive(Resource, Clone, ExtractResource, ShaderType)]124struct GameOfLifeUniforms {125alive_color: LinearRgba,126}127128#[derive(Resource)]129struct GameOfLifeImageBindGroups([BindGroup; 2]);130131fn prepare_bind_group(132mut commands: Commands,133pipeline: Res<GameOfLifePipeline>,134gpu_images: Res<RenderAssets<GpuImage>>,135game_of_life_images: Res<GameOfLifeImages>,136game_of_life_uniforms: Res<GameOfLifeUniforms>,137render_device: Res<RenderDevice>,138queue: Res<RenderQueue>,139) {140let view_a = gpu_images.get(&game_of_life_images.texture_a).unwrap();141let view_b = gpu_images.get(&game_of_life_images.texture_b).unwrap();142143// Uniform buffer is used here to demonstrate how to set up a uniform in a compute shader144// Alternatives such as storage buffers or push constants may be more suitable for your use case145let mut uniform_buffer = UniformBuffer::from(game_of_life_uniforms.into_inner());146uniform_buffer.write_buffer(&render_device, &queue);147148let bind_group_0 = render_device.create_bind_group(149None,150&pipeline.texture_bind_group_layout,151&BindGroupEntries::sequential((152&view_a.texture_view,153&view_b.texture_view,154&uniform_buffer,155)),156);157let bind_group_1 = render_device.create_bind_group(158None,159&pipeline.texture_bind_group_layout,160&BindGroupEntries::sequential((161&view_b.texture_view,162&view_a.texture_view,163&uniform_buffer,164)),165);166commands.insert_resource(GameOfLifeImageBindGroups([bind_group_0, bind_group_1]));167}168169#[derive(Resource)]170struct GameOfLifePipeline {171texture_bind_group_layout: BindGroupLayout,172init_pipeline: CachedComputePipelineId,173update_pipeline: CachedComputePipelineId,174}175176fn init_game_of_life_pipeline(177mut commands: Commands,178render_device: Res<RenderDevice>,179asset_server: Res<AssetServer>,180pipeline_cache: Res<PipelineCache>,181) {182let texture_bind_group_layout = render_device.create_bind_group_layout(183"GameOfLifeImages",184&BindGroupLayoutEntries::sequential(185ShaderStages::COMPUTE,186(187texture_storage_2d(TextureFormat::Rgba32Float, StorageTextureAccess::ReadOnly),188texture_storage_2d(TextureFormat::Rgba32Float, StorageTextureAccess::WriteOnly),189uniform_buffer::<GameOfLifeUniforms>(false),190),191),192);193let shader = asset_server.load(SHADER_ASSET_PATH);194let init_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {195layout: vec![texture_bind_group_layout.clone()],196shader: shader.clone(),197entry_point: Some(Cow::from("init")),198..default()199});200let update_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {201layout: vec![texture_bind_group_layout.clone()],202shader,203entry_point: Some(Cow::from("update")),204..default()205});206207commands.insert_resource(GameOfLifePipeline {208texture_bind_group_layout,209init_pipeline,210update_pipeline,211});212}213214enum GameOfLifeState {215Loading,216Init,217Update(usize),218}219220struct GameOfLifeNode {221state: GameOfLifeState,222}223224impl Default for GameOfLifeNode {225fn default() -> Self {226Self {227state: GameOfLifeState::Loading,228}229}230}231232impl render_graph::Node for GameOfLifeNode {233fn update(&mut self, world: &mut World) {234let pipeline = world.resource::<GameOfLifePipeline>();235let pipeline_cache = world.resource::<PipelineCache>();236237// if the corresponding pipeline has loaded, transition to the next stage238match self.state {239GameOfLifeState::Loading => {240match pipeline_cache.get_compute_pipeline_state(pipeline.init_pipeline) {241CachedPipelineState::Ok(_) => {242self.state = GameOfLifeState::Init;243}244// If the shader hasn't loaded yet, just wait.245CachedPipelineState::Err(PipelineCacheError::ShaderNotLoaded(_)) => {}246CachedPipelineState::Err(err) => {247panic!("Initializing assets/{SHADER_ASSET_PATH}:\n{err}")248}249_ => {}250}251}252GameOfLifeState::Init => {253if let CachedPipelineState::Ok(_) =254pipeline_cache.get_compute_pipeline_state(pipeline.update_pipeline)255{256self.state = GameOfLifeState::Update(1);257}258}259GameOfLifeState::Update(0) => {260self.state = GameOfLifeState::Update(1);261}262GameOfLifeState::Update(1) => {263self.state = GameOfLifeState::Update(0);264}265GameOfLifeState::Update(_) => unreachable!(),266}267}268269fn run(270&self,271_graph: &mut render_graph::RenderGraphContext,272render_context: &mut RenderContext,273world: &World,274) -> Result<(), render_graph::NodeRunError> {275let bind_groups = &world.resource::<GameOfLifeImageBindGroups>().0;276let pipeline_cache = world.resource::<PipelineCache>();277let pipeline = world.resource::<GameOfLifePipeline>();278279let mut pass = render_context280.command_encoder()281.begin_compute_pass(&ComputePassDescriptor::default());282283// select the pipeline based on the current state284match self.state {285GameOfLifeState::Loading => {}286GameOfLifeState::Init => {287let init_pipeline = pipeline_cache288.get_compute_pipeline(pipeline.init_pipeline)289.unwrap();290pass.set_bind_group(0, &bind_groups[0], &[]);291pass.set_pipeline(init_pipeline);292pass.dispatch_workgroups(SIZE.x / WORKGROUP_SIZE, SIZE.y / WORKGROUP_SIZE, 1);293}294GameOfLifeState::Update(index) => {295let update_pipeline = pipeline_cache296.get_compute_pipeline(pipeline.update_pipeline)297.unwrap();298pass.set_bind_group(0, &bind_groups[index], &[]);299pass.set_pipeline(update_pipeline);300pass.dispatch_workgroups(SIZE.x / WORKGROUP_SIZE, SIZE.y / WORKGROUP_SIZE, 1);301}302}303304Ok(())305}306}307308309