UNPKG

@motion-core/motion-gpu

Version:

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

184 lines (166 loc) 4.8 kB
import type { AnyPass, RenderPass, RenderPassInputSlot, RenderPassOutputSlot } from './types.js'; /** * Resolved render-pass step with defaults applied. */ export interface RenderGraphStep { /** * Step kind. 'render' for post-scene render passes, 'compute' for pre-scene compute passes. */ kind: 'render' | 'compute'; /** * User pass instance. */ pass: AnyPass; /** * Resolved input slot. Ignored for compute steps. */ input: RenderPassInputSlot; /** * Resolved output slot. Ignored for compute steps. */ output: RenderPassOutputSlot; /** * Whether ping-pong swap should be performed after render. */ needsSwap: boolean; /** * Whether pass should clear output before drawing. */ clear: boolean; /** * Effective clear color. */ clearColor: [number, number, number, number]; /** * Whether output should be preserved after pass ends. */ preserve: boolean; } /** * Immutable render-graph execution plan for one frame. */ export interface RenderGraphPlan { /** * Resolved enabled steps in declaration order. */ steps: RenderGraphStep[]; /** * Enabled compute steps. These always execute before the base scene render. */ computeSteps: RenderGraphStep[]; /** * Enabled render steps. These always execute after the base scene render. */ renderSteps: RenderGraphStep[]; /** * Output slot holding final post-scene render result before presentation. * Remains 'canvas' when there are no render steps. */ finalOutput: RenderPassOutputSlot; } /** * Creates a copy of RGBA clear color. */ function cloneClearColor( color: [number, number, number, number] ): [number, number, number, number] { return [color[0], color[1], color[2], color[3]]; } /** * Builds validated render graph plan from runtime pass list. * * @param passes - Runtime passes. * @param defaultClearColor - Global clear color fallback. * @returns Resolved render graph plan. */ export function planRenderGraph( passes: AnyPass[] | undefined, defaultClearColor: [number, number, number, number], renderTargetSlots?: Iterable<string> ): RenderGraphPlan { const steps: RenderGraphStep[] = []; const computeSteps: RenderGraphStep[] = []; const renderSteps: RenderGraphStep[] = []; const declaredTargets = new Set(renderTargetSlots ?? []); const availableSlots = new Set<RenderPassInputSlot | RenderPassOutputSlot>(['source']); let finalOutput: RenderPassOutputSlot = 'canvas'; let enabledIndex = 0; for (const pass of passes ?? []) { if (pass.enabled === false) { continue; } // Compute passes don't participate in slot routing const isCompute = 'isCompute' in pass && (pass as { isCompute?: boolean }).isCompute === true; if (isCompute) { const step: RenderGraphStep = { kind: 'compute', pass, input: 'source', output: 'source', needsSwap: false, clear: false, clearColor: cloneClearColor(defaultClearColor), preserve: true }; steps.push(step); computeSteps.push(step); continue; } // After compute guard, pass is a render pass const rp = pass as RenderPass; const needsSwap = rp.needsSwap ?? true; const input: RenderPassInputSlot = rp.input ?? 'source'; const output: RenderPassOutputSlot = rp.output ?? (needsSwap ? 'target' : 'source'); if (input === 'canvas') { throw new Error(`Render pass #${enabledIndex} cannot read from "canvas".`); } const inputIsNamed = input !== 'source' && input !== 'target'; if (inputIsNamed && !declaredTargets.has(input)) { throw new Error(`Render pass #${enabledIndex} reads unknown target "${input}".`); } const outputIsNamed = output !== 'source' && output !== 'target' && output !== 'canvas'; if (outputIsNamed && !declaredTargets.has(output)) { throw new Error(`Render pass #${enabledIndex} writes unknown target "${output}".`); } if (needsSwap && (input !== 'source' || output !== 'target')) { throw new Error( `Render pass #${enabledIndex} uses needsSwap=true but does not follow source->target flow.` ); } if (!availableSlots.has(input)) { throw new Error(`Render pass #${enabledIndex} reads "${input}" before it is written.`); } const clear = rp.clear ?? false; const clearColor = cloneClearColor(rp.clearColor ?? defaultClearColor); const preserve = rp.preserve ?? true; const step: RenderGraphStep = { kind: 'render', pass, input, output, needsSwap, clear, clearColor, preserve }; steps.push(step); renderSteps.push(step); if (needsSwap) { availableSlots.add('target'); availableSlots.add('source'); finalOutput = 'source'; } else { if (output !== 'canvas') { availableSlots.add(output); } finalOutput = output; } enabledIndex += 1; } return { steps, computeSteps, renderSteps, finalOutput }; }