game_of_life
Conway's Game of Life on the GPU. Both cell grids live in a single retained record buffer as
fields "a" and "b", so ping-pong is a sub-view swap rather than two separate parcels.
Two schemes are recorded once (AB writes b, BA writes a) and resubmitted on alternate
steps. Idle redraws skip submit and leave the last present on the surface.
cargo run --features examples --example game_of_life
What it demonstrates
- Sub-views of one retained mosaic parcel for ping-pong state
- Two retained schemes, alternating resubmit
- Compute → render → present in each scheme
- Skipping submit on idle redraws
Source
examples/game_of_life.rs:
//! Conway's Game of Life — two retained schemes, alternating resubmit.
//!
//! Ping-pong cell grids live in one retained record buffer (fields `"a"` / `"b"`).
//! Orientation AB reads `a` and writes `b`; BA is the swap. Each is recorded once
//! (and again on resize). Simulation steps alternate which scheme submits. Idle
//! redraws skip submit and leave the last present on the surface.
//!
//! Run with: `cargo run --example game_of_life`
use anyhow::Result;
use goldy::{
field, Buffer, ComputePipeline, Context, Init, Instance, Lease, LeaseRenderTarget, NodeAccess, PrimitiveTopology,
RenderPipeline, RenderPipelineDesc, RequestAdapterOptions, RuntimeDescriptor, Scheme, ShaderModule, Submission,
SurfaceConfig, SurfaceExchange, TargetLoad, Texture, TextureFormat, Transaction, VertexBufferLayout,
};
use std::ops::Shr;
use std::sync::Arc;
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
keyboard::{Key, NamedKey},
window::{Window, WindowId},
};
mod common;
use common::CaptureDump;
const GRID_WIDTH: u32 = 128;
const GRID_HEIGHT: u32 = 128;
const CELL_COUNT: u32 = GRID_WIDTH * GRID_HEIGHT;
fn record_scheme(
scheme: &mut Scheme,
cells: &Buffer,
read_field: &str,
write_field: &str,
compute_pipeline: &ComputePipeline,
render_pipeline: &RenderPipeline,
scene_rt: &Lease<LeaseRenderTarget>,
) {
scheme
.node("game_of_life", compute_pipeline)
.with_parcel(&cells[read_field], NodeAccess::Read)
.with_parcel(&cells[write_field], NodeAccess::Overwrite)
.dispatch(GRID_WIDTH.div_ceil(8), GRID_HEIGHT.div_ceil(8), 1);
let mut pass = scheme.render_pass("game_of_life_render", scene_rt, TargetLoad::Discard);
pass.with_parcel(&cells[write_field], NodeAccess::Read);
pass.set_pipeline(render_pipeline);
pass.draw(0..3, 0..1);
pass.finish();
}
struct FrameBind<'a> {
format: TextureFormat,
width: u32,
height: u32,
surface: Option<&'a SurfaceExchange>,
readback: Option<&'a Texture>,
}
struct Recorded {
scheme: Scheme,
present: Option<Transaction>,
}
fn bind_frame(
scheme: &mut Scheme,
scene_rt: &Lease<LeaseRenderTarget>,
bind: &FrameBind<'_>,
) -> anyhow::Result<Option<Transaction>> {
if let Some(surface) = bind.surface {
let present = surface.bind_render_target(scheme, scene_rt)?;
Ok(Some(present))
} else {
let readback = bind.readback.expect("capture readback");
scheme.copy_to_texture(scene_rt, readback)?;
Ok(None)
}
}
fn build_scheme(
ctx: &Context,
cells: &Buffer,
read_field: &str,
write_field: &str,
compute_pipeline: &ComputePipeline,
render_pipeline: &RenderPipeline,
bind: &FrameBind<'_>,
) -> anyhow::Result<Recorded> {
let mut scheme = Scheme::new(ctx);
let scene_rt = ctx.lease_render_target(bind.width.max(1), bind.height.max(1), bind.format, None)?;
record_scheme(
&mut scheme,
cells,
read_field,
write_field,
compute_pipeline,
render_pipeline,
&scene_rt,
);
let present = bind_frame(&mut scheme, &scene_rt, bind)?;
Ok(Recorded { scheme, present })
}
fn build_schemes(
ctx: &Context,
cells: &Buffer,
compute_pipeline: &ComputePipeline,
render_pipeline: &RenderPipeline,
bind: &FrameBind<'_>,
) -> anyhow::Result<(Recorded, Recorded)> {
Ok((
build_scheme(ctx, cells, "a", "b", compute_pipeline, render_pipeline, bind)?,
build_scheme(ctx, cells, "b", "a", compute_pipeline, render_pipeline, bind)?,
))
}
fn create_initial_state() -> Vec<u32> {
let mut cells = vec![0u32; CELL_COUNT as usize];
let gun = [
(1, 5),
(1, 6),
(2, 5),
(2, 6),
(11, 5),
(11, 6),
(11, 7),
(12, 4),
(12, 8),
(13, 3),
(13, 9),
(14, 3),
(14, 9),
(15, 6),
(16, 4),
(16, 8),
(17, 5),
(17, 6),
(17, 7),
(18, 6),
(21, 3),
(21, 4),
(21, 5),
(22, 3),
(22, 4),
(22, 5),
(23, 2),
(23, 6),
(25, 1),
(25, 2),
(25, 6),
(25, 7),
(35, 3),
(35, 4),
(36, 3),
(36, 4),
];
let offset_x = 10;
let offset_y = 10;
for (x, y) in gun.iter() {
let px = (x + offset_x) as u32;
let py = (y + offset_y) as u32;
if px < GRID_WIDTH && py < GRID_HEIGHT {
cells[(py * GRID_WIDTH + px) as usize] = 1;
}
}
let mut rng = 42u64;
for y in 60..100 {
for x in 60..100 {
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
if (rng >> 32).is_multiple_of(4) {
cells[(y * GRID_WIDTH + x) as usize] = 1;
}
}
}
cells
}
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
)
.init();
if common::capture_requested() {
let mut state = RenderState::new(None)?;
while !state.capture_done() {
state.render()?;
}
return Ok(());
}
let event_loop = EventLoop::new()?;
event_loop.set_control_flow(ControlFlow::Poll);
let mut app = App::default();
event_loop.run_app(&mut app)?;
Ok(())
}
#[derive(Default)]
struct App {
state: Option<RenderState>,
}
struct RenderState {
window: Option<Arc<Window>>,
ctx: Context,
surface: Option<SurfaceExchange>,
capture: Option<CaptureDump>,
readback: Option<Texture>,
scheme_ab: Scheme,
scheme_ba: Scheme,
present_ab: Option<Transaction>,
present_ba: Option<Transaction>,
compute_pipeline: ComputePipeline,
render_pipeline: RenderPipeline,
cells: Buffer,
use_buffer_a: bool,
frame_count: u32,
last_update: std::time::Instant,
start_time: std::time::Instant,
}
impl RenderState {
fn target(&self) -> (TextureFormat, u32, u32) {
if let Some(surface) = &self.surface {
let (width, height) = surface.size();
(surface.format(), width, height)
} else {
let capture = self.capture.as_ref().expect("capture dump");
let (width, height) = capture.size();
(CaptureDump::format(), width, height)
}
}
fn capture_done(&self) -> bool {
self.capture.as_ref().is_none_or(CaptureDump::finished)
}
fn record_orientations(&mut self) -> Result<()> {
let (format, width, height) = self.target();
let bind = FrameBind {
format,
width,
height,
surface: self.surface.as_ref(),
readback: self.readback.as_ref(),
};
let (ab, ba) = build_schemes(
&self.ctx,
&self.cells,
&self.compute_pipeline,
&self.render_pipeline,
&bind,
)?;
self.scheme_ab = ab.scheme;
self.present_ab = ab.present;
self.scheme_ba = ba.scheme;
self.present_ba = ba.present;
Ok(())
}
fn new(window: Option<Arc<Window>>) -> Result<Self> {
let instance = Instance::new()?;
let device = Arc::new(
instance
.request_adapter(&RequestAdapterOptions::default())?
.request_runtime(&RuntimeDescriptor::default())?,
);
let ctx = device.create_context()?;
let (surface, capture, readback, format, width, height) = if let Some(window) = window.as_deref() {
let surface = SurfaceExchange::new(&ctx, window, SurfaceConfig::default())?;
let format = surface.format();
let (width, height) = surface.size();
(Some(surface), None, None, format, width, height)
} else {
let capture = CaptureDump::from_env()?;
let (width, height) = capture.size();
let readback = common::capture_readback(&device, width, height)?;
(
None,
Some(capture),
Some(readback),
CaptureDump::format(),
width,
height,
)
};
let compute_shader = ShaderModule::from_slang(&device, include_str!("../shaders/game_of_life.slang"))?;
let render_shader = ShaderModule::from_slang(&device, include_str!("../shaders/game_of_life_render.slang"))?;
let initial_state = create_initial_state();
let cells = device.acquire_record([
field("a", Init::data(&initial_state)),
field("b", Init::data(&initial_state)),
])?;
let compute_pipeline = ComputePipeline::new(&device, &compute_shader)?;
let render_pipeline = RenderPipeline::new(
&device,
&render_shader,
&render_shader,
&RenderPipelineDesc {
vertex_layout: VertexBufferLayout::default(),
topology: PrimitiveTopology::TriangleList,
target_format: format,
..Default::default()
},
)?;
let bind = FrameBind {
format,
width,
height,
surface: surface.as_ref(),
readback: readback.as_ref(),
};
let (ab, ba) = build_schemes(&ctx, &cells, &compute_pipeline, &render_pipeline, &bind)?;
println!("Game of Life initialized: {}x{} grid", GRID_WIDTH, GRID_HEIGHT);
println!("Features Gosper Glider Gun + random cells");
println!("Press Escape or close window to exit");
Ok(Self {
window,
ctx,
surface,
capture,
readback,
scheme_ab: ab.scheme,
scheme_ba: ba.scheme,
present_ab: ab.present,
present_ba: ba.present,
compute_pipeline,
render_pipeline,
cells,
use_buffer_a: true,
frame_count: 0,
last_update: std::time::Instant::now(),
start_time: std::time::Instant::now(),
})
}
fn settle(
present: Option<&Transaction>,
readback: Option<&Texture>,
capture: Option<&mut CaptureDump>,
submission: &mut Submission,
) -> Result<()> {
if let Some(present) = present {
(submission >> present).take()?;
} else {
let pixels = (submission >> readback.expect("capture readback"))
.take::<u8>()?
.to_vec();
capture.expect("capture dump").write_rgba(&pixels)?;
}
Ok(())
}
fn step(&mut self) -> Result<()> {
if self.use_buffer_a {
let mut submission = self.scheme_ab.submit()?;
Self::settle(
self.present_ab.as_ref(),
self.readback.as_ref(),
self.capture.as_mut(),
&mut submission,
)?;
} else {
let mut submission = self.scheme_ba.submit()?;
Self::settle(
self.present_ba.as_ref(),
self.readback.as_ref(),
self.capture.as_mut(),
&mut submission,
)?;
}
self.use_buffer_a = !self.use_buffer_a;
Ok(())
}
fn render(&mut self) -> Result<()> {
let now = std::time::Instant::now();
let should_step =
self.capture.is_some() || self.frame_count == 0 || now.duration_since(self.last_update).as_millis() > 33;
if should_step {
self.last_update = now;
self.step()?;
self.frame_count += 1;
}
if let Some(window) = &self.window {
window.request_redraw();
}
Ok(())
}
}
impl Drop for RenderState {
fn drop(&mut self) {
let elapsed = self.start_time.elapsed().as_secs_f64();
let fps = if elapsed > 0.0 {
self.frame_count as f64 / elapsed
} else {
0.0
};
println!(
"GOLDY_PERF: frames={} elapsed={elapsed:.2}s avg_fps={fps:.1}",
self.frame_count
);
}
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.state.is_none() {
let window = Arc::new(
event_loop
.create_window(common::hidden_window("Game of Life", 800, 800))
.expect("Failed to create window"),
);
match RenderState::new(Some(window.clone())) {
Ok(mut state) => {
if let Err(e) = state.render() {
tracing::error!("First frame error: {e}");
}
common::reveal_window(&window);
self.state = Some(state);
window.request_redraw();
}
Err(e) => {
tracing::error!("Failed to create render state: {:#}", e);
event_loop.exit();
}
}
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
if let Some(state) = &self.state {
common::exit_if_timed_out(event_loop, state.start_time);
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => {
event_loop.exit();
}
WindowEvent::KeyboardInput { event, .. } if event.state.is_pressed() => {
if matches!(event.logical_key, Key::Named(NamedKey::Escape)) {
event_loop.exit();
}
}
WindowEvent::Resized(size) => {
if let Some(state) = &mut self.state {
if size.width > 0 && size.height > 0 {
let Some(surface) = state.surface.as_ref() else {
return;
};
if let Err(e) = surface.resize(size.width, size.height) {
tracing::error!("Failed to resize surface: {e}");
return;
}
if let Err(e) = state.record_orientations() {
tracing::error!("Failed to rerecord schemes: {e}");
return;
}
state.last_update = std::time::Instant::now()
.checked_sub(std::time::Duration::from_millis(34))
.unwrap_or(state.last_update);
}
}
}
WindowEvent::RedrawRequested => {
if let Some(state) = &mut self.state {
if let Err(e) = state.render() {
tracing::error!("Render error: {:#}", e);
}
}
}
_ => {}
}
}
}
The example pulls in examples/common.rs — see Shared Helpers.
Shaders
shaders/game_of_life.slang:
// Conway's Game of Life compute shader
// Uses ping-pong buffers: reads from one, writes to the other
import goldy_exp;
static const uint GRID_WIDTH = 128;
static const uint GRID_HEIGHT = 128;
uint getCell(Scattered<uint> state, int x, int y) {
x = (x + GRID_WIDTH) % GRID_WIDTH;
y = (y + GRID_HEIGHT) % GRID_HEIGHT;
return state[y * GRID_WIDTH + x];
}
uint countNeighbors(Scattered<uint> state, int x, int y) {
uint count = 0;
count += getCell(state, x - 1, y - 1);
count += getCell(state, x, y - 1);
count += getCell(state, x + 1, y - 1);
count += getCell(state, x - 1, y);
count += getCell(state, x + 1, y);
count += getCell(state, x - 1, y + 1);
count += getCell(state, x, y + 1);
count += getCell(state, x + 1, y + 1);
return count;
}
[goldy_compute]
[numthreads(8, 8, 1)]
void cs_main(Scattered<uint> CURRENT_STATE, Scattered<uint> NEXT_STATE,
ThreadId id) {
if (id.x >= GRID_WIDTH || id.y >= GRID_HEIGHT) return;
uint idx = id.y * GRID_WIDTH + id.x;
uint cell = CURRENT_STATE[idx];
uint neighbors = countNeighbors(CURRENT_STATE, int(id.x), int(id.y));
// Conway's rules
uint newState = 0;
if (cell == 1) {
newState = (neighbors == 2 || neighbors == 3) ? 1 : 0;
} else {
newState = (neighbors == 3) ? 1 : 0;
}
NEXT_STATE[idx] = newState;
}
shaders/game_of_life_render.slang:
// Game of Life rendering shader
// Renders the grid as a fullscreen quad with cell colors
import goldy_exp;
static const uint GRID_WIDTH = 128;
static const uint GRID_HEIGHT = 128;
struct VSOutput {
float4 position : SV_Position;
float2 uv : TEXCOORD0;
};
static const float2 positions[3] = {
float2(-1, -1),
float2( 3, -1),
float2(-1, 3)
};
static const float2 uvs[3] = {
float2(0, 1),
float2(2, 1),
float2(0, -1)
};
[goldy_vertex]
VSOutput vs_main(VertexId vertexID) {
VSOutput output;
output.position = float4(positions[vertexID.value], 0.0, 1.0);
output.uv = uvs[vertexID.value];
return output;
}
[goldy_fragment]
float4 fs_main(Scattered<uint> CELLS, VSOutput input) : SV_Target {
float2 uv = input.uv;
uv.y = 1.0 - uv.y;
int x = int(uv.x * GRID_WIDTH);
int y = int(uv.y * GRID_HEIGHT);
x = clamp(x, 0, int(GRID_WIDTH) - 1);
y = clamp(y, 0, int(GRID_HEIGHT) - 1);
uint idx = y * GRID_WIDTH + x;
uint cell = CELLS[idx];
float2 cellUV = frac(float2(uv.x * GRID_WIDTH, uv.y * GRID_HEIGHT));
float gridLine = (cellUV.x < 0.05 || cellUV.y < 0.05) ? 0.15 : 0.0;
if (cell == 1) {
float3 alive = float3(0.2, 0.9, 0.3);
return float4(alive + gridLine, 1.0);
} else {
float3 dead = float3(0.05, 0.08, 0.1);
return float4(dead + gridLine, 1.0);
}
}