UNPKG

@motion-core/motion-gpu

Version:

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

413 lines (362 loc) 11.4 kB
import type { StorageBufferAccess, StorageBufferType, UniformLayout } from './types.js'; /** * Regex contract for compute entrypoint. * Matches: @compute @workgroup_size(...) fn compute( * with @builtin(global_invocation_id) parameter. */ export const COMPUTE_ENTRY_CONTRACT = /@compute\s+@workgroup_size\s*\([^)]+\)\s*fn\s+compute\s*\(/; /** * Regex to extract @workgroup_size values. */ const WORKGROUP_SIZE_PATTERN = /@workgroup_size\s*\(\s*(\d+)(?:\s*,\s*(\d+))?(?:\s*,\s*(\d+))?\s*\)/; /** * Regex to verify @builtin(global_invocation_id) parameter. */ const GLOBAL_INVOCATION_ID_PATTERN = /@builtin\s*\(\s*global_invocation_id\s*\)/; const WORKGROUP_DIMENSION_MIN = 1; const WORKGROUP_DIMENSION_MAX = 65535; function extractComputeParamList(compute: string): string | null { 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: number): void { 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. */ const 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. */ export function assertComputeContract(compute: string): void { 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. */ export function extractWorkgroupSize(compute: string): [number, number, number] { 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: StorageBufferAccess): string { 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: UniformLayout): string { if (layout.entries.length === 0) { return DEFAULT_UNIFORM_FIELD; } return layout.entries.map((entry) => `${entry.name}: ${entry.type},`).join('\n\t'); } /** * 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. */ export function buildComputeStorageBufferBindings( storageBufferKeys: string[], definitions: Record<string, { type: StorageBufferType; access: StorageBufferAccess }>, groupIndex: number ): string { if (storageBufferKeys.length === 0) { return ''; } const declarations: string[] = []; for (let index = 0; index < storageBufferKeys.length; index += 1) { const key = storageBufferKeys[index]; if (key === undefined) { 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. */ export function buildComputeStorageTextureBindings( storageTextureKeys: string[], definitions: Record<string, { format: GPUTextureFormat }>, groupIndex: number ): string { if (storageTextureKeys.length === 0) { return ''; } const declarations: string[] = []; for (let index = 0; index < storageTextureKeys.length; index += 1) { const key = storageTextureKeys[index]; if (key === undefined) { 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>`. */ export function storageTextureSampleScalarType(format: GPUTextureFormat): 'f32' | 'u32' | 'i32' { 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>`) */ export function buildPingPongComputeShaderSource(options: { compute: string; uniformLayout: UniformLayout; storageBufferKeys: string[]; storageBufferDefinitions: Record< string, { type: StorageBufferType; access: StorageBufferAccess } >; target: string; targetFormat: GPUTextureFormat; }): string { 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} `; } /** * Source location for generated compute shader lines. */ export interface ComputeShaderSourceLocation { kind: 'compute'; line: number; } /** * 1-based line map from generated compute WGSL to user compute source. */ export type ComputeShaderLineMap = Array<ComputeShaderSourceLocation | null>; /** * Result of compute shader source generation with line mapping metadata. */ export interface BuiltComputeShaderSource { code: string; lineMap: ComputeShaderLineMap; } /** * Assembles full compute shader WGSL with preamble. * * @param options - Compute shader build options. * @returns Complete WGSL source for compute stage. */ export function buildComputeShaderSource(options: { compute: string; uniformLayout: UniformLayout; storageBufferKeys: string[]; storageBufferDefinitions: Record< string, { type: StorageBufferType; access: StorageBufferAccess } >; storageTextureKeys: string[]; storageTextureDefinitions: Record<string, { format: GPUTextureFormat }>; }): string { 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: string, userComputeSource: string ): ComputeShaderLineMap { const lineCount = generatedCode.split('\n').length; const lineMap: ComputeShaderLineMap = 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. */ export function buildComputeShaderSourceWithMap(options: { compute: string; uniformLayout: UniformLayout; storageBufferKeys: string[]; storageBufferDefinitions: Record< string, { type: StorageBufferType; access: StorageBufferAccess } >; storageTextureKeys: string[]; storageTextureDefinitions: Record<string, { format: GPUTextureFormat }>; }): BuiltComputeShaderSource { const code = buildComputeShaderSource(options); return { code, lineMap: buildComputeLineMap(code, options.compute) }; } /** * Assembles ping-pong compute shader WGSL with source line mapping metadata. */ export function buildPingPongComputeShaderSourceWithMap(options: { compute: string; uniformLayout: UniformLayout; storageBufferKeys: string[]; storageBufferDefinitions: Record< string, { type: StorageBufferType; access: StorageBufferAccess } >; target: string; targetFormat: GPUTextureFormat; }): BuiltComputeShaderSource { const code = buildPingPongComputeShaderSource(options); return { code, lineMap: buildComputeLineMap(code, options.compute) }; }