158 lines
7.6 KiB
TypeScript
158 lines
7.6 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { createHash } from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const root = path.resolve(import.meta.dirname, "../../..");
|
|
const expected = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M10-07/shader-compile.json"), "utf8"));
|
|
const blendBytes = fs.readFileSync(path.join(root, expected.fixture));
|
|
|
|
function sockets() {
|
|
return {
|
|
rgb: [{ id: "color", name: "Color", direction: "OUTPUT", dataType: "COLOR", defaultValue: [0.15, 0.25, 0.35, 1] }],
|
|
value: [{ id: "value", name: "Value", direction: "OUTPUT", dataType: "VALUE", defaultValue: 0.2 }],
|
|
math: [
|
|
{ id: "a", name: "Value", direction: "INPUT", dataType: "VALUE", defaultValue: 0 },
|
|
{ id: "b", name: "Value_001", direction: "INPUT", dataType: "VALUE", defaultValue: 0 },
|
|
{ id: "value", name: "Value", direction: "OUTPUT", dataType: "VALUE" },
|
|
],
|
|
principled: [
|
|
{ id: "base", name: "Base Color", direction: "INPUT", dataType: "COLOR", defaultValue: [0.2, 0.3, 0.4, 1] },
|
|
{ id: "roughness", name: "Roughness", direction: "INPUT", dataType: "VALUE", defaultValue: 0.5 },
|
|
{ id: "bsdf", name: "BSDF", direction: "OUTPUT", dataType: "SHADER" },
|
|
],
|
|
output: [{ id: "surface", name: "Surface", direction: "INPUT", dataType: "SHADER" }],
|
|
};
|
|
}
|
|
|
|
test("M10-07 produces a bounded compile report from the real Main Shader graph", async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
expect(createHash("sha256").update(blendBytes).digest("hex")).toBe(expected.fixtureSha256);
|
|
await page.goto("/");
|
|
const result = await page.evaluate(async ({ bytes, socketSet }) => {
|
|
const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
|
|
const client = new WebEngineClient({ timeoutMs: 60_000 });
|
|
await client.init();
|
|
const source = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
|
|
const opened = await client.openBlend(source);
|
|
const material = opened.snapshot.materials[0];
|
|
if (!material) throw new Error("basic fixture has no editable material");
|
|
const graph = {
|
|
schemaVersion: 1 as const,
|
|
id: "shader:m10-07",
|
|
materialId: material.id,
|
|
outputNodeId: "output",
|
|
nodes: [
|
|
{ id: "rgb", type: "RGB" as const, name: "RGB", sockets: socketSet.rgb },
|
|
{ id: "a", type: "VALUE" as const, name: "A", sockets: socketSet.value },
|
|
{ id: "b", type: "VALUE" as const, name: "B", sockets: [{ ...socketSet.value[0], defaultValue: 0.22 }] },
|
|
{ id: "math", type: "MATH" as const, name: "Add", properties: { operation: "ADD" as const }, sockets: socketSet.math },
|
|
{ id: "principled", type: "PRINCIPLED" as const, name: "Principled", sockets: socketSet.principled },
|
|
{ id: "output", type: "MATERIAL_OUTPUT" as const, name: "Output", sockets: socketSet.output },
|
|
],
|
|
links: [
|
|
{ fromNodeId: "rgb", fromSocketId: "color", toNodeId: "principled", toSocketId: "base" },
|
|
{ fromNodeId: "a", fromSocketId: "value", toNodeId: "math", toSocketId: "a" },
|
|
{ fromNodeId: "b", fromSocketId: "value", toNodeId: "math", toSocketId: "b" },
|
|
{ fromNodeId: "math", fromSocketId: "value", toNodeId: "principled", toSocketId: "roughness" },
|
|
{ fromNodeId: "principled", fromSocketId: "bsdf", toNodeId: "output", toSocketId: "surface" },
|
|
],
|
|
};
|
|
const applied = await client.applyCommand({ type: "setShaderGraph", materialId: material.id, graph });
|
|
const report = applied.shaderCompile;
|
|
const after = await client.snapshot();
|
|
client.terminate();
|
|
return {
|
|
openedHash: material.shaderGraphHash,
|
|
status: report?.status,
|
|
taskId: report?.taskId,
|
|
backend: report?.backend,
|
|
graphHash: report?.graphHash,
|
|
roughness: report?.material?.roughness,
|
|
nodeTypes: report?.compiledNodeTypes,
|
|
revisionDelta: after.snapshot.revision - opened.snapshot.revision,
|
|
};
|
|
}, { bytes: new Uint8Array(blendBytes), socketSet: sockets() });
|
|
|
|
expect(result.status).toBe("COMPILED");
|
|
expect(result.openedHash).toMatch(/^[0-9a-f]{64}$/);
|
|
expect(result.taskId).toBe("M10-07");
|
|
expect(result.backend).toBe(expected.backend);
|
|
expect(result.graphHash).toMatch(/^[0-9a-f]{64}$/);
|
|
expect(result.roughness).toBeCloseTo(expected.expectedMathResult, 5);
|
|
expect(result.nodeTypes).toEqual(expect.arrayContaining(expected.allowlist.filter((type: string) => !["IMAGE_TEXTURE", "NORMAL_MAP"].includes(type))));
|
|
expect(result.revisionDelta).toBe(1);
|
|
});
|
|
|
|
test("M10-07 shared viewport material path compiles Image/Normal/Math bindings", async ({ page }) => {
|
|
await page.goto("/");
|
|
const result = await page.evaluate(async () => {
|
|
const { createPBRMaterial } = await import("/src/three-adapter/pbr.ts");
|
|
const material = createPBRMaterial({
|
|
id: "material:viewport",
|
|
name: "Viewport",
|
|
baseColor: [0.8, 0.8, 0.8, 1],
|
|
roughness: 0.5,
|
|
metallic: 0,
|
|
emissionColor: [0, 0, 0, 1],
|
|
alpha: 1,
|
|
ior: 1.45,
|
|
nodes: [
|
|
{ id: "a", type: "VALUE", name: "A", defaultValue: [0.2] },
|
|
{ id: "b", type: "VALUE", name: "B", defaultValue: [0.22] },
|
|
{ id: "math", type: "MATH", name: "Add", properties: { operation: "ADD" } },
|
|
{ id: "image", type: "IMAGE_TEXTURE", name: "Image", imageId: "image:normal" },
|
|
{ id: "normal", type: "NORMAL_MAP", name: "Normal" },
|
|
{ id: "principled", type: "PRINCIPLED", name: "Principled" },
|
|
{ id: "output", type: "OUTPUT", name: "Output" },
|
|
],
|
|
links: [
|
|
{ fromNodeId: "a", fromSocket: "Value", toNodeId: "math", toSocket: "Value" },
|
|
{ fromNodeId: "b", fromSocket: "Value", toNodeId: "math", toSocket: "Value_001" },
|
|
{ fromNodeId: "math", fromSocket: "Value", toNodeId: "principled", toSocket: "Roughness" },
|
|
{ fromNodeId: "image", fromSocket: "Color", toNodeId: "normal", toSocket: "Color" },
|
|
{ fromNodeId: "normal", fromSocket: "Normal", toNodeId: "principled", toSocket: "Normal" },
|
|
{ fromNodeId: "principled", fromSocket: "BSDF", toNodeId: "output", toSocket: "Surface" },
|
|
],
|
|
});
|
|
const compile = material.userData.shaderCompile;
|
|
const report = {
|
|
status: compile.status,
|
|
graphHash: compile.graphHash,
|
|
roughness: material.roughness,
|
|
textures: compile.textureBindings,
|
|
};
|
|
material.dispose();
|
|
return report;
|
|
});
|
|
expect(result.status).toBe("COMPILED");
|
|
expect(result.graphHash).toMatch(/^[0-9a-f]{64}$/);
|
|
expect(result.roughness).toBeCloseTo(expected.expectedMathResult, 5);
|
|
expect(result.textures).toEqual([{ imageId: "image:normal", usage: "NORMAL" }]);
|
|
});
|
|
|
|
test("M10-07 compiler blocks a non-declared node without mutating the input graph", async ({ page }) => {
|
|
await page.goto("/");
|
|
const result = await page.evaluate(async () => {
|
|
const { createPBRMaterial } = await import("/src/three-adapter/pbr.ts");
|
|
const material = {
|
|
id: "material:blocked",
|
|
name: "Blocked",
|
|
baseColor: [1, 1, 1, 1],
|
|
roughness: 0.5,
|
|
metallic: 0,
|
|
emissionColor: [0, 0, 0, 1],
|
|
alpha: 1,
|
|
ior: 1.45,
|
|
nodes: [{ id: "mix", type: "UNSUPPORTED", name: "Mix" }, { id: "output", type: "OUTPUT", name: "Output" }],
|
|
links: [],
|
|
} as const;
|
|
const before = JSON.stringify(material);
|
|
const threeMaterial = createPBRMaterial(material);
|
|
const report = threeMaterial.userData.shaderCompile;
|
|
threeMaterial.dispose();
|
|
return { status: report.status, code: report.issues[0]?.code, preserved: JSON.stringify(material) === before };
|
|
});
|
|
expect(result).toEqual({ status: "BLOCKED", code: expected.unsupportedNodeCode, preserved: true });
|
|
});
|