Skip to content

Commit

Permalink
Finish game of life etc.
Browse files Browse the repository at this point in the history
  • Loading branch information
hakolao committed Feb 2, 2022
1 parent 2920009 commit 37c2788
Show file tree
Hide file tree
Showing 9 changed files with 677 additions and 10 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ features = []
[dev-dependencies]
anyhow = "1.0.40"
vulkano-shaders = "0.28"
rand = "0.8.4"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ cargo run --example circle --features example_has_gui
cargo run --example circle
cargo run --example multi_window_gui --features example_has_gui
cargo run --example windowless_compute
cargo run --example game_of_life
```

### Contributing
Expand Down
238 changes: 238 additions & 0 deletions examples/game_of_life/game_of_life.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// Copyright (c) 2022 The vulkano developers
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or https://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.

use std::sync::Arc;

use bevy::math::IVec2;
use bevy_vulkano::{create_device_image, DeviceImageView};
use rand::Rng;
use vulkano::{
buffer::{BufferUsage, CpuAccessibleBuffer},
command_buffer::{AutoCommandBufferBuilder, CommandBufferUsage, PrimaryAutoCommandBuffer},
descriptor_set::{PersistentDescriptorSet, WriteDescriptorSet},
device::Queue,
format::Format,
image::ImageAccess,
pipeline::{ComputePipeline, Pipeline, PipelineBindPoint},
sync::GpuFuture,
};

/// Pipeline holding double buffered grid & color image.
/// Grids are used to calculate the state, and color image is used to show the output.
/// Because each step we determine state in parallel, we need to write the output to
/// another grid. Otherwise the state would not be correctly determined as one thread might read
/// data that was just written by another thread
pub struct GameOfLifeComputePipeline {
compute_queue: Arc<Queue>,
compute_life_pipeline: Arc<ComputePipeline>,
life_in: Arc<CpuAccessibleBuffer<[u32]>>,
life_out: Arc<CpuAccessibleBuffer<[u32]>>,
image: DeviceImageView,
}

fn rand_grid(compute_queue: &Arc<Queue>, size: [u32; 2]) -> Arc<CpuAccessibleBuffer<[u32]>> {
CpuAccessibleBuffer::from_iter(
compute_queue.device().clone(),
BufferUsage::all(),
false,
(0..(size[0] * size[1]))
.map(|_| rand::thread_rng().gen_range(0u32..=1))
.collect::<Vec<u32>>(),
)
.unwrap()
}

impl GameOfLifeComputePipeline {
pub fn new(compute_queue: Arc<Queue>, size: [u32; 2]) -> GameOfLifeComputePipeline {
let life_in = rand_grid(&compute_queue, size);
let life_out = rand_grid(&compute_queue, size);

let compute_life_pipeline = {
let shader = compute_life_cs::load(compute_queue.device().clone()).unwrap();
ComputePipeline::new(
compute_queue.device().clone(),
shader.entry_point("main").unwrap(),
&(),
None,
|_| {},
)
.unwrap()
};

let image = create_device_image(compute_queue.clone(), size, Format::R8G8B8A8_UNORM);
GameOfLifeComputePipeline {
compute_queue,
compute_life_pipeline,
life_in,
life_out,
image,
}
}

pub fn color_image(&self) -> DeviceImageView {
self.image.clone()
}

pub fn draw_life(&mut self, pos: IVec2) {
let mut life_in = self.life_in.write().unwrap();
let size = self.image.image().dimensions().width_height();
if pos.y < 0 || pos.y >= size[1] as i32 || pos.x < 0 || pos.x >= size[0] as i32 {
return;
}
let index = (pos.y * size[0] as i32 + pos.x) as usize;
life_in[index] = 1;
}

pub fn compute(
&mut self,
before_future: Box<dyn GpuFuture>,
life_color: [f32; 4],
dead_color: [f32; 4],
) -> Box<dyn GpuFuture> {
let mut builder = AutoCommandBufferBuilder::primary(
self.compute_queue.device().clone(),
self.compute_queue.family(),
CommandBufferUsage::OneTimeSubmit,
)
.unwrap();

// Dispatch will mutate the builder adding commands which won't be sent before we build the command buffer
// after dispatches. This will minimize the commands we send to the GPU. For example, we could be doing
// tens of dispatches here depending on our needs. Maybe we wanted to simulate 10 steps at a time...

// First compute the next state
self.dispatch(&mut builder, life_color, dead_color, 0);
// Then color based on the next state
self.dispatch(&mut builder, life_color, dead_color, 1);

let command_buffer = builder.build().unwrap();
let finished = before_future
.then_execute(self.compute_queue.clone(), command_buffer)
.unwrap();
let after_pipeline = finished.then_signal_fence_and_flush().unwrap().boxed();

// Swap input and output so the output becomes the input for next frame
std::mem::swap(&mut self.life_in, &mut self.life_out);

after_pipeline
}

/// Build the command for a dispatch.
fn dispatch(
&mut self,
builder: &mut AutoCommandBufferBuilder<PrimaryAutoCommandBuffer>,
life_color: [f32; 4],
dead_color: [f32; 4],
// Step determines whether we color or compute life (see branch in the shader)s
step: i32,
) {
// Resize image if needed
let img_dims = self.image.image().dimensions().width_height();
let pipeline_layout = self.compute_life_pipeline.layout();
let desc_layout = pipeline_layout.descriptor_set_layouts().get(0).unwrap();
let set = PersistentDescriptorSet::new(desc_layout.clone(), [
WriteDescriptorSet::image_view(0, self.image.clone()),
WriteDescriptorSet::buffer(1, self.life_in.clone()),
WriteDescriptorSet::buffer(2, self.life_out.clone()),
])
.unwrap();

let push_constants = compute_life_cs::ty::PushConstants {
life_color,
dead_color,
step,
};
builder
.bind_pipeline_compute(self.compute_life_pipeline.clone())
.bind_descriptor_sets(PipelineBindPoint::Compute, pipeline_layout.clone(), 0, set)
.push_constants(pipeline_layout.clone(), 0, push_constants)
.dispatch([img_dims[0] / 8, img_dims[1] / 8, 1])
.unwrap();
}
}

mod compute_life_cs {
vulkano_shaders::shader! {
ty: "compute",
src: "
#version 450
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
layout(set = 0, binding = 0, rgba8) uniform writeonly image2D img;
layout(set = 0, binding = 1) buffer LifeInBuffer { uint life_in[]; };
layout(set = 0, binding = 2) buffer LifeOutBuffer { uint life_out[]; };
layout(push_constant) uniform PushConstants {
vec4 life_color;
vec4 dead_color;
int step;
} push_constants;
int get_index(ivec2 pos) {
ivec2 dims = ivec2(imageSize(img));
return pos.y * dims.x + pos.x;
}
// https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
void compute_life() {
ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
int index = get_index(pos);
ivec2 up_left = pos + ivec2(-1, 1);
ivec2 up = pos + ivec2(0, 1);
ivec2 up_right = pos + ivec2(1, 1);
ivec2 right = pos + ivec2(1, 0);
ivec2 down_right = pos + ivec2(1, -1);
ivec2 down = pos + ivec2(0, -1);
ivec2 down_left = pos + ivec2(-1, -1);
ivec2 left = pos + ivec2(-1, 0);
int alive_count = 0;
if (life_out[get_index(up_left)] == 1) { alive_count += 1; }
if (life_out[get_index(up)] == 1) { alive_count += 1; }
if (life_out[get_index(up_right)] == 1) { alive_count += 1; }
if (life_out[get_index(right)] == 1) { alive_count += 1; }
if (life_out[get_index(down_right)] == 1) { alive_count += 1; }
if (life_out[get_index(down)] == 1) { alive_count += 1; }
if (life_out[get_index(down_left)] == 1) { alive_count += 1; }
if (life_out[get_index(left)] == 1) { alive_count += 1; }
// Dead becomes alive
if (life_out[index] == 0 && alive_count == 3) {
life_out[index] = 1;
} // Becomes dead
else if (life_out[index] == 1 && alive_count < 2 || alive_count > 3) {
life_out[index] = 0;
} // Else Do nothing
else {
life_out[index] = life_in[index];
}
}
void compute_color() {
ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
int index = get_index(pos);
if (life_out[index] == 1) {
imageStore(img, pos, push_constants.life_color);
} else {
imageStore(img, pos, push_constants.dead_color);
}
}
void main() {
if (push_constants.step == 0) {
compute_life();
} else {
compute_color();
}
}"
}
}
Loading

0 comments on commit 37c2788

Please sign in to comment.