UNPKG

@motion-core/motion-gpu

Version:

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

132 lines (128 loc) 5.94 kB
//#region src/lib/core/compute-shader.ts /** * Regex contract for the single public compute entrypoint. * * Keep the workgroup-size grammar aligned with `extractWorkgroupSize` so a * malformed attribute cannot make the expression scan across later attributes. */ var COMPUTE_ENTRY_CONTRACT = /@compute\s+@workgroup_size\s*\(\s*\d+(?:\s*,\s*\d+){0,2}\s*\)\s*fn\s+compute\s*\(/; var WORKGROUP_SIZE_PATTERN = /@workgroup_size\s*\(\s*(\d+)(?:\s*,\s*(\d+))?(?:\s*,\s*(\d+))?\s*\)/; var GLOBAL_INVOCATION_ID_PATTERN = /@builtin\s*\(\s*global_invocation_id\s*\)/; var WORKGROUP_DIMENSION_MIN = 1; var WORKGROUP_DIMENSION_MAX = 65535; var DEFAULT_UNIFORM_FIELD = "motiongpu_unused: vec4f,"; function extractComputeParamList(compute) { const entrypoint = COMPUTE_ENTRY_CONTRACT.exec(compute); if (!entrypoint) return null; const openParenIndex = entrypoint.index + entrypoint[0].length - 1; let depth = 0; for (let index = openParenIndex; index < compute.length; index += 1) { const char = compute[index]; if (char === "(") depth += 1; else 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}.`); } 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); } 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 ]; } function buildUniformStructForCompute(layout) { if (layout.entries.length === 0) return DEFAULT_UNIFORM_FIELD; return layout.entries.map((entry) => `${entry.name}: ${entry.type},`).join("\n "); } function assertBindingOrder(resources) { for (let index = 0; index < resources.length; index += 1) { const resource = resources[index]; if (!resource || resource.binding !== index) throw new Error(`Resolved compute shader resources must use contiguous binding order; expected binding ${index}.`); } } /** * Emits the heterogeneous group 1 declarations from the resolver-owned order. */ function buildComputeResourceBindings(resources) { assertBindingOrder(resources); return resources.map((resource) => { switch (resource.kind) { case "sampled-texture": return `@group(1) @binding(${resource.binding}) var ${resource.alias}: texture_2d<${resource.scalarType}>;`; case "storage-texture": return `@group(1) @binding(${resource.binding}) var ${resource.alias}: texture_storage_2d<${resource.format}, write>;`; case "storage-buffer": return `@group(1) @binding(${resource.binding}) var<storage, ${resource.access === "storage-read" ? "read" : "read_write"}> ${resource.alias}: ${resource.wgslType};`; case "sampler": return `@group(1) @binding(${resource.binding}) var ${resource.alias}: ${resource.samplerType === "comparison" ? "sampler_comparison" : "sampler"};`; default: { const unsupportedKind = resource.kind; throw new Error(`Unsupported resolved compute shader resource kind: ${String(unsupportedKind)}`); } } }).join("\n"); } /** Maps storage texture format to sampled `texture_2d<T>` scalar type. */ function storageTextureSampleScalarType(format) { const normalized = String(format).toLowerCase(); if (normalized.endsWith("uint")) return "u32"; if (normalized.endsWith("sint")) return "i32"; return "f32"; } function buildComputeShaderSource(options) { const uniformFields = buildUniformStructForCompute(options.uniformLayout); const resourceBindings = buildComputeResourceBindings(options.resources); 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; ${resourceBindings ? "\n" + resourceBindings : ""} ${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; } /** The renderer's only compute WGSL generation path. */ function buildComputeShaderSourceWithMap(options) { const code = buildComputeShaderSource(options); return { code, lineMap: buildComputeLineMap(code, options.compute) }; } //#endregion export { COMPUTE_ENTRY_CONTRACT, assertComputeContract, buildComputeResourceBindings, buildComputeShaderSource, buildComputeShaderSourceWithMap, extractWorkgroupSize, storageTextureSampleScalarType }; //# sourceMappingURL=compute-shader.js.map