import assert from "node:assert/strict"; import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { createRequire } from "node:module"; import { execFileSync } from "node:child_process"; import { fileURLToPath, pathToFileURL } from "node:url"; import factory from "../../web/app/src/vendor/blender/web_engine.js"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const golden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-07/compositor-node-golden.json"), "utf8")); const fixturePath = path.join(root, golden.fixture.path); const generatorPath = path.join(root, golden.fixture.generator); const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex"); assert.equal(sha256(fs.readFileSync(fixturePath)), golden.fixture.sha256); assert.equal(sha256(fs.readFileSync(generatorPath)), golden.fixture.generatorSha256); assert.match(execFileSync(blender, ["--version"], { encoding: "utf8" }), new RegExp(`^Blender ${golden.fixture.blenderVersion.replaceAll(".", "\\.")}\\b`, "m")); const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m11-compositor-golden-")); const requireFromWeb = createRequire(path.join(root, "web/package.json")); const ts = requireFromWeb("typescript"); for (const [sourceName, outputName] of [["capability-gates.ts", "capability-gates.mjs"], ["compositor.ts", "compositor.mjs"]]) { const sourcePath = path.join(root, "web/protocol", sourceName); 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, []); const output = sourceName === "compositor.ts" ? transpiled.outputText.replaceAll('from "./capability-gates"', 'from "./capability-gates.mjs"') : transpiled.outputText; fs.writeFileSync(path.join(temporary, outputName), output); } const compositor = await import(pathToFileURL(path.join(temporary, "compositor.mjs"))); const wasmBinary = fs.readFileSync(path.join(root, "web/app/src/vendor/blender/web_engine.wasm")); const fixture = fs.readFileSync(fixturePath); function open(engine, handle, bytes) { const pointer = engine._malloc(bytes.byteLength); try { engine.HEAPU8.set(bytes, pointer); assert.equal(engine._web_engine_open_blend(handle, pointer, bytes.byteLength), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); } finally { engine._free(pointer); } } function snapshot(engine, handle) { const dataOut = engine._malloc(4); const lengthOut = engine._malloc(4); try { assert.equal(engine._web_engine_get_scene_snapshot(handle, dataOut, lengthOut), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); const pointer = engine.HEAPU32[dataOut >>> 2]; const length = engine.HEAPU32[lengthOut >>> 2]; return JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(pointer, pointer + length))); } finally { engine._free(dataOut); engine._free(lengthOut); } } try { const engine = await factory({ wasmBinary: wasmBinary.slice() }); const handle = engine._web_engine_create(); try { open(engine, handle, fixture); const value = snapshot(engine, handle); for (const candidate of golden.cases) { const scene = value.scenes.find((item) => item.name === candidate.scene); assert.equal(scene?.compositorStatus, "AVAILABLE", `${candidate.scene} has no Main compositor graph`); assert.deepEqual(scene.compositorGraph.nodes.map((node) => node.type), candidate.nodeTypes); assert.deepEqual(compositor.compileCompositorWebGPUPlan(scene.compositorGraph).instructions.map((instruction) => instruction.type), candidate.nodeTypes); const cpu = compositor.executeCompositorGraph(scene.compositorGraph, new Map(), { width: golden.width, height: golden.height }); assert.deepEqual(Array.from(cpu.composite.data.slice(0, 4)), candidate.pixel); assert.equal(sha256(new Uint8Array(cpu.composite.data.buffer)), candidate.float32Sha256); } } finally { engine._web_engine_destroy(handle); } process.stdout.write(`compositor-node-golden-ok fixture=${golden.fixture.sha256} scenes=${golden.cases.length} allowlist=${golden.allowlist.join(",")} cpu-hashes=4\n`); } finally { fs.rmSync(temporary, { recursive: true, force: true }); }