Fix I/O bugs - #2230
Conversation
Signed-off-by: Dan Bailey <danbailey@ilm.com>
…he value mask was used instead of the child mask Signed-off-by: Dan Bailey <danbailey@ilm.com>
…ect transform Signed-off-by: Dan Bailey <danbailey@ilm.com>
…chive assignment operator Signed-off-by: Dan Bailey <danbailey@ilm.com>
Signed-off-by: Dan Bailey <danbailey@ilm.com>
Signed-off-by: Dan Bailey <danbailey@ilm.com>
… stream is not seekable Signed-off-by: Dan Bailey <danbailey@ilm.com>
…nd align with all the other initialize methods Signed-off-by: Dan Bailey <danbailey@ilm.com>
Signed-off-by: Dan Bailey <danbailey@ilm.com>
Signed-off-by: Dan Bailey <danbailey@ilm.com>
…nstead of StorageValueT causing inactive values to be read incorrectly Signed-off-by: Dan Bailey <danbailey@ilm.com>
415fabf to
2a116b4
Compare
Signed-off-by: Dan Bailey <danbailey@ilm.com>
…mance Signed-off-by: Dan Bailey <danbailey@ilm.com>
There was a problem hiding this comment.
Pull request overview
This PR improves OpenVDB I/O robustness and correctness in the feature/io work by addressing several read/write edge cases (points grids underflow/hang, inactive background reconstruction, old-file compatibility, and instanced-grid clipping), and by adding regression tests to prevent recurrence.
Changes:
- Fix multiple codec/streaming issues: point-data pass-count underflow, non-seekable stream skipping behavior, and inactive-value reconstruction by removing reliance on a stream background pointer.
- Correct instanced-grid clipping by deferring world-space clip application until after the instance transform is applied.
- Add/expand regression tests and clarify/adjust topology-only behavior (including new
MetadataOnlyread mode and leaf-buffer allocation behavior).
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| openvdb/openvdb/unittest/TestStream.cc | Adds regression coverage ensuring io::Stream assignment preserves archive flags. |
| openvdb/openvdb/unittest/TestPointCodec.cc | Expands topology-only expectations; adds tests for empty point grids and non-seekable attribute skipping. |
| openvdb/openvdb/unittest/TestFile.cc | Adds regression test for correct clipping behavior on instanced grids with differing transforms. |
| openvdb/openvdb/unittest/TestCodec.cc | Adds initialization idempotency test and regression test for inactive-value reconstruction. |
| openvdb/openvdb/tree/LeafNode.h | Documents PartialCreate/leaf allocation expectations and threading considerations. |
| openvdb/openvdb/tree/LeafBuffer.h | Tightens data() semantics (debug assertion) and improves PartialCreate/alloc documentation. |
| openvdb/openvdb/tree/InternalNode.h | Documents PartialCreate usage and threading expectations. |
| openvdb/openvdb/points/StreamCompression.cc | Updates page skipping to handle non-seekable streams (but currently uses an allocating discard buffer). |
| openvdb/openvdb/points/PointDataIO.h | Updates point-data compression helpers to accept an explicit background pointer. |
| openvdb/openvdb/points/AttributeArray.h | Updates paged-buffer skipping to handle non-seekable streams (but currently allocates and uses std::vector without including it). |
| openvdb/openvdb/io/Stream.h | Adds io::Stream ctor overload that accepts ReadOptions. |
| openvdb/openvdb/io/Stream.cc | Wires ReadOptions through stream reading and ensures assignment copies Archive state. |
| openvdb/openvdb/io/io.h | Extends StreamMetadata with an allocateLeafBuffers() flag used during topology-only reads. |
| openvdb/openvdb/io/File.cc | Uses MetadataOnly for metadata reads; fixes instanced-grid clipping by clipping after instance assembly. |
| openvdb/openvdb/io/Compression.h | Passes explicit background to (de)compression to avoid stale background pointer usage. |
| openvdb/openvdb/io/Codec.h | Introduces MetadataOnly and adjusts TopologyOnly; changes codec readBuffers signature to include a size. |
| openvdb/openvdb/io/Codec.cc | Makes io::internal::initialize() clear the codec registry to be idempotent. |
| openvdb/openvdb/io/Archive.cc | Updates codec selection fallback and read-mode handling; adds leaf-buffer allocation flagging for topology-only legacy path. |
| openvdb/openvdb/Grid.h | Allocates/fills leaf buffers for topology-only legacy reads when requested via stream metadata. |
| openvdb/openvdb/codecs/ValueMaskCodec.h | Updates readBuffers override signature. |
| openvdb/openvdb/codecs/TopologyCodec.h | Fixes background handling and old-version ordering; stores storage-typed background in codec data and allocates leaf buffers for topology-only. |
| openvdb/openvdb/codecs/ScalarCodec.h | Removes reliance on stream background pointer by threading background explicitly. |
| openvdb/openvdb/codecs/PointIndexCodec.h | Threads storage background into scalar-leaf reads for point index grids. |
| openvdb/openvdb/codecs/PointDataCodec.h | Guards against pass-count underflow; adds attribute skipping and descriptor-homogeneity check; threads storage background. |
| openvdb/openvdb/codecs/impl/ScalarLeafCodec.h | Threads storage background into readCompressedValues calls; updates write path to pass explicit background. |
| openvdb/openvdb/codecs/BoolCodec.h | Updates readBuffers override signature. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| auto meta = io::getStreamMetadataPtr(is); | ||
| if (meta && meta->seekable()) { | ||
| is.seekg(bytes, std::ios_base::cur); | ||
| } else { | ||
| std::vector<char> tempData(bytes); | ||
| is.read(tempData.data(), bytes); | ||
| } |
There was a problem hiding this comment.
This is valid feedback, but this was an intentional choice. We don't use istream::ignore() anywhere in the codebase at present and use this pattern of reading to a temporary buffer.
I would like to convert all of these over so that we can use non-seekable streams. However for consistency, I'd prefer this fits in with the rest of the codebase for now and then do a bulk upgrade and add non-seekable stream unit tests in a separate pass.
| auto meta = io::getStreamMetadataPtr(inputStream); | ||
| if (meta && meta->seekable()) { | ||
| inputStream.seekg(mCompressedBytes, std::ios_base::cur); | ||
| } else { | ||
| std::vector<char> tempData(mCompressedBytes); | ||
| inputStream.read(tempData.data(), mCompressedBytes); | ||
| } |
| // 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(); |
There was a problem hiding this comment.
Actually, the stricter test is what we are after here. The goal is to only filter when using shared descriptors.
| /// 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 zero-filled. | ||
| /// Useful when only the active-voxel mask is needed and avoiding the | ||
| /// cost of reading large value buffers is desirable. | ||
| TopologyOnly, |
There was a problem hiding this comment.
That's a fair comment.
| // When called from the legacy (non-codec) TopologyOnly path, the stream | ||
| // metadata carries a flag requesting allocation+zero-fill of leaf buffers | ||
| // (PartialCreate leaves them unallocated after readTopology). |
There was a problem hiding this comment.
Yup, will change this, this implementation changed and the comment was not updated.
| if (readOptions.readMode == io::ReadMode::TopologyOnly) { | ||
| // signal Grid<TreeT>::readTopology to allocate+zero-fill leaf buffers | ||
| if (auto meta = io::getStreamMetadataPtr(is)) { | ||
| meta->setAllocateLeafBuffers(true); | ||
| } | ||
| } | ||
| grid->readTopology(is); | ||
| } |
There was a problem hiding this comment.
Probably wouldn't hurt to change this, though I'm not sure we'd ever end up in this state where it would be valid to continue.
| gd.readStreamPos(is); | ||
| descriptors.push_back(gd); | ||
| GridBase::Ptr grid = Archive::readGrid(gd, is, io::ReadOptions{}); | ||
| GridBase::Ptr grid = Archive::readGrid(gd, is, readOptions); |
There was a problem hiding this comment.
This can break when readOptions asks for MetadataOnly or TopologyOnly. Archive::readGrid() stops before reading all bytes for the grid, but Stream then tries to read the next grid from the current position. That position is still inside the previous grid’s data, so the next grid header is read from the wrong place. Can we either disallow these read modes for Stream, or skip the unread bytes before reading the next grid?
There was a problem hiding this comment.
This is the failure case:
TEST_F(TestStream, testMetadataOnlyReadMultipleGrids)
{
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});
io::ReadOptions readOptions;
readOptions.readMode = io::ReadMode::MetadataOnly;
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"));
}
with an error of
[ RUN ] TestStream.testMetadataOnlyReadMultipleGrids
unknown file: Failure
C++ exception with description "KeyError: Cannot read grid " thrown in the test body.
[ FAILED ] TestStream.testMetadataOnlyReadMultipleGrids
There was a problem hiding this comment.
Thanks, I'll disable these modes for Stream as you suggest for now. Ultimately, I'd like to skip the bytes but perhaps that could be implemented together with the extension to make this work for non-seekable streams.
There was a problem hiding this comment.
Thanks, Dan. A suggestion: maybe you can add a //TODO for byte skipping as part of non-seekable stream support.
This addresses some issues in the new
feature/iobranch, one of which was discovered in production, a couple more through AI code review, one that I believe pre-dated the I/O changes.