import { expect, test } from "@playwright/test"; test("streams ten-million-triangle SceneIR ranges into a bounded interactive LOD", async ({ page }) => { test.setTimeout(180_000); await page.goto("/"); const result = await page.evaluate(async () => { const [largeGeometry, viewportModule, offscreenModule, storageModule, engineModule] = await Promise.all([ import("/src/performance/large-geometry.ts"), import("/src/three-adapter/viewport.ts"), import("/src/three-adapter/offscreen-viewport.ts"), import("/src/storage/StorageClient.ts"), import("/src/engine-client/WebEngineClient.ts"), ]); const { MeshGeometryStreamValidator, parseMeshGeometryStreamRequest, buildLODCacheKey, encodeLODGeometry, decodeLODGeometry } = largeGeometry; const { ViewportRenderer } = viewportModule; const { OffscreenViewportRenderer } = offscreenModule; const { StorageClient } = storageModule; const { WebEngineClient } = engineModule; const worker = new Worker("/src/workers/geometry-stream.worker.ts", { type: "module" }); const started = performance.now(); const heap = (): number => Number((performance as Performance & { memory?: { usedJSHeapSize?: number } }).memory?.usedJSHeapSize ?? 0); const baselineHeapBytes = heap(); let peakHeapBytes = baselineHeapBytes; const engine = new WebEngineClient({ timeoutMs: 60_000 }); const wasm = await engine.init(); const createCanvas = (): HTMLCanvasElement => { const canvas = document.createElement("canvas"); canvas.width = 320; canvas.height = 240; canvas.style.cssText = "position:fixed;left:0;top:0;width:320px;height:240px"; document.body.append(canvas); return canvas; }; const waitFor = async (condition: () => boolean, timeoutMs = 30_000): Promise => { const deadline = performance.now() + timeoutMs; while (!condition()) { if (performance.now() > deadline) throw new Error("GEOMETRY_STREAM_BUDGET_EXCEEDED: viewport did not become interactive"); await new Promise((resolve) => setTimeout(resolve, 20)); } }; const visiblePixels = (renderer: InstanceType): number => { const gl = renderer.renderer.getContext(); const pixels = new Uint8Array(64 * 64 * 4); gl.readPixels( Math.max(0, Math.floor((gl.drawingBufferWidth - 64) / 2)), Math.max(0, Math.floor((gl.drawingBufferHeight - 64) / 2)), 64, 64, gl.RGBA, gl.UNSIGNED_BYTE, pixels, ); let visible = 0; for (let index = 0; index < pixels.length; index += 4) { if (pixels[index] + pixels[index + 1] + pixels[index + 2] > 80) visible++; } return visible; }; const snapshot = (meshId: string, triangleCount: number) => ({ schemaVersion: 1 as const, revision: 1, sceneId: "scene:geometry-10m", source: { kind: "mock" as const }, coordinateSystem: { upAxis: "Z" as const, forwardAxis: "-Y" as const, handedness: "RIGHT" as const, unitSystem: 0, unitScale: 1 }, activeObjectId: "object:geometry-10m", frame: { current: 1, start: 1, end: 250 }, nodes: [{ id: "object:geometry-10m", name: "Ten Million Triangle Mesh", type: "MESH" as const, parentId: null, dataId: meshId, visible: true, selectable: true, localMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], worldMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], transform: { translation: [0, 0, 0] as [number, number, number], rotationEuler: [0, 0, 0] as [number, number, number], scale: [1, 1, 1] as [number, number, number], rotationMode: 1 }, }], meshes: [{ id: meshId, name: "Ten Million Triangle Mesh", vertexCount: 5_100_000, edgeCount: 0, faceCount: triangleCount, cornerCount: triangleCount * 3, triangleCount, geometryStatus: "binary" as const, geometryBufferId: meshId, bounds: { min: [-2, -2, -0.1] as [number, number, number], max: [2, 2, 0.1] as [number, number, number] }, }], materials: [], cameras: [], lights: [], worlds: [], images: [], animations: [], collections: [], scenes: [{ id: "scene:geometry-10m", name: "Geometry 10M" }], }); type Geometry = import("/src/performance/large-geometry.ts").MeshGeometryStreamChunkIR; type Report = import("/src/performance/large-geometry.ts").MeshGeometryStreamReportIR; interface SessionResult { report: Report; lod?: Geometry; acceptedChunks: number; detachedTransfers: number; lateChunks: number; validator?: Awaited["finish"]>>; viewport?: Awaited>; cancelLatencyMs?: number; } const renderLOD = async (geometry: Geometry, acceptedChunks: () => number) => { const meshId = "mesh:geometry-10m"; const lodMeshId = `${meshId}:lod:0`; const geometryBuffer = { schemaVersion: 1 as const, meshId: lodMeshId, byteLength: geometry.byteLength, positions: geometry.positions, indices: geometry.indices, }; const sourceBuffer = { ...geometryBuffer, meshId }; const level = { level: 0, meshId: lodMeshId, triangleBudget: geometry.triangleCount, outputTriangleCount: geometry.triangleCount, outputVertexCount: geometry.vertexCount, geometryBuffers: [geometryBuffer], }; const lowColumns = 32; const lowRows = 32; const lowPositions = new Float32Array((lowColumns + 1) * (lowRows + 1) * 3); for (let y = 0; y <= lowRows; y++) for (let x = 0; x <= lowColumns; x++) { const offset = (y * (lowColumns + 1) + x) * 3; lowPositions.set([x / lowColumns * 4 - 2, y / lowRows * 4 - 2, 0], offset); } const lowIndices = new Uint32Array(lowColumns * lowRows * 6); for (let cell = 0; cell < lowColumns * lowRows; cell++) { const x = cell % lowColumns; const y = Math.floor(cell / lowColumns); const a = y * (lowColumns + 1) + x; lowIndices.set([a, a + 1, a + lowColumns + 2, a, a + lowColumns + 2, a + lowColumns + 1], cell * 6); } const lowGeometryBuffer = { schemaVersion: 1 as const, meshId: `${meshId}:lod:1`, byteLength: lowPositions.byteLength + lowIndices.byteLength, positions: lowPositions.buffer, indices: lowIndices.buffer, }; const lowLevel = { level: 1, meshId: lowGeometryBuffer.meshId, triangleBudget: lowColumns * lowRows * 2, outputTriangleCount: lowColumns * lowRows * 2, outputVertexCount: lowPositions.length / 3, geometryBuffers: [lowGeometryBuffer], }; const cacheKey = buildLODCacheKey({ objectId: "object:geometry-10m", meshRevision: 1, modifierStackHash: "none", profileHash: "geometry-10m-lod-v1", poseOrRestState: "REST" }); const encoded = encodeLODGeometry([level, lowLevel]); const encodedBytes = encoded.byteLength; const projectId = `geometry-10m-${Date.now()}`; const manifest = { schemaVersion: 1 as const, meshId, sourceMeshRevision: 1, levels: [ { level: 0, sourceMeshRevision: 1, triangleBudget: geometry.triangleCount, meshId: lodMeshId }, { level: 1, sourceMeshRevision: 1, triangleBudget: lowLevel.triangleBudget, meshId: lowLevel.meshId }, ], cacheKey, objectId: "object:geometry-10m", modifierStackHash: "none", profileHash: "geometry-10m-lod-v1", poseOrRestState: "REST" as const, generatedAt: "2026-08-14T00:00:00.000Z", byteLength: encodedBytes, }; const writer = new StorageClient(); const saved = await writer.saveLOD(projectId, cacheKey, encoded); await writer.putLODManifest(projectId, manifest); writer.terminate(); const reader = new StorageClient(); const reopenedManifest = await reader.getLODManifest(projectId, cacheKey); const reopened = await reader.readLOD(projectId, cacheKey); const decoded = decodeLODGeometry(reopened.data); let selectedObjectId = ""; const mainCanvas = createCanvas(); const main = new ViewportRenderer(mainCanvas, (objectId) => { selectedObjectId = objectId; }); main.setSnapshot(snapshot(meshId, 10_000_000), [sourceBuffer]); main.installLODLevels(meshId, [level, lowLevel]); main.camera.position.set(0.1, -0.1, 0.1); const nearLOD = main.updateLODSelection().get(meshId)?.level; main.camera.position.set(100, -100, 100); const farLOD = main.updateLODSelection().get(meshId)?.level; main.camera.position.set(4.5, -4.5, 3.5); main.updateLODSelection(); await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); const mainVisible = visiblePixels(main); const bounds = mainCanvas.getBoundingClientRect(); mainCanvas.dispatchEvent(new MouseEvent("click", { clientX: bounds.left + bounds.width / 2, clientY: bounds.top + bounds.height / 2, bubbles: true })); mainCanvas.dispatchEvent(new WheelEvent("wheel", { deltaY: -20, bubbles: true, cancelable: true })); await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); const mainGeometries = main.renderer.info.memory.geometries; const offscreenCanvas = createCanvas(); const offscreen = new OffscreenViewportRenderer(offscreenCanvas); offscreen.setSnapshot(snapshot(meshId, 10_000_000), [sourceBuffer]); await waitFor(() => Number(offscreenCanvas.dataset.rendererPixels ?? 0) > 0); const offscreenVisible = Number(offscreenCanvas.dataset.rendererPixels ?? 0); offscreenCanvas.dispatchEvent(new WheelEvent("wheel", { deltaY: 20, bubbles: true, cancelable: true })); await new Promise((resolve) => setTimeout(resolve, 50)); const firstInteractiveMs = Math.round(performance.now() - started); const chunksAtFirstInteractive = acceptedChunks(); const estimatedGpuBytes = geometry.byteLength * 3 + lowGeometryBuffer.byteLength; main.dispose(); offscreen.dispose(); mainCanvas.remove(); offscreenCanvas.remove(); const deleted = await reader.deleteLOD(projectId, cacheKey); const missing = await reader.getLODManifest(projectId, cacheKey); reader.terminate(); return { firstInteractiveMs, chunksAtFirstInteractive, mainVisible, offscreenVisible, selectedObjectId, lodSelections: [nearLOD, farLOD], mainGeometries, estimatedGpuBytes, cache: { key: cacheKey, savedBytes: saved.bytes, encodedBytes, reopenedBytes: reopened.bytes, persisted: reopenedManifest.persisted, decodedTriangles: decoded.map((item) => item.outputTriangleCount), deleted: deleted.persisted && !missing.persisted, }, }; }; const runSession = ( streamId: string, cancelAfterChunks?: number, onLOD?: (geometry: Geometry, acceptedChunks: () => number) => Promise>>, ): Promise => { const request = parseMeshGeometryStreamRequest({ schemaVersion: 1, streamId, meshId: "mesh:geometry-10m", triangleCount: 10_000_000, chunkTriangleCount: 250_000, lodTriangleCount: 20_000, }); const validator = new MeshGeometryStreamValidator(request); let acceptedChunks = 0; let detachedTransfers = 0; let lateChunks = 0; let cancellationRequested = false; let cancellationStarted = 0; let lod: Geometry | undefined; let viewport: Promise>> | undefined; return new Promise((resolve, reject) => { worker.onerror = (event) => reject(new Error(event.message)); worker.onmessage = async (event: MessageEvent) => { try { const message = event.data; if (message.type === "error") throw new Error(`${message.code}: ${message.message}`); if (message.type === "detached") { if (message.positionsByteLength !== 0 || message.indicesByteLength !== 0) throw new Error("GEOMETRY_STREAM_BUDGET_EXCEEDED: sender retained transferred buffers"); detachedTransfers++; return; } if (message.type === "lod") { lod = message.geometry; if (onLOD) viewport = onLOD(lod, () => acceptedChunks); worker.postMessage({ type: "ack", streamId, chunkIndex: -1 }); return; } if (message.type === "chunk") { if (cancellationRequested) { lateChunks++; return; } await validator.accept(message.chunk); acceptedChunks++; peakHeapBytes = Math.max(peakHeapBytes, heap()); if (cancelAfterChunks !== undefined && acceptedChunks >= cancelAfterChunks) { cancellationRequested = true; cancellationStarted = performance.now(); worker.postMessage({ type: "cancel", streamId }); } else worker.postMessage({ type: "ack", streamId, chunkIndex: message.chunk.chunkIndex }); return; } if (message.type === "cancelled") { await new Promise((done) => setTimeout(done, 50)); resolve({ report: message.report, lod, acceptedChunks, detachedTransfers, lateChunks, cancelLatencyMs: Math.round(performance.now() - cancellationStarted) }); return; } if (message.type === "complete") { const validated = await validator.finish(); const rendered = await viewport; resolve({ report: message.report, lod, acceptedChunks, detachedTransfers, lateChunks, validator: validated, viewport: rendered }); } } catch (error) { reject(error); } }; worker.postMessage({ type: "start", request }); }); }; const cancelled = await runSession("geometry-10m-cancel", 2); const completed = await runSession("geometry-10m-complete", undefined, renderLOD); const invalidCode = await new Promise((resolve, reject) => { worker.onerror = (event) => reject(new Error(event.message)); worker.onmessage = (event: MessageEvent) => { if (event.data.type === "error") resolve(event.data.code); }; worker.postMessage({ type: "start", request: { schemaVersion: 1, streamId: "geometry-invalid", meshId: "mesh:invalid", triangleCount: 0, chunkTriangleCount: 1, lodTriangleCount: 1 } }); }); worker.terminate(); engine.terminate(); const recoveryCanvas = createCanvas(); const recovery = new ViewportRenderer(recoveryCanvas); const recoveryPositions = new Float32Array([-1, -1, 0, 1, -1, 0, 0, 1, 0]).buffer; const recoveryIndices = new Uint32Array([0, 1, 2]).buffer; recovery.setSnapshot(snapshot("mesh:geometry-10m", 1), [{ schemaVersion: 1, meshId: "mesh:geometry-10m", byteLength: recoveryPositions.byteLength + recoveryIndices.byteLength, positions: recoveryPositions, indices: recoveryIndices }]); await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); const recoveryVisible = visiblePixels(recovery); recovery.dispose(); recoveryCanvas.remove(); const afterHeapBytes = heap(); return { elapsedMs: Math.round(performance.now() - started), baselineHeapBytes, peakHeapBytes, afterHeapBytes, wasmAllocatedBytes: wasm.allocatedBytes, invalidCode, cancelled, completed, recoveryVisible, }; }); console.log("geometry-10m-performance", JSON.stringify({ triangleCount: result.completed.report.triangleCount, chunkCount: result.completed.report.chunkCount, transferredBytes: result.completed.report.transferredBytes, peakWorkingSetBytes: result.completed.report.peakWorkingSetBytes, manifestSha256: result.completed.report.manifestSha256, firstInteractiveMs: result.completed.viewport?.firstInteractiveMs, chunksAtFirstInteractive: result.completed.viewport?.chunksAtFirstInteractive, estimatedGpuBytes: result.completed.viewport?.estimatedGpuBytes, wasmAllocatedBytes: result.wasmAllocatedBytes, baselineHeapBytes: result.baselineHeapBytes, peakHeapBytes: result.peakHeapBytes, afterHeapBytes: result.afterHeapBytes, cacheDeleted: result.completed.viewport?.cache.deleted, recoveryVisible: result.recoveryVisible, cancelLatencyMs: result.cancelled.cancelLatencyMs, elapsedMs: result.elapsedMs, })); expect(result.invalidCode).toBe("GEOMETRY_STREAM_INVALID"); expect(result.cancelled.report).toMatchObject({ status: "CANCELLED", chunkCount: 2, triangleCount: 500_000 }); expect(result.cancelled.acceptedChunks).toBe(2); expect(result.cancelled.lateChunks).toBe(0); expect(result.cancelled.cancelLatencyMs).toBeLessThan(2_000); expect(result.completed.report).toMatchObject({ status: "COMPLETED", triangleCount: 10_000_000, chunkCount: 40 }); expect(result.completed.acceptedChunks).toBe(40); expect(result.completed.detachedTransfers).toBe(41); expect(result.completed.report.transferredBytes).toBe(result.completed.validator?.transferredBytes); expect(result.completed.report.peakWorkingSetBytes).toBeLessThanOrEqual(8 * 1024 * 1024); expect(result.completed.report.manifestSha256).toBe(result.completed.validator?.manifestSha256); expect(result.completed.report.manifestSha256).toBe("224738e0c3aa28ae42167bf8974455a3d0f940f59f19eb51c5768f435c66b24f"); expect(result.completed.viewport?.firstInteractiveMs).toBeLessThan(60_000); expect(result.completed.viewport?.chunksAtFirstInteractive).toBeLessThan(40); expect(result.completed.viewport?.mainVisible).toBeGreaterThan(100); expect(result.completed.viewport?.offscreenVisible).toBeGreaterThan(10); expect(result.completed.viewport?.selectedObjectId).toBe("object:geometry-10m"); expect(result.completed.viewport?.lodSelections).toEqual([0, 1]); expect(result.completed.viewport?.estimatedGpuBytes).toBeLessThan(16 * 1024 * 1024); expect(result.completed.viewport?.cache).toMatchObject({ persisted: true, decodedTriangles: [20_000, 2_048], deleted: true }); expect(result.completed.viewport?.cache.savedBytes).toBe(result.completed.viewport?.cache.encodedBytes); expect(result.completed.viewport?.cache.reopenedBytes).toBe(result.completed.viewport?.cache.encodedBytes); expect(result.wasmAllocatedBytes).toBeLessThan(512 * 1024 * 1024); if (result.baselineHeapBytes > 0) expect(result.peakHeapBytes - result.baselineHeapBytes).toBeLessThan(768 * 1024 * 1024); expect(result.recoveryVisible).toBeGreaterThan(100); expect(result.elapsedMs).toBeLessThan(150_000); });