I built Conway's Game of Life using WebGPU to demonstrate GPU compute shaders in web browsers. The result: 60 FPS simulation of 1,024 cells with room to scale much larger.
Traditional CPU implementations process cells sequentially. For a 32×32 grid, that's 1,024 operations per generation. WebGPU's compute shaders enable parallel processing - all cells update simultaneously.
Architecture
Modular design with clear separation:
WebGPUContext.js
- Device initializationShaderManager.js
- WGSL shader loadingBufferManager.js
- GPU memory managementPipelineManager.js
- Render/compute pipelinesGameOfLife.js
- Main simulation class
Ping-Pong Buffers: Two storage buffers alternate read/write roles each frame to avoid race conditions.
// Frame N: Read from buffer A, write to buffer B
// Frame N+1: Read from buffer B, write to buffer A
const cellStateBuffers = [bufferA, bufferB];
Compute Shader: WGSL shader processes cells in 8×8 workgroups:
@compute @workgroup_size(8, 8)
fn simulationMain(@builtin(global_invocation_id) cell: vec3<u32>) {
let index = cell.x + cell.y * grid.size;
// Count neighbors with wrapping
var neighbors = 0u;
for (var di = 0u; di < 3u; di++) {
for (var dj = 0u; dj < 3u; dj++) {
if (di == 1u && dj == 1u) { continue; }
let ni = (cell.x + di + grid.size - 1u) % grid.size;
let nj = (cell.y + dj + grid.size - 1u) % grid.size;
neighbors += cellStateIn[ni + nj * grid.size];
}
}
// Apply Conway's rules
let alive = cellStateIn[index];
cellStateOut[index] = select(
select(0u, 1u, neighbors == 3u), // Dead cell
select(0u, 1u, neighbors == 2u || neighbors == 3u), // Live cell
alive == 1u
);
}
Instanced Rendering: Single draw call renders all 1,024 cells as instanced quads.
Testing on a MacBook Pro M1 shows impressive scaling. The 32×32 grid runs at 60 FPS with ~0.5ms frame time, while 64×64 maintains the same performance. Even at 128×128 (16,384 cells), it sustains 45+ FPS with only ~5% GPU utilization. A CPU-based implementation would struggle with anything beyond 64×64.
The biggest challenge was race conditions - when thousands of threads update cells simultaneously, you need ping-pong buffers to ensure each cell reads the previous generation's state. Memory management required a dedicated buffer manager to handle GPU memory lifecycle properly. Browser support meant adding feature detection with graceful fallbacks, while debugging GPU code demanded extensive validation since GPU debugging is notoriously difficult.
The interface uses modern glassmorphism with translucent elements and backdrop blur. The color palette (#78ff9a
, #ff77aa
, #77aaff
) matches the shader gradients to create visual harmony. The design works responsively across mobile and desktop, with a real-time generation counter that updates as the simulation runs.