UNPKG

@motion-core/motion-gpu

Version:

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

204 lines (203 loc) 7.56 kB
import { createMotionGPUError } from "./error-report.js"; //#region src/lib/core/render-graph.ts /** * Creates a copy of RGBA clear color. */ function cloneClearColor(color) { return [ color[0], color[1], color[2], color[3] ]; } function logicalResourceMapKey(access) { return access.logicalId; } function formatLogicalResource(access) { const id = typeof access.logicalId === "symbol" ? access.logicalId.description ?? access.logicalId.toString() : access.logicalId; return `${access.resourceKind} "${id}"`; } function stableTopologicalComputeSegment(segment) { if (segment.length < 2) return segment; const textureWriters = /* @__PURE__ */ new Map(); const bufferWriters = /* @__PURE__ */ new Map(); for (let index = 0; index < segment.length; index += 1) { const step = segment[index]; if (!step?.resolvedResources) continue; for (const access of step.resolvedResources.writes) { const writers = access.resourceKind === "texture" ? textureWriters : bufferWriters; const logicalId = logicalResourceMapKey(access); const previous = writers.get(logicalId); if (previous !== void 0 && previous !== index) { const previousStep = segment[previous]; throw createMotionGPUError("COMPUTE_GRAPH_MULTIPLE_WRITERS", `Compute graph has multiple writers for ${formatLogicalResource(access)}: ${previousStep?.computeLabel ?? `compute pass #${previous}`} and ${step.computeLabel ?? `compute pass #${index}`} (alias "${access.alias}").`); } writers.set(logicalId, index); } } const edges = []; const edgeKeys = /* @__PURE__ */ new Set(); const addEdge = (from, to, access) => { if (from === to) return; const key = `${from}:${to}`; if (edgeKeys.has(key)) return; edgeKeys.add(key); edges.push({ from, to, access }); }; for (let readerIndex = 0; readerIndex < segment.length; readerIndex += 1) { const resources = segment[readerIndex]?.resolvedResources; if (!resources) continue; for (const access of resources.reads) { const writerIndex = (access.resourceKind === "texture" ? textureWriters : bufferWriters).get(logicalResourceMapKey(access)); if (writerIndex === void 0) continue; if (access.version === "initial") addEdge(readerIndex, writerIndex, access); else addEdge(writerIndex, readerIndex, access); } } const outgoing = Array.from({ length: segment.length }, () => []); const indegree = new Array(segment.length).fill(0); for (const edge of edges) { outgoing[edge.from]?.push(edge); indegree[edge.to] = (indegree[edge.to] ?? 0) + 1; } const ready = []; for (let index = 0; index < segment.length; index += 1) if (indegree[index] === 0) ready.push(index); const ordered = []; while (ready.length > 0) { ready.sort((left, right) => left - right); const index = ready.shift(); if (index === void 0) break; const step = segment[index]; if (step) ordered.push(step); for (const edge of outgoing[index] ?? []) { indegree[edge.to] = (indegree[edge.to] ?? 0) - 1; if (indegree[edge.to] === 0) ready.push(edge.to); } } if (ordered.length !== segment.length) { const blocked = indegree.map((count, index) => ({ count, index })).filter(({ count }) => count > 0).map(({ index }) => segment[index]?.computeLabel ?? `compute pass #${index}`); const cycleEdges = edges.filter((edge) => (indegree[edge.from] ?? 0) > 0 && (indegree[edge.to] ?? 0) > 0).map((edge) => `${segment[edge.from]?.computeLabel ?? `compute pass #${edge.from}`} -> ${segment[edge.to]?.computeLabel ?? `compute pass #${edge.to}`} via ${formatLogicalResource(edge.access)} (alias "${edge.access.alias}")`); throw createMotionGPUError("COMPUTE_GRAPH_CYCLE", `Compute dependency cycle detected among ${blocked.join(", ")}: ${cycleEdges.join("; ")}.`); } return ordered; } function planComputeSegments(preSceneSteps) { const ordered = []; let segment = []; const flush = () => { if (segment.length === 0) return; ordered.push(...stableTopologicalComputeSegment(segment)); segment = []; }; for (const step of preSceneSteps) if (step.kind === "compute") segment.push(step); else { flush(); ordered.push(step); } flush(); return ordered; } /** * Builds validated render graph plan from runtime pass list. * * @param passes - Runtime passes. * @param defaultClearColor - Global clear color fallback. * @returns Resolved render graph plan. */ function planRenderGraph(passes, defaultClearColor, renderTargetSlots, computeOptions) { const steps = []; const preSceneSteps = []; const computeSteps = []; const renderSteps = []; const declaredTargets = new Set(renderTargetSlots ?? []); const availableSlots = /* @__PURE__ */ new Set(["source"]); let finalOutput = "canvas"; let enabledIndex = 0; for (const pass of passes ?? []) { if (pass.enabled === false) continue; if ("isCompute" in pass && pass.isCompute === true) { const resolvedResources = computeOptions?.getResolvedResources(pass); const step = { kind: "compute", pass, input: "source", output: "source", needsSwap: false, clear: false, clearColor: cloneClearColor(defaultClearColor), preserve: true, ...resolvedResources ? { resolvedResources } : {}, ...computeOptions?.getPassLabel ? { computeLabel: computeOptions.getPassLabel(pass) } : {} }; steps.push(step); preSceneSteps.push(step); computeSteps.push(step); continue; } if ("isPingPongShader" in pass && pass.isPingPongShader === true) { const step = { kind: "feedback", pass, input: "source", output: "source", needsSwap: false, clear: false, clearColor: cloneClearColor(defaultClearColor), preserve: true }; steps.push(step); preSceneSteps.push(step); continue; } const rp = pass; const needsSwap = rp.needsSwap ?? true; const input = rp.input ?? "source"; const output = rp.output ?? (needsSwap ? "target" : "source"); if (input === "canvas") throw new Error(`Render pass #${enabledIndex} cannot read from "canvas".`); if (input !== "source" && input !== "target" && !declaredTargets.has(input)) throw new Error(`Render pass #${enabledIndex} reads unknown target "${input}".`); if (output !== "source" && output !== "target" && output !== "canvas" && !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 step = { kind: "render", pass, input, output, needsSwap, clear: rp.clear ?? false, clearColor: cloneClearColor(rp.clearColor ?? defaultClearColor), preserve: rp.preserve ?? true }; 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; } const orderedPreSceneSteps = computeOptions ? planComputeSegments(preSceneSteps) : preSceneSteps; const orderedComputeSteps = orderedPreSceneSteps.filter((step) => step.kind === "compute"); return { steps, preSceneSteps: orderedPreSceneSteps, computeSteps: computeOptions ? orderedComputeSteps : computeSteps, renderSteps, finalOutput }; } //#endregion export { planRenderGraph }; //# sourceMappingURL=render-graph.js.map