initial: game of life compute shader
This commit is contained in:
commit
d55ec71edc
55
compute.wgsl
Normal file
55
compute.wgsl
Normal 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
22
index.html
Normal 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
251
index.js
Normal 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
43
index.wgsl
Normal 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);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user