import { expect, test } from "@playwright/test"; test("M8-11 main-thread WebGPU loads a missing page and renders deterministic pixels", async ({ page }) => { test.setTimeout(90_000); await page.goto("/"); const result = await page.evaluate(async () => { const [viewport, renderer, { NanoVDBFloat32Sampler }] = await Promise.all([ import("/src/volume/nanovdb-viewport.ts"), import("/src/render/nanovdb-volume-renderer.ts"), import("/src/volume/nanovdb-float32.ts"), ]); const manifest = await viewport.loadNanoVDBViewportAsset("main-thread-fixture", "/__vdb_fixture__/manifest", "/__vdb_fixture__/bundle", new AbortController().signal); const density = manifest.manifest.grids.find((grid) => grid.name === manifest.manifest.material.densityGrid); if (!density || !manifest.manifest.gpu.float32TreeLayout) throw new Error("NANOVDB_MANIFEST_INVALID: main-thread fixture is incomplete"); const response = await fetch("/__vdb_fixture__/bundle", { cache: "no-store", headers: { Range: `bytes=${density.byteOffset}-${density.byteOffset + density.byteLength - 1}` }, }); if (response.status !== 206) throw new Error("NANOVDB_STREAM_INCOMPLETE: main-thread fixture range was not served"); const payload = await response.arrayBuffer(); const sampler = new NanoVDBFloat32Sampler(payload, density, manifest.manifest.gpu.float32TreeLayout); const coordinate = [0, 0, 0] as const; const leafByteOffset = sampler.leafByteOffset(coordinate); if (leafByteOffset === null) throw new Error("NANOVDB_GRID_UNSUPPORTED: main-thread fixture has no leaf"); const pageByteLength = 256 * 1024; const pageCount = Math.ceil(payload.byteLength / pageByteLength); const leafPageId = Math.floor(leafByteOffset / pageByteLength); if (leafPageId <= 0 || leafPageId >= pageCount) throw new Error("NANOVDB_GRID_UNSUPPORTED: main-thread leaf page is not pageable"); const session = new renderer.NanoVDBWebGPUDeviceSession(); const device = await session.open(pageCount * pageByteLength, 6); const grid = renderer.createNanoVDBFloat32GridPaged(device, payload.byteLength, pageByteLength, pageCount * pageByteLength); for (let pageId = 0; pageId < pageCount; pageId++) { if (pageId === leafPageId) continue; grid.uploadPage(pageId, payload.slice(pageId * pageByteLength, Math.min(payload.byteLength, (pageId + 1) * pageByteLength))); } const feedbackBuffer = renderer.createNanoVDBPageFeedbackGPUBuffer(device, 4); const beforeLoad = await renderer.sampleNanoVDBFloat32WebGPU(device, grid, [coordinate], feedbackBuffer); const missing = await renderer.readNanoVDBPageFeedbackGPUBuffer(device, feedbackBuffer, 4, pageCount, 17); const pageSource = async (pageId: number, signal: AbortSignal): Promise => { if (signal.aborted) throw new DOMException("main-thread page request cancelled", "AbortError"); const start = density.byteOffset + pageId * pageByteLength; const end = Math.min(density.byteOffset + density.byteLength, start + pageByteLength) - 1; const pageResponse = await fetch("/__vdb_fixture__/bundle", { cache: "no-store", headers: { Range: `bytes=${start}-${end}` }, signal }); if (pageResponse.status !== 206) throw new Error(`NANOVDB_STREAM_INCOMPLETE: page range returned ${pageResponse.status}`); return pageResponse.arrayBuffer(); }; const loadedPages: number[] = []; const page = await pageSource(missing.pageIds[0], new AbortController().signal); const frames: Array<() => void> = []; let redraws = 0; const scheduler = new renderer.NanoVDBProgressiveRedrawScheduler((callback) => frames.push(callback), () => { redraws++; }); const uploader = new renderer.NanoVDBProgressivePageUploader(grid, scheduler); loadedPages.push(missing.pageIds[0]); const upload = uploader.upload(missing.pageIds[0], page.slice(0)); const queuedBeforeRedraw = frames.length; frames.shift()?.(); const afterLoad = await renderer.sampleNanoVDBFloat32WebGPU(device, grid, [coordinate]); const material = { ...manifest.manifest.material, interpolation: "LINEAR" as const }; const pixelsA = await renderer.renderNanoVDBFloat32WebGPU(device, grid, density, material, 64, 64); const pixelsB = await renderer.renderNanoVDBFloat32WebGPU(device, grid, density, material, 64, 64); const digest = async (pixels: Uint8Array): Promise => Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", pixels))).map((byte) => byte.toString(16).padStart(2, "0")).join(""); const visible = (pixels: Uint8Array): number => { let count = 0; for (let index = 0; index < pixels.length; index += 4) if (pixels[index] + pixels[index + 1] + pixels[index + 2] > 24) count++; return count; }; const output = { pageCount, leafPageId, beforeLoad, missing, loadedPages, upload, queuedBeforeRedraw, redraws, afterLoad, pixels: { width: 64, height: 64, bytes: pixelsA.byteLength, visible: visible(pixelsA), sha256A: await digest(pixelsA), sha256B: await digest(pixelsB) }, scheduler: scheduler.stats(), }; scheduler.dispose(); grid.dispose(); feedbackBuffer.destroy(); session.dispose(); return output; }); expect(result.pageCount).toBeGreaterThan(1); expect(result.leafPageId).toBeGreaterThan(0); expect(result.beforeLoad[0].valid).toBe(true); expect(result.missing.status).toBe("READY"); expect(result.missing.pageIds).toEqual([result.leafPageId]); expect(result.loadedPages).toEqual([result.leafPageId]); expect(result.upload).toEqual({ pageId: result.leafPageId, redrawScheduled: true }); expect(result.queuedBeforeRedraw).toBe(1); expect(result.redraws).toBe(1); expect(result.afterLoad[0].valid).toBe(true); expect(result.pixels).toMatchObject({ width: 64, height: 64, bytes: 64 * 64 * 4 }); expect(result.pixels.visible).toBeGreaterThan(0); expect(result.pixels.sha256A).toBe("87d08a77a644f37ed59609ebdd18555ab1924f9a3b30d8d084db35e7776cfb9c"); expect(result.pixels.sha256A).toBe(result.pixels.sha256B); expect(result.scheduler).toMatchObject({ pending: false, scheduledCount: 1, redrawCount: 1, capped: false, errorCode: null }); });