Files
workinf_Blender_Wasm/web/tests/unit/shader-compiler.test.mjs
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

180 lines
8.4 KiB
JavaScript

import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import test from "node:test";
import ts from "typescript";
const root = path.resolve(import.meta.dirname, "../../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "shader-compiler-unit-"));
const sourcePath = path.join(root, "web/protocol/shader-compiler.ts");
const outputPath = path.join(temporary, "shader-compiler.mjs");
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: sourcePath,
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
fs.writeFileSync(outputPath, transpiled.outputText);
const compiler = await import(pathToFileURL(outputPath));
function material(overrides = {}) {
return {
id: "material:ShaderUnit",
name: "ShaderUnit",
baseColor: [0.2, 0.3, 0.4, 1],
roughness: 0.5,
metallic: 0.1,
emissionColor: [0, 0, 0, 1],
alpha: 1,
ior: 1.45,
shaderGraphHash: "a".repeat(64),
nodes: [
{ id: "rgb", type: "RGB", name: "RGB", defaultValue: [0.1, 0.2, 0.3, 1] },
{ id: "value-a", type: "VALUE", name: "A", defaultValue: [0.2] },
{ id: "value-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: "rgb", fromSocket: "Color", toNodeId: "principled", toSocket: "Base Color" },
{ fromNodeId: "value-a", fromSocket: "Value", toNodeId: "math", toSocket: "Value" },
{ fromNodeId: "value-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" },
],
...overrides,
};
}
test("M10-07 compiles the declared Principled/Image/Normal/Math closure", () => {
const report = compiler.compileMaterialGraph(material(), { imageIds: new Set(["image:Normal"]) });
assert.equal(report.status, "COMPILED");
assert.equal(report.taskId, "M10-07");
assert.equal(report.backend, "WEBGL2_THREE_PHYSICAL");
assert.equal(report.material.baseColor[2], 0.3);
assert.ok(Math.abs(report.material.roughness - 0.42) < 1e-8);
assert.deepEqual(report.textureBindings, [{ imageId: "image:Normal", usage: "NORMAL" }]);
assert.match(report.graphHash, /^[0-9a-f]{64}$/);
assert.ok(report.instructions.some((instruction) => instruction.operation === "ADD"));
assert.ok(report.nodeOrder.indexOf("value-a") < report.nodeOrder.indexOf("math"));
assert.ok(report.nodeOrder.indexOf("math") < report.nodeOrder.indexOf("principled"));
assert.ok(report.nodeOrder.indexOf("principled") < report.nodeOrder.indexOf("output"));
});
test("M10-07 blocks unknown nodes and preserves the graph metadata", () => {
const source = material();
source.nodes.push({ id: "mix", type: "UNSUPPORTED", name: "ShaderNodeMix" });
const report = compiler.compileMaterialGraph(source);
assert.equal(report.status, "BLOCKED");
assert.equal(report.issues[0].code, "SHADER_NODE_UNSUPPORTED");
assert.equal(source.nodes.at(-1).type, "UNSUPPORTED");
});
test("M10-07 rejects cycles, duplicate links, and missing image resources", () => {
const cyclic = material({
links: [
{ fromNodeId: "value-a", fromSocket: "Value", toNodeId: "math", toSocket: "Value" },
{ fromNodeId: "math", fromSocket: "Value", toNodeId: "math", toSocket: "Value_001" },
{ fromNodeId: "math", fromSocket: "Value", toNodeId: "principled", toSocket: "Roughness" },
{ fromNodeId: "principled", fromSocket: "BSDF", toNodeId: "output", toSocket: "Surface" },
{ fromNodeId: "image", fromSocket: "Color", toNodeId: "normal", toSocket: "Color" },
{ fromNodeId: "normal", fromSocket: "Normal", toNodeId: "principled", toSocket: "Normal" },
],
});
const cyclicReport = compiler.compileMaterialGraph(cyclic);
assert.equal(cyclicReport.status, "BLOCKED");
assert.ok(cyclicReport.issues.some((issue) => issue.code === "SHADER_GRAPH_CYCLE"));
const missingReport = compiler.compileMaterialGraph(material(), { imageIds: new Set() });
assert.equal(missingReport.status, "BLOCKED");
assert.ok(missingReport.issues.some((issue) => issue.code === "SHADER_EXTERNAL_RESOURCE_MISSING"));
});
test("M10-07 keeps the fallback graph fingerprint deterministic", () => {
const source = material();
delete source.shaderGraphHash;
const first = compiler.compileMaterialGraph(source);
const second = compiler.compileMaterialGraph(source);
assert.equal(first.graphHash, second.graphHash);
assert.match(first.graphHash, /^[0-9a-f]{64}$/);
const canonical = JSON.stringify({
schemaVersion: 1,
materialId: source.id,
nodes: source.nodes.map((node) => ({
id: node.id,
type: node.type,
name: node.name,
imageId: node.imageId ?? null,
defaultValue: node.defaultValue ?? null,
properties: node.properties ?? null,
})),
links: source.links.map((link) => ({
fromNodeId: link.fromNodeId,
fromSocket: link.fromSocket,
toNodeId: link.toNodeId,
toSocket: link.toSocket,
})),
});
assert.equal(first.graphHash, createHash("sha256").update(canonical).digest("hex"));
});
test("M10-07 fails closed before oversized topology or forged graph hashes are compiled", () => {
const oversized = material({
nodes: Array.from({ length: compiler.SHADER_COMPILE_BUDGET.maxNodes + 1 }, (_value, index) => ({
id: `value:${index}`,
type: "VALUE",
name: `Value ${index}`,
defaultValue: [0],
})),
links: [],
});
const oversizedReport = compiler.compileMaterialGraph(oversized);
assert.equal(oversizedReport.status, "BLOCKED");
assert.equal(oversizedReport.issues[0].code, "SHADER_NODE_UNSUPPORTED");
const forged = compiler.compileMaterialGraph(material({ shaderGraphHash: "A".repeat(64) }));
assert.equal(forged.status, "BLOCKED");
assert.equal(forged.issues[0].code, "SHADER_INVALID_GRAPH");
});
test("M10-08 binds graph, texture identity, color space and backend into compileKey", () => {
const textureIdentities = new Map([["image:Normal", {
assetId: "asset:normal-v1",
sha256: "b".repeat(64),
colorSpace: "NON_COLOR",
}]]);
const first = compiler.compileMaterialGraph(material(), { imageIds: new Set(["image:Normal"]), textureIdentities });
const second = compiler.compileMaterialGraph(material(), { imageIds: new Set(["image:Normal"]), textureIdentities: new Map([["image:Normal", { ...textureIdentities.get("image:Normal"), sha256: "c".repeat(64) }]]) });
const linear = compiler.compileMaterialGraph(material(), { imageIds: new Set(["image:Normal"]), textureIdentities: new Map([["image:Normal", { ...textureIdentities.get("image:Normal"), colorSpace: "LINEAR" }]]) });
assert.equal(first.status, "COMPILED");
assert.match(first.compileKey, /^[0-9a-f]{64}$/);
assert.notEqual(first.compileKey, second.compileKey);
assert.notEqual(first.compileKey, linear.compileKey);
assert.notEqual(
first.compileKey,
compiler.createShaderCompileKey({
graphHash: first.graphHash,
rendererBackend: "WEBGPU",
textures: [{ imageId: "image:Normal", usage: "NORMAL", assetId: "asset:normal-v1", sha256: "b".repeat(64), colorSpace: "NON_COLOR" }],
}),
);
});
test("M10-08 rejects an unknown renderer backend and malformed texture identity", () => {
const backend = compiler.compileMaterialGraph(material(), { rendererBackend: "WEBGPU" });
assert.equal(backend.status, "BLOCKED");
assert.equal(backend.issues[0].code, "CAPABILITY_MISSING");
const malformed = compiler.compileMaterialGraph(material(), {
imageIds: new Set(["image:Normal"]),
textureIdentities: new Map([["image:Normal", { sha256: "not-a-digest" }]]),
});
assert.equal(malformed.status, "BLOCKED");
assert.equal(malformed.issues.at(-1).code, "SHADER_INVALID_GRAPH");
});