feat: add FPS display & grid state management

This commit is contained in:
Florian Sylvain
2025-04-06 02:51:45 +02:00
parent c6b9d1a9bc
commit 63420f39a1
4 changed files with 106 additions and 33 deletions
+1
View File
@@ -8,6 +8,7 @@
</head> </head>
<body> <body>
<canvas id="canvas" width="512" height="512"></canvas> <canvas id="canvas" width="512" height="512"></canvas>
<p id="fps">FPS:</p>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
+77 -15
View File
@@ -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, device: GPUDevice,
cellPipeline: GPURenderPipeline, cellPipeline: GPURenderPipeline,
gridUniformBuffer: GPUBuffer, gridUniformBuffer: GPUBuffer,
timeBuffer: GPUBuffer, timeBuffer: GPUBuffer,
): GPUBindGroup { cellStateStorage: GPUBuffer[],
return device.createBindGroup({ ): GPUBindGroup[] {
label: "Cell renderer bind group", return [
layout: cellPipeline.getBindGroupLayout(0), device.createBindGroup({
entries: [ label: "Cell renderer bind group A",
{ binding: 0, resource: { buffer: gridUniformBuffer } }, layout: cellPipeline.getBindGroupLayout(0),
{ binding: 1, resource: { buffer: timeBuffer } }, 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 { function createVertexBuffer(device: GPUDevice): GPUBuffer {
@@ -113,18 +145,44 @@ function createVertexBuffer(device: GPUDevice): GPUBuffer {
} }
class Renderer { class Renderer {
private lastFrameTime: number = 0
private frameCount: number = 0
private fps: number = 0
private bindGroupIndex: number = 0
private lastBindGroupSwitchTime: number = 0
constructor( constructor(
private device: GPUDevice, private device: GPUDevice,
private context: GPUCanvasContext, private context: GPUCanvasContext,
private cellPipeline: GPURenderPipeline, private cellPipeline: GPURenderPipeline,
private vertexBuffer: GPUBuffer, private vertexBuffer: GPUBuffer,
private bindGroup: GPUBindGroup, private bindGroups: GPUBindGroup[],
private timeBuffer: GPUBuffer, private timeBuffer: GPUBuffer,
private vertices: Float32Array, 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<void> {
const time = timeMs / 1000 const time = timeMs / 1000
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]))
const encoder = this.device.createCommandEncoder() const encoder = this.device.createCommandEncoder()
@@ -141,7 +199,9 @@ class Renderer {
pass.setPipeline(this.cellPipeline) pass.setPipeline(this.cellPipeline)
pass.setVertexBuffer(0, this.vertexBuffer) 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.draw(this.vertices.length / 2, GRID_SIZE * GRID_SIZE)
pass.end() pass.end()
@@ -158,11 +218,13 @@ async function init() {
const cellPipeline = await createCellPipeline(device, canvasFormat) const cellPipeline = await createCellPipeline(device, canvasFormat)
const gridUniformBuffer = createGridUniformBuffer(device) const gridUniformBuffer = createGridUniformBuffer(device)
const timeBuffer = createTimeBuffer(device) const timeBuffer = createTimeBuffer(device)
const bindGroup = createBindGroup( const cellStateStorage = createStateStorageBuffers(device)
const bindGroups = createBindGroups(
device, device,
cellPipeline, cellPipeline,
gridUniformBuffer, gridUniformBuffer,
timeBuffer, timeBuffer,
cellStateStorage,
) )
const vertexBuffer = createVertexBuffer(device) const vertexBuffer = createVertexBuffer(device)
const vertices = new Float32Array([ const vertices = new Float32Array([
@@ -174,7 +236,7 @@ async function init() {
context, context,
cellPipeline, cellPipeline,
vertexBuffer, vertexBuffer,
bindGroup, bindGroups,
timeBuffer, timeBuffer,
vertices, vertices,
) )
+21 -18
View File
@@ -1,40 +1,43 @@
@group(0) @binding(0) var<uniform> grid: vec2f; @group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<uniform> time: f32; @group(0) @binding(1) var<uniform> time: f32;
@group(0) @binding(2) var<storage> cellState: array<u32>;
const TAU: f32 = 6.28318530718; const TAU: f32 = 6.28318530718;
struct VertexInput { struct VertexInput {
@location(0) pos: vec2f, @location(0) pos: vec2f,
@builtin(instance_index) instance: u32, @builtin(instance_index) instance: u32,
}; };
struct VertexOutput { struct VertexOutput {
@builtin(position) pos: vec4f, @builtin(position) pos: vec4f,
@location(0) cell: vec2f, @location(0) cell: vec2f,
}; };
@vertex @vertex
fn vertexMain(input: VertexInput) -> VertexOutput { fn vertexMain(input: VertexInput) -> VertexOutput {
let i = f32(input.instance); let i = f32(input.instance);
let cell = vec2f(i % grid.x, floor(i / grid.x)); let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2.0; let state = f32(cellState[input.instance]);
let gridPos = (input.pos + 1.0) / grid - 1.0 + cellOffset;
var output: VertexOutput; let cellOffset = cell / grid * 2.0;
output.pos = vec4f(gridPos, 0.0, 1.0); let gridPos = (input.pos * state + 1.0) / grid - 1.0 + cellOffset;
output.cell = cell;
return output; var output: VertexOutput;
output.pos = vec4f(gridPos, 0.0, 1.0);
output.cell = cell;
return output;
} }
fn hueToRGB(hue: f32) -> vec3f { 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 @fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
let delta = input.cell - grid * 0.5; let delta = input.cell - grid * 0.5;
let angle = atan2(delta.y, delta.x); let angle = atan2(delta.y, delta.x);
let normalizedAngle = (angle / TAU) + 0.5; let normalizedAngle = (angle / TAU) + 0.5;
let hue = fract(normalizedAngle + time * 0.3); let hue = fract(normalizedAngle + time * 0.3);
return vec4f(hueToRGB(hue), 1.0); return vec4f(hueToRGB(hue), 1.0);
} }
+7
View File
@@ -1,11 +1,18 @@
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-direction: column;
height: 100vh; height: 100vh;
background-color: #222; background-color: #222;
color: #fff;
font-family: "Courier New", Courier, monospace;
} }
canvas { canvas {