Advance M8-M11 parity workflows
This commit is contained in:
187
web/tests/e2e/render-resource-budget.spec.ts
Normal file
187
web/tests/e2e/render-resource-budget.spec.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const root = path.resolve(import.meta.dirname, "../../..");
|
||||
const blend = fs.readFileSync(path.join(root, "tests/files/web/basic_scene.blend"));
|
||||
const texturePng = fs.readFileSync(path.join(root, "tests/files/web/resources/udim_1001.png"));
|
||||
const golden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-03/render-resource-budget.json"), "utf8"));
|
||||
|
||||
for (const offscreen of [false, true]) {
|
||||
test(`M11-03 ${offscreen ? "Offscreen" : "main-thread"} renderer enforces light, shadow and texture budgets`, async ({ page }) => {
|
||||
test.setTimeout(60_000);
|
||||
await page.goto("/");
|
||||
const result = await page.evaluate(async ({ input, useOffscreen, requestedLights }) => {
|
||||
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
|
||||
const { ViewportRenderer } = await import("/src/three-adapter/viewport.ts");
|
||||
const { OffscreenViewportRenderer } = await import("/src/three-adapter/offscreen-viewport.ts");
|
||||
const client = new WebEngineClient({ timeoutMs: 30_000 });
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.style.width = "256px";
|
||||
canvas.style.height = "256px";
|
||||
document.body.append(canvas);
|
||||
const renderer = useOffscreen ? new OffscreenViewportRenderer(canvas) : new ViewportRenderer(canvas);
|
||||
const waitFor = async (attribute: string): Promise<string> => {
|
||||
for (let attempt = 0; attempt < 300; attempt++) {
|
||||
const value = canvas.getAttribute(attribute);
|
||||
if (value) return value;
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
throw new Error(`${attribute} was not published`);
|
||||
};
|
||||
try {
|
||||
const opened = await client.openBlend(Uint8Array.from(input).buffer);
|
||||
const snapshot = structuredClone(opened.snapshot);
|
||||
const sourceNode = snapshot.nodes.find((node) => node.type === "LIGHT")!;
|
||||
const sourceLight = snapshot.lights.find((light) => light.id === sourceNode.dataId)!;
|
||||
snapshot.nodes = [
|
||||
...snapshot.nodes.filter((node) => node.type !== "LIGHT"),
|
||||
...Array.from({ length: requestedLights }, (_, index) => ({
|
||||
...sourceNode,
|
||||
id: `object:M11BudgetLight${index.toString().padStart(2, "0")}`,
|
||||
name: `M11BudgetLight${index.toString().padStart(2, "0")}`,
|
||||
dataId: `light:M11BudgetLight${index.toString().padStart(2, "0")}`,
|
||||
visible: true,
|
||||
})),
|
||||
];
|
||||
snapshot.lights = Array.from({ length: requestedLights }, (_, index) => ({
|
||||
...sourceLight,
|
||||
id: `light:M11BudgetLight${index.toString().padStart(2, "0")}`,
|
||||
name: `M11BudgetLight${index.toString().padStart(2, "0")}`,
|
||||
lightType: 2,
|
||||
castsShadow: true,
|
||||
}));
|
||||
renderer.setSnapshot(snapshot, [], []);
|
||||
await waitFor("data-render-budget-status");
|
||||
|
||||
const textures = Array.from({ length: 257 }, (_, index) => ({
|
||||
schemaVersion: 1 as const,
|
||||
assetId: `asset:m11-budget-${index}`,
|
||||
imageId: `image:m11-budget-${index}`,
|
||||
mimeType: "image/png",
|
||||
width: 1,
|
||||
height: 1,
|
||||
usage: "BASE_COLOR" as const,
|
||||
colorSpace: "SRGB" as const,
|
||||
sha256: "0".repeat(64),
|
||||
byteLength: 1,
|
||||
data: Uint8Array.of(index & 0xff).buffer,
|
||||
}));
|
||||
renderer.setTextureAssets(textures);
|
||||
await waitFor("data-texture-budget-status");
|
||||
|
||||
let actualLights: number | null = null;
|
||||
let actualShadows: number | null = null;
|
||||
if (!useOffscreen) {
|
||||
actualLights = 0;
|
||||
actualShadows = 0;
|
||||
(renderer as InstanceType<typeof ViewportRenderer>).scene.traverse((object) => {
|
||||
if (!("isLight" in object) || !object.isLight) return;
|
||||
actualLights! += 1;
|
||||
if ("castShadow" in object && object.castShadow) actualShadows! += 1;
|
||||
});
|
||||
}
|
||||
return {
|
||||
render: {
|
||||
backend: canvas.dataset.renderBudgetBackend,
|
||||
status: canvas.dataset.renderBudgetStatus,
|
||||
code: canvas.dataset.renderBudgetCode,
|
||||
requestedLights: Number(canvas.dataset.renderBudgetLights),
|
||||
renderedLights: Number(canvas.dataset.renderBudgetRenderedLights),
|
||||
droppedLights: Number(canvas.dataset.renderBudgetDroppedLights),
|
||||
requestedShadows: Number(canvas.dataset.renderBudgetShadows),
|
||||
renderedShadows: Number(canvas.dataset.renderBudgetRenderedShadows),
|
||||
blockedShadows: Number(canvas.dataset.renderBudgetBlockedShadows),
|
||||
dimension: Number(canvas.dataset.renderBudgetShadowMapDimension),
|
||||
},
|
||||
texture: {
|
||||
status: canvas.dataset.textureBudgetStatus,
|
||||
code: canvas.dataset.textureBudgetCode,
|
||||
assets: Number(canvas.dataset.textureBudgetAssets),
|
||||
loaded: Number(canvas.dataset.textureLoaded),
|
||||
bytes: Number(canvas.dataset.textureBytes),
|
||||
},
|
||||
actualLights,
|
||||
actualShadows,
|
||||
};
|
||||
}
|
||||
finally {
|
||||
renderer.dispose();
|
||||
client.terminate();
|
||||
canvas.remove();
|
||||
}
|
||||
}, { input: Array.from(blend), useOffscreen: offscreen, requestedLights: golden.overflow.requestedLights });
|
||||
|
||||
expect(result.render).toEqual({
|
||||
backend: "THREE_WEBGL2",
|
||||
status: "blocked",
|
||||
code: "GPU_LIGHT_BUDGET_EXCEEDED",
|
||||
requestedLights: golden.overflow.requestedLights,
|
||||
renderedLights: golden.overflow.renderedLights,
|
||||
droppedLights: golden.overflow.droppedLights,
|
||||
requestedShadows: golden.overflow.requestedShadowMaps,
|
||||
renderedShadows: golden.overflow.renderedShadowMaps,
|
||||
blockedShadows: golden.overflow.blockedShadowMaps,
|
||||
dimension: golden.webgl2.shadowMapDimension,
|
||||
});
|
||||
expect(result.texture).toEqual({
|
||||
status: "blocked",
|
||||
code: "GPU_TEXTURE_BUDGET_EXCEEDED",
|
||||
assets: 257,
|
||||
loaded: 0,
|
||||
bytes: 0,
|
||||
});
|
||||
if (!offscreen) {
|
||||
expect(result.actualLights).toBe(golden.webgl2.maxLights);
|
||||
expect(result.actualShadows).toBe(golden.webgl2.maxShadowMaps);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test("M11-03 keeps the previous texture set when an aggregate batch is over budget", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const result = await page.evaluate(async (input) => {
|
||||
const { GPUTextureStore } = await import("/src/three-adapter/texture-assets.ts");
|
||||
const store = new GPUTextureStore("THREE_WEBGL2");
|
||||
try {
|
||||
const data = Uint8Array.from(input).buffer;
|
||||
const digest = await crypto.subtle.digest("SHA-256", data);
|
||||
const kept = {
|
||||
schemaVersion: 1 as const,
|
||||
assetId: "asset:kept",
|
||||
imageId: "image:kept",
|
||||
mimeType: "image/png",
|
||||
width: 8,
|
||||
height: 8,
|
||||
usage: "BASE_COLOR",
|
||||
colorSpace: "SRGB",
|
||||
byteLength: data.byteLength,
|
||||
sha256: [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join(""),
|
||||
data,
|
||||
} as const;
|
||||
const accepted = await store.upload([kept]);
|
||||
const before = store.getAsset("image:kept", "BASE_COLOR");
|
||||
const overBudget = Array.from({ length: 257 }, (_, index) => ({
|
||||
...kept,
|
||||
assetId: `asset:overflow:${index}`,
|
||||
imageId: `image:overflow:${index}`,
|
||||
data: kept.data.slice(0),
|
||||
}));
|
||||
const blocked = await store.upload(overBudget);
|
||||
const after = store.getAsset("image:kept", "BASE_COLOR");
|
||||
return {
|
||||
accepted: [accepted.loaded, accepted.rejected, accepted.budget.status],
|
||||
blocked: [blocked.loaded, blocked.rejected, blocked.budget.status, blocked.budget.requestedAssets, blocked.errorCodes[0]],
|
||||
retained: before === after && after?.sha256 === kept.sha256,
|
||||
revision: store.getRevision(),
|
||||
};
|
||||
}
|
||||
finally {
|
||||
store.dispose();
|
||||
}
|
||||
}, Array.from(texturePng));
|
||||
expect(result.accepted).toEqual([1, 0, "READY"]);
|
||||
expect(result.blocked).toEqual([0, 257, "BLOCKED", 258, "GPU_TEXTURE_BUDGET_EXCEEDED"]);
|
||||
expect(result.retained).toBe(true);
|
||||
expect(result.revision).toBe(2);
|
||||
});
|
||||
Reference in New Issue
Block a user