Advance M8-M11 parity workflows
This commit is contained in:
103
web/tests/e2e/render-reference.spec.ts
Normal file
103
web/tests/e2e/render-reference.spec.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const root = path.resolve(import.meta.dirname, "../../..");
|
||||
const goldenRoot = path.join(root, "tests/golden/M11-04");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(goldenRoot, "manifest.json"), "utf8"));
|
||||
const blend = fs.readFileSync(path.join(root, manifest.source.fixture));
|
||||
const referencePng = fs.readFileSync(path.join(goldenRoot, manifest.reference.file));
|
||||
|
||||
async function decodeAndCompare(page: import("@playwright/test").Page, reference: Buffer, actual: Buffer) {
|
||||
return page.evaluate(async ({ referenceBytes, actualBytes, thresholds }) => {
|
||||
const { compareRenderImages } = await import("/src/three-adapter/render-image-comparison.ts");
|
||||
const decode = async (bytes: number[]) => {
|
||||
const bitmap = await createImageBitmap(new Blob([Uint8Array.from(bytes)], { type: "image/png" }), {
|
||||
colorSpaceConversion: "none",
|
||||
premultiplyAlpha: "none",
|
||||
});
|
||||
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
||||
const context = canvas.getContext("2d", { willReadFrequently: true });
|
||||
if (!context) throw new Error("2D decode context is unavailable");
|
||||
context.drawImage(bitmap, 0, 0);
|
||||
const pixels = new Uint8Array(context.getImageData(0, 0, bitmap.width, bitmap.height).data);
|
||||
const result = { width: bitmap.width, height: bitmap.height, pixels };
|
||||
bitmap.close();
|
||||
return result;
|
||||
};
|
||||
const [referenceFrame, actualFrame] = await Promise.all([decode(referenceBytes), decode(actualBytes)]);
|
||||
if (referenceFrame.width !== actualFrame.width || referenceFrame.height !== actualFrame.height) {
|
||||
throw new Error("render frame dimensions differ");
|
||||
}
|
||||
return compareRenderImages(
|
||||
referenceFrame.pixels,
|
||||
actualFrame.pixels,
|
||||
referenceFrame.width,
|
||||
referenceFrame.height,
|
||||
thresholds,
|
||||
);
|
||||
}, { referenceBytes: Array.from(reference), actualBytes: Array.from(actual), thresholds: manifest.thresholds });
|
||||
}
|
||||
|
||||
for (const offscreen of [false, true]) {
|
||||
test(`M11-04 ${offscreen ? "Offscreen" : "main-thread"} production render matches the Blender reference metrics`, async ({ page }) => {
|
||||
test.setTimeout(60_000);
|
||||
await page.goto("/");
|
||||
await page.evaluate(async ({ input, useOffscreen }) => {
|
||||
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
|
||||
const { ViewportRenderer } = await import("/src/three-adapter/viewport.ts");
|
||||
const { OffscreenViewportRenderer } = await import("/src/three-adapter/offscreen-viewport.ts");
|
||||
document.body.replaceChildren();
|
||||
document.body.style.margin = "0";
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.id = "m11-reference-canvas";
|
||||
canvas.style.width = "256px";
|
||||
canvas.style.height = "256px";
|
||||
document.body.append(canvas);
|
||||
const client = new WebEngineClient({ timeoutMs: 30_000 });
|
||||
const opened = await client.openBlend(Uint8Array.from(input).buffer);
|
||||
opened.snapshot.activeObjectId = null;
|
||||
const renderer = useOffscreen ? new OffscreenViewportRenderer(canvas) : new ViewportRenderer(canvas);
|
||||
renderer.setSnapshot(opened.snapshot, opened.geometryBuffers, opened.nonMeshGeometryBuffers ?? []);
|
||||
for (let attempt = 0; attempt < 300; attempt++) {
|
||||
if (!useOffscreen || (Number(canvas.dataset.rendererPixels) > 0 && canvas.dataset.renderBudgetStatus === "ready")) break;
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
(globalThis as typeof globalThis & { __m11Reference?: { renderer: { dispose(): void }; client: { terminate(): void } } }).__m11Reference = { renderer, client };
|
||||
}, { input: Array.from(blend), useOffscreen: offscreen });
|
||||
|
||||
const canvas = page.locator("#m11-reference-canvas");
|
||||
await expect(canvas).toHaveAttribute("data-render-budget-status", "ready");
|
||||
const actualPng = await canvas.screenshot({ animations: "disabled" });
|
||||
const report = await decodeAndCompare(page, referencePng, actualPng);
|
||||
console.log(`m11-render-reference-${offscreen ? "offscreen" : "main"}`, JSON.stringify(report));
|
||||
expect(report.status).toBe("READY");
|
||||
expect(report.errorCode).toBeNull();
|
||||
expect(report.checks.every((item) => item.passed)).toBe(true);
|
||||
|
||||
await page.evaluate(() => {
|
||||
const state = (globalThis as typeof globalThis & { __m11Reference?: { renderer: { dispose(): void }; client: { terminate(): void } } }).__m11Reference;
|
||||
state?.renderer.dispose();
|
||||
state?.client.terminate();
|
||||
delete (globalThis as typeof globalThis & { __m11Reference?: unknown }).__m11Reference;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("M11-04 rejects a non-empty frame with the wrong composition", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const wrong = await page.evaluate(async () => {
|
||||
const canvas = new OffscreenCanvas(256, 256);
|
||||
const context = canvas.getContext("2d")!;
|
||||
context.fillStyle = "rgb(58,58,58)";
|
||||
context.fillRect(0, 0, 256, 256);
|
||||
context.fillStyle = "rgb(255,0,0)";
|
||||
context.fillRect(0, 0, 32, 32);
|
||||
return Array.from(new Uint8Array(await (await canvas.convertToBlob({ type: "image/png" })).arrayBuffer()));
|
||||
});
|
||||
const report = await decodeAndCompare(page, referencePng, Buffer.from(wrong));
|
||||
expect(report.status).toBe("BLOCKED");
|
||||
expect(report.errorCode).toBe("RENDER_REFERENCE_MISMATCH");
|
||||
expect(report.checks.some((item) => !item.passed)).toBe(true);
|
||||
});
|
||||
Reference in New Issue
Block a user