Skip to content

Commit 2e2d403

Browse files
committed
basic webGPU setup
1 parent a0c7fb1 commit 2e2d403

File tree

5 files changed

+191
-2
lines changed

5 files changed

+191
-2
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@group(0) @binding(0) var<storage, read_write> data: array<f32>;
2+
3+
@compute @workgroup_size(1) fn computeSomething(
4+
@builtin(global_invocation_id) id: vec3u
5+
) {
6+
let i = id.x;
7+
data[i] = data[i] * 2.0;
8+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
@vertex fn vs(
2+
@builtin(vertex_index) vertexIndex : u32
3+
) -> @builtin(position) vec4f {
4+
let pos = array(
5+
vec2f( 0.0, 0.5), // top center
6+
vec2f(-0.5, -0.5), // bottom left
7+
vec2f( 0.5, -0.5) // bottom right
8+
);
9+
10+
return vec4f(pos[vertexIndex], 0.0, 1.0);
11+
}
12+
13+
@fragment fn fs() -> @location(0) vec4f {
14+
return vec4f(1.0, 0.0, 0.0, 1.0);
15+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import styles from './styles.module.css'
2+
import { Head } from "#components/Head"
3+
import type { RouteMeta } from "#router"
4+
import { use, useEffect, useRef, useState } from "react"
5+
import { getFormValue } from "#components/getFormValue"
6+
import { makeFrameCounter } from "#components/makeFrameCounter"
7+
import shader from './foo.wgsl?raw'
8+
import compute from './compute.wgsl?raw'
9+
10+
export const meta: RouteMeta = {
11+
title: 'Particle Life GPU',
12+
tags: ['simulation', 'webgpu', 'particles', 'wip'],
13+
}
14+
15+
export default function ParticleLifeGPUPage() {
16+
const canvasRef = useRef<HTMLCanvasElement>(null)
17+
const [supported] = useState(() => Boolean(navigator.gpu))
18+
const [fps, setFps] = useState(0)
19+
20+
useEffect(() => {
21+
if (!supported) return
22+
const canvas = canvasRef.current!
23+
const controller = new AbortController()
24+
25+
canvas.width = window.innerWidth * devicePixelRatio
26+
canvas.height = window.innerHeight * devicePixelRatio
27+
28+
const frameCounter = makeFrameCounter(60)
29+
start(controller.signal, canvas, (dt) => setFps(Math.round(frameCounter(dt / 1000))))
30+
31+
return () => {
32+
controller.abort()
33+
}
34+
}, [supported])
35+
36+
return (
37+
<div className={styles.main}>
38+
<div className={styles.head}>
39+
<Head />
40+
{supported && <pre>{fps} FPS</pre>}
41+
{!supported && <pre>Your browser does not support WebGPU.</pre>}
42+
</div>
43+
<canvas ref={canvasRef} />
44+
</div>
45+
)
46+
}
47+
48+
async function start(
49+
signal: AbortSignal,
50+
canvas: HTMLCanvasElement,
51+
onFrame: (dt: number) => void,
52+
) {
53+
const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' })
54+
if (!adapter) throw new Error('No GPU adapter found')
55+
if (signal.aborted) return
56+
57+
const device = await adapter.requestDevice()
58+
if (!device) throw new Error('No GPU device found')
59+
if (signal.aborted) return
60+
signal.addEventListener('abort', () => device.destroy(), { once: true })
61+
62+
const ctx = canvas.getContext('webgpu')!
63+
if (!ctx) throw new Error('No WebGPU context found')
64+
const format = navigator.gpu.getPreferredCanvasFormat()
65+
ctx.configure({ device, format, alphaMode: 'opaque' })
66+
signal.addEventListener('abort', () => ctx.unconfigure(), { once: true })
67+
68+
const module = device.createShaderModule({ code: shader, label: 'our hardcoded red triangle shaders' })
69+
70+
const pipeline = device.createRenderPipeline({
71+
label: 'our hardcoded red triangle pipeline',
72+
layout: 'auto',
73+
vertex: {
74+
entryPoint: 'vs',
75+
module,
76+
},
77+
fragment: {
78+
entryPoint: 'fs',
79+
module,
80+
targets: [{ format }],
81+
},
82+
})
83+
84+
85+
const renderPassDescriptor = {
86+
label: 'our basic canvas renderPass',
87+
colorAttachments: [
88+
{
89+
view: null! as GPUTextureView,
90+
clearValue: [0.3, 0.3, 0.3, 1],
91+
loadOp: 'clear',
92+
storeOp: 'store',
93+
},
94+
],
95+
} satisfies GPURenderPassDescriptor
96+
97+
function render() {
98+
// Get the current texture from the canvas context and
99+
// set it as the texture to render to.
100+
renderPassDescriptor.colorAttachments[0]!.view =
101+
ctx.getCurrentTexture().createView()
102+
103+
const encoder = device.createCommandEncoder({ label: 'our encoder' })
104+
const pass = encoder.beginRenderPass(renderPassDescriptor)
105+
pass.setPipeline(pipeline)
106+
pass.draw(3)
107+
pass.end()
108+
109+
const commandBuffer = encoder.finish()
110+
device.queue.submit([commandBuffer])
111+
}
112+
113+
let lastTime = performance.now()
114+
let rafId = requestAnimationFrame(function frame(time) {
115+
if (signal.aborted) return
116+
rafId = requestAnimationFrame(frame)
117+
const dt = time - lastTime
118+
lastTime = time
119+
onFrame(dt)
120+
render()
121+
})
122+
signal.addEventListener('abort', () => cancelAnimationFrame(rafId), { once: true })
123+
124+
const width = ctx.canvas.width
125+
const height = ctx.canvas.height
126+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
.main {
2+
margin: 0;
3+
background: #051016;
4+
color: white;
5+
touch-action: none;
6+
width: 100vw;
7+
height: 100svh;
8+
padding: 1em;
9+
10+
position: fixed;
11+
inset: 0;
12+
overflow: auto;
13+
14+
canvas {
15+
position: fixed;
16+
inset: 0;
17+
width: 100%;
18+
height: 100%;
19+
z-index: 0;
20+
pointer-events: none;
21+
}
22+
}
23+
24+
.head {
25+
position: relative;
26+
z-index: 1;
27+
user-select: none;
28+
width: fit-content;
29+
}

src/router.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import ants_shader_image from "./pages/ants-shader/screen.png"
3838
import ants_image from "./pages/ants/screen.png"
3939
import a_star_image from "./pages/a-star/screen.png"
4040

41-
export type Routes = "wave-function-collapse-ascii" | "wave-function-collapse" | "visual-exec" | "tinkerbell-map" | "swarm-pathfinding" | "star-rating" | "spring-fluid" | "spider-inverse-kinematics" | "snakebird" | "quad-tree-collisions" | "quad-tree" | "pong-pang" | "polka-fireflies" | "perlin-ripples" | "particle-life" | "paint-worklet" | "pacman" | "normal-map" | "neat" | "modern-modal" | "minesweeper" | "maze-generation" | "lightning" | "intl-tuesday" | "hex-a-star" | "hacker-background" | "grainy-texture" | "fragment-portal" | "fourrier-series" | "flow-field" | "flask" | "fireflies" | "deterministic-plinko" | "cursor-projection" | "collision-threads" | "cellular-automata" | "boids" | "bird-inverse-kinematics" | "ants-shader" | "ants" | "a-star"
41+
export type Routes = "wave-function-collapse-ascii" | "wave-function-collapse" | "visual-exec" | "tinkerbell-map" | "swarm-pathfinding" | "star-rating" | "spring-fluid" | "spider-inverse-kinematics" | "snakebird" | "quad-tree-collisions" | "quad-tree" | "pong-pang" | "polka-fireflies" | "perlin-ripples" | "particle-life-gpu" | "particle-life" | "paint-worklet" | "pacman" | "normal-map" | "neat" | "modern-modal" | "minesweeper" | "maze-generation" | "lightning" | "intl-tuesday" | "hex-a-star" | "hacker-background" | "grainy-texture" | "fragment-portal" | "fourrier-series" | "flow-field" | "flask" | "fireflies" | "deterministic-plinko" | "cursor-projection" | "collision-threads" | "cellular-automata" | "boids" | "bird-inverse-kinematics" | "ants-shader" | "ants" | "a-star"
4242

4343
export type RouteMeta = {
4444
title: string
@@ -227,6 +227,17 @@ export const ROUTES = {
227227
firstAdded: 1721823247000
228228
},
229229
},
230+
"particle-life-gpu": {
231+
Component: lazy(() => import("./pages/particle-life-gpu/index.tsx")),
232+
meta: {
233+
title: 'Particle Life GPU',
234+
tags: ['simulation', 'webgpu', 'particles', 'wip'],
235+
},
236+
git: {
237+
lastModified: 0,
238+
firstAdded: 0
239+
},
240+
},
230241
"particle-life": {
231242
Component: lazy(() => import("./pages/particle-life/index.tsx")),
232243
meta: {
@@ -235,7 +246,7 @@ export const ROUTES = {
235246
tags: ['simulation', 'canvas', 'particles'],
236247
},
237248
git: {
238-
lastModified: 1762282920000,
249+
lastModified: 1762433687000,
239250
firstAdded: 1760880322000
240251
},
241252
},

0 commit comments

Comments
 (0)