Game of Life
Conway's B3/S23 automaton, stepped entirely in device memory. One gpu frame chains five passes — the CA step, a decaying trail, a pointer splat, a double-click reseed, and the RGBA paint — over ping-ponged grids. Every cell reads its eight neighbours and decides its fate in parallel.
use system.collections.array
use system.math
use system.io
// Simulation grid, display canvas, and the cell-to-pixel magnification.
const GW = 320
const GH = 180
const CELLS = GW * GH
const CW = 1280
const CH = 720
const PIXELS = CW * CH
const SCALE = 4
// Grid state (ping-ponged across frames) and the RGBA paint output.
gpu var state_a = Array<f32, CELLS>()
gpu var state_b = Array<f32, CELLS>()
gpu var paint = Array<f32, PIXELS * 4>()
// Seed the grid: a deterministic ~18%-density soup. The mix is deliberately
// non-linear — a squaring step between two linear ones — because a purely
// linear hash of the cell index lays the soup out on a visible lattice, and a
// Life field that starts patterned keeps looking patterned for many
// generations. Every intermediate stays well inside 32-bit range.
fn soup(idx int) f32
let a = (idx * 1597 + 51749) % 40000
let b = (a * a + a * 37 + 11) % 40009
let c = (b * 613 + 7919) % 40009
return 1.0 if c % 100 < 18 else 0.0
forall idx in 0..CELLS
state_a[idx] = soup(idx)
// Clear the canvas to the background color. This 2-D pass over the exact
// display extent also tells the web-gpu backend the canvas is 1280×720 (a flat
// paint buffer otherwise reads as square).
forall px, py in 0..CW, 0..CH
let base = (py * CW + px) * 4
paint[base] = 0.012
paint[base + 1] = 0.02
paint[base + 2] = 0.046
paint[base + 3] = 1.0
gpu frame
// Pass 1: one Life step. A cell is alive iff its state is exactly 1.0; a
// dead cell keeps its trail decaying at 0.94×/frame, which never re-reads as
// alive (0.94 < the 0.99 alive threshold).
forall idx in 0..CELLS
let y = idx / GW
let x = idx % GW
let ym = (y - 1 + GH) % GH
let yp = (y + 1) % GH
let xm = (x - 1 + GW) % GW
let xp = (x + 1) % GW
let n1 = 1 if state_a[ym * GW + xm] > 0.99 else 0
let n2 = 1 if state_a[ym * GW + x] > 0.99 else 0
let n3 = 1 if state_a[ym * GW + xp] > 0.99 else 0
let n4 = 1 if state_a[y * GW + xm] > 0.99 else 0
let n5 = 1 if state_a[y * GW + xp] > 0.99 else 0
let n6 = 1 if state_a[yp * GW + xm] > 0.99 else 0
let n7 = 1 if state_a[yp * GW + x] > 0.99 else 0
let n8 = 1 if state_a[yp * GW + xp] > 0.99 else 0
let n = n1 + n2 + n3 + n4 + n5 + n6 + n7 + n8
let self_alive = state_a[idx] > 0.99
var alive_next = false
if self_alive
alive_next = n == 2 or n == 3
else
alive_next = n == 3
state_b[idx] = 1.0 if alive_next else state_a[idx] * 0.94
// Pass 2: paint held pointer as live cells (click / drag to seed).
forall idx in 0..CELLS
if frame.mouse_down
let x = idx % GW
let y = idx / GW
let mx = frame.mouse_x * (GW as f32)
let my = frame.mouse_y * (GH as f32)
let dx = (x as f32) - mx
let dy = (y as f32) - my
if dx * dx + dy * dy < 36.0
state_b[idx] = 1.0
// Pass 3: reseed a fresh soup on double-click.
forall idx in 0..CELLS
if frame.double_clicked
state_b[idx] = soup(idx)
// Pass 4: shade each display pixel. Cells are drawn as rounded dots (an
// `inner` factor that falls off toward the cell edge) over a dark-blue
// background; live cells glow yellow, fading trails glow blue.
forall pidx in 0..PIXELS
let ppx = pidx % CW
let ppy = pidx / CW
let cell = (ppy / SCALE) * GW + (ppx / SCALE)
// Sub-cell position → distance to the cell edge → dot falloff.
let fx = (ppx % SCALE) as f32
let fy = (ppy % SCALE) as f32
var ex = (fx + 0.5) / (SCALE as f32) - 0.5
ex = ex if ex >= 0.0 else 0.0 - ex
var ey = (fy + 0.5) / (SCALE as f32) - 0.5
ey = ey if ey >= 0.0 else 0.0 - ey
let inner = 1.0 - smoothstep(0.32, 0.46, max(ex, ey)) as f32
let st = state_b[cell]
let alive = 1.0 if st > 0.99 else 0.0
let trail = st * (1.0 - alive)
let blue_w = trail * trail * (0.25 + 0.75 * inner)
let yellow_w = alive * (0.22 + 0.95 * inner)
let base = pidx * 4
paint[base] = 0.012 + 0.18 * blue_w + 1.0 * yellow_w
paint[base + 1] = 0.02 + 0.32 * blue_w + 0.84 * yellow_w
paint[base + 2] = 0.046 + 0.8 * blue_w + 0.24 * yellow_w
paint[base + 3] = 1.0
Related docs: the gpu frame that chains five passes in order and one forall thread per cell, reading its eight neighbours.
From this page to your own GPU
Four steps. You'll need a WebGPU-capable browser (Chrome or Edge 113+, or Safari 18+).
-
1
Install Miri
Build the compiler from source (full install guide):
git clone https://github.com/miri-lang/miri.git cd miri && cargo build --releaseThe binary lands at
target/release/miri. -
2
Grab the program
Hit copy program above and save it as
game_of_life.mi. -
3
Compile it to WebGPU
miri build game_of_life.mi --target web-gpu --out life-webOut comes a self-contained
index.html— the runtime and every compiled WGSL kernel are inlined. -
4
Open it
Double-click
life-web/index.html. It runs straight fromfile://— same interaction as the preview above.
No browser needed to try it: miri run game_of_life.mi runs the
same kernels on your local GPU through Metal, Vulkan or DX12. Same language, same code, three
backends and a browser — that's the point.