mirror of
https://github.com/Floriansylvain/WhateverWebGPU.git
synced 2026-08-19 19:53:18 +02:00
feat: game of life compute shader
This commit is contained in:
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
<title>Whatever WebGPU</title>
|
<title>Whatever WebGPU</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="canvas" width="512" height="512"></canvas>
|
<canvas id="canvas" width="1024" height="1024"></canvas>
|
||||||
<p id="fps">FPS:</p>
|
<p id="fps">FPS:</p>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+98
-21
@@ -1,6 +1,7 @@
|
|||||||
import "./style.css"
|
import "./style.css"
|
||||||
|
|
||||||
const GRID_SIZE = 32
|
const GRID_SIZE = 64
|
||||||
|
const COMPUTE_MS_INTERVAL = 100
|
||||||
|
|
||||||
async function getCanvas(): Promise<HTMLCanvasElement> {
|
async function getCanvas(): Promise<HTMLCanvasElement> {
|
||||||
const canvas = document.querySelector<HTMLCanvasElement>("#canvas")
|
const canvas = document.querySelector<HTMLCanvasElement>("#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<GPUComputePipeline> {
|
||||||
|
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(
|
async function createCellPipeline(
|
||||||
device: GPUDevice,
|
device: GPUDevice,
|
||||||
canvasFormat: GPUTextureFormat,
|
canvasFormat: GPUTextureFormat,
|
||||||
|
pipelineLayout: GPUPipelineLayout,
|
||||||
): Promise<GPURenderPipeline> {
|
): Promise<GPURenderPipeline> {
|
||||||
const cellShaderModule = device.createShaderModule({
|
const cellShaderModule = device.createShaderModule({
|
||||||
label: "Cell shader",
|
label: "Cell shader",
|
||||||
@@ -50,7 +79,7 @@ async function createCellPipeline(
|
|||||||
|
|
||||||
return device.createRenderPipeline({
|
return device.createRenderPipeline({
|
||||||
label: "Cell pipeline",
|
label: "Cell pipeline",
|
||||||
layout: "auto",
|
layout: pipelineLayout,
|
||||||
vertex: {
|
vertex: {
|
||||||
module: cellShaderModule,
|
module: cellShaderModule,
|
||||||
entryPoint: "vertexMain",
|
entryPoint: "vertexMain",
|
||||||
@@ -75,7 +104,7 @@ function createGridUniformBuffer(device: GPUDevice): GPUBuffer {
|
|||||||
return gridUniformBuffer
|
return gridUniformBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTimeBuffer(device: GPUDevice): GPUBuffer {
|
function createTimeUniformBuffer(device: GPUDevice): GPUBuffer {
|
||||||
return device.createBuffer({
|
return device.createBuffer({
|
||||||
label: "Time Uniform",
|
label: "Time Uniform",
|
||||||
size: 4,
|
size: 4,
|
||||||
@@ -91,41 +120,70 @@ function createStateStorageBuffers(device: GPUDevice): GPUBuffer[] {
|
|||||||
device.createBuffer({ label: "Cell State A", size, usage }),
|
device.createBuffer({ label: "Cell State A", size, usage }),
|
||||||
device.createBuffer({ label: "Cell State B", size, usage }),
|
device.createBuffer({ label: "Cell State B", size, usage }),
|
||||||
]
|
]
|
||||||
for (let i = 0; i < cellStateArray.length; i += 3) {
|
for (let i = 0; i < cellStateArray.length; ++i) {
|
||||||
cellStateArray[i] = 1
|
cellStateArray[i] = Math.random() > 0.6 ? 1 : 0
|
||||||
}
|
}
|
||||||
device.queue.writeBuffer(cellStateStorage[0], 0, cellStateArray)
|
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
|
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(
|
function createBindGroups(
|
||||||
device: GPUDevice,
|
device: GPUDevice,
|
||||||
cellPipeline: GPURenderPipeline,
|
|
||||||
gridUniformBuffer: GPUBuffer,
|
gridUniformBuffer: GPUBuffer,
|
||||||
timeBuffer: GPUBuffer,
|
timeUniformBuffer: GPUBuffer,
|
||||||
cellStateStorage: GPUBuffer[],
|
cellStateStorage: GPUBuffer[],
|
||||||
|
bindGroupLayout: GPUBindGroupLayout,
|
||||||
): GPUBindGroup[] {
|
): GPUBindGroup[] {
|
||||||
return [
|
return [
|
||||||
device.createBindGroup({
|
device.createBindGroup({
|
||||||
label: "Cell renderer bind group A",
|
label: "Cell renderer bind group A",
|
||||||
layout: cellPipeline.getBindGroupLayout(0),
|
layout: bindGroupLayout,
|
||||||
entries: [
|
entries: [
|
||||||
{ binding: 0, resource: { buffer: gridUniformBuffer } },
|
{ binding: 0, resource: { buffer: gridUniformBuffer } },
|
||||||
{ binding: 1, resource: { buffer: timeBuffer } },
|
{ binding: 1, resource: { buffer: timeUniformBuffer } },
|
||||||
{ binding: 2, resource: { buffer: cellStateStorage[0] } },
|
{ binding: 2, resource: { buffer: cellStateStorage[0] } },
|
||||||
|
{ binding: 3, resource: { buffer: cellStateStorage[1] } },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
device.createBindGroup({
|
device.createBindGroup({
|
||||||
label: "Cell updater bind group B",
|
label: "Cell updater bind group B",
|
||||||
layout: cellPipeline.getBindGroupLayout(0),
|
layout: bindGroupLayout,
|
||||||
entries: [
|
entries: [
|
||||||
{ binding: 0, resource: { buffer: gridUniformBuffer } },
|
{ binding: 0, resource: { buffer: gridUniformBuffer } },
|
||||||
{ binding: 1, resource: { buffer: timeBuffer } },
|
{ binding: 1, resource: { buffer: timeUniformBuffer } },
|
||||||
{ binding: 2, resource: { buffer: cellStateStorage[1] } },
|
{ binding: 2, resource: { buffer: cellStateStorage[1] } },
|
||||||
|
{ binding: 3, resource: { buffer: cellStateStorage[0] } },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
@@ -155,6 +213,7 @@ class Renderer {
|
|||||||
private device: GPUDevice,
|
private device: GPUDevice,
|
||||||
private context: GPUCanvasContext,
|
private context: GPUCanvasContext,
|
||||||
private cellPipeline: GPURenderPipeline,
|
private cellPipeline: GPURenderPipeline,
|
||||||
|
private simulationPipeline: GPUComputePipeline,
|
||||||
private vertexBuffer: GPUBuffer,
|
private vertexBuffer: GPUBuffer,
|
||||||
private bindGroups: GPUBindGroup[],
|
private bindGroups: GPUBindGroup[],
|
||||||
private timeBuffer: GPUBuffer,
|
private timeBuffer: GPUBuffer,
|
||||||
@@ -173,19 +232,29 @@ class Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateBindGroupIndex(timeMs: number): void {
|
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.bindGroupIndex = (this.bindGroupIndex + 1) % this.bindGroups.length
|
||||||
this.lastBindGroupSwitchTime = timeMs
|
this.lastBindGroupSwitchTime = timeMs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async render(timeMs: number): Promise<void> {
|
public async render(timeMs: number): Promise<void> {
|
||||||
|
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
|
const time = timeMs / 1000
|
||||||
this.updateFpsCount(timeMs)
|
this.updateFpsCount(timeMs)
|
||||||
this.updateBindGroupIndex(timeMs)
|
|
||||||
this.device.queue.writeBuffer(this.timeBuffer, 0, new Float32Array([time]))
|
this.device.queue.writeBuffer(this.timeBuffer, 0, new Float32Array([time]))
|
||||||
|
this.updateBindGroupIndex(timeMs)
|
||||||
|
|
||||||
const encoder = this.device.createCommandEncoder()
|
|
||||||
const pass = encoder.beginRenderPass({
|
const pass = encoder.beginRenderPass({
|
||||||
colorAttachments: [
|
colorAttachments: [
|
||||||
{
|
{
|
||||||
@@ -215,17 +284,24 @@ async function init() {
|
|||||||
const context = configureContext(await getCanvas(), device)
|
const context = configureContext(await getCanvas(), device)
|
||||||
const canvasFormat = navigator.gpu.getPreferredCanvasFormat()
|
const canvasFormat = navigator.gpu.getPreferredCanvasFormat()
|
||||||
|
|
||||||
const cellPipeline = await createCellPipeline(device, canvasFormat)
|
|
||||||
const gridUniformBuffer = createGridUniformBuffer(device)
|
const gridUniformBuffer = createGridUniformBuffer(device)
|
||||||
const timeBuffer = createTimeBuffer(device)
|
const timeBuffer = createTimeUniformBuffer(device)
|
||||||
const cellStateStorage = createStateStorageBuffers(device)
|
const cellStateStorage = createStateStorageBuffers(device)
|
||||||
|
const bindGroupLayout = createBindGroupLayout(device)
|
||||||
const bindGroups = createBindGroups(
|
const bindGroups = createBindGroups(
|
||||||
device,
|
device,
|
||||||
cellPipeline,
|
|
||||||
gridUniformBuffer,
|
gridUniformBuffer,
|
||||||
timeBuffer,
|
timeBuffer,
|
||||||
cellStateStorage,
|
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 vertexBuffer = createVertexBuffer(device)
|
||||||
const vertices = new Float32Array([
|
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,
|
-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,
|
device,
|
||||||
context,
|
context,
|
||||||
cellPipeline,
|
cellPipeline,
|
||||||
|
simulationPipeline,
|
||||||
vertexBuffer,
|
vertexBuffer,
|
||||||
bindGroups,
|
bindGroups,
|
||||||
timeBuffer,
|
timeBuffer,
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
@group(0) @binding(0) var<uniform> grid: vec2f;
|
||||||
|
|
||||||
|
@group(0) @binding(2) var<storage> cellStateIn: array<u32>;
|
||||||
|
@group(0) @binding(3) var<storage, read_write> cellStateOut: array<u32>;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user