diff --git a/index.html b/index.html
index b598337..153a597 100644
--- a/index.html
+++ b/index.html
@@ -7,7 +7,7 @@
Whatever WebGPU
-
+
FPS:
diff --git a/src/main.ts b/src/main.ts
index 9a53ce5..faba201 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,6 +1,7 @@
import "./style.css"
-const GRID_SIZE = 32
+const GRID_SIZE = 64
+const COMPUTE_MS_INTERVAL = 100
async function getCanvas(): Promise {
const canvas = document.querySelector("#canvas")
@@ -39,9 +40,37 @@ function createVertexBufferLayout(): GPUVertexBufferLayout {
}
}
+function createPipelineLayout(
+ device: GPUDevice,
+ bindGroupLayout: GPUBindGroupLayout,
+): GPUPipelineLayout {
+ return device.createPipelineLayout({
+ label: "Cell Pipeline Layout",
+ bindGroupLayouts: [bindGroupLayout],
+ })
+}
+
+async function createComputePipeline(
+ device: GPUDevice,
+ pipelineLayout: GPUPipelineLayout,
+): Promise {
+ return device.createComputePipeline({
+ label: "Simulation pipeline",
+ layout: pipelineLayout,
+ compute: {
+ module: device.createShaderModule({
+ label: "Game of Life simulation shader",
+ code: (await import("./shaders/simulation.wgsl?raw")).default,
+ }),
+ entryPoint: "computeMain",
+ },
+ })
+}
+
async function createCellPipeline(
device: GPUDevice,
canvasFormat: GPUTextureFormat,
+ pipelineLayout: GPUPipelineLayout,
): Promise {
const cellShaderModule = device.createShaderModule({
label: "Cell shader",
@@ -50,7 +79,7 @@ async function createCellPipeline(
return device.createRenderPipeline({
label: "Cell pipeline",
- layout: "auto",
+ layout: pipelineLayout,
vertex: {
module: cellShaderModule,
entryPoint: "vertexMain",
@@ -75,7 +104,7 @@ function createGridUniformBuffer(device: GPUDevice): GPUBuffer {
return gridUniformBuffer
}
-function createTimeBuffer(device: GPUDevice): GPUBuffer {
+function createTimeUniformBuffer(device: GPUDevice): GPUBuffer {
return device.createBuffer({
label: "Time Uniform",
size: 4,
@@ -91,41 +120,70 @@ function createStateStorageBuffers(device: GPUDevice): GPUBuffer[] {
device.createBuffer({ label: "Cell State A", size, usage }),
device.createBuffer({ label: "Cell State B", size, usage }),
]
- for (let i = 0; i < cellStateArray.length; i += 3) {
- cellStateArray[i] = 1
+ for (let i = 0; i < cellStateArray.length; ++i) {
+ cellStateArray[i] = Math.random() > 0.6 ? 1 : 0
}
device.queue.writeBuffer(cellStateStorage[0], 0, cellStateArray)
- for (let i = 0; i < cellStateArray.length; i++) {
- cellStateArray[i] = i % 2
- }
- device.queue.writeBuffer(cellStateStorage[1], 0, cellStateArray)
return cellStateStorage
}
+function createBindGroupLayout(device: GPUDevice): GPUBindGroupLayout {
+ return device.createBindGroupLayout({
+ label: "Cell Bind Group Layout",
+ entries: [
+ {
+ binding: 0,
+ visibility:
+ GPUShaderStage.FRAGMENT |
+ GPUShaderStage.VERTEX |
+ GPUShaderStage.COMPUTE,
+ buffer: { type: "uniform" },
+ },
+ {
+ binding: 1,
+ visibility: GPUShaderStage.FRAGMENT,
+ buffer: { type: "uniform" },
+ },
+ {
+ binding: 2,
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
+ buffer: { type: "read-only-storage" },
+ },
+ {
+ binding: 3,
+ visibility: GPUShaderStage.COMPUTE,
+ buffer: { type: "storage" },
+ },
+ ],
+ })
+}
+
function createBindGroups(
device: GPUDevice,
- cellPipeline: GPURenderPipeline,
gridUniformBuffer: GPUBuffer,
- timeBuffer: GPUBuffer,
+ timeUniformBuffer: GPUBuffer,
cellStateStorage: GPUBuffer[],
+ bindGroupLayout: GPUBindGroupLayout,
): GPUBindGroup[] {
return [
device.createBindGroup({
label: "Cell renderer bind group A",
- layout: cellPipeline.getBindGroupLayout(0),
+ layout: bindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: gridUniformBuffer } },
- { binding: 1, resource: { buffer: timeBuffer } },
+ { binding: 1, resource: { buffer: timeUniformBuffer } },
{ binding: 2, resource: { buffer: cellStateStorage[0] } },
+ { binding: 3, resource: { buffer: cellStateStorage[1] } },
],
}),
device.createBindGroup({
label: "Cell updater bind group B",
- layout: cellPipeline.getBindGroupLayout(0),
+ layout: bindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: gridUniformBuffer } },
- { binding: 1, resource: { buffer: timeBuffer } },
+ { binding: 1, resource: { buffer: timeUniformBuffer } },
{ binding: 2, resource: { buffer: cellStateStorage[1] } },
+ { binding: 3, resource: { buffer: cellStateStorage[0] } },
],
}),
]
@@ -155,6 +213,7 @@ class Renderer {
private device: GPUDevice,
private context: GPUCanvasContext,
private cellPipeline: GPURenderPipeline,
+ private simulationPipeline: GPUComputePipeline,
private vertexBuffer: GPUBuffer,
private bindGroups: GPUBindGroup[],
private timeBuffer: GPUBuffer,
@@ -173,19 +232,29 @@ class Renderer {
}
private updateBindGroupIndex(timeMs: number): void {
- if (timeMs - this.lastBindGroupSwitchTime >= 500) {
+ if (timeMs - this.lastBindGroupSwitchTime >= COMPUTE_MS_INTERVAL) {
this.bindGroupIndex = (this.bindGroupIndex + 1) % this.bindGroups.length
this.lastBindGroupSwitchTime = timeMs
}
}
public async render(timeMs: number): Promise {
+ const encoder = this.device.createCommandEncoder()
+
+ const computePass = encoder.beginComputePass()
+ computePass.setPipeline(this.simulationPipeline)
+ computePass.setBindGroup(0, this.bindGroups[this.bindGroupIndex])
+
+ const workgroupCount = Math.ceil(GRID_SIZE / 8)
+ computePass.dispatchWorkgroups(workgroupCount, workgroupCount)
+
+ computePass.end()
+
const time = timeMs / 1000
this.updateFpsCount(timeMs)
- this.updateBindGroupIndex(timeMs)
this.device.queue.writeBuffer(this.timeBuffer, 0, new Float32Array([time]))
+ this.updateBindGroupIndex(timeMs)
- const encoder = this.device.createCommandEncoder()
const pass = encoder.beginRenderPass({
colorAttachments: [
{
@@ -215,17 +284,24 @@ async function init() {
const context = configureContext(await getCanvas(), device)
const canvasFormat = navigator.gpu.getPreferredCanvasFormat()
- const cellPipeline = await createCellPipeline(device, canvasFormat)
const gridUniformBuffer = createGridUniformBuffer(device)
- const timeBuffer = createTimeBuffer(device)
+ const timeBuffer = createTimeUniformBuffer(device)
const cellStateStorage = createStateStorageBuffers(device)
+ const bindGroupLayout = createBindGroupLayout(device)
const bindGroups = createBindGroups(
device,
- cellPipeline,
gridUniformBuffer,
timeBuffer,
cellStateStorage,
+ bindGroupLayout,
)
+ const pipelineLayout = createPipelineLayout(device, bindGroupLayout)
+ const cellPipeline = await createCellPipeline(
+ device,
+ canvasFormat,
+ pipelineLayout,
+ )
+ const simulationPipeline = await createComputePipeline(device, pipelineLayout)
const vertexBuffer = createVertexBuffer(device)
const vertices = new Float32Array([
-0.8, -0.8, 0.8, -0.8, 0.8, 0.8, -0.8, -0.8, 0.8, 0.8, -0.8, 0.8,
@@ -235,6 +311,7 @@ async function init() {
device,
context,
cellPipeline,
+ simulationPipeline,
vertexBuffer,
bindGroups,
timeBuffer,
diff --git a/src/shaders/simulation.wgsl b/src/shaders/simulation.wgsl
new file mode 100644
index 0000000..258cc5b
--- /dev/null
+++ b/src/shaders/simulation.wgsl
@@ -0,0 +1,40 @@
+@group(0) @binding(0) var grid: vec2f;
+
+@group(0) @binding(2) var cellStateIn: array;
+@group(0) @binding(3) var cellStateOut: array;
+
+fn cellIndex(cell: vec2u) -> u32 {
+ return (cell.y % u32(grid.y)) * u32(grid.x) + (cell.x % u32(grid.x));
+}
+
+fn cellActive(x: u32, y: u32) -> u32 {
+ return cellStateIn[cellIndex(vec2(x, y))];
+}
+
+@compute
+@workgroup_size(8, 8)
+fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
+ let activeNeighbors =
+ cellActive(cell.x+1, cell.y+1) +
+ cellActive(cell.x+1, cell.y) +
+ cellActive(cell.x+1, cell.y-1) +
+ cellActive(cell.x, cell.y-1) +
+ cellActive(cell.x-1, cell.y-1) +
+ cellActive(cell.x-1, cell.y) +
+ cellActive(cell.x-1, cell.y+1) +
+ cellActive(cell.x, cell.y+1);
+
+ let i = cellIndex(cell.xy);
+
+ switch activeNeighbors {
+ case 2: {
+ cellStateOut[i] = cellStateIn[i];
+ }
+ case 3: {
+ cellStateOut[i] = 1;
+ }
+ default: {
+ cellStateOut[i] = 0;
+ }
+ }
+}