188 lines
8.0 KiB
TypeScript
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);
|
|
});
|