Complete V1 RC deployment capability gates

This commit is contained in:
mes123456
2026-08-15 01:01:23 -04:00
parent a3f3071c03
commit 17ab961485
37 changed files with 2031 additions and 184 deletions

View File

@@ -319,11 +319,13 @@ test("streams ten-million-triangle SceneIR ranges into a bounded interactive LOD
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,
@@ -342,6 +344,11 @@ test("streams ten-million-triangle SceneIR ranges into a bounded interactive LOD
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,
}));

View File

@@ -76,7 +76,7 @@ test("indexes, seeks, cancels and reopens a bounded one-million-frame media time
init(timelineValue: unknown, assets: Array<{ sourceId: string; data: ArrayBuffer }>): Promise<{ stripCount: number; bucketCount: number; referenceCount: number; estimatedBytes: number }>;
seek(frame: number, decodeDelayMs?: number): Promise<import("/src/sequencer/LongMediaTimeline.ts").LongMediaSeekResultIR>;
cancel(): void;
dispose(): Promise<number>;
dispose(): Promise<{ releasedCacheBytes: number; cacheBytesAfter: number }>;
terminate(): void;
}
const createWorkerClient = (): WorkerClient => {
@@ -84,12 +84,12 @@ test("indexes, seeks, cancels and reopens a bounded one-million-frame media time
let counter = 0;
let readyResolve: ((value: { stripCount: number; bucketCount: number; referenceCount: number; estimatedBytes: number }) => void) | undefined;
let readyReject: ((reason: Error) => void) | undefined;
let disposeResolve: ((bytes: number) => void) | undefined;
let disposeResolve: ((status: { releasedCacheBytes: number; cacheBytesAfter: number }) => void) | undefined;
const pending = new Map<string, { resolve: (value: import("/src/sequencer/LongMediaTimeline.ts").LongMediaSeekResultIR) => void; reject: (reason: Error) => void }>();
worker.onmessage = (event: MessageEvent<any>) => {
const message = event.data;
if (message.type === "ready") { readyResolve?.(message.index); readyResolve = undefined; readyReject = undefined; return; }
if (message.type === "disposed") { disposeResolve?.(message.cacheBytes); disposeResolve = undefined; return; }
if (message.type === "disposed") { disposeResolve?.({ releasedCacheBytes: message.releasedCacheBytes, cacheBytesAfter: message.cacheBytesAfter }); disposeResolve = undefined; return; }
if (message.type === "error") {
if (message.requestId) { pending.get(message.requestId)?.reject(new Error(message.message)); pending.delete(message.requestId); }
else { readyReject?.(new Error(message.message)); readyResolve = undefined; readyReject = undefined; }
@@ -164,7 +164,7 @@ test("indexes, seeks, cancels and reopens a bounded one-million-frame media time
const serialized = serializeLongMediaSessionManifest(manifest);
const manifestSha256 = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", serialized.slice(0))), (value) => value.toString(16).padStart(2, "0")).join("");
const storedSession = await writer.putAsset(projectId, serialized, "application/vnd.blender.long-media+json", "cache/long-media-session.json");
const cacheBytesBeforeDispose = await client.dispose();
const disposed = await client.dispose();
client.terminate();
writer.terminate();
@@ -180,7 +180,7 @@ test("indexes, seeks, cancels and reopens a bounded one-million-frame media time
{ sourceId: "media:audio", data: reopenedAudio.data },
]);
const reopenedSeek = await restarted.seek(reopenedManifest.currentFrame);
const restartedCacheBytes = await restarted.dispose();
const restartedDisposed = await restarted.dispose();
restarted.terminate();
const codec = gateSequencerCodec("video/mp4", new Set(["image/png", "audio/wav"]));
@@ -207,8 +207,8 @@ test("indexes, seeks, cancels and reopens a bounded one-million-frame media time
randomSeekMs,
latest: [superseded.status, latest.status, latest.frame],
cancelledSeek: cancelledSeek.status,
cacheBytesBeforeDispose,
restartedCacheBytes,
disposed,
restartedDisposed,
manifestSha256,
storedSessionSha256: storedSession.sha256,
reopenedFrame: reopenedSeek.frame,
@@ -230,6 +230,11 @@ test("indexes, seeks, cancels and reopens a bounded one-million-frame media time
randomSeekMs: result.randomSeekMs,
cacheBytes: result.randomCache?.bytes,
evictions: result.randomCache?.evictions,
releasedCacheBytes: result.disposed.releasedCacheBytes,
cacheBytesAfterDispose: result.disposed.cacheBytesAfter,
restartedReleasedCacheBytes: result.restartedDisposed.releasedCacheBytes,
restartedCacheBytesAfterDispose: result.restartedDisposed.cacheBytesAfter,
workerRestartRecovered: result.reopenedStatus === "COMPLETED",
manifestSha256: result.manifestSha256,
elapsedMs: result.elapsedMs,
}));
@@ -264,8 +269,10 @@ test("indexes, seeks, cancels and reopens a bounded one-million-frame media time
expect(result.randomCache?.evictions).toBeGreaterThan(0);
expect(result.randomCache?.keys).toEqual(expect.arrayContaining(["media:audio:597927", "media:image:1"]));
expect(result.randomSeekMs).toBeLessThan(15_000);
expect(result.cacheBytesBeforeDispose).toBeLessThanOrEqual(20 * 1024);
expect(result.restartedCacheBytes).toBeLessThanOrEqual(20 * 1024);
expect(result.disposed.releasedCacheBytes).toBeLessThanOrEqual(20 * 1024);
expect(result.disposed.cacheBytesAfter).toBe(0);
expect(result.restartedDisposed.releasedCacheBytes).toBeLessThanOrEqual(20 * 1024);
expect(result.restartedDisposed.cacheBytesAfter).toBe(0);
expect(result.manifestSha256).toBe(result.storedSessionSha256);
expect(result.manifestSha256).toBe("d2e7e3c5ed9ae4fda6fe358cebb474f5774f1d2037dff3df9b6dfde9e0bebd2d");
expect(result.elapsedMs).toBeLessThan(30_000);

View File

@@ -54,6 +54,7 @@ test("meets the Chromium OPFS Simulation cache playback performance gate", async
const saved = await writer.saveProject(projectId, 1, sourceBlend.slice(0));
const stored = await writer.putSimulationCache(projectId, manifest, payload);
writer.terminate();
const writerPendingAfterTerminate = writer.getPendingRequestCount();
const storedAt = performance.now();
const reader = new StorageClient();
@@ -87,6 +88,11 @@ test("meets the Chromium OPFS Simulation cache playback performance gate", async
const playbackResult = await playback.play();
const finished = performance.now();
reader.terminate();
const readerPendingAfterTerminate = reader.getPendingRequestCount();
const recovery = new StorageClient();
const recovered = await recovery.readSimulationCacheFrame(projectId, stored.cacheKey, frameCount);
recovery.terminate();
const recoveryPendingAfterTerminate = recovery.getPendingRequestCount();
return {
backend: saved.backend,
frameCount,
@@ -99,12 +105,18 @@ test("meets the Chromium OPFS Simulation cache playback performance gate", async
storeMs: Math.round(storedAt - started),
playbackMs: Math.round(finished - storedAt),
elapsedMs: Math.round(finished - started),
peakCacheBytes: manifest.byteLength,
pendingRequestsAfterTerminate: writerPendingAfterTerminate + readerPendingAfterTerminate + recoveryPendingAfterTerminate,
workerRestartRecovered: recovered.data.byteLength === frameBytes,
};
});
console.log("simulation-cache-performance", JSON.stringify(result));
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);
expect(result.pendingRequestsAfterTerminate).toBe(0);
expect(result.workerRestartRecovered).toBe(true);
});

View File

@@ -48,7 +48,7 @@ test("opens the Blender-style editor and initializes workers", async ({ page })
await page.goto("/");
appOrigin = new URL(page.url()).origin;
await expect(page.getByText("Blender Web", { exact: true }).first()).toBeVisible();
await expect(page.getByText("Web Blender Modeler V1", { exact: true }).first()).toBeVisible();
await expect(page.getByRole("button", { name: "Layout" })).toHaveClass(/active/);
await expect(page.locator(".status-bar")).toContainText("Manifest: verified r1");
await expect(page.locator(".status-bar")).toContainText("Engine: ready, open a .blend file", { timeout: 20_000 });
@@ -2291,7 +2291,7 @@ test("undoes and redoes native scene commands without a Three.js shadow state",
expect(result.redoRevision).toBe(4);
});
test("wires Blender Web top-bar undo and redo to native history", async ({ page }) => {
test("wires Web Blender Modeler top-bar undo and redo to native history", async ({ page }) => {
await page.goto("/");
await page.setInputFiles("[data-testid=blend-file-input]", basicBlend);
await expect(page.getByText("BasicCube", { exact: true })).toBeVisible();

View File

@@ -23,15 +23,27 @@ test("decodes, uploads and renders a validated 4K texture in Chromium", async ({
const renderer = new WebGLRenderer({ canvas, preserveDrawingBuffer: true }); renderer.setSize(64, 64, false);
const scene = new Scene(); const camera = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10); camera.position.z = 1;
const material = new MeshBasicMaterial({ map: store.get("image:4k", "BASE_COLOR") });
scene.add(new Mesh(new PlaneGeometry(2, 2), material)); renderer.render(scene, camera);
const geometry = new PlaneGeometry(2, 2);
scene.add(new Mesh(geometry, material)); renderer.render(scene, camera);
const gl = renderer.getContext(); const pixels = new Uint8Array(64 * 64 * 4); gl.readPixels(0, 0, 64, 64, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
let colored = 0; for (let index = 0; index < pixels.length; index += 4) if (pixels[index] > 60 || pixels[index + 1] > 60) colored += 1;
const elapsedMs = Math.round(performance.now() - started);
material.dispose(); renderer.dispose(); store.dispose(); canvas.remove();
return { status, byteLength: data.byteLength, colored, elapsedMs };
material.dispose(); geometry.dispose(); renderer.dispose(); store.dispose(); canvas.remove();
const released = store.get("image:4k", "BASE_COLOR") === undefined;
const recoveryCanvas = document.createElement("canvas"); recoveryCanvas.width = 16; recoveryCanvas.height = 16; document.body.append(recoveryCanvas);
const recoveryRenderer = new WebGLRenderer({ canvas: recoveryCanvas, preserveDrawingBuffer: true }); recoveryRenderer.setSize(16, 16, false);
const recoveryScene = new Scene(); const recoveryMaterial = new MeshBasicMaterial({ color: 0x33cc66 }); const recoveryGeometry = new PlaneGeometry(2, 2);
recoveryScene.add(new Mesh(recoveryGeometry, recoveryMaterial)); recoveryRenderer.render(recoveryScene, camera);
const recoveryPixels = new Uint8Array(16 * 16 * 4); recoveryRenderer.getContext().readPixels(0, 0, 16, 16, recoveryRenderer.getContext().RGBA, recoveryRenderer.getContext().UNSIGNED_BYTE, recoveryPixels);
const recoveryVisible = recoveryPixels.some((value, index) => index % 4 !== 3 && value > 40);
recoveryMaterial.dispose(); recoveryGeometry.dispose(); recoveryRenderer.dispose(); recoveryCanvas.remove();
return { width: 4096, height: 4096, status, byteLength: data.byteLength, estimatedPeakGpuBytes: 4096 * 4096 * 4, released, recoveryVisible, colored, elapsedMs };
});
console.log("texture-4k-performance", JSON.stringify(result));
expect(result.status).toMatchObject({ loaded: 1, rejected: 0, bytes: result.byteLength });
expect(result.byteLength).toBeGreaterThan(0);
expect(result.colored).toBeGreaterThan(3_000);
expect(result.elapsedMs).toBeLessThan(30_000);
expect(result.released).toBe(true);
expect(result.recoveryVisible).toBe(true);
});

View File

@@ -21,16 +21,27 @@ test("decodes, uploads and renders a validated 8K texture in Chromium", async ({
const asset = await createGPUTextureAsset({ assetId: "asset:8k", imageId: "image:8k", mimeType: "image/png", width: 8192, height: 8192, usage: "BASE_COLOR", colorSpace: "SRGB" }, data);
const started = performance.now(); const store = new GPUTextureStore(); const status = await store.upload([asset]);
const scene = new Scene(); const camera = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10); camera.position.z = 1;
const material = new MeshBasicMaterial({ map: store.get("image:8k", "BASE_COLOR") }); scene.add(new Mesh(new PlaneGeometry(2, 2), material)); renderer.render(scene, camera);
const material = new MeshBasicMaterial({ map: store.get("image:8k", "BASE_COLOR") }); const geometry = new PlaneGeometry(2, 2); scene.add(new Mesh(geometry, material)); renderer.render(scene, camera);
const pixels = new Uint8Array(64 * 64 * 4); gl.readPixels(0, 0, 64, 64, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
let colored = 0; for (let index = 0; index < pixels.length; index += 4) if (pixels[index] > 60 || pixels[index + 1] > 60 || pixels[index + 2] > 60) colored += 1;
const elapsedMs = Math.round(performance.now() - started);
material.dispose(); renderer.dispose(); store.dispose(); target.remove();
return { supported: true, maxTextureSize, status, byteLength: data.byteLength, colored, elapsedMs };
material.dispose(); geometry.dispose(); renderer.dispose(); store.dispose(); target.remove();
const released = store.get("image:8k", "BASE_COLOR") === undefined;
const recoveryCanvas = document.createElement("canvas"); recoveryCanvas.width = 16; recoveryCanvas.height = 16; document.body.append(recoveryCanvas);
const recoveryRenderer = new WebGLRenderer({ canvas: recoveryCanvas, preserveDrawingBuffer: true }); recoveryRenderer.setSize(16, 16, false);
const recoveryScene = new Scene(); const recoveryMaterial = new MeshBasicMaterial({ color: 0x3366cc }); const recoveryGeometry = new PlaneGeometry(2, 2);
recoveryScene.add(new Mesh(recoveryGeometry, recoveryMaterial)); recoveryRenderer.render(recoveryScene, camera);
const recoveryPixels = new Uint8Array(16 * 16 * 4); recoveryRenderer.getContext().readPixels(0, 0, 16, 16, recoveryRenderer.getContext().RGBA, recoveryRenderer.getContext().UNSIGNED_BYTE, recoveryPixels);
const recoveryVisible = recoveryPixels.some((value, index) => index % 4 !== 3 && value > 40);
recoveryMaterial.dispose(); recoveryGeometry.dispose(); recoveryRenderer.dispose(); recoveryCanvas.remove();
return { supported: true, maxTextureSize, width: 8192, height: 8192, status, byteLength: data.byteLength, estimatedPeakGpuBytes: 8192 * 8192 * 4, released, recoveryVisible, colored, elapsedMs };
});
console.log("texture-8k-performance", JSON.stringify(result));
expect(result.supported, `WebGL MAX_TEXTURE_SIZE=${result.maxTextureSize}`).toBe(true);
expect(result.status).toMatchObject({ loaded: 1, rejected: 0, bytes: result.byteLength });
expect(result.byteLength).toBeGreaterThan(0);
expect(result.colored).toBeGreaterThan(3_000);
expect(result.elapsedMs).toBeLessThan(45_000);
expect(result.released).toBe(true);
expect(result.recoveryVisible).toBe(true);
});

View File

@@ -0,0 +1,41 @@
import { expect, test } from "@playwright/test";
test("gates only the pthread WASM engine on its three platform requirements", async ({ page }) => {
await page.goto("/");
const gates = await page.evaluate(async () => {
const { gateWasmThreadingCapability } = await import("/src/platform/capabilities.ts");
return {
ready: gateWasmThreadingCapability({ crossOriginIsolated: true, sharedArrayBuffer: true, worker: true }),
noIsolation: gateWasmThreadingCapability({ crossOriginIsolated: false, sharedArrayBuffer: true, worker: true }),
noSharedArrayBuffer: gateWasmThreadingCapability({ crossOriginIsolated: true, sharedArrayBuffer: false, worker: true }),
noWorker: gateWasmThreadingCapability({ crossOriginIsolated: true, sharedArrayBuffer: true, worker: false }),
none: gateWasmThreadingCapability({ crossOriginIsolated: false, sharedArrayBuffer: false, worker: false }),
};
});
expect(gates.ready).toEqual({
taskId: "M6-02",
capability: "WASM_PTHREAD_ENGINE",
status: "READY",
issues: [],
});
expect(gates.noIsolation.issues.map(({ code, path }) => ({ code, path }))).toEqual([
{ code: "PLATFORM_CAPABILITY_UNAVAILABLE", path: "crossOriginIsolated" },
]);
expect(gates.noSharedArrayBuffer.issues.map(({ code, path }) => ({ code, path }))).toEqual([
{ code: "PLATFORM_CAPABILITY_UNAVAILABLE", path: "sharedArrayBuffer" },
]);
expect(gates.noWorker.issues.map(({ code, path }) => ({ code, path }))).toEqual([
{ code: "PLATFORM_CAPABILITY_UNAVAILABLE", path: "worker" },
]);
expect(gates.none).toMatchObject({
taskId: "M6-02",
capability: "WASM_PTHREAD_ENGINE",
status: "BLOCKED",
});
expect(gates.none.issues.map(({ code, path }) => ({ code, path }))).toEqual([
{ code: "PLATFORM_CAPABILITY_UNAVAILABLE", path: "crossOriginIsolated" },
{ code: "PLATFORM_CAPABILITY_UNAVAILABLE", path: "sharedArrayBuffer" },
{ code: "PLATFORM_CAPABILITY_UNAVAILABLE", path: "worker" },
]);
});