initial: game of life compute shader

This commit is contained in:
Zack Buhman 2026-07-14 20:58:43 -05:00
commit d55ec71edc
4 changed files with 371 additions and 0 deletions

55
compute.wgsl Normal file
View File

@ -0,0 +1,55 @@
struct Configuration {
gridSize: vec2f,
aspect: vec2f
};
@group(0) @binding(0) var<uniform> config: Configuration;
@group(0) @binding(1) var<storage> stateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> stateOut: array<u32>;
fn cellIndex(cell: vec2i) -> u32
{
let x = (cell.x % i32(config.gridSize.x));
let y = (cell.y % i32(config.gridSize.y));
return u32(y * i32(config.gridSize.x) + x);
}
fn cellActive(cell: vec2i) -> u32
{
return stateIn[cellIndex(cell)];
}
const directions: array<vec2i, 8> = array(vec2i(-1, -1),
vec2i(-1, 0),
vec2i(-1, 1),
vec2i(0, -1),
//0, 0)
vec2i(0, 1),
vec2i(1, -1),
vec2i(1, 0),
vec2i(1, 1));
@compute
@workgroup_size(16)
fn computeMain(@builtin(global_invocation_id) globalInvocationId: vec3u)
{
let cell: vec2i = vec2i(i32(globalInvocationId.x) % i32(config.gridSize.x),
i32(globalInvocationId.x) / i32(config.gridSize.y));
var activeNeighbors: u32 = 0;
for (var i: u32 = 0; i < 8; i++) {
activeNeighbors += cellActive(directions[i] + cell);
}
let index = cellIndex(cell);
switch activeNeighbors {
case 2: {
stateOut[index] = stateIn[index];
}
case 3: {
stateOut[index] = 1;
}
default: {
stateOut[index] = 0;
}
}
}

22
index.html Normal file
View File

@ -0,0 +1,22 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>WebGPU</title>
<style>
canvas {
position: absolute;
left: 0;
top: 0;
padding: 0;
border: 0;
width: 100%;
height: 100%;
}
</style>
<script src="index.js" type="module"></script>
</head>
<body>
<canvas></canvas>
</body>
</html>

251
index.js Normal file
View File

@ -0,0 +1,251 @@
if (!navigator.gpu) {
throw new Error("WebGPU not supported on this browser.");
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error("No WebGPU adapter");
}
const device = await adapter.requestDevice();
const canvas = document.querySelector("canvas");
canvas.width = canvas.clientWidth
canvas.height = canvas.clientHeight
const context = canvas.getContext("webgpu");
const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
//console.log(canvasFormat);
context.configure({
device: device,
format: canvasFormat,
});
const encoder = device.createCommandEncoder();
const renderPass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: "clear",
clearValue: { r: 0.9, g: 0.5, b: 0.2, a: 1 },
storeOp: "store",
}]
});
renderPass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
const vertices = new Float32Array([
// X, Y,
-0.8, -0.8, // Triangle 1 (Blue)
0.8, -0.8,
0.8, 0.8,
-0.8, -0.8, // Triangle 2 (Red)
0.8, 0.8,
-0.8, 0.8,
]);
const vertexBuffer = device.createBuffer({
label: "cell vertices",
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertices);
const vertexBufferLayout = {
arrayStride: 8,
attributes: [{
format: "float32x2",
offset: 0,
shaderLocation: 0,
}],
stepMode: "vertex",
};
function getPath(path)
{
return fetch(path).then((response) => {
if (!response.ok) {
throw new Error(`${path}: ${response.status}`)
}
return response.blob()
}).then((blob) => { return blob.text() })
}
const cellShaderModule = device.createShaderModule({
label: "cell shader",
code: await getPath("index.wgsl")
});
const bindGroupLayout = device.createBindGroupLayout({
label: "bind group layout",
entries: [{
binding: 0,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
buffer: { type: "uniform" }
}, {
binding: 1,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
buffer: { type: "read-only-storage" }
}, {
binding: 2,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: "storage" }
}]
});
const pipelineLayout = device.createPipelineLayout({
label: "pipeline layout",
bindGroupLayouts: [ bindGroupLayout ],
});
const cellPipeline = device.createRenderPipeline({
label: "cell pipeline",
layout: pipelineLayout,
vertex: {
module: cellShaderModule,
entryPoint: "vertexMain",
buffers: [vertexBufferLayout],
},
fragment: {
module: cellShaderModule,
entryPoint: "fragmentMain",
targets: [{
format: canvasFormat,
}]
}
});
const computeShaderModule = device.createShaderModule({
label: "compute shader",
code: await getPath("compute.wgsl")
});
const computePipeline = device.createComputePipeline({
label: "compute pipeline",
layout: pipelineLayout,
compute: {
module: computeShaderModule,
entryPoint: "computeMain",
},
});
const gridSize = 32;
const workGroupSize = 16;
const uniformArray = new Float32Array([gridSize, gridSize, 1, 1]);
const uniformBuffer = device.createBuffer({
label: "grid uniform",
size: uniformArray.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(uniformBuffer, 0, uniformArray);
const cellStateArray = new Uint32Array(gridSize * gridSize);
const cellStateStorage = [
device.createBuffer({
label: "cell state 0",
size: cellStateArray.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
}),
device.createBuffer({
label: "cell state 1",
size: cellStateArray.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
}),
];
for (let i = 0; i < cellStateArray.length; i += 1) {
cellStateArray[i] = Math.random() > 0.6 ? 1 : 0;
}
device.queue.writeBuffer(cellStateStorage[0], 0, cellStateArray);
device.queue.writeBuffer(cellStateStorage[1], 0, cellStateArray);
const bindGroups = [
device.createBindGroup({
label: "cell bind group 0",
layout: bindGroupLayout,
entries: [{
binding: 0,
resource: { buffer: uniformBuffer },
}, {
binding: 1,
resource: { buffer: cellStateStorage[0] },
}, {
binding: 2,
resource: { buffer: cellStateStorage[1] },
}],
}),
device.createBindGroup({
label: "cell bind group 1",
layout: bindGroupLayout,
entries: [{
binding: 0,
resource: { buffer: uniformBuffer },
}, {
binding: 1,
resource: { buffer: cellStateStorage[1] },
}, {
binding: 2,
resource: { buffer: cellStateStorage[0] },
}],
})
];
let tick = 0;
let which = 0;
function render()
{
const encoder = device.createCommandEncoder();
const computePass = encoder.beginComputePass();
computePass.setPipeline(computePipeline);
computePass.setBindGroup(0, bindGroups[which]);
const workgroupCount = Math.ceil((gridSize * gridSize) / workGroupSize);
computePass.dispatchWorkgroups(workgroupCount);
computePass.end();
tick += 1;
if ((tick % 5) == 0) {
which ^= 1;
}
canvas.width = canvas.clientWidth
canvas.height = canvas.clientHeight
const aspect = canvas.height / canvas.width;
if (canvas.width > canvas.height) {
uniformArray[2] = aspect;
uniformArray[3] = 1;
} else {
uniformArray[2] = 1;
uniformArray[3] = 1 / aspect;
}
device.queue.writeBuffer(uniformBuffer, 0, uniformArray, 0);
const renderPass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: "clear",
clearValue: { r: 0.2, g: 0.2, b: 0.4, a: 1 },
storeOp: "store",
}]
});
renderPass.setPipeline(cellPipeline);
renderPass.setVertexBuffer(0, vertexBuffer);
renderPass.setBindGroup(0, bindGroups[which]);
renderPass.draw(vertices.length / 2, gridSize * gridSize);
renderPass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
requestAnimationFrame(render)
}
requestAnimationFrame(render)

43
index.wgsl Normal file
View File

@ -0,0 +1,43 @@
struct Configuration {
gridSize: vec2f,
aspect: vec2f
};
@group(0) @binding(0) var<uniform> config: Configuration;
@group(0) @binding(1) var<storage> state: array<u32>;
struct VertexInput {
@location(0) position: vec2f,
@builtin(instance_index) instance: u32,
};
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) cell: vec2f,
};
struct FragmentInput {
@location(0) cell: vec2f,
};
@vertex
fn vertexMain(input: VertexInput) -> VertexOutput
{
let position = input.position * 0.5 + 0.5;
let instance = f32(input.instance);
let state = f32(state[input.instance]);
let cell = vec2f(instance % config.gridSize.x, floor(instance / config.gridSize.y));
let grid = (position * state + cell) / config.gridSize;
var output: VertexOutput;
output.position = vec4f((grid * 2 - 1) * config.aspect, 0, 1);
output.cell = cell / config.gridSize;
return output;
}
@fragment
fn fragmentMain(input: FragmentInput) -> @location(0) vec4f
{
return vec4f(input.cell, 0, 1);
}