#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; constexpr uint64_t MAX_SOURCE_BYTES = 512ULL * 1024ULL * 1024ULL; constexpr uint64_t MAX_OUTPUT_BYTES = 1024ULL * 1024ULL * 1024ULL; constexpr uint64_t MAX_ACTIVE_VOXELS = 64ULL * 1000ULL * 1000ULL; constexpr size_t MAX_GRIDS = 64; struct Options { fs::path input; fs::path output; fs::path report; std::vector grids; std::string quantization = "LOSSLESS"; fs::path cancel_file; uint64_t timeout_ms = 0; }; static std::atomic interrupted(false); static void request_interrupt(int) { interrupted.store(true, std::memory_order_relaxed); } struct GridReport { std::string name; std::string source_type; std::string value_type; std::string grid_class; uint64_t active_voxels = 0; uint64_t segment_offset = 0; uint64_t segment_length = 0; uint64_t grid_offset = 0; uint64_t grid_length = 0; openvdb::CoordBBox index_bounds; openvdb::BBoxd world_bounds; openvdb::Vec3d voxel_size; std::array index_to_world{}; struct ScalarSample { openvdb::Coord coord; float value; bool active; }; struct VectorSample { openvdb::Coord coord; std::array value; bool active; }; std::vector scalar_samples; std::vector vector_samples; }; static std::string json_string(const std::string &value) { std::ostringstream stream; stream << '"'; for (const unsigned char character : value) { switch (character) { case '"': stream << "\\\""; break; case '\\': stream << "\\\\"; break; case '\b': stream << "\\b"; break; case '\f': stream << "\\f"; break; case '\n': stream << "\\n"; break; case '\r': stream << "\\r"; break; case '\t': stream << "\\t"; break; default: if (character < 0x20) { stream << "\\u" << std::hex << std::setw(4) << std::setfill('0') << int(character) << std::dec; } else { stream << character; } } } stream << '"'; return stream.str(); } static Options parse_options(int argc, char **argv) { Options options; for (int index = 1; index < argc; ++index) { const std::string argument = argv[index]; auto value = [&](const char *name) -> std::string { if (++index >= argc) { throw std::runtime_error(std::string("missing value for ") + name); } return argv[index]; }; if (argument == "--input") options.input = value("--input"); else if (argument == "--output") options.output = value("--output"); else if (argument == "--report") options.report = value("--report"); else if (argument == "--grid") options.grids.push_back(value("--grid")); else if (argument == "--quantization") options.quantization = value("--quantization"); else if (argument == "--cancel-file") options.cancel_file = value("--cancel-file"); else if (argument == "--timeout-ms") { const std::string raw = value("--timeout-ms"); size_t consumed = 0; options.timeout_ms = std::stoull(raw, &consumed); if (consumed != raw.size() || options.timeout_ms < 1 || options.timeout_ms > 60ULL * 60ULL * 1000ULL) { throw std::runtime_error("timeout must be between 1ms and 1h"); } } else if (argument == "--help") { std::cout << "usage: vdb_to_nanovdb --input FILE.vdb --output FILE.nvdb --report REPORT.json " "[--grid NAME] [--quantization LOSSLESS|FP16] [--cancel-file FILE] [--timeout-ms MS]\n"; std::exit(0); } else { throw std::runtime_error("unknown argument: " + argument); } } if (options.input.empty() || options.output.empty() || options.report.empty()) { throw std::runtime_error("--input, --output and --report are required"); } if (options.input.extension() != ".vdb" || options.output.extension() != ".nvdb" || options.report.extension() != ".json") { throw std::runtime_error("input/output/report extensions must be .vdb/.nvdb/.json"); } if (options.quantization != "LOSSLESS" && options.quantization != "FP16") { throw std::runtime_error("quantization must be LOSSLESS or FP16"); } if (options.grids.size() > MAX_GRIDS || std::set(options.grids.begin(), options.grids.end()).size() != options.grids.size()) { throw std::runtime_error("selected grid list is duplicated or exceeds the budget"); } return options; } static void check_interrupted(const Options &options, const std::chrono::steady_clock::time_point started, const char *stage) { if (interrupted.load(std::memory_order_relaxed) || (!options.cancel_file.empty() && fs::exists(options.cancel_file))) { throw std::runtime_error(std::string("conversion cancelled during ") + stage); } if (options.timeout_ms > 0) { const uint64_t elapsed = std::chrono::duration_cast( std::chrono::steady_clock::now() - started) .count(); if (elapsed >= options.timeout_ms) { throw std::runtime_error(std::string("conversion timed out during ") + stage); } } } static std::string grid_class_name(openvdb::GridClass grid_class) { switch (grid_class) { case openvdb::GRID_LEVEL_SET: return "LEVEL_SET"; case openvdb::GRID_FOG_VOLUME: return "FOG_VOLUME"; case openvdb::GRID_STAGGERED: return "STAGGERED"; default: return "UNKNOWN"; } } static openvdb::BBoxd world_bounds(const openvdb::GridBase &grid, const openvdb::CoordBBox &bbox) { auto corner_point = [&](int corner) { return openvdb::Vec3d( corner & 1 ? bbox.max().x() + 1.0 : bbox.min().x(), corner & 2 ? bbox.max().y() + 1.0 : bbox.min().y(), corner & 4 ? bbox.max().z() + 1.0 : bbox.min().z()); }; const openvdb::Vec3d first = grid.transform().indexToWorld(corner_point(0)); openvdb::BBoxd result(first, first); for (int corner = 1; corner < 8; ++corner) { result.expand(grid.transform().indexToWorld(corner_point(corner))); } return result; } static std::array index_to_world(const openvdb::GridBase &grid) { const openvdb::Vec3d origin = grid.transform().indexToWorld(openvdb::Vec3d(0.0)); const openvdb::Vec3d x = grid.transform().indexToWorld(openvdb::Vec3d(1.0, 0.0, 0.0)) - origin; const openvdb::Vec3d y = grid.transform().indexToWorld(openvdb::Vec3d(0.0, 1.0, 0.0)) - origin; const openvdb::Vec3d z = grid.transform().indexToWorld(openvdb::Vec3d(0.0, 0.0, 1.0)) - origin; return {x.x(), y.x(), z.x(), origin.x(), x.y(), y.y(), z.y(), origin.y(), x.z(), y.z(), z.z(), origin.z(), 0.0, 0.0, 0.0, 1.0}; } static nanovdb::GridHandle convert_grid( const openvdb::GridBase::Ptr &grid, const std::string &quantization) { if (grid->isType()) { auto typed = openvdb::GridBase::grid(grid); if (quantization == "FP16") { nanovdb::tools::CreateNanoGrid converter(*typed); converter.setStats(nanovdb::tools::StatsMode::All); converter.setChecksum(nanovdb::CheckMode::Full); return converter.getHandle(); } } else if (!grid->isType()) { throw std::runtime_error("unsupported OpenVDB grid type for " + grid->getName() + ": " + grid->valueType()); } if (quantization != "LOSSLESS") { throw std::runtime_error("FP16 is supported only for FloatGrid: " + grid->getName()); } return nanovdb::tools::openToNanoVDB( grid, nanovdb::tools::StatsMode::All, nanovdb::CheckMode::Full, 0); } static void write_vec3(std::ostream &output, const openvdb::Vec3d &value) { output << '[' << value.x() << ',' << value.y() << ',' << value.z() << ']'; } static void write_coord(std::ostream &output, const openvdb::Coord &value) { output << '[' << value.x() << ',' << value.y() << ',' << value.z() << ']'; } static void write_report(const Options &options, const fs::path &report_path, const std::vector &grids) { std::ofstream output(report_path, std::ios::out | std::ios::trunc); if (!output) throw std::runtime_error("failed to create conversion report"); char nano_version[16]; nanovdb::toStr(nano_version, nanovdb::Version()); output << std::setprecision(17) << "{\n \"schemaVersion\":1,\n" << " \"input\":" << json_string(fs::absolute(options.input).string()) << ",\n" << " \"output\":" << json_string(fs::absolute(options.output).string()) << ",\n" << " \"quantization\":" << json_string(options.quantization) << ",\n" << " \"openVDBVersion\":" << json_string(openvdb::getLibraryVersionString()) << ",\n" << " \"nanoVDBVersion\":" << json_string(nano_version) << ",\n"; using FloatRootData = nanovdb::RootData>; using FloatUpperData = nanovdb::InternalData, 5>; using FloatLowerData = nanovdb::InternalData, 4>; using FloatLeafData = nanovdb::LeafData; using Vec3RootData = nanovdb::RootData>; using Vec3UpperData = nanovdb::InternalData, 5>; using Vec3LowerData = nanovdb::InternalData, 4>; using Vec3LeafData = nanovdb::LeafData; output << " \"float32TreeLayout\":{" << "\"gridDataBytes\":" << sizeof(nanovdb::GridData) << ",\"treeDataBytes\":" << sizeof(nanovdb::TreeData) << ",\"treeRootOffsetOffset\":" << offsetof(nanovdb::TreeData, mNodeOffset[3]) << ",\"rootDataBytes\":" << sizeof(FloatRootData) << ",\"rootTableSizeOffset\":" << offsetof(FloatRootData, mTableSize) << ",\"rootTileBytes\":" << sizeof(FloatRootData::Tile) << ",\"rootTileKeyOffset\":" << offsetof(FloatRootData::Tile, key) << ",\"rootTileChildOffset\":" << offsetof(FloatRootData::Tile, child) << ",\"rootTileStateOffset\":" << offsetof(FloatRootData::Tile, state) << ",\"rootTileValueOffset\":" << offsetof(FloatRootData::Tile, value) << ",\"upperNodeBytes\":" << sizeof(FloatUpperData) << ",\"upperValueMaskOffset\":" << offsetof(FloatUpperData, mValueMask) << ",\"upperChildMaskOffset\":" << offsetof(FloatUpperData, mChildMask) << ",\"upperTableOffset\":" << offsetof(FloatUpperData, mTable) << ",\"lowerNodeBytes\":" << sizeof(FloatLowerData) << ",\"lowerValueMaskOffset\":" << offsetof(FloatLowerData, mValueMask) << ",\"lowerChildMaskOffset\":" << offsetof(FloatLowerData, mChildMask) << ",\"lowerTableOffset\":" << offsetof(FloatLowerData, mTable) << ",\"leafNodeBytes\":" << sizeof(FloatLeafData) << ",\"leafValueMaskOffset\":" << offsetof(FloatLeafData, mValueMask) << ",\"leafValuesOffset\":" << offsetof(FloatLeafData, mValues) << "},\n" << " \"vec3fTreeLayout\":{" << "\"gridDataBytes\":" << sizeof(nanovdb::GridData) << ",\"treeDataBytes\":" << sizeof(nanovdb::TreeData) << ",\"treeRootOffsetOffset\":" << offsetof(nanovdb::TreeData, mNodeOffset[3]) << ",\"rootDataBytes\":" << sizeof(Vec3RootData) << ",\"rootTableSizeOffset\":" << offsetof(Vec3RootData, mTableSize) << ",\"rootTileBytes\":" << sizeof(Vec3RootData::Tile) << ",\"rootTileKeyOffset\":" << offsetof(Vec3RootData::Tile, key) << ",\"rootTileChildOffset\":" << offsetof(Vec3RootData::Tile, child) << ",\"rootTileStateOffset\":" << offsetof(Vec3RootData::Tile, state) << ",\"rootTileValueOffset\":" << offsetof(Vec3RootData::Tile, value) << ",\"upperNodeBytes\":" << sizeof(Vec3UpperData) << ",\"upperValueMaskOffset\":" << offsetof(Vec3UpperData, mValueMask) << ",\"upperChildMaskOffset\":" << offsetof(Vec3UpperData, mChildMask) << ",\"upperTableOffset\":" << offsetof(Vec3UpperData, mTable) << ",\"lowerNodeBytes\":" << sizeof(Vec3LowerData) << ",\"lowerValueMaskOffset\":" << offsetof(Vec3LowerData, mValueMask) << ",\"lowerChildMaskOffset\":" << offsetof(Vec3LowerData, mChildMask) << ",\"lowerTableOffset\":" << offsetof(Vec3LowerData, mTable) << ",\"leafNodeBytes\":" << sizeof(Vec3LeafData) << ",\"leafValueMaskOffset\":" << offsetof(Vec3LeafData, mValueMask) << ",\"leafValuesOffset\":" << offsetof(Vec3LeafData, mValues) << "},\n" << " \"grids\":[\n"; for (size_t index = 0; index < grids.size(); ++index) { const GridReport &grid = grids[index]; output << " {\"name\":" << json_string(grid.name) << ",\"sourceType\":" << json_string(grid.source_type) << ",\"valueType\":" << json_string(grid.value_type) << ",\"gridClass\":" << json_string(grid.grid_class) << ",\"activeVoxelCount\":" << grid.active_voxels << ",\"segmentByteOffset\":" << grid.segment_offset << ",\"segmentByteLength\":" << grid.segment_length << ",\"byteOffset\":" << grid.grid_offset << ",\"byteLength\":" << grid.grid_length << ",\"indexBounds\":{\"min\":"; write_coord(output, grid.index_bounds.min()); output << ",\"max\":"; write_coord(output, grid.index_bounds.max()); output << "},\"worldBounds\":{\"min\":"; write_vec3(output, grid.world_bounds.min()); output << ",\"max\":"; write_vec3(output, grid.world_bounds.max()); output << "},\"voxelSize\":"; write_vec3(output, grid.voxel_size); output << ",\"indexToWorld\":["; for (size_t matrix_index = 0; matrix_index < grid.index_to_world.size(); ++matrix_index) { if (matrix_index) output << ','; output << grid.index_to_world[matrix_index]; } output << ']'; if (!grid.scalar_samples.empty()) { output << ",\"scalarSamples\":["; for (size_t sample_index = 0; sample_index < grid.scalar_samples.size(); ++sample_index) { const auto &sample = grid.scalar_samples[sample_index]; if (sample_index) output << ','; output << "{\"coord\":"; write_coord(output, sample.coord); output << ",\"value\":" << sample.value << ",\"active\":" << (sample.active ? "true" : "false") << '}'; } output << ']'; } if (!grid.vector_samples.empty()) { output << ",\"vectorSamples\":["; for (size_t sample_index = 0; sample_index < grid.vector_samples.size(); ++sample_index) { const auto &sample = grid.vector_samples[sample_index]; if (sample_index) output << ','; output << "{\"coord\":"; write_coord(output, sample.coord); output << ",\"value\":[" << sample.value[0] << ',' << sample.value[1] << ',' << sample.value[2] << "],\"active\":" << (sample.active ? "true" : "false") << '}'; } output << ']'; } output << '}' << (index + 1 == grids.size() ? "\n" : ",\n"); } output << " ]\n}\n"; if (!output) throw std::runtime_error("failed to write conversion report"); } int main(int argc, char **argv) { fs::path staged_output; fs::path staged_report; try { const Options options = parse_options(argc, argv); const auto started = std::chrono::steady_clock::now(); std::signal(SIGINT, request_interrupt); std::signal(SIGTERM, request_interrupt); check_interrupted(options, started, "startup"); if (!fs::is_regular_file(options.input)) throw std::runtime_error("input VDB does not exist"); const uint64_t source_size = fs::file_size(options.input); if (source_size == 0 || source_size > MAX_SOURCE_BYTES) throw std::runtime_error("input VDB exceeds the source byte budget"); fs::create_directories(fs::absolute(options.output).parent_path()); fs::create_directories(fs::absolute(options.report).parent_path()); const std::string stage_suffix = "." + std::to_string(static_cast(getpid())) + ".stage"; staged_output = options.output.string() + stage_suffix; staged_report = options.report.string() + stage_suffix; fs::remove(staged_output); fs::remove(staged_report); openvdb::initialize(); openvdb::io::File input(options.input.string()); input.open(false); check_interrupted(options, started, "OpenVDB inventory"); openvdb::GridPtrVecPtr source_grids = input.getGrids(); if (!source_grids || source_grids->empty() || source_grids->size() > MAX_GRIDS) throw std::runtime_error("VDB grid count exceeds the budget"); const std::set selected(options.grids.begin(), options.grids.end()); std::set found; uint64_t total_active_voxels = 0; std::ofstream output(staged_output, std::ios::binary | std::ios::trunc); if (!output) throw std::runtime_error("failed to create NanoVDB output"); std::vector report; for (const openvdb::GridBase::Ptr &grid : *source_grids) { check_interrupted(options, started, "grid inventory"); if (!selected.empty() && !selected.count(grid->getName())) continue; if (!found.insert(grid->getName()).second) throw std::runtime_error("duplicate source grid name: " + grid->getName()); total_active_voxels += grid->activeVoxelCount(); if (total_active_voxels > MAX_ACTIVE_VOXELS) throw std::runtime_error("active voxel budget exceeded"); auto handle = convert_grid(grid, options.quantization); check_interrupted(options, started, "NanoVDB conversion"); const uint64_t segment_offset = static_cast(output.tellp()); nanovdb::io::writeGrid(output, handle, nanovdb::io::Codec::NONE); const uint64_t segment_end = static_cast(output.tellp()); const uint64_t name_size = grid->getName().size() + 1; const uint64_t grid_offset = segment_offset + sizeof(nanovdb::io::FileHeader) + sizeof(nanovdb::io::FileMetaData) + name_size; if (grid_offset + handle.gridSize() != segment_end) throw std::runtime_error("unexpected NanoVDB segment layout"); GridReport item; item.name = grid->getName(); item.source_type = grid->valueType(); item.value_type = options.quantization == "FP16" ? "FLOAT16" : grid->isType() ? "FLOAT32" : "VEC3F32"; item.grid_class = grid_class_name(grid->getGridClass()); item.active_voxels = grid->activeVoxelCount(); item.segment_offset = segment_offset; item.segment_length = segment_end - segment_offset; item.grid_offset = grid_offset; item.grid_length = handle.gridSize(); item.index_bounds = grid->evalActiveVoxelBoundingBox(); item.world_bounds = world_bounds(*grid, item.index_bounds); item.voxel_size = grid->voxelSize(); item.index_to_world = index_to_world(*grid); if (options.quantization == "LOSSLESS" && grid->isType()) { const nanovdb::NanoGrid *nano_grid = handle.grid(); if (!nano_grid) throw std::runtime_error("NanoVDB Float32 grid payload is unavailable"); const std::array sample_coords = { item.index_bounds.min(), openvdb::Coord(0, 0, 0), item.index_bounds.max(), openvdb::Coord(item.index_bounds.min().x() - 1, 0, 0), openvdb::Coord(item.index_bounds.max().x() + 1, 0, 0)}; for (const openvdb::Coord &coord : sample_coords) { float value = 0.0f; const bool active = nano_grid->tree().probeValue(nanovdb::Coord(coord.x(), coord.y(), coord.z()), value); item.scalar_samples.push_back({coord, value, active}); } } else if (options.quantization == "LOSSLESS" && grid->isType()) { const nanovdb::NanoGrid *nano_grid = handle.grid(); if (!nano_grid) throw std::runtime_error("NanoVDB Vec3f grid payload is unavailable"); const std::array sample_coords = { item.index_bounds.min(), openvdb::Coord(0, 0, 0), item.index_bounds.max(), openvdb::Coord(item.index_bounds.min().x() - 1, 0, 0), openvdb::Coord(item.index_bounds.max().x() + 1, 0, 0)}; for (const openvdb::Coord &coord : sample_coords) { nanovdb::Vec3f value(0.0f); const bool active = nano_grid->tree().probeValue(nanovdb::Coord(coord.x(), coord.y(), coord.z()), value); item.vector_samples.push_back({coord, {value[0], value[1], value[2]}, active}); } } report.push_back(std::move(item)); if (segment_end > MAX_OUTPUT_BYTES) throw std::runtime_error("NanoVDB output exceeds the byte budget"); } input.close(); output.close(); check_interrupted(options, started, "artifact commit"); if (!selected.empty() && found != selected) throw std::runtime_error("one or more selected grids were not found"); if (report.empty()) throw std::runtime_error("no supported grids were selected"); write_report(options, staged_report, report); check_interrupted(options, started, "report commit"); fs::rename(staged_output, options.output); fs::rename(staged_report, options.report); openvdb::uninitialize(); std::cout << "vdb-to-nanovdb-ok input=" << options.input.string() << " output=" << options.output.string() << " grids=" << report.size() << " bytes=" << fs::file_size(options.output) << '\n'; return 0; } catch (const std::exception &error) { if (!staged_output.empty()) fs::remove(staged_output); if (!staged_report.empty()) fs::remove(staged_report); std::cerr << "VDB_CONVERSION_FAILED: " << error.what() << '\n'; return 1; } }