Advance WebGPU volume and bounded workflows
This commit is contained in:
110
web/tests/e2e/simulation-cache-performance.spec.ts
Normal file
110
web/tests/e2e/simulation-cache-performance.spec.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("meets the Chromium OPFS Simulation cache playback performance gate", async ({ page }) => {
|
||||
test.setTimeout(45_000);
|
||||
await page.goto("/");
|
||||
const result = await page.evaluate(async () => {
|
||||
const { StorageClient } = await import("/src/storage/StorageClient.ts");
|
||||
const { BrowserTransformCachePlaybackSession } = await import("/src/simulation/BrowserTransformCachePlayback.ts");
|
||||
const frameCount = 600;
|
||||
const frameBytes = 88;
|
||||
const digest = async (data: ArrayBuffer): Promise<string> => {
|
||||
const hash = await crypto.subtle.digest("SHA-256", data);
|
||||
return Array.from(new Uint8Array(hash), (value) => value.toString(16).padStart(2, "0")).join("");
|
||||
};
|
||||
const payload = new ArrayBuffer(frameCount * frameBytes);
|
||||
const payloadBytes = new Uint8Array(payload);
|
||||
const objectId = new TextEncoder().encode("object:CacheTarget");
|
||||
for (let frame = 1; frame <= frameCount; frame += 1) {
|
||||
const offset = (frame - 1) * frameBytes;
|
||||
const view = new DataView(payload, offset, frameBytes);
|
||||
view.setUint32(0, 0x31465442, true);
|
||||
view.setUint16(4, 1, true);
|
||||
view.setUint16(6, 16, true);
|
||||
view.setInt32(8, frame, true);
|
||||
view.setUint32(12, 1, true);
|
||||
view.setUint8(16, objectId.length);
|
||||
payloadBytes.set(objectId, offset + 17);
|
||||
[frame / 10, 0, 0, 0, 0, 0, 1, 1, 1, 1].forEach((value, index) => view.setFloat32(48 + index * 4, value, true));
|
||||
}
|
||||
const frames = await Promise.all(Array.from({ length: frameCount }, async (_, index) => ({
|
||||
frame: index + 1,
|
||||
byteOffset: index * frameBytes,
|
||||
byteLength: frameBytes,
|
||||
sha256: await digest(payload.slice(index * frameBytes, (index + 1) * frameBytes)),
|
||||
})));
|
||||
const sourceBlend = Uint8Array.from([0x42, 0x4c, 0x45, 0x4e, 0x44, 0x45, 0x52]).buffer;
|
||||
const fixedHash = await digest(Uint8Array.from([1, 2, 3]).buffer);
|
||||
const manifest = {
|
||||
schemaVersion: 1 as const,
|
||||
graphId: "geometry-node-tree:simulation-performance",
|
||||
graphHash: fixedHash,
|
||||
sourceBlendSha256: await digest(sourceBlend),
|
||||
inputHash: await digest(Uint8Array.from([4, 5, 6]).buffer),
|
||||
cacheSha256: await digest(payload),
|
||||
blenderVersion: "5.2.0",
|
||||
frameStart: 1,
|
||||
frameEnd: frameCount,
|
||||
byteLength: payload.byteLength,
|
||||
frames,
|
||||
};
|
||||
const projectId = `simulation-performance-${Date.now()}`;
|
||||
const started = performance.now();
|
||||
const writer = new StorageClient();
|
||||
const saved = await writer.saveProject(projectId, 1, sourceBlend.slice(0));
|
||||
const stored = await writer.putSimulationCache(projectId, manifest, payload);
|
||||
writer.terminate();
|
||||
const storedAt = performance.now();
|
||||
|
||||
const reader = new StorageClient();
|
||||
const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
||||
const scene = {
|
||||
schemaVersion: 1 as const,
|
||||
revision: 1,
|
||||
sceneId: "scene:CachePerformance",
|
||||
source: { kind: "mock" as const },
|
||||
coordinateSystem: { upAxis: "Z" as const, forwardAxis: "-Y" as const, handedness: "RIGHT" as const, unitSystem: 0, unitScale: 1 },
|
||||
activeObjectId: "object:CacheTarget",
|
||||
frame: { current: 1, start: 1, end: frameCount },
|
||||
nodes: [{ id: "object:CacheTarget", name: "CacheTarget", type: "MESH" as const, parentId: null, dataId: "mesh:CacheTarget", 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: [], materials: [], cameras: [], lights: [], worlds: [], images: [], animations: [], collections: [], scenes: [],
|
||||
};
|
||||
let publishedFrames = 0;
|
||||
let lastTranslation = 0;
|
||||
const playback = new BrowserTransformCachePlaybackSession(scene, {
|
||||
frameStart: 1,
|
||||
frameEnd: frameCount,
|
||||
readFrame: async (frame, signal) => {
|
||||
if (signal.aborted) throw new DOMException("Playback aborted", "AbortError");
|
||||
const read = await reader.readSimulationCacheFrame(projectId, stored.cacheKey, frame);
|
||||
if (signal.aborted) throw new DOMException("Playback aborted", "AbortError");
|
||||
return read.data;
|
||||
},
|
||||
}, (preview) => {
|
||||
publishedFrames += 1;
|
||||
lastTranslation = preview.nodes[0].transform.translation[0];
|
||||
});
|
||||
const playbackResult = await playback.play();
|
||||
const finished = performance.now();
|
||||
reader.terminate();
|
||||
return {
|
||||
backend: saved.backend,
|
||||
frameCount,
|
||||
byteLength: manifest.byteLength,
|
||||
status: playbackResult.status,
|
||||
appliedFrames: playbackResult.appliedFrames,
|
||||
lastFrame: playbackResult.lastFrame,
|
||||
publishedFrames,
|
||||
lastTranslation,
|
||||
storeMs: Math.round(storedAt - started),
|
||||
playbackMs: Math.round(finished - storedAt),
|
||||
elapsedMs: Math.round(finished - started),
|
||||
};
|
||||
});
|
||||
expect(result.backend).toBe("opfs");
|
||||
expect(result).toMatchObject({ frameCount: 600, byteLength: 52_800, status: "COMPLETED", appliedFrames: 600, lastFrame: 600, publishedFrames: 600 });
|
||||
expect(result.lastTranslation).toBeCloseTo(60, 5);
|
||||
expect(result.storeMs).toBeLessThan(30_000);
|
||||
expect(result.playbackMs).toBeLessThan(30_000);
|
||||
expect(result.elapsedMs).toBeLessThan(30_000);
|
||||
});
|
||||
Reference in New Issue
Block a user