Advance M8-M11 parity workflows
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-08-17 04:37:07 -04:00
parent 7c16b279ae
commit 0fe8d2bb56
324 changed files with 31920 additions and 863 deletions

View File

@@ -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");