UNPKG

@motion-core/motion-gpu

Version:

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

238 lines (237 loc) 9.15 kB
import { createMotionGPUError } from "./error-report.js"; import { assertMotionGPUPass, isManagedComputePass, isManagedFeedbackPass } from "./pass-contract.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] ]; } /** * Returns the concrete resource identity used to relate logical aliases. */ function physicalResourceMapKey(access) { return access.physicalId; } /** * Reports whether two accesses may touch the same texture subresource. * Non-texture and unspecified ranges overlap conservatively. */ function textureSubresourcesOverlap(left, right) { if (left.resourceKind !== "texture" || right.resourceKind !== "texture") return true; if (!left.subresource || !right.subresource) return true; const leftMipEnd = left.subresource.baseMipLevel + left.subresource.mipLevelCount; const rightMipEnd = right.subresource.baseMipLevel + right.subresource.mipLevelCount; const leftLayerEnd = left.subresource.baseArrayLayer + left.subresource.arrayLayerCount; const rightLayerEnd = right.subresource.baseArrayLayer + right.subresource.arrayLayerCount; return left.subresource.baseMipLevel < rightMipEnd && right.subresource.baseMipLevel < leftMipEnd && left.subresource.baseArrayLayer < rightLayerEnd && right.subresource.baseArrayLayer < leftLayerEnd; } /** * Formats a logical compute resource for actionable graph diagnostics. */ function formatLogicalResource(access) { const id = typeof access.logicalId === "symbol" ? access.logicalId.description ?? access.logicalId.toString() : access.logicalId; return `${access.resourceKind} "${id}"`; } /** * Orders a compute segment by physical hazards while preserving stable source order. */ 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 physicalId = physicalResourceMapKey(access); const resourceWriters = writers.get(physicalId) ?? []; const previous = resourceWriters.find((writer) => writer.index !== index && textureSubresourcesOverlap(writer.access, access)); if (previous) { const previousStep = segment[previous.index]; throw createMotionGPUError("COMPUTE_GRAPH_MULTIPLE_WRITERS", `Compute graph has multiple writers for ${formatLogicalResource(access)}: ${previousStep?.computeLabel ?? `compute pass #${previous.index}`} and ${step.computeLabel ?? `compute pass #${index}`} (alias "${access.alias}").`); } resourceWriters.push({ index, access }); writers.set(physicalId, resourceWriters); } } 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 resourceWriters = (access.resourceKind === "texture" ? textureWriters : bufferWriters).get(physicalResourceMapKey(access)) ?? []; for (const writer of resourceWriters) { if (!textureSubresourcesOverlap(writer.access, access)) continue; if (access.version === "initial") addEdge(readerIndex, writer.index, access); else addEdge(writer.index, 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; } /** * Reorders only contiguous compute blocks, leaving render-pass boundaries fixed. */ 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 ?? []) { assertMotionGPUPass(pass); if (pass.enabled === false) continue; if (isManagedComputePass(pass)) { 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 (isManagedFeedbackPass(pass)) { 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