-
Notifications
You must be signed in to change notification settings - Fork 285
Expand file tree
/
Copy path07-webgpu.tsx
More file actions
132 lines (112 loc) · 4.29 KB
/
Copy path07-webgpu.tsx
File metadata and controls
132 lines (112 loc) · 4.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/* WebGPU owning a <surface>: a triangle whose colors cycle with the playhead.
*
* cp examples/07-webgpu.tsx ~/Projects/webgpu/index.tsx
* diffusion open ~/Projects/webgpu
*
* `ref={surfaceRef}` assigns the surface's node, whose `element` is its
* detached canvas; onMount a WebGPU context takes it over, and the engine
* samples the bitmap into the node's box every frame. Device setup is async,
* so the draw effect is created synchronously in the component body and a
* signal wakes it once the pipeline exists — and `hold` keeps an export from
* writing the frames that setup hasn't finished for, since a capture or an
* export mounts the module again and sets up a device of its own.
* Composition time feeds a uniform and the fragment shader derives the colors
* from it (a phase-shifted cosine palette), so the playhead is the only
* clock: scrubbing and exports stay frame-accurate.
* Fails with a console error where WebGPU is unavailable.
*/
import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
import { useTicker } from "@diffusionstudio/jsx";
import type { SceneNode } from "@diffusionstudio/jsx";
const SHADER = /* wgsl */ `
@group(0) @binding(0) var<uniform> time: f32;
struct VSOut {
@builtin(position) position: vec4f,
@location(0) local: vec2f,
}
@vertex
fn vs(@builtin(vertex_index) i: u32) -> VSOut {
var corners = array<vec2f, 3>(vec2f(0.0, 0.75), vec2f(-0.8, -0.6), vec2f(0.8, -0.6));
var out: VSOut;
out.position = vec4f(corners[i], 0.0, 1.0);
out.local = corners[i];
return out;
}
@fragment
fn fs(in: VSOut) -> @location(0) vec4f {
// cosine palette; each corner leads the cycle by a different phase
let phase = in.local.x * 1.5 + in.local.y;
let rgb = 0.5 + 0.5 * cos(time + phase + vec3f(0.0, 2.094, 4.188));
return vec4f(rgb, 1.0);
}
`;
type Gpu = {
device: GPUDevice;
context: GPUCanvasContext;
pipeline: GPURenderPipeline;
uniforms: GPUBuffer;
bindGroup: GPUBindGroup;
};
export default function WebgpuTriangle() {
const { time, hold } = useTicker();
const [gpu, setGpu] = createSignal<Gpu>();
let surfaceRef: SceneNode | undefined;
const setup = async () => {
const el = surfaceRef!.element;
if (!el) return;
const adapter = await navigator.gpu?.requestAdapter();
if (!adapter) throw new Error("WebGPU is not available");
const device = await adapter.requestDevice();
const context = el.getContext("webgpu");
if (!context) throw new Error("No webgpu context on the surface canvas");
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format, alphaMode: "premultiplied" });
const module = device.createShaderModule({ code: SHADER });
const pipeline = device.createRenderPipeline({
layout: "auto",
vertex: { module, entryPoint: "vs" },
fragment: { module, entryPoint: "fs", targets: [{ format }] },
});
const uniforms = device.createBuffer({
size: 16,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: uniforms } }],
});
setGpu({ device, context, pipeline, uniforms, bindGroup });
};
// Held, so the frames wait for the pipeline instead of being sampled empty.
onMount(() => hold(setup()));
createEffect(() => {
const g = gpu();
const t = time();
if (!g) return;
g.device.queue.writeBuffer(g.uniforms, 0, new Float32Array([t]));
const encoder = g.device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: g.context.getCurrentTexture().createView(),
clearValue: { r: 0.04, g: 0.05, b: 0.08, a: 1 },
loadOp: "clear",
storeOp: "store",
},
],
});
pass.setPipeline(g.pipeline);
pass.setBindGroup(0, g.bindGroup);
pass.draw(3);
pass.end();
g.device.queue.submit([encoder.finish()]);
});
onCleanup(() => gpu()?.device.destroy());
return (
<stage camera={[0.6, 0, 0, 0.6, 85, 150]}>
<scene name="WebGPU triangle" width={960} height={540} fill="#0b0d12" active>
<surface x={0} y={0} width={960} height={540} ref={surfaceRef} />
</scene>
</stage>
);
}