馃悕馃悕馃悕
1
2$css(`
3 canvas.webgpu {
4 position: absolute;
5 top: 0;
6 left: 0;
7 width: 100%;
8 height: 100%;
9 user-select: none;
10 }
11`);
12
13import "/code/math/constants.js";
14
15let webgpu_working = true;
16
17if (!navigator.gpu) {
18 console.error("WebGPU not supported.");
19 webgpu_working = false;
20}
21
22const adapter = await navigator.gpu.requestAdapter();
23
24if (!adapter) {
25 console.error("No WebGPU adapter.");
26 webgpu_working = false;
27}
28
29
30const device = await adapter.requestDevice({
31 requiredLimits: {
32 //maxTextureDimension2D: 32767,
33 //maxBufferSize: 1073741824,
34 //maxStorageBufferBindingSize: 1073741824,
35 },
36 requiredFeatures: [ /*'float32-filterable'*/ ], // TODO: fallback for when this doesnt work
37});
38// todo ensure device
39
40const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
41
42const wgslSizes = {
43 f32: 4, i32: 4, u32: 4,
44 f16: 2, i16: 2, u16: 2
45};
46
47const wgslAligns = {
48 vec2f: 8,
49 vec2u: 8,
50 f32: 4, i32: 4, u32: 4,
51 f16: 2, i16: 2, u16: 2,
52}
53
54const wgslFloatTypes = new Set(["f32", "f16"]);
55
56const wgslSetters = {
57 f32: DataView.prototype.setFloat32,
58 f16: DataView.prototype.setFloat16,
59 i32: DataView.prototype.setInt32,
60 i16: DataView.prototype.setInt16,
61 u32: DataView.prototype.setUint32,
62 u16: DataView.prototype.setUint16,
63 i8: DataView.prototype.setInt8,
64 u8: DataView.prototype.setUint8
65};
66
67const compositeTypes = {
68 vec2f: [
69 { type: "f32", name: "x" },
70 { type: "f32", name: "y" }
71 ],
72 vec2u: [
73 { type: "u32", name: "x" },
74 { type: "u32", name: "y" }
75 ],
76};
77
78const constants = {
79 pi: $tau / 2,
80 tau: $tau
81}
82
83import { greek } from "/code/math/math.js";
84
85function getUiName(varName) {
86 return greek[varName] || varName.replace(/_/g, ' ');
87};
88
89function processNumeric(value) {
90 if (value === undefined) return value;
91 if (value[0] === '-') {
92 return -constants[value.substring(1)] || value;
93 }
94 return constants[value] || value;
95}
96
97// TODO: make this more debuggable lol
98// var_name: type, // hard? min to hard? max = default
99const VAR_RE =/^\s*(\w+)\s*:\s*(\w+),?\s*(?:\/\/\s*((?:(hard)\s+)?([\w.+-]+)\s+to\s+(?:(hard)\s+)?([\w.+-]+)(?:\s*=\s*([\w.+-,]+))?))?\s*$/;
100
101async function loadShader(shaderName, substitutions = {}) {
102 const response = await fetch(`/code/shaders/${shaderName}.wgsl`);
103 const shaderSource = await response.text();
104
105
106 // [full match, group, binding, content]
107 const notatedBuffers = [
108 ...shaderSource.matchAll(/\/\* buffer (\d+) (\d+) \*\/\s*\{([^}]*)\}/g)
109 ];
110
111 const bufferDefinitions = notatedBuffers.map(regexMatch => {
112 const lines = regexMatch[3].split('\n');
113
114 const vars = lines.filter(Boolean).flatMap(line => {
115 const parsed = line.match(VAR_RE);
116 const type = parsed[2];
117
118
119 if (type in compositeTypes) {
120 const vals = parsed[8] ? parsed[8].split(',') : undefined;
121 return compositeTypes[type].map((member, index) => ({
122 varName: `${parsed[1]}_${member.name}`,
123 type: member.type,
124 uiName: `${getUiName(parsed[1])}.${member.name}`,
125 bytes: wgslSizes[member.type],
126 aligns: [wgslAligns[member.type]].concat(
127 index === 0 ? wgslAligns[type] : []
128 ),
129 showControl: parsed[3] !== undefined,
130 hardMin: parsed[4] !== undefined,
131 min: processNumeric(parsed[5]),
132 hardMax: parsed[6] !== undefined,
133 max: processNumeric(parsed[7]),
134 value: processNumeric(vals ? vals[index] : undefined)
135 }));
136 }
137
138 return {
139 varName: parsed[1],
140 type,
141 uiName: getUiName(parsed[1]),
142 bytes: wgslSizes[type],
143 aligns: [wgslAligns[type]],
144 isIntegral: !wgslFloatTypes.has(type),
145 dataViewSetter: wgslSetters[type],
146 showControl: parsed[3] !== undefined,
147 hardMin: parsed[4] !== undefined,
148 min: processNumeric(parsed[5]),
149 hardMax: parsed[6] !== undefined,
150 max: processNumeric(parsed[7]),
151 value: processNumeric(parsed[8])
152 };
153 });
154
155 const buildControl = (v, afterChangeCallback) => ({
156 type: "number",
157 label: v.uiName,
158 value: v.value,
159 min: v.min,
160 max: v.max,
161 step: v.isIntegral ? 1 : 0.001,
162 onUpdate: (value, set) => {
163 v.value = value;
164 if (v.hardMin && v.value < v.min) {
165 v.value = v.min;
166 set(v.value);
167 }
168 if (v.hardMax && v.value > v.max) {
169 v.value = v.max;
170 set(v.value);
171 }
172 afterChangeCallback();
173 }
174 });
175
176 const getControlSettings = (afterChangeCallback) =>
177 vars.filter(v => v.showControl).map(v => buildControl(v, afterChangeCallback));
178
179 let bufferSize = 0;
180 const bufferAlign = Math.max(...vars.flatMap(v => v.aligns));
181
182 vars.forEach(v => {
183 const align = Math.max(...v.aligns);
184 if (bufferSize % align !== 0) {
185 bufferSize = bufferSize + (align - bufferSize % align);
186 }
187 bufferSize += v.bytes;
188 });
189
190 if (bufferSize % bufferAlign !== 0) {
191 bufferSize = bufferSize + (bufferAlign - bufferSize % bufferAlign);
192 }
193
194 const cpuBuffer = new ArrayBuffer(bufferSize);
195 const gpuBuffer = device.createBuffer({
196 size: bufferSize,
197 usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
198 });
199 const cpuView = new DataView(cpuBuffer);
200
201 const updateBuffers = () => {
202 var position = 0;
203 vars.forEach(v => {
204 const align = Math.max(...v.aligns);
205 if (position % align !== 0) {
206 position = position + (align - position % align);
207 }
208 wgslSetters[v.type].call(cpuView, position, v.value, true);
209 position += v.bytes;
210 });
211
212 device.queue.writeBuffer(gpuBuffer, 0, cpuBuffer);
213 };
214
215 const varMap = {};
216 vars.forEach(v => {
217 varMap[v.varName] = v;
218 });
219
220 return {
221 group: regexMatch[1],
222 binding: regexMatch[2],
223 vars: varMap,
224 getControlSettings,
225 gpuBuffer,
226 updateBuffers
227 };
228 });
229
230
231 const bufferDefinitionsMap = {};
232
233 bufferDefinitions.forEach(bd => {
234 bufferDefinitionsMap[`${bd.group},${bd.binding}`] = bd;
235 });
236
237 var substitutionFailure = false;
238 const adjustedSource = shaderSource.replace(/\${(\w+)}/g, (match, key) => {
239 if (!(key in substitutions)) {
240 substitutionFailure = true;
241 return "";
242 }
243 else {
244 return substitutions[key];
245 }
246 });
247
248 const module = device.createShaderModule({ code: adjustedSource });
249 const info = await module.getCompilationInfo();
250 if (info.messages.some(m => m.type === "error") || substitutionFailure) {
251 return null;
252 }
253 return {
254 module,
255 bufferDefinitions: bufferDefinitionsMap
256 };
257}
258
259function getOffscreenContext(dims) {
260 const canvas = new OffscreenCanvas(dims.x, dims.y);
261 const context = canvas.getContext("webgpu");
262
263 context.configure({ device, format: canvasFormat });
264
265 return {
266 canvas,
267 context
268 };
269}
270
271window.$gpu = {
272 device,
273 canvasFormat,
274 loadShader,
275 getOffscreenContext
276};
277
278export async function main(target) {
279 if (!webgpu_working) {
280 console.error("WebGPU not working; aborting module load.");
281 return;
282 }
283
284 const canvas = document.createElement("canvas");
285 canvas.className = "webgpu";
286
287 canvas.tabIndex = 0;
288
289 const context = canvas.getContext("webgpu");
290
291 context.configure({ device, format: canvasFormat });
292
293 target.appendChild(canvas);
294
295 return {
296 replace: true,
297 canvas,
298 context
299 };
300}
301