import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const resourceRoot = path.resolve(process.env.VDB_RESOURCE_ROOT ?? path.join(os.homedir(), "resource-library/blender-web-vdb")); const source = path.join(resourceRoot, "generated", "generated-smoke.vdb"); const generator = path.join(root, "build_vdb_tools", "vdb_volume_golden"); const committedRoot = path.join(root, "tests", "golden", "M8-19"); const check = process.argv.includes("--check"); const outputRoot = check ? fs.mkdtempSync(path.join(os.tmpdir(), "vdb-volume-golden-")) : committedRoot; const outputPrefix = path.join(outputRoot, "generated-smoke"); const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); if (!fs.existsSync(generator)) throw new Error(`VDB_VOLUME_GOLDEN_TOOL_MISSING: ${generator}`); if (!fs.existsSync(source)) throw new Error(`VDB_VOLUME_GOLDEN_SOURCE_MISSING: ${source}`); fs.mkdirSync(outputRoot, { recursive: true }); try { const result = spawnSync(generator, [source, outputPrefix], { encoding: "utf8" }); if (result.status !== 0) throw new Error(result.stderr || `vdb_volume_golden exited ${result.status}`); const native = JSON.parse(result.stdout.trim()); const axes = ["X", "Y", "Z"]; const images = axes.map((axis) => { const fileName = `generated-smoke-${axis.toLowerCase()}.rgba`; const file = path.join(outputRoot, fileName); const bytes = fs.readFileSync(file); let alphaPixels = 0; for (let index = 3; index < bytes.byteLength; index += 4) if (bytes[index] > 0) alphaPixels++; return { axis, file: fileName, byteLength: bytes.byteLength, sha256: sha256(file), alphaPixels }; }); const manifest = { schemaVersion: 1, taskId: "M8-19", source: { resourceId: "generated-smoke-vdb", sha256: sha256(source), grid: native.grid, activeVoxelCount: native.activeVoxelCount, indexBounds: native.indexBounds, }, native: { openVDBVersion: native.openVDBVersion, generatorSourceSha256: sha256(path.join(root, "tools", "vdb", "vdb_volume_golden.cc")), }, renderContract: { shaderSemanticVersion: "volume-wgsl-v1", width: native.width, height: native.height, format: "RGBA8_UNORM", interpolation: "LINEAR", densityScale: 1, emissionScale: 0, anisotropy: 0, color: [0.72, 0.78, 0.86], axes, thresholds: { maxChannelError: 2, meanAbsoluteError: 0.1, rmsError: 0.5, alphaCoverageDeltaRatio: 0.002, }, }, images, }; const manifestText = `${JSON.stringify(manifest, null, 2)}\n`; const manifestFile = path.join(outputRoot, "manifest.json"); fs.writeFileSync(manifestFile, manifestText); if (check) { for (const image of images) { const expected = fs.readFileSync(path.join(committedRoot, image.file)); const actual = fs.readFileSync(path.join(outputRoot, image.file)); if (!expected.equals(actual)) throw new Error(`VDB_VOLUME_GOLDEN_MISMATCH: ${image.file}`); } const expectedManifest = fs.readFileSync(path.join(committedRoot, "manifest.json"), "utf8"); if (expectedManifest !== manifestText) throw new Error("VDB_VOLUME_GOLDEN_MISMATCH: manifest.json"); } process.stdout.write(`vdb-volume-golden-${check ? "check" : "generated"} axes=3 bytes=${images.reduce((total, image) => total + image.byteLength, 0)} sha256=${images.map((image) => image.sha256).join(",")}\n`); } finally { if (check) fs.rmSync(outputRoot, { recursive: true, force: true }); }