UNPKG

@motion-core/motion-gpu

Version:

Framework-agnostic WebGPU runtime for fullscreen WGSL shaders with explicit Svelte, React, and Vue adapter entrypoints.

238 lines (231 loc) 9.27 kB
//#region src/lib/core/compute-shader.ts /** * Regex contract for compute entrypoint. * Matches: @compute @workgroup_size(...) fn compute( * with @builtin(global_invocation_id) parameter. */ var COMPUTE_ENTRY_CONTRACT = /@compute\s+@workgroup_size\s*\([^)]+\)\s*fn\s+compute\s*\(/; /** * Regex to extract @workgroup_size values. */ var WORKGROUP_SIZE_PATTERN = /@workgroup_size\s*\(\s*(\d+)(?:\s*,\s*(\d+))?(?:\s*,\s*(\d+))?\s*\)/; /** * Regex to verify @builtin(global_invocation_id) parameter. */ var GLOBAL_INVOCATION_ID_PATTERN = /@builtin\s*\(\s*global_invocation_id\s*\)/; var WORKGROUP_DIMENSION_MIN = 1; var WORKGROUP_DIMENSION_MAX = 65535; function extractComputeParamList(compute) { const computeFnIndex = compute.indexOf("fn compute"); if (computeFnIndex === -1) return null; const openParenIndex = compute.indexOf("(", computeFnIndex); if (openParenIndex === -1) return null; let depth = 0; for (let index = openParenIndex; index < compute.length; index += 1) { const char = compute[index]; if (char === "(") { depth += 1; continue; } if (char === ")") { depth -= 1; if (depth === 0) return compute.slice(openParenIndex + 1, index); } } return null; } function assertWorkgroupDimension(value) { if (!Number.isFinite(value) || !Number.isInteger(value) || value < WORKGROUP_DIMENSION_MIN || value > WORKGROUP_DIMENSION_MAX) throw new Error(`@workgroup_size dimensions must be integers in range ${WORKGROUP_DIMENSION_MIN}-${WORKGROUP_DIMENSION_MAX}, got ${value}.`); } /** * Default uniform field used when no custom uniforms are provided in compute. */ var DEFAULT_UNIFORM_FIELD = "motiongpu_unused: vec4f,"; /** * Validates compute shader user code matches the compute contract. * * @param compute - User compute shader WGSL source. * @throws {Error} When shader does not match the compute contract. */ function assertComputeContract(compute) { if (!COMPUTE_ENTRY_CONTRACT.test(compute)) throw new Error("Compute shader must declare `@compute @workgroup_size(...) fn compute(...)`. Ensure the function is named `compute` and includes @compute and @workgroup_size annotations."); const params = extractComputeParamList(compute); if (!params || !GLOBAL_INVOCATION_ID_PATTERN.test(params)) throw new Error("Compute shader must include a `@builtin(global_invocation_id)` parameter."); extractWorkgroupSize(compute); } /** * Extracts @workgroup_size values from WGSL compute shader. * * @param compute - Validated compute shader source. * @returns Tuple [x, y, z] with defaults of 1 for omitted dimensions. */ function extractWorkgroupSize(compute) { const match = compute.match(WORKGROUP_SIZE_PATTERN); if (!match) throw new Error("Could not extract @workgroup_size from compute shader source."); const x = Number.parseInt(match[1] ?? "1", 10); const y = Number.parseInt(match[2] ?? "1", 10); const z = Number.parseInt(match[3] ?? "1", 10); assertWorkgroupDimension(x); assertWorkgroupDimension(y); assertWorkgroupDimension(z); return [ x, y, z ]; } /** * Maps StorageBufferAccess to WGSL var qualifier. */ function toWgslAccessMode(access) { switch (access) { case "read": return "read"; case "read-write": return "read_write"; default: throw new Error(`Unsupported storage buffer access mode "${String(access)}".`); } } /** * Builds WGSL struct fields for uniforms used in compute shader preamble. */ function buildUniformStructForCompute(layout) { if (layout.entries.length === 0) return DEFAULT_UNIFORM_FIELD; return layout.entries.map((entry) => `${entry.name}: ${entry.type},`).join("\n "); } /** * Builds storage buffer binding declarations for compute shader. * * @param storageBufferKeys - Sorted buffer keys. * @param definitions - Type/access definitions per key. * @param groupIndex - Bind group index for storage buffers. * @returns WGSL binding declaration string. */ function buildComputeStorageBufferBindings(storageBufferKeys, definitions, groupIndex) { if (storageBufferKeys.length === 0) return ""; const declarations = []; for (let index = 0; index < storageBufferKeys.length; index += 1) { const key = storageBufferKeys[index]; if (key === void 0) continue; const definition = definitions[key]; if (!definition) continue; const accessMode = toWgslAccessMode(definition.access); declarations.push(`@group(${groupIndex}) @binding(${index}) var<storage, ${accessMode}> ${key}: ${definition.type};`); } return declarations.join("\n"); } /** * Builds storage texture binding declarations for compute shader. * * @param storageTextureKeys - Sorted storage texture keys. * @param definitions - Format definitions per key. * @param groupIndex - Bind group index for storage textures. * @returns WGSL binding declaration string. */ function buildComputeStorageTextureBindings(storageTextureKeys, definitions, groupIndex) { if (storageTextureKeys.length === 0) return ""; const declarations = []; for (let index = 0; index < storageTextureKeys.length; index += 1) { const key = storageTextureKeys[index]; if (key === void 0) continue; const definition = definitions[key]; if (!definition) continue; declarations.push(`@group(${groupIndex}) @binding(${index}) var ${key}: texture_storage_2d<${definition.format}, write>;`); } return declarations.join("\n"); } /** * Maps storage texture format to sampled texture scalar type for `texture_2d<T>`. */ function storageTextureSampleScalarType(format) { const normalized = String(format).toLowerCase(); if (normalized.endsWith("uint")) return "u32"; if (normalized.endsWith("sint")) return "i32"; return "f32"; } /** * Assembles compute shader WGSL for ping-pong workflows. * * Exposes two generated bindings under group(2): * - `${target}A`: sampled read texture (`texture_2d<T>`) * - `${target}B`: storage write texture (`texture_storage_2d<format, write>`) */ function buildPingPongComputeShaderSource(options) { const uniformFields = buildUniformStructForCompute(options.uniformLayout); const storageBufferBindings = buildComputeStorageBufferBindings(options.storageBufferKeys, options.storageBufferDefinitions, 1); const sampledType = storageTextureSampleScalarType(options.targetFormat); const pingPongTextureBindings = [`@group(2) @binding(0) var ${options.target}A: texture_2d<${sampledType}>;`, `@group(2) @binding(1) var ${options.target}B: texture_storage_2d<${options.targetFormat}, write>;`].join("\n"); return `struct MotionGPUFrame { time: f32, delta: f32, resolution: vec2f, }; struct MotionGPUUniforms { ${uniformFields} }; @group(0) @binding(0) var<uniform> motiongpuFrame: MotionGPUFrame; @group(0) @binding(1) var<uniform> motiongpuUniforms: MotionGPUUniforms; ${storageBufferBindings ? "\n" + storageBufferBindings : ""} ${pingPongTextureBindings ? "\n" + pingPongTextureBindings : ""} ${options.compute} `; } /** * Assembles full compute shader WGSL with preamble. * * @param options - Compute shader build options. * @returns Complete WGSL source for compute stage. */ function buildComputeShaderSource(options) { const uniformFields = buildUniformStructForCompute(options.uniformLayout); const storageBufferBindings = buildComputeStorageBufferBindings(options.storageBufferKeys, options.storageBufferDefinitions, 1); const storageTextureBindings = buildComputeStorageTextureBindings(options.storageTextureKeys, options.storageTextureDefinitions, 2); return `struct MotionGPUFrame { time: f32, delta: f32, resolution: vec2f, }; struct MotionGPUUniforms { ${uniformFields} }; @group(0) @binding(0) var<uniform> motiongpuFrame: MotionGPUFrame; @group(0) @binding(1) var<uniform> motiongpuUniforms: MotionGPUUniforms; ${storageBufferBindings ? "\n" + storageBufferBindings : ""} ${storageTextureBindings ? "\n" + storageTextureBindings : ""} ${options.compute} `; } function buildComputeLineMap(generatedCode, userComputeSource) { const lineCount = generatedCode.split("\n").length; const lineMap = new Array(lineCount + 1).fill(null); const computeStartIndex = generatedCode.indexOf(userComputeSource); if (computeStartIndex === -1) return lineMap; const computeStartLine = generatedCode.slice(0, computeStartIndex).split("\n").length; const computeLineCount = userComputeSource.split("\n").length; for (let line = 0; line < computeLineCount; line += 1) lineMap[computeStartLine + line] = { kind: "compute", line: line + 1 }; return lineMap; } /** * Assembles full compute shader WGSL with source line mapping metadata. */ function buildComputeShaderSourceWithMap(options) { const code = buildComputeShaderSource(options); return { code, lineMap: buildComputeLineMap(code, options.compute) }; } /** * Assembles ping-pong compute shader WGSL with source line mapping metadata. */ function buildPingPongComputeShaderSourceWithMap(options) { const code = buildPingPongComputeShaderSource(options); return { code, lineMap: buildComputeLineMap(code, options.compute) }; } //#endregion export { COMPUTE_ENTRY_CONTRACT, assertComputeContract, buildComputeShaderSource, buildComputeShaderSourceWithMap, buildComputeStorageBufferBindings, buildComputeStorageTextureBindings, buildPingPongComputeShaderSource, buildPingPongComputeShaderSourceWithMap, extractWorkgroupSize, storageTextureSampleScalarType }; //# sourceMappingURL=compute-shader.js.map