import { expect, test } from "@playwright/test"; test("M9-09 derives the same occlusion set from real main-thread and Offscreen GPU depth passes", async ({ page }) => { test.setTimeout(60_000); await page.goto("/"); const result = await page.evaluate(async () => { const { ViewportRenderer } = await import("/src/three-adapter/viewport.ts"); const { OffscreenViewportRenderer } = await import("/src/three-adapter/offscreen-viewport.ts"); const normalize = (value: number[]) => { const length = Math.hypot(...value); return value.map((component) => component / length); }; const cross = (left: number[], right: number[]) => [ left[1] * right[2] - left[2] * right[1], left[2] * right[0] - left[0] * right[2], left[0] * right[1] - left[1] * right[0], ]; const add = (...values: number[][]) => values[0].map((_, axis) => values.reduce((sum, value) => sum + value[axis], 0)); const scale = (value: number[], factor: number) => value.map((component) => component * factor); const camera = [ 7 * Math.cos(0.55) * Math.cos(-Math.PI / 4), 7 * Math.cos(0.55) * Math.sin(-Math.PI / 4), 7 * Math.sin(0.55), ]; const direction = normalize(camera.map((component) => -component)); const horizontal = normalize(cross(direction, [0, 0, 1])); const vertical = normalize(cross(direction, horizontal)); const front = [ add(scale(horizontal, -2), scale(vertical, -2)), add(scale(horizontal, -2), scale(vertical, 2)), add(scale(horizontal, 2), scale(vertical, 2)), add(scale(horizontal, 2), scale(vertical, -2)), ]; const behind = scale(direction, 1.5); const back = [ add(behind, scale(horizontal, -0.12), scale(vertical, -0.08)), add(behind, scale(horizontal, 0.12), scale(vertical, -0.08)), add(behind, scale(vertical, 0.12)), ]; const toBlender = (value: number[]) => [value[0], -value[2], value[1]]; const positions = new Float32Array([...front, ...back].flatMap(toBlender)); const indices = new Uint32Array([0, 1, 2, 0, 2, 3, 4, 6, 5]); const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; const snapshot = { schemaVersion: 1 as const, revision: 9, sceneId: "scene:paint-depth", source: { kind: "mock" as const }, coordinateSystem: { upAxis: "Z" as const, forwardAxis: "-Y" as const, handedness: "RIGHT" as const, unitSystem: 0, unitScale: 1 }, nodes: [{ id: "object:PaintDepth", name: "PaintDepth", type: "MESH" as const, parentId: null, dataId: "mesh:PaintDepth", visible: true, selectable: true, localMatrix: identity, worldMatrix: identity, 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: "mesh:PaintDepth", name: "PaintDepth", vertexCount: 7, edgeCount: 0, faceCount: 3, cornerCount: 9, triangleCount: 3, geometryStatus: "binary" as const }], materials: [], cameras: [], lights: [], worlds: [], images: [], animations: [], collections: [], scenes: [], activeObjectId: "object:PaintDepth", frame: { current: 1, start: 1, end: 1 }, }; const geometry = { schemaVersion: 1 as const, meshId: "mesh:PaintDepth", byteLength: positions.byteLength + indices.byteLength, positions: positions.buffer, indices: indices.buffer, }; const request = { schemaVersion: 1 as const, objectId: "object:PaintDepth", meshId: "mesh:PaintDepth", revision: 9, vertexIndices: [0, 1, 2, 3, 4, 5, 6] }; const createCanvas = () => { const canvas = document.createElement("canvas"); canvas.style.width = "320px"; canvas.style.height = "240px"; document.body.append(canvas); return canvas; }; const mainCanvas = createCanvas(); const mainRenderer = new ViewportRenderer(mainCanvas); mainRenderer.setSnapshot(snapshot, [geometry]); await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); const main = await mainRenderer.samplePaintVisibility(request); mainRenderer.dispose(); mainCanvas.remove(); const offscreenCanvas = createCanvas(); const offscreenRenderer = new OffscreenViewportRenderer(offscreenCanvas); offscreenRenderer.setSnapshot(snapshot, [geometry]); let stale = ""; try { await offscreenRenderer.samplePaintVisibility({ ...request, revision: 8 }); } catch (error) { stale = error instanceof Error ? error.message : String(error); } const offscreen = await offscreenRenderer.samplePaintVisibility(request); offscreenRenderer.dispose(); offscreenCanvas.remove(); return { main, offscreen, stale }; }); expect(result.main.backend).toBe("MAIN_THREAD_WEBGL2"); expect(result.offscreen.backend).toBe("OFFSCREEN_WEBGL2"); expect(result.main.source).toBe("GPU_RGBA_DEPTH_READBACK"); expect(result.main.occluderPixelCount).toBeGreaterThan(0); expect(result.offscreen.occluderPixelCount).toBeGreaterThan(0); expect(result.main.depthReadbackBytes).toBe(result.main.width * result.main.height * 4); expect(result.offscreen.depthReadbackBytes).toBe(result.offscreen.width * result.offscreen.height * 4); expect(result.main.visibleVertexIndices).toEqual([0, 1, 2, 3]); expect(result.offscreen.visibleVertexIndices).toEqual(result.main.visibleVertexIndices); expect(result.stale).toContain("REVISION_CONFLICT"); });