馃悕馃悕馃悕
at main 301 lines 8.4 kB view raw
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