From 63420f39a1045152265caf9969c7af093bbb0bb9 Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Sun, 6 Apr 2025 02:51:45 +0200 Subject: [PATCH] feat: add FPS display & grid state management --- index.html | 1 + src/main.ts | 92 ++++++++++++++++++++++++++++++++++++------- src/shaders/cell.wgsl | 39 +++++++++--------- src/style.css | 7 ++++ 4 files changed, 106 insertions(+), 33 deletions(-) diff --git a/index.html b/index.html index b7e5d24..b598337 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,7 @@ +

FPS:

diff --git a/src/main.ts b/src/main.ts index f6920eb..9a53ce5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -83,20 +83,52 @@ function createTimeBuffer(device: GPUDevice): GPUBuffer { }) } -function createBindGroup( +function createStateStorageBuffers(device: GPUDevice): GPUBuffer[] { + const cellStateArray = new Uint32Array(GRID_SIZE * GRID_SIZE) + const size = cellStateArray.byteLength + const usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST + const cellStateStorage = [ + 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 + } + 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 createBindGroups( device: GPUDevice, cellPipeline: GPURenderPipeline, gridUniformBuffer: GPUBuffer, timeBuffer: GPUBuffer, -): GPUBindGroup { - return device.createBindGroup({ - label: "Cell renderer bind group", - layout: cellPipeline.getBindGroupLayout(0), - entries: [ - { binding: 0, resource: { buffer: gridUniformBuffer } }, - { binding: 1, resource: { buffer: timeBuffer } }, - ], - }) + cellStateStorage: GPUBuffer[], +): GPUBindGroup[] { + return [ + device.createBindGroup({ + label: "Cell renderer bind group A", + layout: cellPipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: { buffer: gridUniformBuffer } }, + { binding: 1, resource: { buffer: timeBuffer } }, + { binding: 2, resource: { buffer: cellStateStorage[0] } }, + ], + }), + device.createBindGroup({ + label: "Cell updater bind group B", + layout: cellPipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: { buffer: gridUniformBuffer } }, + { binding: 1, resource: { buffer: timeBuffer } }, + { binding: 2, resource: { buffer: cellStateStorage[1] } }, + ], + }), + ] } function createVertexBuffer(device: GPUDevice): GPUBuffer { @@ -113,18 +145,44 @@ function createVertexBuffer(device: GPUDevice): GPUBuffer { } class Renderer { + private lastFrameTime: number = 0 + private frameCount: number = 0 + private fps: number = 0 + private bindGroupIndex: number = 0 + private lastBindGroupSwitchTime: number = 0 + constructor( private device: GPUDevice, private context: GPUCanvasContext, private cellPipeline: GPURenderPipeline, private vertexBuffer: GPUBuffer, - private bindGroup: GPUBindGroup, + private bindGroups: GPUBindGroup[], private timeBuffer: GPUBuffer, private vertices: Float32Array, ) {} - public render(timeMs: number): void { + private updateFpsCount(timeMs: number): void { + if (this.lastFrameTime === 0) this.lastFrameTime = timeMs + this.frameCount++ + if (timeMs - this.lastFrameTime >= 1000) { + this.fps = this.frameCount + this.frameCount = 0 + this.lastFrameTime = timeMs + document.querySelector("#fps")!.textContent = `FPS: ${this.fps}` + } + } + + private updateBindGroupIndex(timeMs: number): void { + if (timeMs - this.lastBindGroupSwitchTime >= 500) { + this.bindGroupIndex = (this.bindGroupIndex + 1) % this.bindGroups.length + this.lastBindGroupSwitchTime = timeMs + } + } + + public async render(timeMs: number): Promise { const time = timeMs / 1000 + this.updateFpsCount(timeMs) + this.updateBindGroupIndex(timeMs) this.device.queue.writeBuffer(this.timeBuffer, 0, new Float32Array([time])) const encoder = this.device.createCommandEncoder() @@ -141,7 +199,9 @@ class Renderer { pass.setPipeline(this.cellPipeline) pass.setVertexBuffer(0, this.vertexBuffer) - pass.setBindGroup(0, this.bindGroup) + + pass.setBindGroup(0, this.bindGroups[this.bindGroupIndex]) + pass.draw(this.vertices.length / 2, GRID_SIZE * GRID_SIZE) pass.end() @@ -158,11 +218,13 @@ async function init() { const cellPipeline = await createCellPipeline(device, canvasFormat) const gridUniformBuffer = createGridUniformBuffer(device) const timeBuffer = createTimeBuffer(device) - const bindGroup = createBindGroup( + const cellStateStorage = createStateStorageBuffers(device) + const bindGroups = createBindGroups( device, cellPipeline, gridUniformBuffer, timeBuffer, + cellStateStorage, ) const vertexBuffer = createVertexBuffer(device) const vertices = new Float32Array([ @@ -174,7 +236,7 @@ async function init() { context, cellPipeline, vertexBuffer, - bindGroup, + bindGroups, timeBuffer, vertices, ) diff --git a/src/shaders/cell.wgsl b/src/shaders/cell.wgsl index cb74dcb..afe46a5 100644 --- a/src/shaders/cell.wgsl +++ b/src/shaders/cell.wgsl @@ -1,40 +1,43 @@ @group(0) @binding(0) var grid: vec2f; @group(0) @binding(1) var time: f32; +@group(0) @binding(2) var cellState: array; const TAU: f32 = 6.28318530718; struct VertexInput { - @location(0) pos: vec2f, - @builtin(instance_index) instance: u32, + @location(0) pos: vec2f, + @builtin(instance_index) instance: u32, }; struct VertexOutput { - @builtin(position) pos: vec4f, - @location(0) cell: vec2f, + @builtin(position) pos: vec4f, + @location(0) cell: vec2f, }; @vertex fn vertexMain(input: VertexInput) -> VertexOutput { - let i = f32(input.instance); - let cell = vec2f(i % grid.x, floor(i / grid.x)); - let cellOffset = cell / grid * 2.0; - let gridPos = (input.pos + 1.0) / grid - 1.0 + cellOffset; + let i = f32(input.instance); + let cell = vec2f(i % grid.x, floor(i / grid.x)); + let state = f32(cellState[input.instance]); - var output: VertexOutput; - output.pos = vec4f(gridPos, 0.0, 1.0); - output.cell = cell; - return output; + let cellOffset = cell / grid * 2.0; + let gridPos = (input.pos * state + 1.0) / grid - 1.0 + cellOffset; + + var output: VertexOutput; + output.pos = vec4f(gridPos, 0.0, 1.0); + output.cell = cell; + return output; } fn hueToRGB(hue: f32) -> vec3f { - return 0.5 + 0.5 * sin(TAU * (vec3f(hue) + vec3f(0.0, 0.33, 0.66))); + return 0.5 + 0.5 * sin(TAU * (vec3f(hue) + vec3f(0.0, 0.33, 0.66))); } @fragment fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { - let delta = input.cell - grid * 0.5; - let angle = atan2(delta.y, delta.x); - let normalizedAngle = (angle / TAU) + 0.5; - let hue = fract(normalizedAngle + time * 0.3); - return vec4f(hueToRGB(hue), 1.0); + let delta = input.cell - grid * 0.5; + let angle = atan2(delta.y, delta.x); + let normalizedAngle = (angle / TAU) + 0.5; + let hue = fract(normalizedAngle + time * 0.3); + return vec4f(hueToRGB(hue), 1.0); } diff --git a/src/style.css b/src/style.css index 2136832..741704d 100644 --- a/src/style.css +++ b/src/style.css @@ -1,11 +1,18 @@ body { margin: 0; padding: 0; + display: flex; justify-content: center; align-items: center; + flex-direction: column; + height: 100vh; + background-color: #222; + + color: #fff; + font-family: "Courier New", Courier, monospace; } canvas {