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