@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
JavaScript
//#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