Files
workinf_Blender_Wasm/web/tests/e2e/nanovdb-main-thread.spec.ts
mes123456 0fe8d2bb56
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

102 lines
6.1 KiB
TypeScript

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