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 => 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); } });