Advance M8-M11 parity workflows
This commit is contained in:
@@ -31,6 +31,27 @@ export const COMPOSITOR_NODE_TYPES = [
|
||||
|
||||
export type CompositorNodeType = typeof COMPOSITOR_NODE_TYPES[number];
|
||||
|
||||
export const COMPOSITOR_WEBGPU_NODE_ALLOWLIST = [
|
||||
"CONSTANT_COLOR",
|
||||
"EXPOSURE",
|
||||
"INVERT",
|
||||
"COMPOSITE",
|
||||
] as const;
|
||||
export type CompositorWebGPUNodeType = typeof COMPOSITOR_WEBGPU_NODE_ALLOWLIST[number];
|
||||
|
||||
export type CompositorWebGPUInstructionIR =
|
||||
| { nodeId: string; type: "CONSTANT_COLOR"; color: readonly [number, number, number, number] }
|
||||
| { nodeId: string; type: "EXPOSURE"; exposure: number }
|
||||
| { nodeId: string; type: "INVERT" }
|
||||
| { nodeId: string; type: "COMPOSITE" };
|
||||
|
||||
export interface CompositorWebGPUPlanIR {
|
||||
schemaVersion: typeof COMPOSITOR_SCHEMA;
|
||||
graphId: string;
|
||||
outputNodeId: string;
|
||||
instructions: CompositorWebGPUInstructionIR[];
|
||||
}
|
||||
|
||||
export interface CompositorResourceIR {
|
||||
id: string;
|
||||
kind: "IMAGE" | "RENDER_LAYER";
|
||||
@@ -223,6 +244,19 @@ function validateNodeProperties(node: CompositorNodeIR, index: number): void {
|
||||
}
|
||||
}
|
||||
|
||||
function unsupportedCompositorNodeNames(graph: CompositorGraphIR): string[] {
|
||||
return [...new Set(graph.nodes
|
||||
.filter((node) => !SUPPORTED.has(node.type))
|
||||
.map((node) => node.blenderType ?? node.type))].sort();
|
||||
}
|
||||
|
||||
function assertCompositorGraphExecutable(graph: CompositorGraphIR): void {
|
||||
const unsupported = unsupportedCompositorNodeNames(graph);
|
||||
if (unsupported.length > 0) {
|
||||
throw new CompositorValidationError("COMPOSITOR_NODE_UNSUPPORTED", `Compositor graph contains unsupported nodes: ${unsupported.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCompositorGraph(value: unknown): CompositorGraphIR {
|
||||
if (!record(value) || value.schemaVersion !== COMPOSITOR_SCHEMA || !Array.isArray(value.nodes) ||
|
||||
!Array.isArray(value.links) || !Array.isArray(value.resources)) {
|
||||
@@ -282,6 +316,51 @@ export function parseCompositorGraph(value: unknown): CompositorGraphIR {
|
||||
return { schemaVersion: COMPOSITOR_SCHEMA, id: text(value.id, "id"), name: text(value.name, "name"), outputNodeId, nodes, links, resources };
|
||||
}
|
||||
|
||||
/** Compiles only the node types with an independent CPU/WebGPU golden. */
|
||||
export function compileCompositorWebGPUPlan(value: unknown): CompositorWebGPUPlanIR {
|
||||
const graph = parseCompositorGraph(value);
|
||||
if (graph.resources.length !== 0) throw new CompositorValidationError("COMPOSITOR_NODE_UNSUPPORTED", "WebGPU compositor allowlist does not include resource inputs");
|
||||
const unsupported = graph.nodes.filter((node) => !COMPOSITOR_WEBGPU_NODE_ALLOWLIST.includes(node.type as CompositorWebGPUNodeType));
|
||||
if (unsupported.length > 0) {
|
||||
const names = [...new Set(unsupported.map((node) => node.blenderType ?? node.type))].sort();
|
||||
throw new CompositorValidationError("COMPOSITOR_NODE_UNSUPPORTED", `WebGPU compositor nodes are not allowlisted: ${names.join(", ")}`);
|
||||
}
|
||||
const byId = new Map(graph.nodes.map((node) => [node.id, node]));
|
||||
const incoming = new Map<string, CompositorLinkIR[]>();
|
||||
for (const link of graph.links) {
|
||||
const links = incoming.get(link.toNodeId) ?? [];
|
||||
links.push(link);
|
||||
incoming.set(link.toNodeId, links);
|
||||
}
|
||||
const visited = new Set<string>();
|
||||
const instructions: CompositorWebGPUInstructionIR[] = [];
|
||||
const visit = (nodeId: string): void => {
|
||||
if (visited.has(nodeId)) return;
|
||||
const node = byId.get(nodeId);
|
||||
if (!node) throw new CompositorValidationError("COMPOSITOR_GRAPH_INVALID", `Missing WebGPU compositor node ${nodeId}`);
|
||||
const links = incoming.get(nodeId) ?? [];
|
||||
if (node.type === "CONSTANT_COLOR") {
|
||||
if (links.length !== 0) throw new CompositorValidationError("COMPOSITOR_GRAPH_INVALID", `${nodeId} constant input must be disconnected`);
|
||||
const color = node.properties.color as number[];
|
||||
instructions.push({ nodeId, type: "CONSTANT_COLOR", color: [color[0], color[1], color[2], color[3]] });
|
||||
}
|
||||
else {
|
||||
if (links.length !== 1 || links[0].toSocket !== "Image") throw new CompositorValidationError("COMPOSITOR_GRAPH_INVALID", `${nodeId}.Image requires exactly one input`);
|
||||
visit(links[0].fromNodeId);
|
||||
if (node.type === "EXPOSURE") instructions.push({ nodeId, type: "EXPOSURE", exposure: Number(node.properties.exposure ?? 0) });
|
||||
else if (node.type === "INVERT") instructions.push({ nodeId, type: "INVERT" });
|
||||
else if (node.type === "COMPOSITE") instructions.push({ nodeId, type: "COMPOSITE" });
|
||||
else throw new CompositorValidationError("COMPOSITOR_NODE_UNSUPPORTED", `${node.type} has no WebGPU golden`);
|
||||
}
|
||||
visited.add(nodeId);
|
||||
};
|
||||
visit(graph.outputNodeId);
|
||||
if (visited.size !== graph.nodes.length || graph.links.length !== graph.nodes.length - 1 || instructions[0]?.type !== "CONSTANT_COLOR") {
|
||||
throw new CompositorValidationError("COMPOSITOR_GRAPH_INVALID", "WebGPU compositor schema 1 requires one connected constant-to-composite chain");
|
||||
}
|
||||
return { schemaVersion: COMPOSITOR_SCHEMA, graphId: graph.id, outputNodeId: graph.outputNodeId, instructions };
|
||||
}
|
||||
|
||||
function validateImage(image: CompositorImageBuffer, name: string): CompositorImageBuffer {
|
||||
const pixels = image.width * image.height;
|
||||
if (!Number.isSafeInteger(image.width) || !Number.isSafeInteger(image.height) || image.width < 1 || image.height < 1 ||
|
||||
@@ -307,7 +386,7 @@ function allocate(width: number, height: number): CompositorImageBuffer {
|
||||
export function gateCompositorGraph(value: unknown, availableResourceIds: ReadonlySet<string>): CapabilityGateResult {
|
||||
try {
|
||||
const graph = parseCompositorGraph(value);
|
||||
const unsupported = graph.nodes.filter((node) => !SUPPORTED.has(node.type)).map((node) => node.blenderType ?? node.type);
|
||||
const unsupported = unsupportedCompositorNodeNames(graph);
|
||||
if (unsupported.length > 0) return blockedGate("N-020", "COMPOSITOR_GRAPH", [capabilityIssue("COMPOSITOR_NODE_UNSUPPORTED", `Unsupported compositor nodes: ${unsupported.join(", ")}`)]);
|
||||
const missing = graph.resources.filter((resource) => !availableResourceIds.has(resource.sourceId));
|
||||
if (missing.length > 0) return blockedGate("N-020", "COMPOSITOR_GRAPH", [capabilityIssue("COMPOSITOR_RESOURCE_MISSING", `Missing compositor resources: ${missing.map((resource) => resource.sourceId).join(", ")}`)]);
|
||||
@@ -325,6 +404,7 @@ export function executeCompositorGraph(
|
||||
options: { width?: number; height?: number; cancelled?: () => boolean } = {},
|
||||
): CompositorExecutionResult {
|
||||
const graph = parseCompositorGraph(value);
|
||||
assertCompositorGraphExecutable(graph);
|
||||
const byId = new Map(graph.nodes.map((node) => [node.id, node]));
|
||||
const incoming = new Map<string, CompositorLinkIR>();
|
||||
graph.links.forEach((link) => incoming.set(`${link.toNodeId}:${link.toSocket}`, link));
|
||||
@@ -479,6 +559,7 @@ export async function executeCompositorGraphCached(
|
||||
cache: CompositorFrameCache,
|
||||
options: { frame: number; width?: number; height?: number; cancelled?: () => boolean },
|
||||
): Promise<CompositorCachedExecutionResult> {
|
||||
assertCompositorGraphExecutable(parseCompositorGraph(value));
|
||||
if (options.cancelled?.()) throw new CompositorValidationError("COMPOSITOR_CANCELLED", "Compositor execution was cancelled");
|
||||
const cacheKey = await compositorFrameCacheKey(value, sourceImages, options.frame, options.width, options.height);
|
||||
if (options.cancelled?.()) throw new CompositorValidationError("COMPOSITOR_CANCELLED", "Compositor execution was cancelled");
|
||||
|
||||
Reference in New Issue
Block a user