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 { spawnSync } from "node:child_process"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; import ts from "../../web/node_modules/typescript/lib/typescript.js"; 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 converter = path.join(root, "build_vdb_tools", "vdb_to_nanovdb"); const generator = path.join(root, "build_vdb_tools", "vdb_fixture_generator"); const source = path.join(resourceRoot, "generated", "generated-smoke.vdb"); const storedBundle = path.join(resourceRoot, "nanovdb", "generated-smoke.nvdb"); const officialSource = path.join(resourceRoot, "official", "sphere.vdb"); const officialBundle = path.join(resourceRoot, "nanovdb", "official-sphere.nvdb"); const browserManifest = path.join(resourceRoot, "manifests", "generated-smoke.nanovdb.json"); const catalog = JSON.parse(fs.readFileSync(path.join(resourceRoot, "manifest.json"), "utf8")); const evidence = JSON.parse(fs.readFileSync(path.join(root, "docs", "status", "vdb-native-evidence.json"), "utf8")); const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); assert.match(fs.readFileSync(path.join(root, "build_web_blender6", "CMakeCache.txt"), "utf8"), /^WITH_OPENVDB:BOOL=OFF$/m); assert.match(fs.readFileSync(path.join(root, "build_blender_5.2.0", "CMakeCache.txt"), "utf8"), /^WITH_OPENVDB:BOOL=ON$/m); assert.equal(catalog.schemaVersion, 1); assert.equal(catalog.toolchain.openVDBVersion, "13.0.0"); assert.equal(catalog.toolchain.converterSha256, sha256(converter)); assert.equal(evidence.schemaVersion, 1); assert.equal(evidence.browserOpenVDB, false); assert.equal(evidence.desktopOpenVDB, true); assert.equal(evidence.serverJobConfigured, true); assert.equal(evidence.opfsStreamingConfigured, true); assert.equal(evidence.webgpuRendererConfigured, true); assert.equal(evidence.mainVolumeRoundtripConfigured, true); assert.equal(evidence.primaryViewportVolumeIntegrated, false); assert.equal(evidence.releaseStatus, "BLOCKED"); assert.deepEqual(evidence.toolchain, catalog.toolchain); assert.deepEqual(evidence.resources, catalog.entries); for (const entry of catalog.entries) { const file = path.join(resourceRoot, entry.path); assert.equal(fs.statSync(file).size, entry.byteLength, `${entry.id} byte length changed`); assert.equal(sha256(file), entry.sha256, `${entry.id} SHA-256 changed`); } assert.equal(catalog.entries.find((entry) => entry.id === "official-sphere")?.sha256, "bb884a9a38354e47191c4ece4a11d1e051594a910fde56501cfc51ff52b171ab"); const ldd = spawnSync("ldd", [converter], { encoding: "utf8" }); assert.equal(ldd.status, 0); assert.match(ldd.stdout, /libopenvdb\.so\.13/); const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "vdb-native-")); try { const output = path.join(temporary, "smoke.nvdb"); const report = path.join(temporary, "smoke.json"); const conversion = spawnSync(converter, ["--input", source, "--output", output, "--report", report, "--grid", "density", "--grid", "temperature", "--grid", "color", "--grid", "velocity", "--quantization", "LOSSLESS"], { encoding: "utf8" }); assert.equal(conversion.status, 0, conversion.stderr); assert.equal(sha256(output), sha256(storedBundle), "OpenVDB to NanoVDB conversion is not deterministic"); const parsedReport = JSON.parse(fs.readFileSync(report, "utf8")); assert.equal(parsedReport.openVDBVersion, "13.0.0"); assert.equal(parsedReport.nanoVDBVersion, "32.9.0"); assert.deepEqual(parsedReport.grids.map((grid) => grid.name), ["color", "density", "temperature", "velocity"]); assert.ok(parsedReport.grids.every((grid) => grid.byteOffset >= grid.segmentByteOffset && grid.byteOffset + grid.byteLength <= grid.segmentByteOffset + grid.segmentByteLength)); const generatedA = path.join(temporary, "generated-a"); const generatedB = path.join(temporary, "generated-b"); fs.mkdirSync(generatedA); fs.mkdirSync(generatedB); assert.equal(spawnSync(generator, [generatedA], { encoding: "utf8" }).status, 0); assert.equal(spawnSync(generator, [generatedB], { encoding: "utf8" }).status, 0); const semanticOutputs = []; for (const [index, generated] of [generatedA, generatedB].entries()) { const semanticOutput = path.join(temporary, `semantic-${index}.nvdb`); const semanticReport = path.join(temporary, `semantic-${index}.json`); const result = spawnSync(converter, ["--input", path.join(generated, "generated-smoke.vdb"), "--output", semanticOutput, "--report", semanticReport, "--grid", "density", "--grid", "temperature", "--grid", "color", "--grid", "velocity", "--quantization", "LOSSLESS"], { encoding: "utf8" }); assert.equal(result.status, 0, result.stderr); semanticOutputs.push(semanticOutput); } assert.equal(sha256(semanticOutputs[0]), sha256(semanticOutputs[1]), "Semantically identical generated VDB grids produce different NanoVDB bundles"); assert.equal(sha256(semanticOutputs[0]), sha256(storedBundle)); const officialOutput = path.join(temporary, "official-sphere.nvdb"); const officialReport = path.join(temporary, "official-sphere.json"); const officialConversion = spawnSync(converter, ["--input", officialSource, "--output", officialOutput, "--report", officialReport, "--quantization", "LOSSLESS"], { encoding: "utf8" }); assert.equal(officialConversion.status, 0, officialConversion.stderr); assert.equal(sha256(officialOutput), sha256(officialBundle), "Official sphere conversion is not deterministic"); const parsedOfficial = JSON.parse(fs.readFileSync(officialReport, "utf8")); assert.deepEqual(parsedOfficial.grids.map((grid) => [grid.name, grid.gridClass]), [["ls_sphere", "LEVEL_SET"]]); const malformed = spawnSync(converter, ["--input", path.join(resourceRoot, "generated", "generated-smoke-truncated.vdb"), "--output", path.join(temporary, "bad.nvdb"), "--report", path.join(temporary, "bad.json")], { encoding: "utf8" }); assert.notEqual(malformed.status, 0); assert.match(malformed.stderr, /VDB_CONVERSION_FAILED/); for (const name of ["asset-path", "capability-gates", "volume-vdb"]) { const sourceText = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); let code = ts.transpileModule(sourceText, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText; code = code.replaceAll('require("./asset-path")', 'require("./asset-path.cjs")').replaceAll('require("./capability-gates")', 'require("./capability-gates.cjs")'); fs.writeFileSync(path.join(temporary, `${name}.cjs`), code); } const require = createRequire(import.meta.url); const protocol = require(path.join(temporary, "volume-vdb.cjs")); const validated = protocol.validateNanoVDBBundleManifest(JSON.parse(fs.readFileSync(browserManifest, "utf8"))); assert.equal(validated.bundleSha256, sha256(storedBundle)); assert.deepEqual(validated.grids.map((grid) => grid.semantic), ["COLOR", "DENSITY", "TEMPERATURE", "VELOCITY"]); } finally { fs.rmSync(temporary, { recursive: true, force: true }); } process.stdout.write(`vdb-native-pipeline-ok resources=${catalog.entries.length} converter=openvdb13-nanovdb32 conversion-deterministic=1 semantic-deterministic=1 official-deterministic=1 malformed=blocked browser-openvdb=off\n`);