Advance M8-M11 parity workflows
This commit is contained in:
81
web/tests/e2e/compositor-node-golden.spec.ts
Normal file
81
web/tests/e2e/compositor-node-golden.spec.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const root = path.resolve(import.meta.dirname, "../../..");
|
||||
const golden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-07/compositor-node-golden.json"), "utf8")) as {
|
||||
width: number;
|
||||
height: number;
|
||||
maxAbsoluteError: number;
|
||||
allowlist: string[];
|
||||
fixture: { path: string };
|
||||
cases: Array<{ scene: string; newNode: string; nodeTypes: string[]; pixel: number[]; float32Sha256: string }>;
|
||||
};
|
||||
const fixture = fs.readFileSync(path.join(root, golden.fixture.path));
|
||||
|
||||
test("M11-07 gives every allowlisted compositor node an independent CPU/WebGPU golden", async ({ page }) => {
|
||||
test.setTimeout(60_000);
|
||||
await page.goto("/");
|
||||
const result = await page.evaluate(async (input) => {
|
||||
const [{ WebEngineClient }, cpu, gpu] = await Promise.all([
|
||||
import("/src/engine-client/WebEngineClient.ts"),
|
||||
import("/src/compositor/CompositorExecutor.ts"),
|
||||
import("/src/compositor/CompositorWebGPU.ts"),
|
||||
]);
|
||||
const hash = async (data: Float32Array): Promise<string> => Array.from(
|
||||
new Uint8Array(await crypto.subtle.digest("SHA-256", data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength))),
|
||||
(byte) => byte.toString(16).padStart(2, "0"),
|
||||
).join("");
|
||||
const client = new WebEngineClient({ timeoutMs: 30_000 });
|
||||
const device = await gpu.requestCompositorWebGPUDevice();
|
||||
try {
|
||||
const opened = await client.openBlend(Uint8Array.from(input.bytes).buffer);
|
||||
const cases = [];
|
||||
for (const expected of input.cases) {
|
||||
const scene = opened.snapshot.scenes.find((candidate) => candidate.name === expected.scene);
|
||||
if (!scene?.compositorGraph) throw new Error(`Missing compositor graph ${expected.scene}`);
|
||||
const plan = cpu.compileCompositorWebGPUPlan(scene.compositorGraph);
|
||||
const cpuResult = cpu.executeCompositorGraph(scene.compositorGraph, new Map(), { width: input.width, height: input.height }).composite;
|
||||
const gpuResult = await gpu.executeCompositorGraphWebGPU(device, scene.compositorGraph, input.width, input.height);
|
||||
let maxAbsoluteError = 0;
|
||||
for (let index = 0; index < cpuResult.data.length; index++) maxAbsoluteError = Math.max(maxAbsoluteError, Math.abs(cpuResult.data[index] - gpuResult.data[index]));
|
||||
cases.push({
|
||||
scene: expected.scene,
|
||||
newNode: expected.newNode,
|
||||
instructionTypes: plan.instructions.map((instruction) => instruction.type),
|
||||
cpuPixel: Array.from(cpuResult.data.slice(0, 4)),
|
||||
gpuPixel: Array.from(gpuResult.data.slice(0, 4)),
|
||||
cpuSha256: await hash(cpuResult.data),
|
||||
gpuSha256: await hash(gpuResult.data),
|
||||
maxAbsoluteError,
|
||||
});
|
||||
}
|
||||
const base = opened.snapshot.scenes.find((candidate) => candidate.name === input.cases[0].scene)?.compositorGraph;
|
||||
if (!base) throw new Error("Missing base compositor graph");
|
||||
let blockedCode = "";
|
||||
try {
|
||||
cpu.compileCompositorWebGPUPlan({ ...base, nodes: [...base.nodes, { id: "blocked:viewer", type: "VIEWER", name: "Blocked Viewer", properties: {} }] });
|
||||
}
|
||||
catch (error) { blockedCode = error instanceof cpu.CompositorValidationError ? error.code : String(error); }
|
||||
return { allowlist: cpu.COMPOSITOR_WEBGPU_NODE_ALLOWLIST, cases, blockedCode };
|
||||
}
|
||||
finally {
|
||||
device.destroy();
|
||||
client.terminate();
|
||||
}
|
||||
}, { bytes: Array.from(fixture), cases: golden.cases, width: golden.width, height: golden.height });
|
||||
|
||||
expect(result.allowlist).toEqual(golden.allowlist);
|
||||
expect(result.blockedCode).toBe("COMPOSITOR_NODE_UNSUPPORTED");
|
||||
for (const [index, candidate] of result.cases.entries()) {
|
||||
const expected = golden.cases[index];
|
||||
expect(candidate.scene).toBe(expected.scene);
|
||||
expect(candidate.newNode).toBe(expected.newNode);
|
||||
expect(candidate.instructionTypes).toEqual(expected.nodeTypes);
|
||||
expect(candidate.cpuPixel).toEqual(expected.pixel);
|
||||
expect(candidate.gpuPixel).toEqual(expected.pixel);
|
||||
expect(candidate.cpuSha256).toBe(expected.float32Sha256);
|
||||
expect(candidate.gpuSha256).toBe(expected.float32Sha256);
|
||||
expect(candidate.maxAbsoluteError).toBeLessThanOrEqual(golden.maxAbsoluteError);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user