diff --git a/ci/install_windows.ps1 b/ci/install_windows.ps1 index 6054cb96bf..a4d44da353 100644 --- a/ci/install_windows.ps1 +++ b/ci/install_windows.ps1 @@ -21,20 +21,52 @@ $vcpkgPackages = @( "nanobind" ) +$maxAttempts = 3 + +# curl's schannel backend reports an unreachable CRL/OCSP responder as a +# certificate verification failure (error 60), which vcpkg then treats as +# permanent. Downgrade a missing revocation answer to a warning; the rest of +# certificate validation still applies. +$env:VCPKG_SSL_REVOKE_BEST_EFFORT = "1" + # Update vcpkg vcpkg update -# Allow the vcpkg command to fail once so we can retry with the latest -try { - vcpkg install $vcpkgPackages -} catch { - Write-Host "vcpkg install failed, retrying with latest ports..." - # Retry the installation with updated ports - Push-Location $env:VCPKG_INSTALLATION_ROOT - git pull - Pop-Location - vcpkg update +$installed = $false + +for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { vcpkg install $vcpkgPackages + + # A failing native command does not raise a terminating error, so the exit + # code has to be inspected explicitly rather than relying on try/catch. + if ($LASTEXITCODE -eq 0) { + $installed = $true + break + } + + if ($attempt -eq $maxAttempts) { + break + } + + # vcpkg fetches port sources directly from upstream hosts and won't retry + # downloads it classifies as permanent failures, so a single flaky TLS + # handshake aborts the whole install. + Write-Host "vcpkg install failed (attempt $attempt of $maxAttempts), retrying..." + Start-Sleep -Seconds 15 + + # Refresh the ports before the last attempt in case the failure is caused + # by a stale port rather than the network. + if ($attempt -eq ($maxAttempts - 1)) { + Write-Host "Retrying with latest ports..." + Push-Location $env:VCPKG_INSTALLATION_ROOT + git pull + Pop-Location + vcpkg update + } +} + +if (-not $installed) { + throw "vcpkg install failed after $maxAttempts attempts" } Write-Host "vcpkg install completed successfully" diff --git a/doc/changes.txt b/doc/changes.txt index 3c05074c81..033231bbaa 100644 --- a/doc/changes.txt +++ b/doc/changes.txt @@ -2034,7 +2034,7 @@ Bug fixes: New features: - Added @vdblink{tools::FindActiveValues,FindActiveValues}, which counts the active values in a tree that intersect a given bounding box. -- Added @vdblink{io::DelayedLoadMetadata,DelayedLoadMetadata}, which stores +- Added @c io::DelayedLoadMetadata, which stores mask offsets and compression sizes on write to accelerate delayed load reading. @@ -2933,7 +2933,7 @@ New features: - Added a toggle to the @vdblink::tools::clip() clip@endlink tool to invert the clipping mask. - Custom leaf node implementations may now optimize their file layout - by inheriting from @vdblink::io::MultiPass io::MultiPass@endlink. + by inheriting from @c io::MultiPass. Voxel data for grids with such leaf nodes will be written and read in multiple passes, allowing blocks of related data to be stored contiguously. [Contributed by Double Negative] diff --git a/openvdb/openvdb/Grid.h b/openvdb/openvdb/Grid.h index a71afad140..d32f064c8d 100644 --- a/openvdb/openvdb/Grid.h +++ b/openvdb/openvdb/Grid.h @@ -9,6 +9,7 @@ #include "Types.h" #include "io/io.h" #include "math/Transform.h" +#include "tree/LeafManager.h" #include "tree/Tree.h" #include "util/Assert.h" #include "util/logging.h" @@ -1632,6 +1633,28 @@ inline void Grid::readTopology(std::istream& is) { tree().readTopology(is, saveFloatAsHalf()); + // When called from the legacy (non-codec) TopologyOnly path, the stream + // metadata carries a flag requesting that leaf buffers be allocated and + // filled with the background value (PartialCreate leaves them + // unallocated after readTopology). + if (io::StreamMetadata::Ptr meta = io::getStreamMetadataPtr(is)) { + if (meta->allocateLeafBuffers()) { + meta->setAllocateLeafBuffers(false); + if constexpr (!std::is_void_v) { + const auto background = tree().root().background(); + tree::LeafManager leafManager(tree()); + leafManager.foreach([&background](auto& leaf, size_t) { + using LeafType = std::decay_t; + if constexpr (!std::is_same_v) { + if (leaf.buffer().empty()) { + leaf.buffer().allocate(); + leaf.buffer().fill(background); + } + } + }); + } + } + } } diff --git a/openvdb/openvdb/codecs/BoolCodec.h b/openvdb/openvdb/codecs/BoolCodec.h index e8537fa662..82c7ae5a38 100644 --- a/openvdb/openvdb/codecs/BoolCodec.h +++ b/openvdb/openvdb/codecs/BoolCodec.h @@ -110,7 +110,7 @@ struct BoolCodec final: public TopologyCodec static inline std::string name() { return GridT::gridType(); } - void readBuffers(std::istream& is, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics&) final + void readBuffers(std::istream& is, Index64 /*size*/, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics&) final { GridT& grid = static_cast(*data.grid); diff --git a/openvdb/openvdb/codecs/PointDataCodec.h b/openvdb/openvdb/codecs/PointDataCodec.h index f4bc713bd7..7cb6219cb7 100644 --- a/openvdb/openvdb/codecs/PointDataCodec.h +++ b/openvdb/openvdb/codecs/PointDataCodec.h @@ -111,13 +111,14 @@ template inline void readPointDataVoxelData(const std::vector& leaves, std::istream& is, bool saveFloatAsHalf, const typename LeafT::ValueType& background, - [[maybe_unused]] const std::unordered_map& voxelBufferSizes) + [[maybe_unused]] const std::unordered_map& voxelBufferSizes, + const typename LeafT::ValueType* storageBackground = nullptr) { using BaseLeaf = typename LeafT::BaseLeaf; for (auto* leaf : leaves) { OPENVDB_ASSERT(voxelBufferSizes.find(leaf->origin()) != voxelBufferSizes.end()); BaseLeaf& baseLeaf = static_cast(*leaf); - readScalarLeafBuffers(baseLeaf, is, saveFloatAsHalf, background); + readScalarLeafBuffers(baseLeaf, is, saveFloatAsHalf, background, /*skip=*/false, /*clipBBox=*/nullptr, storageBackground); } } @@ -310,7 +311,7 @@ struct PointDataCodec final: public TopologyCodec static inline std::string name() { return GridT::gridType(); } - void readBuffers(std::istream& is, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics&) final + void readBuffers(std::istream& is, Index64 /*size*/, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics&) final { OPENVDB_ASSERT(dynamic_cast(data.grid.get())); @@ -334,7 +335,11 @@ struct PointDataCodec final: public TopologyCodec uint16_t numPasses = 1; is.read(reinterpret_cast(&numPasses), sizeof(uint16_t)); - const Index attributes = (numPasses - 4) / 2; + // The pass layout is: voxel sizes (1) + descriptors (1) + attribute + // sizes (N) + voxel data (1) + attribute data (N) = 2N + 4 passes. + // A leafless grid stores numPasses == 0, and malformed files may store + // numPasses < 4; guard against unsigned underflow in either case. + const Index attributes = numPasses >= 4 ? Index(numPasses - 4) / 2 : 0; using LeafT = typename GridT::TreeType::LeafNodeType; std::vector leaves; @@ -351,7 +356,17 @@ struct PointDataCodec final: public TopologyCodec // An empty pointAttributeNames means no filtering (read all attributes). std::set skipIndices; if (!pointAttributeNames.empty() && !leaves.empty()) { - const auto& nameMap = leaves[0]->attributeSet().descriptor().map(); + // Attribute filtering requires homogeneous descriptors across all + // leaves because skip decisions are made per-index across all leaves. + const auto* firstDesc = &leaves[0]->attributeSet().descriptor(); + for (size_t i = 1; i < leaves.size(); ++i) { + if (&leaves[i]->attributeSet().descriptor() != firstDesc) { + OPENVDB_THROW(IoError, + "Attribute filtering is not supported for PointDataGrids " + "with heterogeneous descriptors"); + } + } + const auto& nameMap = firstDesc->map(); const std::set wantedNames( pointAttributeNames.begin(), pointAttributeNames.end()); @@ -373,8 +388,10 @@ struct PointDataCodec final: public TopologyCodec } // Pass N+2: read voxel data + using ValueT = typename GridT::TreeType::ValueType; + auto& topoData = static_cast&>(data); internal::readPointDataVoxelData(leaves, is, saveFloatAsHalf, - tree.background(), voxelBufferSizes); + tree.background(), voxelBufferSizes, &topoData.storageBackground); // Passes N+3..2N+2: read attribute data buffers for (Index i = 0; i < attributes; ++i) { @@ -419,7 +436,9 @@ struct PointDataCodec final: public TopologyCodec static_cast(internal::countPointDataPasses(leaves)); os.write(reinterpret_cast(&numPasses), sizeof(uint16_t)); - const Index attributes = (numPasses - 4) / 2; + // See readBuffers(): a leafless grid yields numPasses == 0, so guard + // against unsigned underflow rather than computing (numPasses - 4) / 2. + const Index attributes = numPasses >= 4 ? Index(numPasses - 4) / 2 : 0; // Pass 0: write voxel data sizes + descriptor tracking bool matching = true; diff --git a/openvdb/openvdb/codecs/PointIndexCodec.h b/openvdb/openvdb/codecs/PointIndexCodec.h index 851ee140fd..d1694d5b96 100644 --- a/openvdb/openvdb/codecs/PointIndexCodec.h +++ b/openvdb/openvdb/codecs/PointIndexCodec.h @@ -26,10 +26,11 @@ struct ReadPointIndexBuffersOp using ValueT = typename TreeT::ValueType; ReadPointIndexBuffersOp(std::istream& _is, bool _saveFloatAsHalf, - const ValueT& _background) + const ValueT& _background, const ValueT* _storageBackground = nullptr) : is(_is) , saveFloatAsHalf(_saveFloatAsHalf) - , background(_background) { } + , background(_background) + , storageBackground(_storageBackground) { } template void operator()(NodeT&, size_t) { } @@ -40,7 +41,7 @@ struct ReadPointIndexBuffersOp // Read the value mask and voxel data via base class BaseLeaf& baseLeaf = static_cast(leaf); - readScalarLeafBuffers(baseLeaf, is, saveFloatAsHalf, background, /*skip=*/false, /*clipBBox=*/nullptr); + readScalarLeafBuffers(baseLeaf, is, saveFloatAsHalf, background, /*skip=*/false, /*clipBBox=*/nullptr, storageBackground); // Read the number of indices. Index64 numIndices = Index64(0); @@ -63,6 +64,7 @@ struct ReadPointIndexBuffersOp std::istream& is; const bool saveFloatAsHalf; const ValueT& background; + const ValueT* storageBackground = nullptr; }; // struct ReadPointIndexBuffersOp template @@ -114,7 +116,7 @@ struct PointIndexCodec final: public TopologyCodec static inline std::string name() { return GridT::gridType(); } - void readBuffers(std::istream& is, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics& diagnostics) final + void readBuffers(std::istream& is, Index64 /*size*/, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics& diagnostics) final { OPENVDB_ASSERT(dynamic_cast(data.grid.get())); @@ -135,7 +137,9 @@ struct PointIndexCodec final: public TopologyCodec diagnostics.addWarning(grid.getName(), "bounding box clipping is not supported for PointIndexGrids"); } - internal::ReadPointIndexBuffersOp readBuffersOp(is, saveFloatAsHalf, tree.background()); + using ValueT = typename GridT::TreeType::ValueType; + auto& topoData = static_cast&>(data); + internal::ReadPointIndexBuffersOp readBuffersOp(is, saveFloatAsHalf, tree.background(), &topoData.storageBackground); tools::visitNodesDepthFirst(grid.tree(), readBuffersOp, /*idx=*/0, /*topDown=*/false); } diff --git a/openvdb/openvdb/codecs/ScalarCodec.h b/openvdb/openvdb/codecs/ScalarCodec.h index 114c575bcf..60b1eb6425 100644 --- a/openvdb/openvdb/codecs/ScalarCodec.h +++ b/openvdb/openvdb/codecs/ScalarCodec.h @@ -23,21 +23,26 @@ template struct WriteBuffersOp { using LeafT = typename TreeT::LeafNodeType; + using ValueT = typename TreeT::ValueType; - WriteBuffersOp(std::ostream& _os, bool _saveFloatAsHalf) + WriteBuffersOp(std::ostream& _os, bool _saveFloatAsHalf, const ValueT& _background) : os(_os) - , saveFloatAsHalf(_saveFloatAsHalf) { } + , saveFloatAsHalf(_saveFloatAsHalf) + , background(_background) { } template void operator()(const NodeT&, size_t) { } void operator()(const LeafT& leaf, size_t) { - writeScalarLeafBuffers(leaf, os, saveFloatAsHalf); + // Pass the background explicitly so leaf compression does not depend on + // the stream's background pointer (which the codec path no longer sets). + writeScalarLeafBuffers(leaf, os, saveFloatAsHalf, &background); } std::ostream& os; const bool saveFloatAsHalf; + const ValueT& background; }; // struct WriteBuffersOp @@ -48,13 +53,15 @@ struct ReadBuffersOp using LeafT = typename TreeT::LeafNodeType; using ValueT = typename TreeT::ValueType; using StorageLeafT = typename StorageTreeT::LeafNodeType; + using StorageValueT = typename StorageTreeT::ValueType; ReadBuffersOp(std::istream& _is, bool _saveFloatAsHalf, const ValueT& _background, - const CoordBBox* _clipBBox) + const CoordBBox* _clipBBox, const StorageValueT* _storageBackground = nullptr) : is(_is) , saveFloatAsHalf(_saveFloatAsHalf) , background(_background) - , clipBBox(_clipBBox) { } + , clipBBox(_clipBBox) + , storageBackground(_storageBackground) { } void operator()(RootT& root, size_t) { @@ -73,20 +80,22 @@ struct ReadBuffersOp void operator()(LeafT& leaf, size_t) { - readScalarLeafBuffers(leaf, is, saveFloatAsHalf, background, /*skip=*/false, clipBBox); + readScalarLeafBuffers(leaf, is, saveFloatAsHalf, background, /*skip=*/false, clipBBox, storageBackground); } std::istream& is; const bool saveFloatAsHalf; const ValueT& background; const CoordBBox* clipBBox = nullptr; + const StorageValueT* storageBackground = nullptr; }; // struct ReadBuffersOp // Free-standing function for both standard and conversion codec cases // Uses StorageGridT = GridT by default, but allows different storage type for conversions template -void scalarCodecReadBuffers(GridT& grid, std::istream& is, const io::ReadOptions& options) +void scalarCodecReadBuffers(GridT& grid, std::istream& is, const io::ReadOptions& options, + const typename StorageGridT::TreeType::ValueType* storageBackground) { if (grid.hasMultiPassIO()) { OPENVDB_THROW(IoError, "Multi-pass IO is not supported in ScalarCodec"); @@ -109,7 +118,7 @@ void scalarCodecReadBuffers(GridT& grid, std::istream& is, const io::ReadOptions // Works for both standard (TreeT == StorageTreeT) and conversion cases ReadBuffersOp readBuffersOp(is, saveFloatAsHalf, tree.background(), - clipIndexBBox.get()); + clipIndexBBox.get(), storageBackground); tools::visitNodesDepthFirst(grid.tree(), readBuffersOp, /*idx=*/0, /*topDown=*/false); } @@ -123,7 +132,7 @@ void scalarCodecWriteBuffers(const GridT& grid, std::ostream& os) OPENVDB_THROW(IoError, "Multi-pass IO is not supported in ScalarCodec"); } - WriteBuffersOp writeBuffersOp(os, grid.saveFloatAsHalf()); + WriteBuffersOp writeBuffersOp(os, grid.saveFloatAsHalf(), grid.tree().background()); tools::visitNodesDepthFirst(grid.tree(), writeBuffersOp); } @@ -150,18 +159,25 @@ struct ScalarCodec final: public TopologyCodec } } - void readBuffers(std::istream& is, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics&) final + void readBuffers(std::istream& is, Index64 /*size*/, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics&) final { + using StorageValueT = typename StorageGridT::TreeType::ValueType; GridT& grid = static_cast(*data.grid); - internal::scalarCodecReadBuffers(grid, is, options); + auto& topoData = static_cast&>(data); + internal::scalarCodecReadBuffers(grid, is, options, &topoData.storageBackground); } void writeBuffers(std::ostream& os, const GridBase& gridBase, const io::WriteOptions&) final { - if constexpr (Mode == io::CodecMode::ReadOnly) return; - - const GridT& grid = static_cast(gridBase); - internal::scalarCodecWriteBuffers(grid, os); + // Note: the write body must live inside the negated if constexpr branch + // so it is not instantiated for read-only codecs. A bare + // `if constexpr (Mode == ReadOnly) return;` would still instantiate the + // code that follows, which fails to compile for the scalar-to-mask/bool + // convert codecs (their leaf buffers expose WordType*, not ValueType*). + if constexpr (Mode != io::CodecMode::ReadOnly) { + const GridT& grid = static_cast(gridBase); + internal::scalarCodecWriteBuffers(grid, os); + } } }; // struct ScalarCodec diff --git a/openvdb/openvdb/codecs/TopologyCodec.h b/openvdb/openvdb/codecs/TopologyCodec.h index e602f176ce..eee4ead6e5 100644 --- a/openvdb/openvdb/codecs/TopologyCodec.h +++ b/openvdb/openvdb/codecs/TopologyCodec.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -15,6 +16,16 @@ namespace openvdb { OPENVDB_USE_VERSION_NAMESPACE namespace OPENVDB_VERSION_NAME { namespace codecs { + +/// Per-read-operation state shared between readTopology() and readBuffers(). +/// Stores the storage-typed background so readBuffers() can pass it directly +/// to readCompressedValues(), bypassing the stream background ptr entirely. +template +struct TopologyCodecData : public io::CodecData +{ + StorageValueT storageBackground{}; +}; // struct TopologyCodecData + namespace internal { template @@ -40,7 +51,6 @@ struct WriteTopologyOp ValueT truncatedVal = io::truncateRealToHalf(*background); os.write(reinterpret_cast(&truncatedVal), sizeof(ValueT)); } - io::setGridBackgroundValuePtr(os, background); const Index numTiles = root.tileCount(), numChildren = root.childCount(); os.write(reinterpret_cast(&numTiles), sizeof(Index)); @@ -83,7 +93,7 @@ struct WriteTopologyOp values[i] = (node.isChildMaskOff(i) ? node.getValueUnsafe(i) : zero); } // Compress (optionally) and write out the contents of the array. - io::writeCompressedValues(os, values, NodeT::NUM_VALUES, valueMask, childMask, saveFloatAsHalf); + io::writeCompressedValues(os, values, NodeT::NUM_VALUES, valueMask, childMask, saveFloatAsHalf, background); } } @@ -129,16 +139,8 @@ struct ReadTopologyOp // Read a RootNode that was stored in the current format. - if constexpr (std::is_same_v) { - is.read(reinterpret_cast(&background), sizeof(ValueT)); - } else { - StorageValueT _background; - is.read(reinterpret_cast(&_background), sizeof(StorageValueT)); - background = static_cast(_background); - } - - root.setBackground(background, false); - io::setGridBackgroundValuePtr(is, &root.background()); + is.read(reinterpret_cast(&storageBackground), sizeof(StorageValueT)); + background = static_cast(storageBackground); Index numTiles = 0, numChildren = 0; is.read(reinterpret_cast(&numTiles), sizeof(Index)); @@ -190,12 +192,16 @@ struct ReadTopologyOp // into a contiguous array. std::unique_ptr valuePtr(new StorageValueT[numValues]); StorageValueT* values = valuePtr.get(); - io::readCompressedValues(is, values, numValues, valueMask, saveFloatAsHalf); + io::readCompressedValues(is, values, numValues, valueMask, saveFloatAsHalf, &storageBackground); // Copy values from the array into this node's table. if (oldVersion) { + // The node's member child mask is still empty at this point + // (PartialCreate; setChildUnsafe runs below), so iterate the + // local childMask's off-bits to match the legacy ordering and + // avoid over-reading the countOff-sized values array. Index n = 0; - for (auto iter = node.beginValueAll(); iter; ++iter) { + for (auto iter = childMask.beginOff(); iter; ++iter) { node.setValueOnlyUnsafe(iter.pos(), static_cast(values[n++])); } OPENVDB_ASSERT(n == numValues); @@ -207,11 +213,12 @@ struct ReadTopologyOp } // Read in all child nodes and insert them into the table at their proper locations. + // Register the child before recursing so that node's destructor frees it on a throw. for (auto iter = childMask.beginOn(); iter; ++iter) { Coord origin = node.offsetToGlobalCoord(iter.pos()); auto* child = new ChildT(PartialCreate(), origin, background); - (*this)(*child); node.setChildUnsafe(iter.pos(), child); + (*this)(*child); } } @@ -225,6 +232,7 @@ struct ReadTopologyOp std::istream& is; bool saveFloatAsHalf; ValueT background; + StorageValueT storageBackground; io::ReadDiagnostics& diagnostics; std::string gridName; }; // struct ReadTopologyOp @@ -273,9 +281,12 @@ void setTilesToBackground(TreeT& tree) nodeManager.foreachTopDown(op); } -// Free-standing function for read case (supports type conversion via StorageGridT) +// Free-standing function for read case (supports type conversion via StorageGridT). +// codecData receives the storage-typed background value so readBuffers() callers +// can pass it explicitly to readCompressedValues(), avoiding the stream background ptr. template -void topologyCodecReadTopology(GridBase& gridBase, std::istream& is, const io::ReadOptions& options, io::ReadDiagnostics& diagnostics) +void topologyCodecReadTopology(GridBase& gridBase, std::istream& is, const io::ReadOptions& options, + io::ReadDiagnostics& diagnostics, TopologyCodecData& codecData) { io::checkFormatVersion(is); @@ -285,8 +296,33 @@ void topologyCodecReadTopology(GridBase& gridBase, std::istream& is, const io::R internal::ReadTopologyOp readTopologyOp(is, grid.saveFloatAsHalf(), diagnostics, grid.getName()); readTopologyOp(grid.tree().root()); + // Restore the (value-typed) background on the root. ReadTopologyOp only reads + // the on-disk background into a local; without this the grid would retain the + // default background from GridT::create(). Pass updateChildNodes=false so the + // already-populated child nodes are left untouched. + grid.tree().root().setBackground(readTopologyOp.background, /*updateChildNodes=*/false); + + // Copy storageBackground out of the stack-local ReadTopologyOp into codecData + // so it stays alive until readBuffers() completes. readBuffers() passes it + // explicitly to readCompressedValues(), so the stream background ptr is never + // used and setGridBackgroundValuePtr() is not needed in the codec path. + codecData.storageBackground = readTopologyOp.storageBackground; + if (options.readMode == io::ReadMode::TopologyOnly) { internal::setTilesToBackground(grid.tree()); + // allocate leaf buffers in parallel and fill with the background value; + // ReadTopologyOp uses PartialCreate which leaves buffers unallocated. + const auto background = grid.tree().root().background(); + tree::LeafManager leafManager(grid.tree()); + leafManager.foreach([&background](auto& leaf, size_t) { + using LeafType = std::decay_t; + if constexpr (!std::is_same_v) { + if (leaf.buffer().empty()) { + leaf.buffer().allocate(); + leaf.buffer().fill(background); + } + } + }); return; } } @@ -307,13 +343,14 @@ void topologyCodecWriteTopology(const GridBase& gridBase, std::ostream& os) template struct TopologyCodec : public io::Codec { + using StorageValueT = typename StorageGridT::TreeType::ValueType; using Ptr = std::unique_ptr>; ~TopologyCodec() noexcept = default; io::CodecData::Ptr createData() override { - auto data = std::make_unique(); + auto data = std::make_unique>(); data->grid = GridT::create(); return data; } @@ -321,15 +358,36 @@ struct TopologyCodec : public io::Codec void readTopology(std::istream& is, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics& diagnostics) final { - internal::topologyCodecReadTopology(*data.grid, is, options, diagnostics); + // Warn when a conversion readMode was requested but this codec is a + // non-conversion instance (GridT == StorageGridT), meaning no conversion + // codec was registered for this grid type and we are falling back to the + // original type. + if constexpr (std::is_same_v) { + if (options.readMode == io::ReadMode::Half || + options.readMode == io::ReadMode::Bool || + options.readMode == io::ReadMode::Mask) + { + const std::string modeStr = + options.readMode == io::ReadMode::Half ? "Half" : + options.readMode == io::ReadMode::Bool ? "Bool" : "Mask"; + diagnostics.addWarning(data.grid->getName(), + "ReadMode::" + modeStr + " conversion is not supported for grid type '" + + GridT::gridType() + "'; reading as original type"); + } + } + auto& topoData = static_cast&>(data); + internal::topologyCodecReadTopology(*data.grid, is, options, diagnostics, topoData); } void writeTopology(std::ostream& os, const GridBase& gridBase, const io::WriteOptions&) final { - // disable implementation when read only - if constexpr (Mode == io::CodecMode::ReadOnly) return; - - internal::topologyCodecWriteTopology(gridBase, os); + // Disable implementation when read only. The body must live inside the + // negated if constexpr branch so it is not instantiated for read-only + // codecs; a bare `if constexpr (...) return;` still instantiates what + // follows. + if constexpr (Mode != io::CodecMode::ReadOnly) { + internal::topologyCodecWriteTopology(gridBase, os); + } } }; // struct TopologyCodec diff --git a/openvdb/openvdb/codecs/ValueMaskCodec.h b/openvdb/openvdb/codecs/ValueMaskCodec.h index 27f6289cb9..3d2b8d7bd0 100644 --- a/openvdb/openvdb/codecs/ValueMaskCodec.h +++ b/openvdb/openvdb/codecs/ValueMaskCodec.h @@ -102,7 +102,7 @@ struct ValueMaskCodec final: public TopologyCodec static inline std::string name() { return GridT::gridType(); } - void readBuffers(std::istream& is, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics&) final + void readBuffers(std::istream& is, Index64 /*size*/, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics&) final { GridT& grid = static_cast(*data.grid); diff --git a/openvdb/openvdb/codecs/impl/ScalarLeafCodec.h b/openvdb/openvdb/codecs/impl/ScalarLeafCodec.h index ad9e088604..2288f8a5a2 100644 --- a/openvdb/openvdb/codecs/impl/ScalarLeafCodec.h +++ b/openvdb/openvdb/codecs/impl/ScalarLeafCodec.h @@ -13,7 +13,8 @@ namespace codecs { namespace internal { template -void writeScalarLeafBuffers(const LeafT& leaf, std::ostream& os, bool saveFloatAsHalf) +void writeScalarLeafBuffers(const LeafT& leaf, std::ostream& os, bool saveFloatAsHalf, + const typename LeafT::ValueType* background = nullptr) { using NodeMaskT = typename LeafT::NodeMaskType; @@ -23,14 +24,15 @@ void writeScalarLeafBuffers(const LeafT& leaf, std::ostream& os, bool saveFloatA leaf.buffer().data(); // load values io::writeCompressedValues(os, leaf.buffer().data(), LeafT::SIZE, - leaf.getValueMask(), /*childMask=*/NodeMaskT(), saveFloatAsHalf); + leaf.getValueMask(), /*childMask=*/NodeMaskT(), saveFloatAsHalf, background); } template void readScalarLeafBuffers(LeafT& leaf, std::istream& is, bool saveFloatAsHalf, const typename LeafT::ValueType& background, bool skip = false, - const math::CoordBBox* clipBBox = nullptr) + const math::CoordBBox* clipBBox = nullptr, + const typename StorageLeafT::ValueType* storageBackground = nullptr) { using ValueT = typename LeafT::ValueType; using NodeMaskT = typename LeafT::NodeMaskType; @@ -65,10 +67,10 @@ void readScalarLeafBuffers(LeafT& leaf, std::istream& is, bool saveFloatAsHalf, if (skip) { if (seekable) { - io::readCompressedValues(is, nullptr, SIZE, valueMask, saveFloatAsHalf); + io::readCompressedValues(is, nullptr, SIZE, valueMask, saveFloatAsHalf, storageBackground); } else { StorageBufferT storageTemp; - io::readCompressedValues(is, storageTemp.data(), SIZE, valueMask, saveFloatAsHalf); + io::readCompressedValues(is, storageTemp.data(), SIZE, valueMask, saveFloatAsHalf, storageBackground); } // Clear the value mask so that the skipped leaf has no active // voxels. Without this, the leaf retains its on-disk active @@ -81,18 +83,18 @@ void readScalarLeafBuffers(LeafT& leaf, std::istream& is, bool saveFloatAsHalf, // ValueMask leaf: value == active state, already captured in the value mask above. // Seek/consume past the storage buffer without populating any separate leaf buffer. if (seekable) { - io::readCompressedValues(is, nullptr, SIZE, valueMask, saveFloatAsHalf); + io::readCompressedValues(is, nullptr, SIZE, valueMask, saveFloatAsHalf, storageBackground); } else { StorageBufferT storageTemp; - io::readCompressedValues(is, storageTemp.data(), SIZE, valueMask, saveFloatAsHalf); + io::readCompressedValues(is, storageTemp.data(), SIZE, valueMask, saveFloatAsHalf, storageBackground); } } else if constexpr (std::is_same_v) { // Bool leaf: must read storage values regardless of seekability, then convert. if constexpr (std::is_same_v) { - io::readCompressedValues(is, leaf.buffer().data(), SIZE, valueMask, saveFloatAsHalf); + io::readCompressedValues(is, leaf.buffer().data(), SIZE, valueMask, saveFloatAsHalf, storageBackground); } else { StorageBufferT storageTemp; - io::readCompressedValues(is, storageTemp.data(), SIZE, valueMask, saveFloatAsHalf); + io::readCompressedValues(is, storageTemp.data(), SIZE, valueMask, saveFloatAsHalf, storageBackground); for (Index i = 0; i < SIZE; ++i) { leaf.buffer().setValue(i, static_cast(storageTemp.getValue(i))); } @@ -100,10 +102,10 @@ void readScalarLeafBuffers(LeafT& leaf, std::istream& is, bool saveFloatAsHalf, } else { leaf.buffer().allocate(); if constexpr (std::is_same_v) { - io::readCompressedValues(is, leaf.buffer().data(), SIZE, valueMask, saveFloatAsHalf); + io::readCompressedValues(is, leaf.buffer().data(), SIZE, valueMask, saveFloatAsHalf, storageBackground); } else { StorageBufferT storageTemp; - io::readCompressedValues(is, storageTemp.data(), SIZE, valueMask, saveFloatAsHalf); + io::readCompressedValues(is, storageTemp.data(), SIZE, valueMask, saveFloatAsHalf, storageBackground); for (Index i = 0; i < SIZE; ++i) { leaf.buffer().setValue(i, static_cast(storageTemp.getValue(i))); } diff --git a/openvdb/openvdb/io/Archive.cc b/openvdb/openvdb/io/Archive.cc index c2cf6869a8..d1d2451943 100644 --- a/openvdb/openvdb/io/Archive.cc +++ b/openvdb/openvdb/io/Archive.cc @@ -182,6 +182,7 @@ struct StreamMetadata::Impl bool mDelayedLoadMeta = false; uint64_t mLeaf = 0; uint32_t mTest = 0; // for testing only + bool mAllocateLeafBuffers = false; }; // struct StreamMetadata @@ -243,6 +244,7 @@ bool StreamMetadata::writeGridStats() const { return mImpl->mWriteGr bool StreamMetadata::seekable() const { return mImpl->mSeekable; } bool StreamMetadata::delayedLoadMeta() const { return mImpl->mDelayedLoadMeta; } bool StreamMetadata::countingPasses() const { return mImpl->mCountingPasses; } +bool StreamMetadata::allocateLeafBuffers() const { return mImpl->mAllocateLeafBuffers; } uint32_t StreamMetadata::pass() const { return mImpl->mPass; } MetaMap& StreamMetadata::gridMetadata() { return mImpl->mGridMetadata; } const MetaMap& StreamMetadata::gridMetadata() const { return mImpl->mGridMetadata; } @@ -260,6 +262,7 @@ void StreamMetadata::setHalfFloat(bool b) { mImpl->mHalfFloat = b; void StreamMetadata::setWriteGridStats(bool b) { mImpl->mWriteGridStats = b; } void StreamMetadata::setSeekable(bool b) { mImpl->mSeekable = b; } void StreamMetadata::setCountingPasses(bool b) { mImpl->mCountingPasses = b; } +void StreamMetadata::setAllocateLeafBuffers(bool b) { mImpl->mAllocateLeafBuffers = b; } void StreamMetadata::setPass(uint32_t i) { mImpl->mPass = i; } void StreamMetadata::__setTest(uint32_t t) { mImpl->mTest = t; } @@ -900,14 +903,15 @@ Archive::findCodec(const std::string& gridType, const io::ReadOptions& options) // if readMode is Half, then search for a codec that converts // from the storage grid type to the grid type if (options.readMode == ReadMode::Half) { - return io::CodecRegistry::get(gridType + "_to_half"); + if (auto* codec = io::CodecRegistry::get(gridType + "_to_half")) return codec; } else if (options.readMode == ReadMode::Bool) { - return io::CodecRegistry::get(gridType + "_to_bool"); + if (auto* codec = io::CodecRegistry::get(gridType + "_to_bool")) return codec; } else if (options.readMode == ReadMode::Mask) { - return io::CodecRegistry::get(gridType + "_to_mask"); + if (auto* codec = io::CodecRegistry::get(gridType + "_to_mask")) return codec; } - // Determine the I/O codec to use to read this grid + // Determine the I/O codec to use to read this grid (also the fallback when + // no conversion codec is registered for a Half/Bool/Mask readMode). return io::CodecRegistry::get(gridType); } @@ -1010,16 +1014,40 @@ Archive::readGrid(const GridDescriptor& gd, std::istream& is, const io::ReadOpti io::setGridClass(is, gridClass); grid->readTransform(is); - if (readOptions.readMode != io::ReadMode::TopologyOnly && !gd.isInstance()) { + const bool readTopology = readOptions.readMode != io::ReadMode::MetadataOnly && !gd.isInstance(); + const bool readBuffers = readTopology && readOptions.readMode != io::ReadMode::TopologyOnly; + if (readTopology) { // read topology if (codec) { codec->readTopology(is, *codecData, readOptions, diagnostics); } else { - grid->readTopology(is); + io::StreamMetadata::Ptr allocateLeafBuffersMeta; + if (readOptions.readMode == io::ReadMode::TopologyOnly) { + // Signal Grid::readTopology to allocate leaf buffers and + // fill them with the background value. + allocateLeafBuffersMeta = io::getStreamMetadataPtr(is); + if (allocateLeafBuffersMeta) { + allocateLeafBuffersMeta->setAllocateLeafBuffers(true); + } + } + try { + grid->readTopology(is); + } catch (...) { + // Grid::readTopology() clears the flag on success, but if + // it throws the flag must not leak into subsequent grid reads. + if (allocateLeafBuffersMeta) { + allocateLeafBuffersMeta->setAllocateLeafBuffers(false); + } + throw; + } } + } + if (readBuffers) { // read buffers if (codec) { - codec->readBuffers(is, *codecData, readOptions, diagnostics); + OPENVDB_ASSERT(gd.getEndPos() >= gd.getGridPos()); + const Index64 size = static_cast(gd.getEndPos() - gd.getGridPos()); + codec->readBuffers(is, size, *codecData, readOptions, diagnostics); } else { const auto& worldBBox = readOptions.clipBBox; const bool clip = worldBBox.isSorted(); diff --git a/openvdb/openvdb/io/Archive.h b/openvdb/openvdb/io/Archive.h index e0414f166a..5d15b9cdc2 100644 --- a/openvdb/openvdb/io/Archive.h +++ b/openvdb/openvdb/io/Archive.h @@ -162,12 +162,14 @@ class OPENVDB_API Archive /// Write the given grid descriptor and grid to an output stream /// and update the GridDescriptor offsets. /// @param seekable if true, the output stream supports seek operations + /// @param writeOptions options controlling how grid data is written void writeGrid(GridDescriptor&, GridBase::ConstPtr, std::ostream&, bool seekable, const io::WriteOptions& writeOptions = io::WriteOptions{}) const; /// Write the given grid descriptor and grid metadata to an output stream /// and update the GridDescriptor offsets, but don't write the grid's tree, /// since it is shared with another grid. /// @param seekable if true, the output stream supports seek operations + /// @param writeOptions options controlling how grid data is written void writeGridInstance(GridDescriptor&, GridBase::ConstPtr, std::ostream&, bool seekable, const io::WriteOptions& writeOptions = io::WriteOptions{}) const; diff --git a/openvdb/openvdb/io/Codec.cc b/openvdb/openvdb/io/Codec.cc index 4873f779aa..e2a3213814 100644 --- a/openvdb/openvdb/io/Codec.cc +++ b/openvdb/openvdb/io/Codec.cc @@ -55,6 +55,7 @@ struct RegisterConvertCodec { void initialize() { + CodecRegistry::clear(); NumericGridTypes::foreach(); Vec3GridTypes::foreach(); diff --git a/openvdb/openvdb/io/Codec.h b/openvdb/openvdb/io/Codec.h index 4fbf3412d1..70eea1c468 100644 --- a/openvdb/openvdb/io/Codec.h +++ b/openvdb/openvdb/io/Codec.h @@ -79,10 +79,15 @@ enum class ReadMode { Mask, /// Deserialize topology only; value buffers are skipped. The resulting /// grid has a valid tree structure (active/inactive state, node - /// hierarchy) but leaf buffer data is left at its default (background) - /// value. Useful when only the active-voxel mask is needed and - /// avoiding the cost of reading large value buffers is desirable. - TopologyOnly + /// hierarchy) and all leaf buffers are allocated and filled with the + /// grid's background value. Useful when only the active-voxel mask is + /// needed and avoiding the cost of reading large value buffers is + /// desirable. + TopologyOnly, + /// Deserialize grid metadata and transform only; no topology, no value + /// buffers. The codec is still used to construct the correct grid type, + /// but its @c readTopology()/@c readBuffers() are not called. + MetadataOnly }; /// @brief Base class for per-grid-type, codec-specific read options. @@ -144,6 +149,8 @@ struct OPENVDB_API ReadTypedOptions /// in-place type conversion as data is read. /// @c ReadMode::TopologyOnly skips value buffers entirely, which can be /// significantly faster when only the active-voxel mask is needed. +/// @c ReadMode::MetadataOnly skips both topology and value buffers, +/// reading only grid metadata and transform. /// /// @par Per-type options /// @c typeData allows callers to attach codec-specific configuration for @@ -402,8 +409,9 @@ struct OPENVDB_API Codec /// that mode internally for safety. If @c options.clipBBox is non-empty, /// restrict the loaded data to the region that intersects it; if the codec /// cannot honour clipping natively, fall back to a post-process and record - /// a warning via @a diagnostics. - virtual void readBuffers(std::istream& /*is*/, CodecData& /*data*/, + /// a warning via @a diagnostics. The @c size argument is the number of bytes + /// occupied by the entire readBuffers data section. + virtual void readBuffers(std::istream& /*is*/, Index64 /*size*/, CodecData& /*data*/, const ReadOptions& /*options*/, ReadDiagnostics& /*diagnostics*/) { } /// @brief Serialize the grid topology (tree structure and active-voxel diff --git a/openvdb/openvdb/io/Compression.h b/openvdb/openvdb/io/Compression.h index ae572c4105..a81b1ef1e5 100644 --- a/openvdb/openvdb/io/Compression.h +++ b/openvdb/openvdb/io/Compression.h @@ -448,10 +448,11 @@ struct HalfWriter { /// which positions in the buffer correspond to active values /// @param fromHalf if true, read 16-bit half floats from the input stream /// and convert them to full floats +/// @param background optional background value used when mask compressed template inline void readCompressedValues(std::istream& is, ValueT* destBuf, Index destCount, - const MaskT& valueMask, bool fromHalf) + const MaskT& valueMask, bool fromHalf, const ValueT* background = nullptr) { checkFormatVersion(is); @@ -475,13 +476,15 @@ readCompressedValues(std::istream& is, ValueT* destBuf, Index destCount, } } - ValueT background = zeroVal(); - if (const void* bgPtr = getGridBackgroundValuePtr(is)) { - background = *static_cast(bgPtr); + ValueT bgValue = zeroVal(); + if (background) { + bgValue = *background; + } else if (const void* bgPtr = getGridBackgroundValuePtr(is)) { + bgValue = *static_cast(bgPtr); } - ValueT inactiveVal1 = background; + ValueT inactiveVal1 = bgValue; ValueT inactiveVal0 = - ((metadata == NO_MASK_OR_INACTIVE_VALS) ? background : math::negative(background)); + ((metadata == NO_MASK_OR_INACTIVE_VALS) ? bgValue : math::negative(bgValue)); if (metadata == NO_MASK_AND_ONE_INACTIVE_VAL || metadata == MASK_AND_ONE_INACTIVE_VAL || @@ -617,10 +620,12 @@ writeCompressedValuesSize(ValueT* srcBuf, Index srcCount, /// @param childMask a bitmask (typically, a node's child mask) indicating /// which positions in the buffer correspond to child node pointers /// @param toHalf if true, convert floating-point values to 16-bit half floats +/// @param background optional background value used when mask compressed template inline void writeCompressedValues(std::ostream& os, const ValueT* srcBuf, Index srcCount, - const MaskT& valueMask, const MaskT& childMask, bool toHalf) + const MaskT& valueMask, const MaskT& childMask, bool toHalf, + const ValueT* background = nullptr) { // Get the stream's compression settings. const uint32_t compress = getDataCompression(os); @@ -642,12 +647,14 @@ writeCompressedValues(std::ostream& os, const ValueT* srcBuf, Index srcCount, // an inside/outside bitmask. const ValueT zero = zeroVal(); - ValueT background = zero; - if (const void* bgPtr = getGridBackgroundValuePtr(os)) { - background = *static_cast(bgPtr); + ValueT bgValue = zero; + if (background) { + bgValue = *background; + } else if (const void* bgPtr = getGridBackgroundValuePtr(os)) { + bgValue = *static_cast(bgPtr); } - MaskCompress maskCompressData(valueMask, childMask, srcBuf, background); + MaskCompress maskCompressData(valueMask, childMask, srcBuf, bgValue); metadata = maskCompressData.metadata; os.write(reinterpret_cast(&metadata), /*bytes=*/1); diff --git a/openvdb/openvdb/io/File.cc b/openvdb/openvdb/io/File.cc index 3aaa31c5b1..b783008986 100644 --- a/openvdb/openvdb/io/File.cc +++ b/openvdb/openvdb/io/File.cc @@ -386,7 +386,7 @@ File::readAllGridMetadata() // Seek to the grid in the file. gd.seekToGrid(inputStream()); io::ReadOptions readOptions; - readOptions.readMode = io::ReadMode::TopologyOnly; + readOptions.readMode = io::ReadMode::MetadataOnly; GridBase::ConstPtr grid = Archive::readGrid(gd, inputStream(), readOptions); // Return copies of the grids, but with empty trees. // (As of 0.98.0, at least, it would suffice to just const cast @@ -426,7 +426,7 @@ File::readGridMetadata(const Name& name) const GridDescriptor& gd = it->second; gd.seekToGrid(inputStream()); io::ReadOptions readOptions; - readOptions.readMode = io::ReadMode::TopologyOnly; + readOptions.readMode = io::ReadMode::MetadataOnly; ret = Archive::readGrid(gd, inputStream(), readOptions); } return ret->copyGridWithNewTree(); @@ -490,11 +490,26 @@ File::readGrid(const Name& name, const io::ReadOptions& readOptions) << " in file " << mFilename); } + // Read the parent without clipping. Archive::readGrid() converts the + // world-space clip region into index space using the grid's own + // transform, but an instance has its own transform that may differ + // from the parent's. Instead, read the full parent tree and clip the + // assembled instance below using the instance's transform, so that the + // retained region matches the requested world-space bbox. + io::ReadOptions parentOptions = readOptions; + parentOptions.clipBBox = BBoxd(); + GridBase::Ptr parent; OPENVDB_ASSERT(inputHasGridOffsets()); parentIt->second.seekToGrid(inputStream()); - parent = Archive::readGrid(parentIt->second, inputStream(), readOptions, mReadDiagnostics); - if (parent) grid->setTree(parent->baseTreePtr()); + parent = Archive::readGrid(parentIt->second, inputStream(), parentOptions, mReadDiagnostics); + if (parent) { + grid->setTree(parent->baseTreePtr()); + const auto& clipBBox = readOptions.clipBBox; + if (clipBBox.isSorted()) { + grid->clipGrid(clipBBox); + } + } } return grid; } diff --git a/openvdb/openvdb/io/Stream.cc b/openvdb/openvdb/io/Stream.cc index 938cc321b0..87f9f99058 100644 --- a/openvdb/openvdb/io/Stream.cc +++ b/openvdb/openvdb/io/Stream.cc @@ -21,7 +21,28 @@ namespace io { Stream::Stream(std::istream& is) + : Stream(is, io::ReadOptions{}) { +} + + +Stream::Stream(std::istream& is, const io::ReadOptions& readOptions) +{ + // Read modes that stop before consuming all of a grid's data are not + // supported, because a stream is read sequentially - the position after + // a partial read is still inside the previous grid's data, so the next + // grid header would be read from the wrong offset. + // TODO: Skip over the unread bytes of each grid instead of disallowing + // these read modes. This is best implemented alongside the extension that + // adds support for byte skipping in non-seekable streams. + if (readOptions.readMode == io::ReadMode::MetadataOnly || + readOptions.readMode == io::ReadMode::TopologyOnly) { + OPENVDB_THROW(ValueError, "io::ReadMode::" + << (readOptions.readMode == io::ReadMode::MetadataOnly + ? "MetadataOnly" : "TopologyOnly") + << " is not supported when reading from a stream"); + } + if (!is) return; // Delayed loading has been removed - always read directly from the stream @@ -51,7 +72,7 @@ Stream::Stream(std::istream& is) gd.readHeader(is); gd.readStreamPos(is); descriptors.push_back(gd); - GridBase::Ptr grid = Archive::readGrid(gd, is, io::ReadOptions{}); + GridBase::Ptr grid = Archive::readGrid(gd, is, readOptions); mGrids->push_back(grid); namedGrids[gd.uniqueName()] = grid; } @@ -83,6 +104,7 @@ Stream& Stream::operator=(const Stream& other) { if (&other != this) { + Archive::operator=(other); mMeta = other.mMeta; mGrids = other.mGrids; mOutputStream = other.mOutputStream; diff --git a/openvdb/openvdb/io/Stream.h b/openvdb/openvdb/io/Stream.h index 418a06b142..432e5eb9a9 100644 --- a/openvdb/openvdb/io/Stream.h +++ b/openvdb/openvdb/io/Stream.h @@ -23,7 +23,16 @@ class OPENVDB_API Stream: public Archive public: /// @brief Read grids from an input stream. /// @param is The input stream to read from - explicit Stream(std::istream&); + explicit Stream(std::istream& is); + + /// @brief Read grids from an input stream using the given read options. + /// @param is The input stream to read from + /// @param readOptions Options controlling how grids are read (e.g. attribute + /// skipping for point data grids) + /// @throw ValueError if @a readOptions requests @c io::ReadMode::MetadataOnly + /// or @c io::ReadMode::TopologyOnly. These modes leave a grid's data + /// partially unread, which a sequentially read stream cannot skip over. + Stream(std::istream& is, const io::ReadOptions& readOptions); OPENVDB_DEPRECATED_MESSAGE("Use Stream(std::istream&) instead. This method is deprecated and will be removed. Delayed loading is no longer supported.") Stream(std::istream& is, bool /*delayLoad*/) : Stream(is) { } diff --git a/openvdb/openvdb/io/io.h b/openvdb/openvdb/io/io.h index 13a9134012..26e513b7f6 100644 --- a/openvdb/openvdb/io/io.h +++ b/openvdb/openvdb/io/io.h @@ -73,6 +73,11 @@ class OPENVDB_API StreamMetadata bool countingPasses() const; void setCountingPasses(bool); + /// @brief Return @c true if readTopology() should allocate and zero-fill + /// leaf buffers after loading the tree structure (topology-only read mode). + bool allocateLeafBuffers() const; + void setAllocateLeafBuffers(bool); + uint32_t pass() const; void setPass(uint32_t); diff --git a/openvdb/openvdb/points/AttributeArray.h b/openvdb/openvdb/points/AttributeArray.h index 23ff8f8149..26f03b71cc 100644 --- a/openvdb/openvdb/points/AttributeArray.h +++ b/openvdb/openvdb/points/AttributeArray.h @@ -1611,7 +1611,13 @@ AttributeArray::skipPagedBuffers(compression::PagedInputStream& is) std::istream& inputStream = is.getInputStream(); uint8_t bloscCompressed(0); if (!mIsUniform) inputStream.read(reinterpret_cast(&bloscCompressed), sizeof(uint8_t)); - inputStream.seekg(mCompressedBytes, std::ios_base::cur); + auto meta = io::getStreamMetadataPtr(inputStream); + if (meta && meta->seekable()) { + inputStream.seekg(mCompressedBytes, std::ios_base::cur); + } else { + std::vector tempData(mCompressedBytes); + inputStream.read(tempData.data(), mCompressedBytes); + } mCompressedBytes = 0; mFlags = static_cast(mFlags & ~PARTIALREAD); } diff --git a/openvdb/openvdb/points/PointDataGrid.h b/openvdb/openvdb/points/PointDataGrid.h index f186717799..35988239af 100644 --- a/openvdb/openvdb/points/PointDataGrid.h +++ b/openvdb/openvdb/points/PointDataGrid.h @@ -94,7 +94,7 @@ makeDescriptorUnique(PointDataTreeT& tree); template OPENVDB_DEPRECATED_MESSAGE("This method is deprecated and will be removed. Delayed loading is no longer supported.") inline void -setStreamingMode(PointDataTreeT&, bool /*on*/ = true) { } +setStreamingMode(PointDataTreeT& tree, bool on = true) { (void)tree; (void)on; } template diff --git a/openvdb/openvdb/points/PointDataIO.h b/openvdb/openvdb/points/PointDataIO.h index fddbaef6c4..110b93972b 100644 --- a/openvdb/openvdb/points/PointDataIO.h +++ b/openvdb/openvdb/points/PointDataIO.h @@ -21,7 +21,8 @@ namespace io template<> inline void readCompressedValues( std::istream& is, PointDataIndex32* destBuf, Index destCount, - const util::NodeMask<3>& /*valueMask*/, bool /*fromHalf*/) + const util::NodeMask<3>& /*valueMask*/, bool /*fromHalf*/, + const PointDataIndex32* /*background*/) { using compression::bloscDecompress; @@ -82,7 +83,8 @@ template<> inline void writeCompressedValues( std::ostream& os, const PointDataIndex32* srcBuf, Index srcCount, const util::NodeMask<3>& /*valueMask*/, - const util::NodeMask<3>& /*childMask*/, bool /*toHalf*/) + const util::NodeMask<3>& /*childMask*/, bool /*toHalf*/, + const PointDataIndex32* /*background*/) { using compression::bloscCompress; diff --git a/openvdb/openvdb/points/StreamCompression.cc b/openvdb/openvdb/points/StreamCompression.cc index f18d016463..fb99554b70 100644 --- a/openvdb/openvdb/points/StreamCompression.cc +++ b/openvdb/openvdb/points/StreamCompression.cc @@ -7,6 +7,7 @@ #include #include #include +#include #ifdef OPENVDB_USE_BLOSC #include #endif @@ -347,7 +348,13 @@ Page::skipBuffers(std::istream& is) std::streamsize bytes = isCompressed ? mInfo->compressedBytes : -mInfo->compressedBytes; - is.seekg(bytes, std::ios_base::cur); + auto meta = io::getStreamMetadataPtr(is); + if (meta && meta->seekable()) { + is.seekg(bytes, std::ios_base::cur); + } else { + std::vector tempData(bytes); + is.read(tempData.data(), bytes); + } mInfo.reset(); } diff --git a/openvdb/openvdb/tree/InternalNode.h b/openvdb/openvdb/tree/InternalNode.h index 913521b570..c61deedbfe 100644 --- a/openvdb/openvdb/tree/InternalNode.h +++ b/openvdb/openvdb/tree/InternalNode.h @@ -82,6 +82,10 @@ class InternalNode /// @param active State assigned to all the tiles InternalNode(const Coord& origin, const ValueType& fillValue, bool active = false); + /// @brief Construct a node without allocating child memory. Children are + /// left unallocated and must be populated single-threaded, or with external + /// synchronization. The valid advanced pattern is: create all nodes + /// single-threaded, then allocate leaf buffers in parallel across distinct leaves. InternalNode(PartialCreate, const Coord&, const ValueType& fillValue, bool active = false); /// @brief Deep copy constructor diff --git a/openvdb/openvdb/tree/LeafBuffer.h b/openvdb/openvdb/tree/LeafBuffer.h index bb6222cf84..7d1a965144 100644 --- a/openvdb/openvdb/tree/LeafBuffer.h +++ b/openvdb/openvdb/tree/LeafBuffer.h @@ -44,7 +44,12 @@ class LeafBuffer explicit inline LeafBuffer(const ValueType&); /// Copy constructor inline LeafBuffer(const LeafBuffer&); - /// Construct a buffer but don't allocate memory for the full array of values. + /// @brief Construct a buffer without allocating the value array. + /// The buffer is left unallocated; call @c allocate() before use. + /// Populating the buffer (via @c allocate() or @c data()) must be done + /// single-threaded per leaf, or externally synchronized. The valid + /// advanced pattern is: create all nodes single-threaded, then call + /// @c allocate() in parallel across distinct leaves. LeafBuffer(PartialCreate, const ValueType&): mData(nullptr) {} /// Destructor inline ~LeafBuffer(); @@ -87,11 +92,15 @@ class LeafBuffer static Index size() { return SIZE; } /// @brief Return a const pointer to the array of voxel values. - /// @details This method guarantees that the buffer is allocated and loaded. + /// @warning The buffer must already be allocated (call @c allocate() first). + /// First-touch allocation via @c data() is not thread-safe; concurrent access + /// to an unallocated buffer is a programming error flagged in debug builds. /// @warning This method should only be used by experts seeking low-level optimizations. const ValueType* data() const; /// @brief Return a pointer to the array of voxel values. - /// @details This method guarantees that the buffer is allocated and loaded. + /// @warning The buffer must already be allocated (call @c allocate() first). + /// First-touch allocation via @c data() is not thread-safe; concurrent access + /// to an unallocated buffer is a programming error flagged in debug builds. /// @warning This method should only be used by experts seeking low-level optimizations. ValueType* data(); @@ -229,9 +238,10 @@ template inline const typename LeafBuffer::ValueType* LeafBuffer::data() const { + OPENVDB_ASSERT(mData != nullptr); if (mData == nullptr) { LeafBuffer* self = const_cast(this); - if (mData == nullptr) self->mData = new ValueType[SIZE]; + self->mData = new ValueType[SIZE]; } return mData; } @@ -240,9 +250,8 @@ template inline typename LeafBuffer::ValueType* LeafBuffer::data() { - if (mData == nullptr) { - if (mData == nullptr) mData = new ValueType[SIZE]; - } + OPENVDB_ASSERT(mData != nullptr); + if (mData == nullptr) mData = new ValueType[SIZE]; return mData; } diff --git a/openvdb/openvdb/tree/LeafNode.h b/openvdb/openvdb/tree/LeafNode.h index a29167e6d5..21cae3d7a2 100644 --- a/openvdb/openvdb/tree/LeafNode.h +++ b/openvdb/openvdb/tree/LeafNode.h @@ -83,6 +83,9 @@ class LeafNode /// @param value a value with which to fill the buffer /// @param active the active state to which to initialize all voxels /// @details This constructor does not allocate memory for voxel values. + /// Call @c buffer().allocate() before accessing voxel data. The valid + /// advanced pattern is: create all leaves single-threaded, then call + /// @c allocate() in parallel across distinct leaves. LeafNode(PartialCreate, const Coord& coords, const ValueType& value = zeroVal(), diff --git a/openvdb/openvdb/unittest/TestCodec.cc b/openvdb/openvdb/unittest/TestCodec.cc index d8981ca3f7..cfa14de91e 100644 --- a/openvdb/openvdb/unittest/TestCodec.cc +++ b/openvdb/openvdb/unittest/TestCodec.cc @@ -78,6 +78,24 @@ TEST_F(TestCodec, testCodecRegistry) } +TEST_F(TestCodec, testInitializeIdempotent) +{ + using namespace openvdb::io; + + // Calling initialize() twice without uninitialize() in between must not throw. + // Previously registerCodecByName() threw KeyError on the duplicate registration. + CodecRegistry::clear(); + EXPECT_NO_THROW(internal::initialize()); + EXPECT_NO_THROW(internal::initialize()); + + // Codecs must still be registered after the second call. + EXPECT_TRUE(CodecRegistry::isRegistered(openvdb::BoolGrid::gridType())); + EXPECT_TRUE(CodecRegistry::isRegistered(openvdb::FloatGrid::gridType())); + + internal::uninitialize(); +} + + TEST_F(TestCodec, testReadDiagnostics) { using namespace openvdb; @@ -219,8 +237,17 @@ void testIOImpl( f.close(); } ASSERT_TRUE(readTopo); - EXPECT_EQ(readTopo->activeVoxelCount(), Index64(0)); - EXPECT_TRUE(readTopo->tree().leafCount() == 0); + // TopologyOnly: full tree structure is read (topology + active-voxel masks), + // leaf buffers are allocated and zero-filled, values are not read. + EXPECT_EQ(readTopo->tree().leafCount(), srcGrid->tree().leafCount()); + EXPECT_TRUE(readTopo->tree().leafCount() > 0); + EXPECT_EQ(readTopo->activeVoxelCount(), srcGrid->activeVoxelCount()); + // verify leaf buffers are allocated (bool/mask buffers are always present, skip empty() check) + if constexpr (!std::is_same_v) { + for (auto leafIter = readTopo->tree().cbeginLeaf(); leafIter; ++leafIter) { + EXPECT_FALSE(leafIter->buffer().empty()); + } + } EXPECT_EQ(readTopo->getName(), gridName); // Cleanup @@ -403,3 +430,85 @@ TEST_F(TestCodec, testNumericToMaskCodecConversion) testConvertCodecImpl(); testConvertCodecImpl(); } + +// Regression test for the dangling storageBackground pointer bug. +// +// ReadTopologyOp stores storageBackground on its stack frame and registers +// &storageBackground with the stream. Before the fix, topologyCodecReadTopology +// returned and destroyed ReadTopologyOp before readBuffers() ran; readCompressedValues +// then dereferenced the dead pointer to reconstruct inactive voxels under +// COMPRESS_ACTIVE_MASK, producing garbage inactive values. +// +// The test is deliberately structured to maximize the chance that the freed +// stack frame has been overwritten: a non-zero background (3.0f / 5) forces the +// reconstructed inactive value to be wrong if the pointer is stale, and +// COMPRESS_ACTIVE_MASK (flag 0x2, always on by default) is the code path that +// uses the background pointer. +TEST_F(TestCodec, testInactiveValuesAfterReadBuffers) +{ + using namespace openvdb; + using namespace openvdb::io; + + openvdb::io::CodecRegistry::clear(); + openvdb::io::internal::initialize(); + + // Float: non-zero background, active region surrounded by inactive background voxels. + { + const float bg = 3.0f; + FloatGrid::Ptr src = FloatGrid::create(bg); + src->setName("float_bg"); + src->fill(CoordBBox(Coord(0), Coord(15)), 1.0f, /*active=*/true); + src->fill(CoordBBox(Coord(4), Coord(11)), bg, /*active=*/false); + + const std::string path = "testInactiveVals_float.vdb"; + { + io::File f(path); + f.setCompression(COMPRESS_ACTIVE_MASK); + f.write(GridPtrVec{src}); + } + FloatGrid::Ptr result; + { + io::File f(path); + f.open(); + result = gridPtrCast(f.readGrid("float_bg")); + f.close(); + } + ASSERT_TRUE(result); + EXPECT_EQ(result->background(), bg); + FloatGrid::ConstAccessor refAcc = src->getConstAccessor(); + for (FloatGrid::ValueAllCIter it = result->cbeginValueAll(); it; ++it) { + EXPECT_EQ(*it, refAcc.getValue(it.getCoord())); + } + std::remove(path.c_str()); + } + + // Int32: non-zero background (5), verify inactive values round-trip. + { + const int bg = 5; + Int32Grid::Ptr src = Int32Grid::create(bg); + src->setName("int_bg"); + src->fill(CoordBBox(Coord(0), Coord(15)), 99, /*active=*/true); + src->fill(CoordBBox(Coord(4), Coord(11)), bg, /*active=*/false); + + const std::string path = "testInactiveVals_int.vdb"; + { + io::File f(path); + f.setCompression(COMPRESS_ACTIVE_MASK); + f.write(GridPtrVec{src}); + } + Int32Grid::Ptr result; + { + io::File f(path); + f.open(); + result = gridPtrCast(f.readGrid("int_bg")); + f.close(); + } + ASSERT_TRUE(result); + EXPECT_EQ(result->background(), bg); + Int32Grid::ConstAccessor refAcc = src->getConstAccessor(); + for (Int32Grid::ValueAllCIter it = result->cbeginValueAll(); it; ++it) { + EXPECT_EQ(*it, refAcc.getValue(it.getCoord())); + } + std::remove(path.c_str()); + } +} diff --git a/openvdb/openvdb/unittest/TestFile.cc b/openvdb/openvdb/unittest/TestFile.cc index 130947fdf1..9aa7693d6a 100644 --- a/openvdb/openvdb/unittest/TestFile.cc +++ b/openvdb/openvdb/unittest/TestFile.cc @@ -557,6 +557,77 @@ TEST_F(TestFile, testWriteInstancedGrids) } +// Verify that clipping an instanced grid uses the instance's own transform, +// not the parent's. The bug was that Archive::readGrid() converted the +// world-space bbox to index space with the parent's transform before the +// instance's transform was applied, so the clipped region was wrong when the +// two transforms differed. +TEST_F(TestFile, testReadClippedInstancedGrid) +{ + using namespace openvdb; + + const char* filename = "testReadClippedInstancedGrid.vdb"; + SharedPtr scopedFile(filename, ::remove); + + // Parent grid: voxel size 1.0. Fill index [-5, 5] with value 1. + FloatTree::Ptr tree(new FloatTree(0.0f)); + tree->fill(CoordBBox(Coord(-5), Coord(5)), 1.0f, /*active=*/true); + + GridBase::Ptr parent = FloatGrid::create(tree); + parent->setName("parent"); + parent->setTransform(math::Transform::createLinearTransform(1.0)); + + // Instance grid: same tree, but voxel size 2.0. + // Index coord n → world coord 2n (double the parent's world-space extent). + GridBase::Ptr instance = FloatGrid::create(tree); + instance->setName("instance"); + instance->setTransform(math::Transform::createLinearTransform(2.0)); + + GridPtrVec grids; + grids.push_back(parent); + grids.push_back(instance); + + { + io::File vdbfile(filename); + vdbfile.write(grids); + } + + // World-space clip: [0, 6]. + // Via parent transform (voxel 1.0): index [0, 6] → voxels 0..5 survive. + // Via instance transform (voxel 2.0): index [0, 3] → voxels 0..3 survive. + // The correct answer uses the instance's transform. + const BBoxd clipBox(Vec3d(0.0), Vec3d(6.0)); + + io::File vdbfile(filename); + vdbfile.open(); + + GridBase::Ptr readGrid = vdbfile.readGrid("instance", clipBox); + EXPECT_TRUE(readGrid.get() != nullptr); + FloatGrid::Ptr clipped = gridPtrCast(readGrid); + EXPECT_TRUE(clipped.get() != nullptr); + + const CoordBBox bbox = clipped->evalActiveVoxelBoundingBox(); + // The instance's transform maps world [0,6] to index [0,3]. + EXPECT_EQ(Coord(0, 0, 0), bbox.min()); + EXPECT_EQ(Coord(3, 3, 3), bbox.max()); + + // No active voxels should survive outside [0,3] in any axis. + FloatGrid::ConstAccessor acc = clipped->getConstAccessor(); + for (int i = -5; i <= 5; ++i) { + for (int j = -5; j <= 5; ++j) { + for (int k = -5; k <= 5; ++k) { + const Coord xyz(i, j, k); + if (i >= 0 && j >= 0 && k >= 0 && i <= 3 && j <= 3 && k <= 3) { + EXPECT_EQ(1.0f, acc.getValue(xyz)); + } else { + EXPECT_EQ(0.0f, acc.getValue(xyz)); + } + } + } + } +} + + void TestFile::testReadGridDescriptors() { diff --git a/openvdb/openvdb/unittest/TestPointCodec.cc b/openvdb/openvdb/unittest/TestPointCodec.cc index 81f8ff2358..0729f6cc98 100644 --- a/openvdb/openvdb/unittest/TestPointCodec.cc +++ b/openvdb/openvdb/unittest/TestPointCodec.cc @@ -1,11 +1,16 @@ // Copyright Contributors to the OpenVDB Project // SPDX-License-Identifier: Apache-2.0 +#include #include #include +#include +#include +#include #include #include #include +#include #include #include "util.h" // for unittest_util::genPoints @@ -138,8 +143,15 @@ TEST_F(TestPointCodec, testPointIndexCodecIO) f.close(); } ASSERT_TRUE(codecTopo); - EXPECT_EQ(codecTopo->activeVoxelCount(), Index64(0)); - EXPECT_TRUE(codecTopo->tree().leafCount() == 0); + // TopologyOnly: topology and active state are preserved; voxel values are zero-filled + EXPECT_EQ(codecTopo->tree().leafCount(), srcGrid->tree().leafCount()); + EXPECT_EQ(codecTopo->activeVoxelCount(), srcGrid->activeVoxelCount()); + for (auto leafIt = codecTopo->tree().cbeginLeaf(); leafIt; ++leafIt) { + EXPECT_FALSE(leafIt->buffer().empty()); + for (auto voxIt = leafIt->cbeginValueOn(); voxIt; ++voxIt) { + EXPECT_EQ(*voxIt, PointIndexGrid::ValueType(0)); + } + } EXPECT_EQ(codecTopo->getName(), std::string("point_index_grid")); // Cleanup @@ -265,8 +277,14 @@ TEST_F(TestPointCodec, testPointDataCodecIO) f.close(); } ASSERT_TRUE(codecTopo); - EXPECT_EQ(codecTopo->activeVoxelCount(), Index64(0)); - EXPECT_TRUE(codecTopo->tree().leafCount() == 0); + // TopologyOnly: topology and active state are preserved; voxel values are zero-filled + EXPECT_EQ(codecTopo->tree().leafCount(), srcGrid->tree().leafCount()); + EXPECT_EQ(codecTopo->activeVoxelCount(), srcGrid->activeVoxelCount()); + for (auto leafIt = codecTopo->tree().cbeginLeaf(); leafIt; ++leafIt) { + for (auto voxIt = leafIt->cbeginValueOn(); voxIt; ++voxIt) { + EXPECT_EQ(*voxIt, PointDataGrid::ValueType(0)); + } + } std::remove(codecPath.c_str()); } @@ -545,3 +563,147 @@ TEST_F(TestPointCodec, testPointDataCodecIO) std::remove(diffPath.c_str()); } } + +// A leafless PointDataGrid stores numPasses == 0. The attribute count is +// derived as (numPasses - 4) / 2; without an underflow guard this wraps to a +// huge unsigned value and the attribute loops spin ~4.3e9 times, hanging +// both write and read. This test exercises an empty grid round-trip. +TEST_F(TestPointCodec, testPointDataCodecEmptyGrid) +{ + using namespace openvdb; + using namespace openvdb::io; + using namespace openvdb::points; + + openvdb::initialize(); + CodecRegistry::clear(); + io::internal::initialize(); + ASSERT_TRUE(CodecRegistry::isRegistered(PointDataGrid::gridType())); + + PointDataGrid::Ptr srcGrid = PointDataGrid::create(); + srcGrid->setName("pdg_empty"); + EXPECT_EQ(srcGrid->tree().leafCount(), Index32(0)); + + const std::string codecPath = "testPDG_empty_codec.vdb"; + + { + io::File f(codecPath); + EXPECT_NO_THROW(f.write(GridPtrVec{srcGrid})); + } + + PointDataGrid::Ptr codecGrid; + { + io::File f(codecPath); + f.open(); + GridBase::Ptr base; + EXPECT_NO_THROW(base = f.readGrid("pdg_empty")); + codecGrid = gridPtrCast(base); + f.close(); + } + ASSERT_TRUE(codecGrid); + EXPECT_EQ(codecGrid->tree().leafCount(), Index32(0)); + EXPECT_EQ(codecGrid->activeVoxelCount(), Index64(0)); + + std::remove(codecPath.c_str()); +} + +// Regression test for the fix in AttributeArray::skipPagedBuffers and +// Page::skipBuffers: when the stream is not seekable (written via io::Stream), +// skip must read-and-discard rather than seekg. Without the fix, both code +// paths called seekg unconditionally, corrupting the stream position on +// non-seekable streams. +TEST_F(TestPointCodec, testPointDataCodecSkipNonSeekable) +{ + using namespace openvdb; + using namespace openvdb::io; + using namespace openvdb::points; + + openvdb::initialize(); + CodecRegistry::clear(); + io::internal::initialize(); + ASSERT_TRUE(CodecRegistry::isRegistered(PointDataGrid::gridType())); + + const std::vector positions = { + Vec3f(0.0f, 1.0f, 0.0f), + Vec3f(1.5f, 3.5f, 1.0f), + Vec3f(-1.0f, 6.0f, -2.0f), + Vec3f(1.1f, 1.25f, 0.06f) + }; + const std::vector velocities = { + Vec3f(1.0f, 0.0f, 0.0f), + Vec3f(0.0f, 1.0f, 0.0f), + Vec3f(0.0f, 0.0f, 1.0f), + Vec3f(1.0f, 1.0f, 0.5f) + }; + const std::vector ids = {0, 1, 2, 3}; + + const float voxelSize = 0.5f; + math::Transform::Ptr transform = math::Transform::createLinearTransform(voxelSize); + + PointAttributeVector posWrapper(positions); + tools::PointIndexGrid::Ptr pointIndexGrid = + tools::createPointIndexGrid(posWrapper, *transform); + + PointDataGrid::Ptr srcGrid = + createPointDataGrid( + *pointIndexGrid, posWrapper, *transform); + srcGrid->setName("pdg_skip"); + + PointDataTree& tree = srcGrid->tree(); + tools::PointIndexTree& indexTree = pointIndexGrid->tree(); + + appendAttribute(tree, "velocity"); + populateAttribute>( + tree, indexTree, "velocity", + PointAttributeVector(velocities)); + + appendAttribute(tree, "id"); + populateAttribute>( + tree, indexTree, "id", + PointAttributeVector(ids)); + + // Write via io::Stream (seekable=false by construction) + std::ostringstream ostr(std::ios_base::binary); + io::Stream(ostr).write(GridPtrVec{srcGrid}); + + // Build ReadOptions requesting only "P" — "velocity" and "id" will be skipped + io::ReadOptions readOptions; + auto typeData = std::make_shared(); + typeData->pointAttributeNames = {"P"}; + readOptions.typeData[PointDataGrid::gridType()] = typeData; + + // Read via io::Stream (non-seekable by construction) with our ReadOptions, so the + // skip path is exercised with seekable == false. + std::istringstream is(ostr.str(), std::ios_base::binary); + io::Stream strm(is, readOptions); + GridPtrVecPtr grids = strm.getGrids(); + ASSERT_TRUE(grids); + ASSERT_EQ(grids->size(), size_t(1)); + + PointDataGrid::Ptr resultGrid = gridPtrCast((*grids)[0]); + ASSERT_TRUE(resultGrid); + + // Only "P" should be present; "velocity" and "id" were skipped + { + auto leafIt = resultGrid->tree().cbeginLeaf(); + ASSERT_TRUE(leafIt); + EXPECT_EQ(leafIt->attributeSet().size(), size_t(1)); + EXPECT_NE(leafIt->attributeSet().find("P"), AttributeSet::INVALID_POS); + EXPECT_EQ(leafIt->attributeSet().find("velocity"), AttributeSet::INVALID_POS); + EXPECT_EQ(leafIt->attributeSet().find("id"), AttributeSet::INVALID_POS); + } + + // All 4 points should be present and readable + EXPECT_EQ(pointCount(resultGrid->tree()), Index64(4)); + { + std::vector readPositions; + for (auto it = resultGrid->tree().cbeginLeaf(); it; ++it) { + AttributeHandle posHandle(it->constAttributeArray("P")); + for (Index i = 0; i < it->pointCount(); ++i) { + readPositions.push_back(posHandle.get(i)); + } + } + EXPECT_EQ(readPositions.size(), size_t(4)); + } +} diff --git a/openvdb/openvdb/unittest/TestStream.cc b/openvdb/openvdb/unittest/TestStream.cc index 35850fab3d..6146b8f96e 100644 --- a/openvdb/openvdb/unittest/TestStream.cc +++ b/openvdb/openvdb/unittest/TestStream.cc @@ -216,3 +216,65 @@ TestStream::testFileReadFromStream() verifyTestGrids(grids, meta); } TEST_F(TestStream, testFileReadFromStream) { testFileReadFromStream(); } + + +TEST_F(TestStream, testUnsupportedReadModes) +{ + using namespace openvdb; + + Int32Grid::Ptr grid1 = Int32Grid::create(0); + grid1->setName("first"); + grid1->tree().setValue(Coord(0, 0, 0), 1); + + FloatGrid::Ptr grid2 = FloatGrid::create(0.0f); + grid2->setName("second"); + grid2->tree().setValue(Coord(1, 2, 3), 2.0f); + + std::ostringstream ostr(std::ios_base::binary); + io::Stream(ostr).write(GridPtrVec{grid1, grid2}); + + // A stream is read sequentially, so read modes that leave part of a grid + // unread cannot be supported - the next grid header would be read from + // the wrong offset. + for (auto readMode: {io::ReadMode::MetadataOnly, io::ReadMode::TopologyOnly}) { + io::ReadOptions readOptions; + readOptions.readMode = readMode; + + std::istringstream is(ostr.str(), std::ios_base::binary); + EXPECT_THROW({ io::Stream strm(is, readOptions); }, ValueError); + } + + // Modes that read every byte of each grid are supported. + for (auto readMode: {io::ReadMode::Original, io::ReadMode::Half, + io::ReadMode::Bool, io::ReadMode::Mask}) + { + io::ReadOptions readOptions; + readOptions.readMode = readMode; + + std::istringstream is(ostr.str(), std::ios_base::binary); + io::Stream strm(is, readOptions); + + GridPtrVecPtr grids = strm.getGrids(); + ASSERT_TRUE(grids); + ASSERT_EQ(grids->size(), size_t(2)); + EXPECT_EQ((*grids)[0]->getName(), std::string("first")); + EXPECT_EQ((*grids)[1]->getName(), std::string("second")); + } +} + + +TEST_F(TestStream, testAssignmentPreservesArchiveFlags) +{ + using namespace openvdb; + + std::ostringstream os1(std::ios_base::binary), os2(std::ios_base::binary); + io::Stream src(os1); + src.setCompression(io::COMPRESS_ZIP); + src.setInstancingEnabled(false); + + io::Stream dst(os2); + dst = src; + + EXPECT_EQ(src.compression(), dst.compression()); + EXPECT_EQ(src.isInstancingEnabled(), dst.isInstancingEnabled()); +} diff --git a/openvdb_cmd/vdb_tool/include/Tool.h b/openvdb_cmd/vdb_tool/include/Tool.h index 2833bf1505..bc9824c4e6 100644 --- a/openvdb_cmd/vdb_tool/include/Tool.h +++ b/openvdb_cmd/vdb_tool/include/Tool.h @@ -622,8 +622,7 @@ void Tool::init() mParser.addAction( {"read", "import", "load", "i"}, "Read one or more geometry or VDB files from disk or STDIN.", {{"files", "", "{file|stdin}.{obj|ply|abc|stl|off|pts|xyz|e57|vdb|nvdb|gltf|glb|geo|usd|usda|usdc|usdz}", "list of files or the input stream, e.g. file.vdb,stdin.vdb. Note that \"files=\" is optional since any argument without \"=\" is intrepreted as a file and appended to \"files\""}, - {"grids", "*", "*|grid_name,...", "list of VDB grids name to be imported (defaults to \"*\", i.e. import all available grids)"}, - {"delayed", "true", "1|0|true|false", "toggle delayed loading of VDB grids (enabled by default). This option is ignored by other file types"}}, + {"grids", "*", "*|grid_name,...", "list of VDB grids name to be imported (defaults to \"*\", i.e. import all available grids)"}}, [](){}, [&](){this->read();}, 0);// anonymous options are treated as to the first option,i.e. "files" mParser.addAction( @@ -1466,7 +1465,7 @@ void Tool::readVDB(const std::string &fileName) } else { if (mParser.verbose) mTimer.start("Reading VDB grid(s) from file named \""+fileName+"\""); io::File file(fileName); - file.open(mParser.get("delayed")); + file.open(); grids = file.getGrids(); } const size_t count = mGrid.size();