89 lines
4.9 KiB
TypeScript
89 lines
4.9 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { compareNanoVDBRenderGolden, type NanoVDBRenderGoldenThresholdsIR } from "../../protocol/nanovdb-render-golden";
|
|
|
|
const goldenRoot = path.resolve(import.meta.dirname, "../../../tests/golden/M8-19");
|
|
const goldenManifest = JSON.parse(fs.readFileSync(path.join(goldenRoot, "manifest.json"), "utf8")) as {
|
|
schemaVersion: number;
|
|
source: { sha256: string };
|
|
renderContract: {
|
|
shaderSemanticVersion: string;
|
|
width: number;
|
|
height: number;
|
|
axes: Array<"X" | "Y" | "Z">;
|
|
thresholds: NanoVDBRenderGoldenThresholdsIR;
|
|
};
|
|
images: Array<{ axis: "X" | "Y" | "Z"; file: string; sha256: string }>;
|
|
};
|
|
const referenceImages = Object.fromEntries(goldenManifest.images.map((image) => [image.axis, new Uint8Array(fs.readFileSync(path.join(goldenRoot, image.file))) ])) as Record<"X" | "Y" | "Z", Uint8Array>;
|
|
|
|
test("M8-19 compares desktop OpenVDB with main-thread and Offscreen WebGPU goldens", async ({ page }) => {
|
|
test.setTimeout(180_000);
|
|
await page.goto("/");
|
|
const main = await page.evaluate(async () => {
|
|
const [viewport, renderer] = await Promise.all([
|
|
import("/src/volume/nanovdb-viewport.ts"),
|
|
import("/src/render/nanovdb-volume-renderer.ts"),
|
|
]);
|
|
const asset = await viewport.loadNanoVDBViewportAsset("m8-19-main", "/__vdb_fixture__/manifest", "/__vdb_fixture__/bundle", new AbortController().signal);
|
|
const density = asset.manifest.grids.find((grid) => grid.name === asset.manifest.material.densityGrid);
|
|
const payload = asset.grids.find((grid) => grid.name === density?.name)?.data;
|
|
if (!density || !payload) throw new Error("NANOVDB_STREAM_INCOMPLETE: M8-19 density payload is missing");
|
|
const session = new renderer.NanoVDBWebGPUDeviceSession();
|
|
const device = await session.open(payload.byteLength, 4);
|
|
const uploaded = renderer.uploadNanoVDBFloat32Grid(device, payload);
|
|
const material = { ...asset.manifest.material, temperatureGrid: undefined, colorGrid: undefined, emissionGrid: undefined, interpolation: "LINEAR" as const };
|
|
const images: Record<string, number[]> = {};
|
|
const hashes: Record<string, string> = {};
|
|
for (const axis of ["X", "Y", "Z"] as const) {
|
|
const pixels = await renderer.renderNanoVDBFloat32WebGPU(device, uploaded, density, material, 64, 64, {}, axis);
|
|
images[axis] = Array.from(pixels);
|
|
hashes[axis] = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", pixels))).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
}
|
|
uploaded.dispose();
|
|
session.dispose();
|
|
return { sourceSha256: asset.manifest.sourceSha256, shaderSemanticVersion: asset.manifest.gpu.shaderSemanticVersion, hashes, images };
|
|
});
|
|
const offscreen = await page.evaluate(() => new Promise<any>((resolve, reject) => {
|
|
const worker = new Worker("/src/workers/nanovdb-render-golden-test.worker.ts", { type: "module" });
|
|
worker.onmessage = (event) => {
|
|
worker.terminate();
|
|
if (event.data.error) { reject(new Error(event.data.error)); return; }
|
|
resolve({
|
|
...event.data,
|
|
images: Object.fromEntries(Object.entries(event.data.images).map(([axis, buffer]) => [axis, Array.from(new Uint8Array(buffer as ArrayBuffer))])),
|
|
});
|
|
};
|
|
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
|
|
worker.postMessage({});
|
|
}));
|
|
|
|
expect(goldenManifest.schemaVersion).toBe(1);
|
|
expect(main.sourceSha256).toBe(goldenManifest.source.sha256);
|
|
expect(offscreen.sourceSha256).toBe(goldenManifest.source.sha256);
|
|
expect(main.shaderSemanticVersion).toBe(goldenManifest.renderContract.shaderSemanticVersion);
|
|
expect(offscreen.shaderSemanticVersion).toBe(goldenManifest.renderContract.shaderSemanticVersion);
|
|
const report: Record<string, unknown> = {};
|
|
for (const image of goldenManifest.images) {
|
|
const reference = referenceImages[image.axis];
|
|
const mainPixels = Uint8Array.from(main.images[image.axis]);
|
|
const offscreenPixels = Uint8Array.from(offscreen.images[image.axis]);
|
|
const mainComparison = compareNanoVDBRenderGolden(reference, mainPixels, goldenManifest.renderContract.thresholds);
|
|
const offscreenComparison = compareNanoVDBRenderGolden(reference, offscreenPixels, goldenManifest.renderContract.thresholds);
|
|
const backendComparison = compareNanoVDBRenderGolden(mainPixels, offscreenPixels, {
|
|
maxChannelError: 0,
|
|
meanAbsoluteError: 0,
|
|
rmsError: 0,
|
|
alphaCoverageDeltaRatio: 0,
|
|
});
|
|
expect(main.hashes[image.axis]).toBe(image.sha256);
|
|
expect(offscreen.hashes[image.axis]).toBe(image.sha256);
|
|
expect(mainComparison.status).toBe("READY");
|
|
expect(offscreenComparison.status).toBe("READY");
|
|
expect(backendComparison.status).toBe("READY");
|
|
report[image.axis] = { main: mainComparison, offscreen: offscreenComparison, backend: backendComparison, sha256: image.sha256 };
|
|
}
|
|
console.log("nanovdb-render-golden", JSON.stringify(report));
|
|
});
|