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 = {}; const hashes: Record = {}; 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((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 = {}; 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)); });