Files
workinf_Blender_Wasm/web/tests/e2e/render-resource-budget.spec.ts
mes123456 0fe8d2bb56
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

188 lines
8.0 KiB
TypeScript

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);
});