diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml
index ccc0911b45..7b805ba35b 100644
--- a/.github/workflows/weekly.yml
+++ b/.github/workflows/weekly.yml
@@ -146,13 +146,14 @@ jobs:
matrix:
config:
- { name: 'all', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=ON -DUSE_ZLIB=ON -DUSE_EXR=ON -DUSE_PNG=ON' }
- - { name: 'lite', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=OFF -DUSE_ZLIB=OFF -DUSE_EXR=OFF -DUSE_PNG=OFF -DOPENVDB_USE_DELAYED_LOADING=OFF' }
+ - { name: 'lite', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=OFF -DUSE_ZLIB=OFF -DUSE_EXR=OFF -DUSE_PNG=OFF' }
- { name: 'half', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=OFF -DUSE_IMATH_HALF=ON' }
- { name: 'simd', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DOPENVDB_X86_INSTRSET=7 -DUSE_VCL=ON' } # 7=AVX
- { name: 'pygrid', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DOPENVDB_PYTHON_WRAP_ALL_GRID_TYPES=ON' }
- { name: 'asan', image: '2026', build: 'asan', components: 'core,test', cmake: '-DDISABLE_DEPENDENCY_VERSION_CHECKS=ON -DNANOVDB_USE_OPENVDB=ON -DOPENVDB_AX_STATIC=OFF -DOPENVDB_CORE_STATIC=OFF -DUSE_BLOSC=OFF' } # We never called blosc_destroy(), so disable blosc to silence these errors
- { name: 'ubsan', image: '2026', build: 'ubsan', components: 'core,test', cmake: '-DDISABLE_DEPENDENCY_VERSION_CHECKS=ON -DCMAKE_CXX_FLAGS="-Wno-deprecated-declarations" ' }
- { name: 'c++20', image: '2026', build: 'Release', components: 'core,test', cmake: '-DCMAKE_CXX_STANDARD=20' }
+ - { name: 'abi14', image: '2026', build: 'Release', components: 'core,test', cmake: '-DOPENVDB_USE_FUTURE_ABI_14=ON -DOPENVDB_ENABLE_ASSERTS=ON' }
- { name: 'conf', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DCMAKE_FIND_PACKAGE_PREFER_CONFIG=ON' }
fail-fast: false
steps:
@@ -215,7 +216,6 @@ jobs:
--components=\"core,axcore,python,bin,render,test,axbin\"
--cargs=\'
-DCMAKE_CXX_STANDARD=20
- -DOPENVDB_USE_DELAYED_LOADING=OFF
-DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/install
${{ matrix.config.cmake }}
\'
@@ -420,7 +420,6 @@ jobs:
--components="${{ matrix.config.components }}"
--cargs=\'
-A x64 -G \"Visual Studio 17 2022\"
- -DOPENVDB_USE_DELAYED_LOADING=OFF
-DUSE_BLOSC=OFF \
-DUSE_ZLIB=OFF \
-DVCPKG_TARGET_TRIPLET=${VCPKG_DEFAULT_TRIPLET}
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b9315ffe82..d42ece28d0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -84,7 +84,6 @@ option(OPENVDB_ENABLE_VEC_RELATIONAL_OPERATIONS [=[
Enabled deprecated relational < and > operators on Vector types. This option
will be switched to OFF and the feature will be subsequently removed in future
releases.]=] ON)
-option(OPENVDB_USE_DELAYED_LOADING "Build the core OpenVDB library with delayed-loading." ON)
option(OPENVDB_CXX_STRICT "Enable or disable pre-defined compiler warnings" OFF)
cmake_dependent_option(OPENVDB_INSTALL_CMAKE_MODULES
"Install the provided OpenVDB CMake modules when building the core library"
@@ -264,7 +263,6 @@ mark_as_advanced(
OPENVDB_BUILD_HOUDINI_ABITESTS
OPENVDB_CXX_STRICT
OPENVDB_ENABLE_RPATH
- OPENVDB_USE_DELAYED_LOADING
OPENVDB_FUTURE_DEPRECATION
OPENVDB_SIMD
SYSTEM_LIBRARY_PATHS
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/cmake/FindOpenVDB.cmake b/cmake/FindOpenVDB.cmake
index 400afd2544..17a4c2da53 100644
--- a/cmake/FindOpenVDB.cmake
+++ b/cmake/FindOpenVDB.cmake
@@ -64,8 +64,6 @@ This will define the following variables:
True if the OpenVDB Library has been built with log4cplus support
``OpenVDB_USES_IMATH_HALF``
True if the OpenVDB Library has been built with Imath half support
-``OpenVDB_USES_DELAYED_LOADING``
- True if the OpenVDB Library has been built with delayed-loading
``OpenVDB_ABI``
Set if this module was able to determine the ABI number the located
OpenVDB Library was built against. Unset otherwise.
@@ -522,7 +520,6 @@ set(OpenVDB_USES_BLOSC ${USE_BLOSC})
set(OpenVDB_USES_ZLIB ${USE_ZLIB})
set(OpenVDB_USES_LOG4CPLUS ${USE_LOG4CPLUS})
set(OpenVDB_USES_IMATH_HALF ${USE_IMATH_HALF})
-set(OpenVDB_USES_DELAYED_LOADING ${OPENVDB_USE_DELAYED_LOADING})
set(OpenVDB_DEFINITIONS)
if(WIN32)
@@ -555,7 +552,6 @@ if(_OPENVDB_HAS_NEW_VERSION_HEADER)
OPENVDB_GET_VERSION_DEFINE(${_OPENVDB_VERSION_HEADER} "OPENVDB_USE_IMATH_HALF" OpenVDB_USES_IMATH_HALF)
OPENVDB_GET_VERSION_DEFINE(${_OPENVDB_VERSION_HEADER} "OPENVDB_USE_BLOSC" OpenVDB_USES_BLOSC)
OPENVDB_GET_VERSION_DEFINE(${_OPENVDB_VERSION_HEADER} "OPENVDB_USE_ZLIB" OpenVDB_USES_ZLIB)
- OPENVDB_GET_VERSION_DEFINE(${_OPENVDB_VERSION_HEADER} "OPENVDB_USE_DELAYED_LOADING" OpenVDB_USES_DELAYED_LOADING)
elseif(NOT OPENVDB_USE_STATIC_LIBS)
# Use GetPrerequisites to see which libraries this OpenVDB lib has linked to
# which we can query for optional deps. This basically runs ldd/otoll/objdump
@@ -602,11 +598,6 @@ elseif(NOT OPENVDB_USE_STATIC_LIBS)
if(NOT ${_HAS_DEP} EQUAL -1)
set(OpenVDB_USES_IMATH_HALF ON)
endif()
-
- string(FIND ${PREREQUISITE} "boost_iostreams" _HAS_DEP)
- if(NOT ${_HAS_DEP} EQUAL -1)
- set(OpenVDB_USES_DELAYED_LOADING ON)
- endif()
endforeach()
unset(_OPENVDB_PREREQUISITE_LIST)
@@ -628,10 +619,6 @@ if(OpenVDB_USES_IMATH_HALF)
find_package(Imath REQUIRED CONFIG)
endif()
-if(OpenVDB_USES_DELAYED_LOADING)
- find_package(Boost REQUIRED COMPONENTS iostreams)
-endif()
-
if(UNIX)
find_package(Threads REQUIRED)
endif()
@@ -643,11 +630,6 @@ endif()
set(_OPENVDB_VISIBLE_DEPENDENCIES "")
-if(OpenVDB_USES_DELAYED_LOADING)
- list(APPEND _OPENVDB_VISIBLE_DEPENDENCIES Boost::iostreams)
- list(APPEND OpenVDB_DEFINITIONS OPENVDB_USE_DELAYED_LOADING)
-endif()
-
if(OpenVDB_USES_IMATH_HALF)
list(APPEND _OPENVDB_VISIBLE_DEPENDENCIES Imath::Imath)
endif()
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/nanovdb/nanovdb/cmd/convert/nanovdb_convert.cc b/nanovdb/nanovdb/cmd/convert/nanovdb_convert.cc
index 2298508a93..bab45dccd6 100644
--- a/nanovdb/nanovdb/cmd/convert/nanovdb_convert.cc
+++ b/nanovdb/nanovdb/cmd/convert/nanovdb_convert.cc
@@ -257,7 +257,7 @@ int main(int argc, char* argv[])
}
if (verbose) std::cout << "Opening OpenVDB file named \"" << inputFile << "\"" << std::endl;
openvdb::io::File file(inputFile);
- file.open(false); //disable delayed loading
+ file.open();
if (gridName.empty()) {// convert all grid in the file
auto grids = file.getGrids();
std::vector > handles;
diff --git a/nanovdb/nanovdb/examples/ex_coarsen_nanovdb_cuda/coarsen_nanovdb_cuda.cpp b/nanovdb/nanovdb/examples/ex_coarsen_nanovdb_cuda/coarsen_nanovdb_cuda.cpp
index 8826a3000c..7154e02740 100644
--- a/nanovdb/nanovdb/examples/ex_coarsen_nanovdb_cuda/coarsen_nanovdb_cuda.cpp
+++ b/nanovdb/nanovdb/examples/ex_coarsen_nanovdb_cuda/coarsen_nanovdb_cuda.cpp
@@ -50,7 +50,7 @@ int main(int argc, char *argv[])
cpuTimer.start("Read input VDB file");
openvdb::initialize();
openvdb::io::File inFile(argv[1]);
- inFile.open(false); // disable delayed loading
+ inFile.open();
auto baseGrids = inFile.getGrids();
inFile.close();
auto grid = openvdb::gridPtrCast(baseGrids->at(0));
diff --git a/nanovdb/nanovdb/examples/ex_dilate_nanovdb_cuda/dilate_nanovdb_cuda.cpp b/nanovdb/nanovdb/examples/ex_dilate_nanovdb_cuda/dilate_nanovdb_cuda.cpp
index 6f6000f03c..6e65006934 100644
--- a/nanovdb/nanovdb/examples/ex_dilate_nanovdb_cuda/dilate_nanovdb_cuda.cpp
+++ b/nanovdb/nanovdb/examples/ex_dilate_nanovdb_cuda/dilate_nanovdb_cuda.cpp
@@ -44,7 +44,7 @@ int main(int argc, char *argv[])
cpuTimer.start("Read input VDB file");
openvdb::initialize();
openvdb::io::File inFile(argv[1]);
- inFile.open(false); // disable delayed loading
+ inFile.open();
auto baseGrids = inFile.getGrids();
inFile.close();
auto grid = openvdb::gridPtrCast(baseGrids->at(0));
diff --git a/nanovdb/nanovdb/examples/ex_merge_nanovdb_cuda/merge_nanovdb_cuda.cpp b/nanovdb/nanovdb/examples/ex_merge_nanovdb_cuda/merge_nanovdb_cuda.cpp
index 8b6626558b..e8cba39e02 100644
--- a/nanovdb/nanovdb/examples/ex_merge_nanovdb_cuda/merge_nanovdb_cuda.cpp
+++ b/nanovdb/nanovdb/examples/ex_merge_nanovdb_cuda/merge_nanovdb_cuda.cpp
@@ -41,7 +41,7 @@ int main(int argc, char *argv[])
cpuTimer.start("Read first input VDB file");
openvdb::initialize();
openvdb::io::File inFile1(argv[1]);
- inFile1.open(false); // disable delayed loading
+ inFile1.open();
auto baseGrids1 = inFile1.getGrids();
inFile1.close();
auto grid1 = openvdb::gridPtrCast(baseGrids1->at(0));
@@ -51,7 +51,7 @@ int main(int argc, char *argv[])
cpuTimer.start("Read second input VDB file");
openvdb::initialize();
openvdb::io::File inFile2(argv[2]);
- inFile2.open(false); // disable delayed loading
+ inFile2.open();
auto baseGrids2 = inFile2.getGrids();
inFile2.close();
auto grid2 = openvdb::gridPtrCast(baseGrids2->at(0));
diff --git a/nanovdb/nanovdb/examples/ex_raytrace_level_set/openvdb.cc b/nanovdb/nanovdb/examples/ex_raytrace_level_set/openvdb.cc
index b8ca62c838..7b8f12256e 100644
--- a/nanovdb/nanovdb/examples/ex_raytrace_level_set/openvdb.cc
+++ b/nanovdb/nanovdb/examples/ex_raytrace_level_set/openvdb.cc
@@ -37,7 +37,7 @@ void runOpenVDB(nanovdb::GridHandle& handle, int nu
openvdb::initialize();
std::string filename = "C:/Users/william/Downloads/dragon.vdb";
openvdb::io::File file(filename);
- file.open(false); //disable delayed loading
+ file.open();
auto srcGrid = file.readGrid("ls_dragon");
std::cout << "Loading OpenVDB grid[" << srcGrid->getName() << "]...\n";
#endif
diff --git a/nanovdb/nanovdb/examples/ex_refine_nanovdb_cuda/refine_nanovdb_cuda.cpp b/nanovdb/nanovdb/examples/ex_refine_nanovdb_cuda/refine_nanovdb_cuda.cpp
index 19795c5dfd..f86980a72b 100644
--- a/nanovdb/nanovdb/examples/ex_refine_nanovdb_cuda/refine_nanovdb_cuda.cpp
+++ b/nanovdb/nanovdb/examples/ex_refine_nanovdb_cuda/refine_nanovdb_cuda.cpp
@@ -81,7 +81,7 @@ int main(int argc, char *argv[])
cpuTimer.start("Read input VDB file");
openvdb::initialize();
openvdb::io::File inFile(argv[1]);
- inFile.open(false); // disable delayed loading
+ inFile.open();
auto baseGrids = inFile.getGrids();
inFile.close();
auto grid = openvdb::gridPtrCast(baseGrids->at(0));
diff --git a/nanovdb/nanovdb/unittest/TestOpenVDB.cc b/nanovdb/nanovdb/unittest/TestOpenVDB.cc
index fd47a7e017..8ada169df9 100644
--- a/nanovdb/nanovdb/unittest/TestOpenVDB.cc
+++ b/nanovdb/nanovdb/unittest/TestOpenVDB.cc
@@ -140,7 +140,7 @@ class TestOpenVDB : public ::testing::Test
mTimer.start("Reading grid from the file \"" + fileName + "\"");
try {
openvdb::io::File file(fileName);
- file.open(false); //disable delayed loading
+ file.open();
grid = openvdb::gridPtrCast(file.readGrid(file.beginName().gridName()));
} catch(const std::exception& e) {
std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl;
@@ -2860,7 +2860,7 @@ TEST_F(TestOpenVDB, LevelSetFiles)
//mTimer.start("\nReading grid from the file \"" + fileName + "\"");
try {
openvdb::io::File file(fileName);
- file.open(false); //disable delayed loading
+ file.open();
auto srcGrid = openvdb::gridPtrCast(file.readGrid(file.beginName().gridName()));
const size_t pos = fileName.find_last_of("/\\") + 1;
@@ -2938,7 +2938,7 @@ TEST_F(TestOpenVDB, FogFiles)
//mTimer.start("Reading grid from the file \"" + fileName + "\"");
try {
openvdb::io::File file(fileName);
- file.open(false); //disable delayed loading
+ file.open();
auto srcGrid = openvdb::gridPtrCast(file.readGrid(file.beginName().gridName()));
const size_t pos = fileName.find_last_of("/\\") + 1;
@@ -3008,7 +3008,7 @@ TEST_F(TestOpenVDB, PointFiles)
//mTimer.start("Reading grid from the file \"" + fileName + "\"");
try {
openvdb::io::File file(fileName);
- file.open(false); //disable delayed loading
+ file.open();
auto srcGrid = openvdb::gridPtrCast(file.readGrid(file.beginName().gridName()));
//std::cerr << "Read PointDataGrid named \"" << srcGrid->getName() << "\"" << std::endl;
diff --git a/openvdb/openvdb/CMakeLists.txt b/openvdb/openvdb/CMakeLists.txt
index 302f890f28..b1aaf5ef3c 100644
--- a/openvdb/openvdb/CMakeLists.txt
+++ b/openvdb/openvdb/CMakeLists.txt
@@ -117,19 +117,6 @@ endif()
# Collect and configure lib dependencies
-if(OPENVDB_USE_DELAYED_LOADING)
- find_package(Boost ${MINIMUM_BOOST_VERSION} REQUIRED COMPONENTS iostreams)
-
- if(OPENVDB_FUTURE_DEPRECATION AND FUTURE_MINIMUM_BOOST_VERSION)
- # The X.Y.Z boost version value isn't available until CMake 3.14
- set(FULL_BOOST_VERSION "${Boost_MAJOR_VERSION}.${Boost_MINOR_VERSION}.${Boost_SUBMINOR_VERSION}")
- if(${FULL_BOOST_VERSION} VERSION_LESS FUTURE_MINIMUM_BOOST_VERSION)
- message(DEPRECATION "Support for Boost versions < ${FUTURE_MINIMUM_BOOST_VERSION} "
- "is deprecated and will be removed.")
- endif()
- endif()
-endif()
-
find_package(TBB ${MINIMUM_TBB_VERSION} REQUIRED COMPONENTS tbb)
if(OPENVDB_FUTURE_DEPRECATION AND FUTURE_MINIMUM_TBB_VERSION)
if(${TBB_VERSION} VERSION_LESS FUTURE_MINIMUM_TBB_VERSION)
@@ -252,27 +239,6 @@ if(UNIX)
list(APPEND OPENVDB_CORE_DEPENDENT_LIBS Threads::Threads)
endif()
-# Pull in Boost last as houdini's boost (hboost) is fully namespaced and libs
-# are renamed too. Boost can be pulled in at any time, but do it last so that,
-# if it's in a shared place (like /usr/local) it doesn't accidently pull in
-# other headers.
-
-if(OPENVDB_USE_DELAYED_LOADING)
- list(APPEND OPENVDB_CORE_DEPENDENT_LIBS Boost::iostreams)
- if(WIN32)
- # Boost headers contain #pragma commands on Windows which causes Boost
- # libraries to be linked in automatically. Custom boost installations
- # may find that these naming conventions don't always match and can
- # cause linker errors. This option disables this feature of Boost. Note
- # -DBOOST_ALL_NO_LIB can also be provided manually.
- if(OPENVDB_DISABLE_BOOST_IMPLICIT_LINKING)
- list(APPEND OPENVDB_CORE_DEPENDENT_LIBS
- Boost::disable_autolinking # add -DBOOST_ALL_NO_LIB
- )
- endif()
- endif()
-endif()
-
##########################################################################
##### Core library configuration
@@ -374,13 +340,12 @@ configure_file(version.h.in openvdb/version.h)
set(OPENVDB_LIBRARY_SOURCE_FILES
Grid.cc
io/Archive.cc
+ io/Codec.cc
io/Compression.cc
- io/DelayedLoadMetadata.cc
io/File.cc
io/GridDescriptor.cc
io/Queue.cc
io/Stream.cc
- io/TempFile.cc
math/Maps.cc
math/Proximity.cc
math/QuantizedUnitVec.cc
@@ -411,16 +376,28 @@ set(OPENVDB_LIBRARY_INCLUDE_FILES
TypeList.h
)
+set(OPENVDB_LIBRARY_CODECS_INCLUDE_FILES
+ codecs/BoolCodec.h
+ codecs/PointDataCodec.h
+ codecs/PointIndexCodec.h
+ codecs/ScalarCodec.h
+ codecs/TopologyCodec.h
+ codecs/ValueMaskCodec.h
+)
+
+set(OPENVDB_LIBRARY_CODECS_IMPL_INCLUDE_FILES
+ codecs/impl/ScalarLeafCodec.h
+)
+
set(OPENVDB_LIBRARY_IO_INCLUDE_FILES
io/Archive.h
+ io/Codec.h
io/Compression.h
- io/DelayedLoadMetadata.h
io/File.h
io/GridDescriptor.h
io/io.h
io/Queue.h
io/Stream.h
- io/TempFile.h
)
set(OPENVDB_LIBRARY_MATH_INCLUDE_FILES
@@ -461,6 +438,7 @@ set(OPENVDB_LIBRARY_POINTS_INCLUDE_FILES
points/PointConversion.h
points/PointCount.h
points/PointDataGrid.h
+ points/PointDataIO.h
points/PointDelete.h
points/PointGroup.h
points/PointMask.h
@@ -612,6 +590,8 @@ if(USE_EXPLICIT_INSTANTIATION)
# inexpensive to generate in this case.
set(OPENVDB_LIBRARY_INSTANTIATION_HEADERS
${OPENVDB_LIBRARY_TOOLS_INCLUDE_FILES}
+ ${OPENVDB_LIBRARY_CODECS_INCLUDE_FILES}
+ ${OPENVDB_LIBRARY_CODECS_IMPL_INCLUDE_FILES}
)
# A new source file is generated by CMake for every header
@@ -686,9 +666,6 @@ endif()
if(USE_LOG4CPLUS)
list(APPEND OPENVDB_CORE_PUBLIC_DEFINES -DOPENVDB_USE_LOG4CPLUS)
endif()
-if(OPENVDB_USE_DELAYED_LOADING)
- list(APPEND OPENVDB_CORE_PUBLIC_DEFINES -DOPENVDB_USE_DELAYED_LOADING)
-endif()
##########################################################################
@@ -799,6 +776,8 @@ endif()
install(FILES ${OPENVDB_LIBRARY_INCLUDE_FILES} DESTINATION ${OPENVDB_INSTALL_INCLUDEDIR}/openvdb)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/openvdb/version.h DESTINATION ${OPENVDB_INSTALL_INCLUDEDIR}/openvdb)
install(FILES ${OPENVDB_LIBRARY_IO_INCLUDE_FILES} DESTINATION ${OPENVDB_INSTALL_INCLUDEDIR}/openvdb/io)
+install(FILES ${OPENVDB_LIBRARY_CODECS_INCLUDE_FILES} DESTINATION ${OPENVDB_INSTALL_INCLUDEDIR}/openvdb/codecs)
+install(FILES ${OPENVDB_LIBRARY_CODECS_IMPL_INCLUDE_FILES} DESTINATION ${OPENVDB_INSTALL_INCLUDEDIR}/openvdb/codecs/impl)
install(FILES ${OPENVDB_LIBRARY_MATH_INCLUDE_FILES} DESTINATION ${OPENVDB_INSTALL_INCLUDEDIR}/openvdb/math)
install(FILES ${OPENVDB_LIBRARY_POINTS_INCLUDE_FILES} DESTINATION ${OPENVDB_INSTALL_INCLUDEDIR}/openvdb/points)
install(FILES ${OPENVDB_LIBRARY_POINTS_IMPL_INCLUDE_FILES} DESTINATION ${OPENVDB_INSTALL_INCLUDEDIR}/openvdb/points/impl)
diff --git a/openvdb/openvdb/Grid.h b/openvdb/openvdb/Grid.h
index fc566b8387..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"
@@ -457,12 +458,12 @@ class OPENVDB_API GridBase: public MetaMap
virtual void readBuffers(std::istream&) = 0;
/// Read all of this grid's data buffers that intersect the given index-space bounding box.
virtual void readBuffers(std::istream&, const CoordBBox&) = 0;
- /// @brief Read all of this grid's data buffers that are not yet resident in memory
- /// (because delayed loading is in effect).
- /// @details If this grid was read from a memory-mapped file, this operation
- /// disconnects the grid from the file.
- /// @sa io::File::open, io::MappedFile
+
+#if OPENVDB_ABI_VERSION_NUMBER < 14
+ OPENVDB_DEPRECATED_MESSAGE("This method is deprecated and will be removed. Delayed loading is no longer supported.")
virtual void readNonresidentBuffers() const = 0;
+#endif
+
/// Write out all data buffers for this grid.
virtual void writeBuffers(std::ostream&) const = 0;
@@ -941,12 +942,12 @@ class Grid: public GridBase
void readBuffers(std::istream&) override;
/// Read all of this grid's data buffers that intersect the given index-space bounding box.
void readBuffers(std::istream&, const CoordBBox&) override;
- /// @brief Read all of this grid's data buffers that are not yet resident in memory
- /// (because delayed loading is in effect).
- /// @details If this grid was read from a memory-mapped file, this operation
- /// disconnects the grid from the file.
- /// @sa io::File::open, io::MappedFile
- void readNonresidentBuffers() const override;
+
+#if OPENVDB_ABI_VERSION_NUMBER < 14
+ OPENVDB_DEPRECATED_MESSAGE("This method is deprecated and will be removed. Delayed loading is no longer supported.")
+ void readNonresidentBuffers() const override { }
+#endif
+
/// Write out all data buffers for this grid.
void writeBuffers(std::ostream&) const override;
@@ -1183,14 +1184,34 @@ struct TreeAdapter >
////////////////////////////////////////
+namespace points {
+
+template class PointDataLeafNode;
+
+/// @brief Type trait that evaluates to true only for @c PointDataLeafNode instantiations.
+template
+struct IsPointDataLeafNode : std::false_type {};
+
+template
+struct IsPointDataLeafNode> : std::true_type {};
+
+} // namespace points
+
+
/// @brief Metafunction that specifies whether a given leaf node, tree, or grid type
-/// requires multiple passes to read and write voxel data
-/// @details Multi-pass I/O allows one to optimize the data layout of leaf nodes
-/// for certain access patterns during delayed loading.
-/// @sa io::MultiPass
+/// requires multiple passes to read and write voxel data.
+/// @details Multi-pass I/O allows leaf nodes to optimize their serialization layout
+/// for delayed-load access patterns. Only @c PointDataLeafNode supports multi-pass I/O.
+/// Defining a custom leaf node that inherits @c io::PointDataGridMultiPass is no longer permitted.
+/// @sa points::IsPointDataLeafNode
template
struct HasMultiPassIO {
- static const bool value = std::is_base_of::value;
+ static_assert(
+ !std::is_base_of::value
+ || points::IsPointDataLeafNode::value,
+ "Only PointDataLeafNode may inherit from io::PointDataGridMultiPass; "
+ "use points::IsPointDataLeafNode to test for multi-pass I/O support.");
+ static const bool value = points::IsPointDataLeafNode::value;
};
// Partial specialization for Tree types
@@ -1612,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);
+ }
+ }
+ });
+ }
+ }
+ }
}
@@ -1672,14 +1715,6 @@ Grid::readBuffers(std::istream& is, const CoordBBox& bbox)
}
-template
-inline void
-Grid::readNonresidentBuffers() const
-{
- tree().readNonresidentBuffers();
-}
-
-
template
inline void
Grid::writeBuffers(std::ostream& os) const
diff --git a/openvdb/openvdb/codecs/BoolCodec.h b/openvdb/openvdb/codecs/BoolCodec.h
new file mode 100644
index 0000000000..3da1839101
--- /dev/null
+++ b/openvdb/openvdb/codecs/BoolCodec.h
@@ -0,0 +1,154 @@
+// Copyright Contributors to the OpenVDB Project
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENVDB_IO_CODECS_BOOLCODEC_HAS_BEEN_INCLUDED
+#define OPENVDB_IO_CODECS_BOOLCODEC_HAS_BEEN_INCLUDED
+
+#include
+
+#include
+
+#include "TopologyCodec.h"
+
+namespace openvdb {
+OPENVDB_USE_VERSION_NAMESPACE
+namespace OPENVDB_VERSION_NAME {
+namespace codecs {
+namespace internal {
+
+template
+struct ReadBoolBuffersOp
+{
+ using TreeT = typename GridT::TreeType;
+ using RootT = typename TreeT::RootNodeType;
+ using LeafT = typename TreeT::LeafNodeType;
+ using ValueT = typename TreeT::ValueType;
+
+ ReadBoolBuffersOp(std::istream& _is, bool _saveFloatAsHalf,
+ const ValueT& _background, const CoordBBox* _clipBBox)
+ : is(_is)
+ , saveFloatAsHalf(_saveFloatAsHalf)
+ , background(_background)
+ , clipBBox(_clipBBox) { }
+
+ void operator()(RootT& root, size_t)
+ {
+ // Clip root-level tiles and prune children that were clipped.
+ if (clipBBox) {
+ root.clip(*clipBBox);
+ }
+ }
+
+ template
+ void operator()(NodeT& node, size_t)
+ {
+ // Clip internal node tiles and prune children that were clipped.
+ if (clipBBox) {
+ node.clip(*clipBBox, background);
+ }
+ }
+
+ void operator()(LeafT& leaf, size_t)
+ {
+ // Read in the value mask.
+ leaf.getValueMask().load(is);
+ // Read in the origin.
+ Coord origin;
+ is.read(reinterpret_cast(&origin), sizeof(Coord::ValueType) * 3);
+ leaf.setOrigin(origin);
+
+ // Read in the mask for the voxel values.
+ typename LeafT::Buffer::NodeMaskType nodeMask;
+ nodeMask.load(is);
+ typename LeafT::Buffer temp(nodeMask);
+ leaf.swap(temp);
+ }
+
+ std::istream& is;
+ const bool saveFloatAsHalf;
+ const ValueT& background;
+ const CoordBBox* clipBBox = nullptr;
+}; // struct ReadBoolBuffersOp
+
+template
+struct WriteBoolBuffersOp
+{
+ using TreeT = typename GridT::TreeType;
+ using LeafT = typename TreeT::LeafNodeType;
+
+ WriteBoolBuffersOp(std::ostream& _os, bool _saveFloatAsHalf)
+ : os(_os)
+ , saveFloatAsHalf(_saveFloatAsHalf) { }
+
+ template
+ void operator()(const NodeT&, size_t) { }
+
+ void operator()(const LeafT& leaf, size_t)
+ {
+ // Write out the value mask.
+ leaf.getValueMask().save(os);
+
+ // Write out the origin.
+ os.write(reinterpret_cast(&leaf.origin()), sizeof(Coord::ValueType) * 3);
+
+ // Write out the voxel values.
+ leaf.buffer().storage().save(os);
+ }
+
+ std::ostream& os;
+ const bool saveFloatAsHalf;
+}; // struct WriteBoolBuffersOp
+
+} // namespace internal
+
+template
+struct BoolCodec final: public TopologyCodec
+{
+ using Ptr = std::unique_ptr>;
+
+ ~BoolCodec() noexcept = default;
+
+ static inline std::string name() { return GridT::gridType(); }
+
+ void readBuffers(std::istream& is, int64_t /*size*/, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics&) const final
+ {
+ GridT& grid = static_cast(*data.grid);
+
+ if (grid.hasMultiPassIO()) {
+ OPENVDB_THROW(IoError, "Multi-pass IO is not supported in BoolCodec");
+ }
+
+ io::checkFormatVersion(is);
+
+ bool saveFloatAsHalf = grid.saveFloatAsHalf();
+
+ auto& tree = grid.tree();
+ tree.clearAllAccessors();
+
+ std::unique_ptr clipIndexBBox;
+ if (options.clipBBox.isSorted()) {
+ clipIndexBBox = std::make_unique(grid.constTransform().worldToIndexNodeCentered(options.clipBBox));
+ }
+
+ internal::ReadBoolBuffersOp readBuffersOp(is, saveFloatAsHalf, tree.background(), clipIndexBBox.get());
+ tools::visitNodesDepthFirst(grid.tree(), readBuffersOp, /*idx=*/0, /*topDown=*/false);
+ }
+
+ void writeBuffers(std::ostream& os, const GridBase& gridBase, const io::WriteOptions&) const final
+ {
+ const GridT& grid = static_cast(gridBase);
+
+ if (grid.hasMultiPassIO()) {
+ OPENVDB_THROW(IoError, "Multi-pass IO is not supported in BoolCodec");
+ }
+
+ internal::WriteBoolBuffersOp writeBuffersOp(os, grid.saveFloatAsHalf());
+ tools::visitNodesDepthFirst(grid.tree(), writeBuffersOp);
+ }
+}; // struct BoolCodec
+
+} // namespace codecs
+} // namespace OPENVDB_VERSION_NAME
+} // namespace openvdb
+
+#endif // OPENVDB_IO_CODECS_BOOLCODEC_HAS_BEEN_INCLUDED
diff --git a/openvdb/openvdb/codecs/PointDataCodec.h b/openvdb/openvdb/codecs/PointDataCodec.h
new file mode 100644
index 0000000000..18484facd1
--- /dev/null
+++ b/openvdb/openvdb/codecs/PointDataCodec.h
@@ -0,0 +1,470 @@
+// Copyright Contributors to the OpenVDB Project
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENVDB_IO_CODECS_POINTDATACODEC_HAS_BEEN_INCLUDED
+#define OPENVDB_IO_CODECS_POINTDATACODEC_HAS_BEEN_INCLUDED
+
+#include
+
+#include
+
+#include
+#include
+#include
+
+#include "impl/ScalarLeafCodec.h"
+#include "TopologyCodec.h"
+
+#include
+#include
+
+namespace openvdb {
+OPENVDB_USE_VERSION_NAMESPACE
+namespace OPENVDB_VERSION_NAME {
+namespace codecs {
+namespace internal {
+
+/// Look up an existing paged stream or create a new one for the given attribute index
+template
+inline typename PagedStreamPtrT::element_type* getOrCreatePagedStream(
+ std::map& pagedStreams, Index attributeIndex)
+{
+ auto it = pagedStreams.find(attributeIndex);
+ if (it != pagedStreams.end()) return it->second.get();
+ using PagedStreamT = typename PagedStreamPtrT::element_type;
+ auto ptr = std::make_shared();
+ auto& stored = (pagedStreams[attributeIndex] = std::move(ptr));
+ return stored.get();
+}
+
+////////////////////////////////////////
+// Read-side functions
+
+template
+inline void readPointDataVoxelSizes(const std::vector& leaves,
+ std::istream& is, std::unordered_map& voxelBufferSizes)
+{
+ for (auto* leaf : leaves) {
+ uint16_t voxelBufferSize;
+ is.read(reinterpret_cast(&voxelBufferSize), sizeof(uint16_t));
+ voxelBufferSizes[leaf->origin()] = voxelBufferSize;
+ }
+}
+
+template
+inline void readPointDataDescriptors(const std::vector& leaves,
+ std::istream& is)
+{
+ points::AttributeSet::Descriptor::Ptr sharedDescriptor;
+ for (auto* leaf : leaves) {
+ points::AttributeSet::UniquePtr attrSet = leaf->stealAttributeSet();
+ if (sharedDescriptor) {
+ // Reuse shared descriptor from first leaf
+ attrSet->resetDescriptor(sharedDescriptor, /*allowMismatchingDescriptors=*/true);
+ }
+ else {
+ uint8_t header;
+ is.read(reinterpret_cast(&header), sizeof(uint8_t));
+ attrSet->readDescriptor(is);
+ if (header & uint8_t(1)) {
+ // Store descriptor for subsequent leaves
+ sharedDescriptor = attrSet->descriptorPtr();
+ }
+ // a forwards-compatibility mechanism for future use,
+ // if a 0x2 bit is set, read and skip over a specific number of bytes
+ if (header & uint8_t(2)) {
+ uint64_t bytesToSkip;
+ is.read(reinterpret_cast(&bytesToSkip), sizeof(uint64_t));
+ if (bytesToSkip > uint64_t(0)) {
+ std::vector tempData(bytesToSkip);
+ is.read(reinterpret_cast(&tempData[0]), bytesToSkip);
+ }
+ }
+ // this reader is only able to read headers with 0x1 and 0x2 bits set
+ if (header > uint8_t(3)) {
+ OPENVDB_THROW(IoError, "Unrecognised header flags in PointDataLeafNode");
+ }
+ }
+ attrSet->readMetadata(is);
+ leaf->replaceAttributeSet(attrSet.release(), /*allowMismatchingDescriptors=*/true);
+ }
+}
+
+template
+inline void readPointDataAttributeSizes(const std::vector& leaves,
+ std::istream& is, Index attributeIndex,
+ std::map& pagedStreams)
+{
+ for (auto* leaf : leaves) {
+ points::AttributeArray* array = attributeIndex < leaf->attributeSet().size() ?
+ &leaf->attributeArray(attributeIndex) : nullptr;
+ if (array) {
+ auto* pagedStream = getOrCreatePagedStream(pagedStreams, attributeIndex);
+ pagedStream->setInputStream(is);
+ pagedStream->setSizeOnly(true);
+ array->readPagedBuffers(*pagedStream);
+ }
+ }
+}
+
+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,
+ 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, /*skip=*/false, /*clipBBox=*/nullptr, storageBackground);
+ }
+}
+
+template
+inline void readPointDataAttributeData(const std::vector& leaves,
+ std::istream& is, Index attributeIndex,
+ std::map& pagedStreams)
+{
+ for (auto* leaf : leaves) {
+ points::AttributeArray* array = attributeIndex < leaf->attributeSet().size() ?
+ &leaf->attributeArray(attributeIndex) : nullptr;
+ if (array) {
+ auto* pagedStream = getOrCreatePagedStream(pagedStreams, attributeIndex);
+ pagedStream->setInputStream(is);
+ pagedStream->setSizeOnly(false);
+ array->readPagedBuffers(*pagedStream);
+ }
+ }
+}
+
+template
+inline void skipPointDataAttributeSizes(const std::vector& leaves,
+ std::istream& is, Index attributeIndex,
+ std::map& pagedStreams)
+{
+ for (auto* leaf : leaves) {
+ points::AttributeArray* array = attributeIndex < leaf->attributeSet().size() ?
+ &leaf->attributeArray(attributeIndex) : nullptr;
+ if (array) {
+ auto* pagedStream = getOrCreatePagedStream(pagedStreams, attributeIndex);
+ pagedStream->setInputStream(is);
+ pagedStream->setSizeOnly(true);
+ array->skipPagedBuffers(*pagedStream);
+ }
+ }
+}
+
+template
+inline void skipPointDataAttributeData(const std::vector& leaves,
+ std::istream& is, Index attributeIndex,
+ std::map& pagedStreams)
+{
+ for (auto* leaf : leaves) {
+ points::AttributeArray* array = attributeIndex < leaf->attributeSet().size() ?
+ &leaf->attributeArray(attributeIndex) : nullptr;
+ if (array) {
+ auto* pagedStream = getOrCreatePagedStream(pagedStreams, attributeIndex);
+ pagedStream->setInputStream(is);
+ pagedStream->setSizeOnly(false);
+ array->skipPagedBuffers(*pagedStream);
+ }
+ }
+}
+
+////////////////////////////////////////
+// Write-side functions
+
+template
+inline Index countPointDataPasses(const std::vector& leaves)
+{
+ Index maxRequiredPasses = 0;
+ for (const auto* leaf : leaves) {
+ const Index requiredPasses = leaf->buffers();
+ if (requiredPasses > maxRequiredPasses) {
+ maxRequiredPasses = requiredPasses;
+ }
+ }
+ return maxRequiredPasses;
+}
+
+template
+inline void writePointDataVoxelSizes(const std::vector& leaves,
+ std::ostream& os, bool& matching,
+ points::AttributeSet::Descriptor::Ptr& sharedDescriptor)
+{
+ bool descriptorChecked = false;
+ matching = true;
+ for (const auto* leaf : leaves) {
+ io::writeCompressedValuesSize(os, leaf->buffer().data(), LeafT::SIZE);
+
+ // Track descriptor matching
+ const auto& descriptor = leaf->attributeSet().descriptorPtr();
+ if (!descriptorChecked) {
+ // First leaf - store descriptor
+ descriptorChecked = true;
+ sharedDescriptor = descriptor;
+ }
+ else if (matching && *sharedDescriptor != *descriptor) {
+ matching = false;
+ }
+ }
+}
+
+template
+inline void writePointDataDescriptors(const std::vector& leaves,
+ std::ostream& os, bool matching,
+ const points::AttributeSet::Descriptor::Ptr&)
+{
+ bool firstWrite = true;
+ for (const auto* leaf : leaves) {
+ const points::AttributeSet& attributeSet = leaf->attributeSet();
+ if (matching) {
+ // Shared descriptor - only write on first leaf
+ if (firstWrite) {
+ firstWrite = false;
+ uint8_t header(1);
+ os.write(reinterpret_cast(&header), sizeof(uint8_t));
+ attributeSet.writeDescriptor(os, /*transient=*/false);
+ }
+ }
+ else {
+ // Non-shared descriptor - write on every leaf
+ uint8_t header(0);
+ os.write(reinterpret_cast(&header), sizeof(uint8_t));
+ attributeSet.writeDescriptor(os, /*transient=*/false);
+ }
+ attributeSet.writeMetadata(os, /*transient=*/false, /*paged=*/true);
+ }
+}
+
+template
+inline void writePointDataAttributeSizes(const std::vector& leaves,
+ std::ostream& os, Index attributeIndex)
+{
+ std::map pagedStreams;
+ for (const auto* leaf : leaves) {
+ const points::AttributeArray* array = attributeIndex < leaf->attributeSet().size() ?
+ leaf->attributeSet().getConst(attributeIndex) : nullptr;
+ if (array) {
+ auto* pagedStream = getOrCreatePagedStream(pagedStreams, attributeIndex);
+ pagedStream->setOutputStream(os);
+ pagedStream->setSizeOnly(true);
+ array->writePagedBuffers(*pagedStream, /*outputTransient*/false);
+ }
+ }
+ // Flush paged streams to write any remaining buffered page headers
+ for (auto& pair : pagedStreams) {
+ pair.second->flush();
+ }
+}
+
+template
+inline void writePointDataVoxelData(const std::vector& leaves,
+ std::ostream& os, bool saveFloatAsHalf)
+{
+ using BaseLeaf = typename LeafT::BaseLeaf;
+ for (const auto* leaf : leaves) {
+ const BaseLeaf& baseLeaf = static_cast(*leaf);
+ writeScalarLeafBuffers(baseLeaf, os, saveFloatAsHalf);
+ }
+}
+
+template
+inline void writePointDataAttributeData(const std::vector& leaves,
+ std::ostream& os, Index attributeIndex)
+{
+ std::map pagedStreams;
+ for (const auto* leaf : leaves) {
+ const points::AttributeArray* array = attributeIndex < leaf->attributeSet().size() ?
+ leaf->attributeSet().getConst(attributeIndex) : nullptr;
+ if (array) {
+ auto* pagedStream = getOrCreatePagedStream(pagedStreams, attributeIndex);
+ pagedStream->setOutputStream(os);
+ pagedStream->setSizeOnly(false);
+ array->writePagedBuffers(*pagedStream, /*outputTransient*/false);
+ }
+ }
+ // Flush paged streams to write any remaining buffered page data
+ for (auto& pair : pagedStreams) {
+ pair.second->flush();
+ }
+}
+
+} // namespace internal
+
+/// Per-grid-type codec-specific options for PointDataCodec
+/// Contains point attribute filtering options
+struct OPENVDB_API PointDataCodecTypeData : public io::ReadTypedOptions
+{
+ // Point Attribute Options - which attributes to read
+ std::vector pointAttributeNames;
+}; // struct PointDataCodecTypeData
+
+template
+struct PointDataCodec final: public TopologyCodec
+{
+ using Ptr = std::unique_ptr>;
+
+ ~PointDataCodec() noexcept = default;
+
+ static inline std::string name() { return GridT::gridType(); }
+
+ void readBuffers(std::istream& is, int64_t /*size*/, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics&) const final
+ {
+ OPENVDB_ASSERT(dynamic_cast(data.grid.get()));
+
+ GridT& grid = static_cast(*data.grid);
+
+ std::vector pointAttributeNames;
+
+ // Look up point-specific options if provided
+ auto it = options.typeData.find(name());
+ if (it != options.typeData.end()) {
+ auto& pointTypeData = io::ReadTypedOptions::cast(it->second);
+ pointAttributeNames = pointTypeData.pointAttributeNames;
+ }
+
+ io::checkFormatVersion(is);
+
+ bool saveFloatAsHalf = grid.saveFloatAsHalf();
+
+ auto& tree = grid.tree();
+ tree.clearAllAccessors();
+
+ uint16_t numPasses = 1;
+ is.read(reinterpret_cast(&numPasses), sizeof(uint16_t));
+ // 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;
+ tree.getNodes(leaves);
+
+ // Pass 0: read voxel data sizes
+ std::unordered_map voxelBufferSizes;
+ internal::readPointDataVoxelSizes(leaves, is, voxelBufferSizes);
+
+ // Pass 1: read descriptor and attribute metadata
+ internal::readPointDataDescriptors(leaves, is);
+
+ // Build set of attribute indices to skip based on pointAttributeNames.
+ // An empty pointAttributeNames means no filtering (read all attributes).
+ std::set skipIndices;
+ if (!pointAttributeNames.empty() && !leaves.empty()) {
+ // 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());
+ for (const auto& namePos : nameMap) {
+ if (wantedNames.find(namePos.first) == wantedNames.end()) {
+ skipIndices.insert(static_cast(namePos.second));
+ }
+ }
+ }
+
+ // Passes 2..N+1: read attribute buffer sizes
+ std::map pagedStreams;
+ for (Index i = 0; i < attributes; ++i) {
+ if (skipIndices.count(i)) {
+ internal::skipPointDataAttributeSizes(leaves, is, i, pagedStreams);
+ } else {
+ internal::readPointDataAttributeSizes(leaves, is, i, pagedStreams);
+ }
+ }
+
+ // 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, &topoData.storageBackground);
+
+ // Passes N+3..2N+2: read attribute data buffers
+ for (Index i = 0; i < attributes; ++i) {
+ if (skipIndices.count(i)) {
+ internal::skipPointDataAttributeData(leaves, is, i, pagedStreams);
+ } else {
+ internal::readPointDataAttributeData(leaves, is, i, pagedStreams);
+ }
+ }
+
+ // Drop skipped attributes from each leaf's AttributeSet
+ if (!skipIndices.empty() && !leaves.empty()) {
+ const std::vector dropPositions(skipIndices.begin(), skipIndices.end());
+ auto filteredDescriptor =
+ leaves[0]->attributeSet().descriptorPtr()->duplicateDrop(dropPositions);
+ for (auto* leaf : leaves) {
+ leaf->dropAttributes(dropPositions,
+ leaf->attributeSet().descriptor(), filteredDescriptor);
+ }
+ }
+
+ // PointDataGrid uses multiple passes, so clip after reading
+ // the buffers if bbox is not infinite.
+ if (options.clipBBox.isSorted()) {
+ CoordBBox indexBBox =
+ grid.constTransform().worldToIndexNodeCentered(options.clipBBox);
+ grid.tree().root().clip(indexBBox);
+ }
+ }
+
+ void writeBuffers(std::ostream& os, const GridBase& gridBase, const io::WriteOptions&) const final
+ {
+ const GridT& grid = static_cast(gridBase);
+ bool saveFloatAsHalf = grid.saveFloatAsHalf();
+
+ using LeafT = typename GridT::TreeType::LeafNodeType;
+ std::vector leaves;
+ grid.tree().getNodes(leaves);
+
+ // Determine how many leaf buffer passes are required for this grid
+ uint16_t numPasses =
+ static_cast(internal::countPointDataPasses(leaves));
+ os.write(reinterpret_cast(&numPasses), sizeof(uint16_t));
+
+ // 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;
+ points::AttributeSet::Descriptor::Ptr sharedDescriptor;
+ internal::writePointDataVoxelSizes(leaves, os, matching, sharedDescriptor);
+
+ // Pass 1: write descriptor and attribute metadata
+ internal::writePointDataDescriptors(leaves, os, matching, sharedDescriptor);
+
+ // Passes 2..N+1: write attribute buffer sizes (page headers)
+ for (Index i = 0; i < attributes; ++i) {
+ internal::writePointDataAttributeSizes(leaves, os, i);
+ }
+
+ // Pass N+2: write voxel data
+ internal::writePointDataVoxelData(leaves, os, saveFloatAsHalf);
+
+ // Passes N+3..2N+2: write attribute data buffers (page data)
+ for (Index i = 0; i < attributes; ++i) {
+ internal::writePointDataAttributeData(leaves, os, i);
+ }
+ }
+}; // struct PointDataCodec
+
+} // namespace codecs
+} // namespace OPENVDB_VERSION_NAME
+} // namespace openvdb
+
+#endif // OPENVDB_IO_CODECS_POINTDATACODEC_HAS_BEEN_INCLUDED
diff --git a/openvdb/openvdb/codecs/PointIndexCodec.h b/openvdb/openvdb/codecs/PointIndexCodec.h
new file mode 100644
index 0000000000..5b4d6b62cf
--- /dev/null
+++ b/openvdb/openvdb/codecs/PointIndexCodec.h
@@ -0,0 +1,163 @@
+// Copyright Contributors to the OpenVDB Project
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENVDB_IO_CODECS_POINTINDEXCODEC_HAS_BEEN_INCLUDED
+#define OPENVDB_IO_CODECS_POINTINDEXCODEC_HAS_BEEN_INCLUDED
+
+#include
+
+#include
+
+#include "impl/ScalarLeafCodec.h"
+#include "TopologyCodec.h"
+
+namespace openvdb {
+OPENVDB_USE_VERSION_NAMESPACE
+namespace OPENVDB_VERSION_NAME {
+namespace codecs {
+namespace internal {
+
+template
+struct ReadPointIndexBuffersOp
+{
+ using TreeT = typename GridT::TreeType;
+ using RootT = typename TreeT::RootNodeType;
+ using LeafT = typename TreeT::LeafNodeType;
+ using ValueT = typename TreeT::ValueType;
+
+ ReadPointIndexBuffersOp(std::istream& _is, bool _saveFloatAsHalf,
+ const ValueT& _background, const ValueT* _storageBackground = nullptr)
+ : is(_is)
+ , saveFloatAsHalf(_saveFloatAsHalf)
+ , background(_background)
+ , storageBackground(_storageBackground) { }
+
+ template
+ void operator()(NodeT&, size_t) { }
+
+ void operator()(LeafT& leaf, size_t)
+ {
+ using BaseLeaf = typename LeafT::BaseLeaf;
+
+ // Read the value mask and voxel data via base class
+ BaseLeaf& baseLeaf = static_cast(leaf);
+ readScalarLeafBuffers(baseLeaf, is, saveFloatAsHalf, background, /*skip=*/false, /*clipBBox=*/nullptr, storageBackground);
+
+ // Read the number of indices.
+ Index64 numIndices = Index64(0);
+ is.read(reinterpret_cast(&numIndices), sizeof(Index64));
+
+ // Read the indices data.
+ leaf.indices().resize(size_t(numIndices));
+ is.read(reinterpret_cast(leaf.indices().data()), numIndices * sizeof(ValueT));
+
+ // Reserved for future use.
+ Index64 auxDataBytes = Index64(0);
+ is.read(reinterpret_cast(&auxDataBytes), sizeof(Index64));
+ if (auxDataBytes > 0) {
+ // For now, read and discard any auxiliary data.
+ std::unique_ptr auxData{new char[auxDataBytes]};
+ is.read(auxData.get(), auxDataBytes);
+ }
+ }
+
+ std::istream& is;
+ const bool saveFloatAsHalf;
+ const ValueT& background;
+ const ValueT* storageBackground = nullptr;
+}; // struct ReadPointIndexBuffersOp
+
+template
+struct WritePointIndexBuffersOp
+{
+ using TreeT = typename GridT::TreeType;
+ using LeafT = typename TreeT::LeafNodeType;
+ using ValueT = typename TreeT::ValueType;
+
+ WritePointIndexBuffersOp(std::ostream& _os, bool _saveFloatAsHalf)
+ : os(_os)
+ , saveFloatAsHalf(_saveFloatAsHalf) { }
+
+ template
+ void operator()(const NodeT&, size_t) { }
+
+ void operator()(const LeafT& leaf, size_t)
+ {
+ using BaseLeaf = typename LeafT::BaseLeaf;
+
+ // Write out the value mask and voxel values via base class
+ const BaseLeaf& baseLeaf = static_cast(leaf);
+ writeScalarLeafBuffers(baseLeaf, os, saveFloatAsHalf);
+
+ // Write the number of indices.
+ Index64 numIndices = Index64(leaf.indices().size());
+ os.write(reinterpret_cast(&numIndices), sizeof(Index64));
+
+ // Write the indices data.
+ os.write(reinterpret_cast(leaf.indices().data()), numIndices * sizeof(ValueT));
+
+ // Reserved for future use.
+ const Index64 auxDataBytes = Index64(0);
+ os.write(reinterpret_cast(&auxDataBytes), sizeof(Index64));
+ }
+
+ std::ostream& os;
+ const bool saveFloatAsHalf;
+}; // struct WritePointIndexBuffersOp
+
+} // namespace internal
+
+template
+struct PointIndexCodec final: public TopologyCodec
+{
+ using Ptr = std::unique_ptr>;
+
+ ~PointIndexCodec() noexcept = default;
+
+ static inline std::string name() { return GridT::gridType(); }
+
+ void readBuffers(std::istream& is, int64_t /*size*/, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics& diagnostics) const final
+ {
+ OPENVDB_ASSERT(dynamic_cast(data.grid.get()));
+
+ GridT& grid = static_cast(*data.grid);
+
+ if (grid.hasMultiPassIO()) {
+ OPENVDB_THROW(IoError, "Multi-pass IO is not supported in PointIndexCodec");
+ }
+
+ io::checkFormatVersion(is);
+
+ bool saveFloatAsHalf = grid.saveFloatAsHalf();
+
+ auto& tree = grid.tree();
+ tree.clearAllAccessors();
+
+ if (options.clipBBox.isSorted()) {
+ diagnostics.addWarning(grid.getName(), "bounding box clipping is not supported for PointIndexGrids");
+ }
+
+ 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);
+ }
+
+ void writeBuffers(std::ostream& os, const GridBase& gridBase, const io::WriteOptions&) const final
+ {
+ const GridT& grid = static_cast(gridBase);
+
+ if (grid.hasMultiPassIO()) {
+ OPENVDB_THROW(IoError, "Multi-pass IO is not supported in PointIndexCodec");
+ }
+
+ internal::WritePointIndexBuffersOp writeBuffersOp(os, grid.saveFloatAsHalf());
+ tools::visitNodesDepthFirst(grid.tree(), writeBuffersOp);
+ }
+}; // struct PointIndexCodec
+
+} // namespace codecs
+} // namespace OPENVDB_VERSION_NAME
+} // namespace openvdb
+
+#endif // OPENVDB_IO_CODECS_POINTINDEXCODEC_HAS_BEEN_INCLUDED
diff --git a/openvdb/openvdb/codecs/ScalarCodec.h b/openvdb/openvdb/codecs/ScalarCodec.h
new file mode 100644
index 0000000000..e181f54df7
--- /dev/null
+++ b/openvdb/openvdb/codecs/ScalarCodec.h
@@ -0,0 +1,194 @@
+// Copyright Contributors to the OpenVDB Project
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENVDB_IO_CODECS_SCALARCODEC_HAS_BEEN_INCLUDED
+#define OPENVDB_IO_CODECS_SCALARCODEC_HAS_BEEN_INCLUDED
+
+#include
+#include
+#include
+#include
+#include
+
+#include "impl/ScalarLeafCodec.h"
+#include "TopologyCodec.h"
+
+namespace openvdb {
+OPENVDB_USE_VERSION_NAMESPACE
+namespace OPENVDB_VERSION_NAME {
+namespace codecs {
+namespace internal {
+
+template
+struct WriteBuffersOp
+{
+ using LeafT = typename TreeT::LeafNodeType;
+ using ValueT = typename TreeT::ValueType;
+
+ WriteBuffersOp(std::ostream& _os, bool _saveFloatAsHalf, const ValueT& _background)
+ : os(_os)
+ , saveFloatAsHalf(_saveFloatAsHalf)
+ , background(_background) { }
+
+ template
+ void operator()(const NodeT&, size_t) { }
+
+ void operator()(const LeafT& leaf, size_t)
+ {
+ // 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
+
+
+template
+struct ReadBuffersOp
+{
+ using RootT = typename TreeT::RootNodeType;
+ 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 StorageValueT* _storageBackground = nullptr)
+ : is(_is)
+ , saveFloatAsHalf(_saveFloatAsHalf)
+ , background(_background)
+ , clipBBox(_clipBBox)
+ , storageBackground(_storageBackground) { }
+
+ void operator()(RootT& root, size_t)
+ {
+ if (clipBBox) {
+ root.clip(*clipBBox);
+ }
+ }
+
+ template
+ void operator()(NodeT& node, size_t)
+ {
+ if (clipBBox) {
+ node.clip(*clipBBox, background);
+ }
+ }
+
+ void operator()(LeafT& leaf, size_t)
+ {
+ 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,
+ const typename StorageGridT::TreeType::ValueType* storageBackground)
+{
+ if (grid.hasMultiPassIO()) {
+ OPENVDB_THROW(IoError, "Multi-pass IO is not supported in ScalarCodec");
+ }
+
+ using TreeT = typename GridT::TreeType;
+ using StorageTreeT = typename StorageGridT::TreeType;
+
+ io::checkFormatVersion(is);
+
+ bool saveFloatAsHalf = grid.saveFloatAsHalf();
+
+ auto& tree = grid.tree();
+ tree.clearAllAccessors();
+
+ std::unique_ptr clipIndexBBox;
+ if (options.clipBBox.isSorted()) {
+ clipIndexBBox = std::make_unique(grid.constTransform().worldToIndexNodeCentered(options.clipBBox));
+ }
+
+ // Works for both standard (TreeT == StorageTreeT) and conversion cases
+ ReadBuffersOp readBuffersOp(is, saveFloatAsHalf, tree.background(),
+ clipIndexBBox.get(), storageBackground);
+ tools::visitNodesDepthFirst(grid.tree(), readBuffersOp, /*idx=*/0, /*topDown=*/false);
+}
+
+// Free-standing function for write case (no StorageGridT needed)
+template
+void scalarCodecWriteBuffers(const GridT& grid, std::ostream& os)
+{
+ using TreeType = typename GridT::TreeType;
+
+ if (grid.hasMultiPassIO()) {
+ OPENVDB_THROW(IoError, "Multi-pass IO is not supported in ScalarCodec");
+ }
+
+ WriteBuffersOp writeBuffersOp(os, grid.saveFloatAsHalf(), grid.tree().background());
+ tools::visitNodesDepthFirst(grid.tree(), writeBuffersOp);
+}
+
+} // namespace internal
+
+template
+struct ScalarCodec final: public TopologyCodec
+{
+ static_assert(GridT::TreeType::RootNodeType::template SameConfiguration<
+ typename StorageGridT::TreeType::RootNodeType>::value,
+ "GridT and StorageGridT must have the same configuration");
+
+ using Ptr = std::unique_ptr>;
+
+ ~ScalarCodec() noexcept = default;
+
+ static inline std::string name()
+ {
+ if constexpr (std::is_same_v) {
+ return GridT::gridType();
+ } else {
+ std::string buildType = typeNameAsString();
+ return StorageGridT::gridType() + "_to_" + buildType;
+ }
+ }
+
+ void readBuffers(std::istream& is, int64_t /*size*/, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics&) const final
+ {
+ using StorageValueT = typename StorageGridT::TreeType::ValueType;
+ GridT& grid = static_cast(*data.grid);
+
+ // readTopology() has already set the tiles and leaf buffers to the
+ // background, so there is nothing left to read.
+ if (options.readMode == io::ReadMode::TopologyOnly) return;
+
+ auto& topoData = static_cast&>(data);
+ internal::scalarCodecReadBuffers(grid, is, options, &topoData.storageBackground);
+ }
+
+ void writeBuffers(std::ostream& os, const GridBase& gridBase, const io::WriteOptions&) const final
+ {
+ // 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
+
+
+} // namespace codecs
+} // namespace OPENVDB_VERSION_NAME
+} // namespace openvdb
+
+#endif // OPENVDB_IO_CODECS_SCALARCODEC_HAS_BEEN_INCLUDED
diff --git a/openvdb/openvdb/codecs/TopologyCodec.h b/openvdb/openvdb/codecs/TopologyCodec.h
new file mode 100644
index 0000000000..47f07b7ba0
--- /dev/null
+++ b/openvdb/openvdb/codecs/TopologyCodec.h
@@ -0,0 +1,399 @@
+// Copyright Contributors to the OpenVDB Project
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENVDB_IO_CODECS_TOPOLOGYCODEC_HAS_BEEN_INCLUDED
+#define OPENVDB_IO_CODECS_TOPOLOGYCODEC_HAS_BEEN_INCLUDED
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+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
+struct WriteTopologyOp
+{
+ using ValueT = typename TreeT::ValueType;
+ using RootT = typename TreeT::RootNodeType;
+ using LeafT = typename TreeT::LeafNodeType;
+
+ WriteTopologyOp(std::ostream& _os, bool _saveFloatAsHalf)
+ : os(_os)
+ , saveFloatAsHalf(_saveFloatAsHalf) { }
+
+ void operator()(const RootT& root, size_t)
+ {
+ int32_t bufferCount = 1;
+ os.write(reinterpret_cast(&bufferCount), sizeof(int32_t));
+
+ background = &root.background();
+ if (!saveFloatAsHalf) {
+ os.write(reinterpret_cast(background), sizeof(ValueT));
+ } else {
+ ValueT truncatedVal = io::truncateRealToHalf(*background);
+ os.write(reinterpret_cast(&truncatedVal), sizeof(ValueT));
+ }
+
+ const Index numTiles = root.tileCount(), numChildren = root.childCount();
+ os.write(reinterpret_cast(&numTiles), sizeof(Index));
+ os.write(reinterpret_cast(&numChildren), sizeof(Index));
+
+ // Write tiles.
+ for (auto iter = root.cbeginValueAll(); iter; ++iter) {
+ const auto& ijk = iter.getCoord();
+ os.write(reinterpret_cast(ijk.asPointer()), 3 * sizeof(Int32));
+ ValueT value;
+ bool active = root.getTileValueUnsafe(ijk, value);
+ os.write(reinterpret_cast(&value), sizeof(ValueT));
+ os.write(reinterpret_cast(&active), sizeof(bool));
+ }
+
+ rootChildLevel = RootT::LEVEL-1;
+ }
+
+ template
+ void operator()(const NodeT& node, size_t)
+ {
+ // Write origin for RootNode children only
+ if (rootChildLevel == NodeT::LEVEL) {
+ const auto& ijk = node.origin();
+ os.write(reinterpret_cast(ijk.asPointer()), 3 * sizeof(Int32));
+ }
+
+ const auto& childMask = node.getChildMask();
+ const auto& valueMask = node.getValueMask();
+
+ childMask.save(os);
+ valueMask.save(os);
+
+ {
+ // Copy all of this node's values into an array.
+ std::unique_ptr valuePtr(new ValueT[NodeT::NUM_VALUES]);
+ ValueT* values = valuePtr.get();
+ const ValueT zero = zeroVal();
+ for (Index i = 0; i < NodeT::NUM_VALUES; ++i) {
+ 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, background);
+ }
+ }
+
+ void operator()(const LeafT& leaf, size_t)
+ {
+ leaf.getValueMask().save(os);
+ }
+
+ std::ostream& os;
+ const bool saveFloatAsHalf;
+ const ValueT* background = nullptr;
+ Index rootChildLevel = std::numeric_limits::max();
+}; // struct WriteTopologyOp
+
+
+template
+struct ReadTopologyOp
+{
+ using ValueT = typename TreeT::ValueType;
+ using RootT = typename TreeT::RootNodeType;
+ using LeafT = typename TreeT::LeafNodeType;
+ using StorageValueT = typename StorageTreeT::ValueType;
+
+ ReadTopologyOp(std::istream& _is, bool _saveFloatAsHalf, io::ReadDiagnostics& _diagnostics,
+ const std::string& _gridName)
+ : is(_is)
+ , saveFloatAsHalf(_saveFloatAsHalf)
+ , diagnostics(_diagnostics)
+ , gridName(_gridName) { }
+
+ void operator()(RootT& root)
+ {
+ using ChildT = typename RootT::ChildNodeType;
+
+ int32_t bufferCount;
+ is.read(reinterpret_cast(&bufferCount), sizeof(int32_t));
+ if (bufferCount != 1) {
+ diagnostics.addWarning(gridName, "multi-buffer trees are no longer supported");
+ }
+
+ // Delete the existing tree.
+ root.clear();
+
+ // Read a RootNode that was stored in the current format.
+
+ is.read(reinterpret_cast(&storageBackground), sizeof(StorageValueT));
+ background = static_cast(storageBackground);
+
+ Index numTiles = 0, numChildren = 0;
+ is.read(reinterpret_cast(&numTiles), sizeof(Index));
+ is.read(reinterpret_cast(&numChildren), sizeof(Index));
+
+ Int32 vec[3];
+ StorageValueT value;
+ bool active;
+
+ // Read tiles.
+ for (Index n = 0; n < numTiles; ++n) {
+ is.read(reinterpret_cast(vec), 3 * sizeof(Int32));
+ is.read(reinterpret_cast(&value), sizeof(StorageValueT));
+ is.read(reinterpret_cast(&active), sizeof(bool));
+ Coord origin(vec);
+ if constexpr (std::is_same_v) {
+ root.addTile(origin, value, active);
+ } else {
+ root.addTile(origin, static_cast(value), active);
+ }
+ }
+
+ // Read child nodes.
+ for (Index n = 0; n < numChildren; ++n) {
+ is.read(reinterpret_cast(vec), 3 * sizeof(Int32));
+ Coord origin(vec);
+ ChildT* child = new ChildT(PartialCreate(), origin, background);
+ (*this)(*child);
+ root.addChild(child);
+ }
+ }
+
+ template
+ void operator()(NodeT& node)
+ {
+ using ChildT = typename NodeT::ChildNodeType;
+ using NodeMaskT = typename NodeT::NodeMaskType;
+
+ NodeMaskT childMask, valueMask;
+ childMask.load(is);
+ valueMask.load(is);
+ node.setValueMaskUnsafe(valueMask);
+
+ const bool oldVersion =
+ (io::getFormatVersion(is) < OPENVDB_FILE_VERSION_NODE_MASK_COMPRESSION);
+ const Index numValues = (oldVersion ? childMask.countOff() : NodeT::NUM_VALUES);
+ {
+ // Read in (and uncompress, if necessary) all of this node's values
+ // into a contiguous array.
+ std::unique_ptr valuePtr(new StorageValueT[numValues]);
+ StorageValueT* values = valuePtr.get();
+ 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 = childMask.beginOff(); iter; ++iter) {
+ node.setValueOnlyUnsafe(iter.pos(), static_cast(values[n++]));
+ }
+ OPENVDB_ASSERT(n == numValues);
+ } else {
+ for (auto iter = node.beginValueAll(); iter; ++iter) {
+ node.setValueOnlyUnsafe(iter.pos(), static_cast(values[iter.pos()]));
+ }
+ }
+ }
+
+ // 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);
+ node.setChildUnsafe(iter.pos(), child);
+ (*this)(*child);
+ }
+ }
+
+ void operator()(LeafT& leaf)
+ {
+ typename LeafT::NodeMaskType valueMask;
+ valueMask.load(is);
+ leaf.setValueMask(valueMask);
+ }
+
+ std::istream& is;
+ bool saveFloatAsHalf;
+ ValueT background;
+ StorageValueT storageBackground;
+ io::ReadDiagnostics& diagnostics;
+ std::string gridName;
+}; // struct ReadTopologyOp
+
+template
+struct SetTilesToBackgroundOp
+{
+ using RootT = typename TreeT::RootNodeType;
+ using LeafT = typename TreeT::LeafNodeType;
+ using ValueT = typename TreeT::ValueType;
+
+ explicit SetTilesToBackgroundOp(const ValueT& background)
+ : mBackground(background) { }
+
+ bool operator()(RootT& root, size_t) const
+ {
+ for (auto it = root.beginValueAll(); it; ++it) {
+ it.setValue(mBackground);
+ }
+ return true;
+ }
+
+ template
+ bool operator()(NodeT& node, size_t) const
+ {
+ for (Index i = 0; i < NodeT::NUM_VALUES; ++i) {
+ if (node.isChildMaskOff(i)) {
+ node.setValueOnlyUnsafe(i, mBackground);
+ }
+ }
+ return true;
+ }
+
+ bool operator()(LeafT&, size_t) const { return true; }
+
+private:
+ const ValueT mBackground;
+}; // struct SetTilesToBackgroundOp
+
+template
+void setTilesToBackground(TreeT& tree)
+{
+ const typename TreeT::ValueType& background = tree.root().background();
+ SetTilesToBackgroundOp op(background);
+ tree::DynamicNodeManager nodeManager(tree);
+ nodeManager.foreachTopDown(op);
+}
+
+// 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, TopologyCodecData& codecData)
+{
+ io::checkFormatVersion(is);
+
+ GridT& grid = static_cast(gridBase);
+ grid.tree().clearAllAccessors();
+
+ 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;
+ }
+}
+
+// Free-standing function for write case (no StorageGridT needed)
+template
+void topologyCodecWriteTopology(const GridBase& gridBase, std::ostream& os)
+{
+ const GridT& grid = static_cast(gridBase);
+ using TreeType = typename GridT::TreeType;
+
+ internal::WriteTopologyOp writeTopologyOp(os, grid.saveFloatAsHalf());
+ tools::visitNodesDepthFirst(grid.tree(), writeTopologyOp);
+}
+
+} // namespace internal
+
+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>();
+ data->grid = GridT::create();
+ return data;
+ }
+
+ void readTopology(std::istream& is, io::CodecData& data, const io::ReadOptions& options,
+ io::ReadDiagnostics& diagnostics) const final
+ {
+ // 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&) const final
+ {
+ // 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
+
+
+} // namespace codecs
+} // namespace OPENVDB_VERSION_NAME
+} // namespace openvdb
+
+#endif // OPENVDB_IO_CODECS_TOPOLOGYCODEC_HAS_BEEN_INCLUDED
diff --git a/openvdb/openvdb/codecs/ValueMaskCodec.h b/openvdb/openvdb/codecs/ValueMaskCodec.h
new file mode 100644
index 0000000000..d3e3e2a938
--- /dev/null
+++ b/openvdb/openvdb/codecs/ValueMaskCodec.h
@@ -0,0 +1,146 @@
+// Copyright Contributors to the OpenVDB Project
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENVDB_IO_CODECS_VALUEMASKCODEC_HAS_BEEN_INCLUDED
+#define OPENVDB_IO_CODECS_VALUEMASKCODEC_HAS_BEEN_INCLUDED
+
+#include
+
+#include
+#include
+
+#include "TopologyCodec.h"
+
+namespace openvdb {
+OPENVDB_USE_VERSION_NAMESPACE
+namespace OPENVDB_VERSION_NAME {
+namespace codecs {
+namespace internal {
+
+template
+struct ReadValueMaskBuffersOp
+{
+ using TreeT = typename GridT::TreeType;
+ using RootT = typename TreeT::RootNodeType;
+ using LeafT = typename TreeT::LeafNodeType;
+ using ValueT = typename TreeT::ValueType;
+
+ ReadValueMaskBuffersOp(std::istream& _is, bool _saveFloatAsHalf,
+ const ValueT& _background, const CoordBBox* _clipBBox)
+ : is(_is)
+ , saveFloatAsHalf(_saveFloatAsHalf)
+ , background(_background)
+ , clipBBox(_clipBBox) { }
+
+ void operator()(RootT& root, size_t)
+ {
+ // Clip root-level tiles and prune children that were clipped.
+ if (clipBBox) {
+ root.clip(*clipBBox);
+ }
+ }
+
+ template
+ void operator()(NodeT& node, size_t)
+ {
+ // Clip internal node tiles and prune children that were clipped.
+ if (clipBBox) {
+ node.clip(*clipBBox, background);
+ }
+ }
+
+ void operator()(LeafT& leaf, size_t)
+ {
+ // Read in the value mask.
+ leaf.getValueMask().load(is);
+ // Read in the origin.
+ Coord origin;
+ origin.read(is);
+ leaf.setOrigin(origin);
+ }
+
+ std::istream& is;
+ const bool saveFloatAsHalf;
+ const ValueT& background;
+ const CoordBBox* clipBBox = nullptr;
+}; // struct ReadValueMaskBuffersOp
+
+template
+struct WriteValueMaskBuffersOp
+{
+ using TreeT = typename GridT::TreeType;
+ using LeafT = typename TreeT::LeafNodeType;
+
+ WriteValueMaskBuffersOp(std::ostream& _os, bool _saveFloatAsHalf)
+ : os(_os)
+ , saveFloatAsHalf(_saveFloatAsHalf) { }
+
+ template
+ void operator()(const NodeT&, size_t) { }
+
+ void operator()(const LeafT& leaf, size_t)
+ {
+ // Write out the value mask.
+ leaf.getValueMask().save(os);
+
+ // Write out the origin.
+ leaf.origin().write(os);
+ }
+
+ std::ostream& os;
+ const bool saveFloatAsHalf;
+}; // struct WriteValueMaskBuffersOp
+
+} // namespace internal
+
+template
+struct ValueMaskCodec final: public TopologyCodec
+{
+ using Ptr = std::unique_ptr>;
+
+ ~ValueMaskCodec() noexcept = default;
+
+ static inline std::string name() { return GridT::gridType(); }
+
+ void readBuffers(std::istream& is, int64_t /*size*/, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics&) const final
+ {
+ GridT& grid = static_cast(*data.grid);
+
+ if (grid.hasMultiPassIO()) {
+ OPENVDB_THROW(IoError, "Multi-pass IO is not supported in ValueMaskCodec");
+ }
+
+ io::checkFormatVersion(is);
+
+ bool saveFloatAsHalf = grid.saveFloatAsHalf();
+
+ auto& tree = grid.tree();
+ tree.clearAllAccessors();
+
+ std::unique_ptr clipIndexBBox;
+ if (options.clipBBox.isSorted()) {
+ clipIndexBBox = std::make_unique(grid.constTransform().worldToIndexNodeCentered(options.clipBBox));
+ }
+
+ internal::ReadValueMaskBuffersOp readBuffersOp(is, saveFloatAsHalf, tree.background(), clipIndexBBox.get());
+ tools::visitNodesDepthFirst(grid.tree(), readBuffersOp, /*idx=*/0, /*topDown=*/false);
+ }
+
+ void writeBuffers(std::ostream& os, const GridBase& gridBase, const io::WriteOptions&) const final
+ {
+ const GridT& grid = static_cast(gridBase);
+
+ if (grid.hasMultiPassIO()) {
+ OPENVDB_THROW(IoError, "Multi-pass IO is not supported in ValueMaskCodec");
+ }
+
+ internal::WriteValueMaskBuffersOp writeBuffersOp(os, grid.saveFloatAsHalf());
+ tools::visitNodesDepthFirst(grid.tree(), writeBuffersOp);
+ }
+}; // struct ValueMaskCodec
+
+} // namespace codecs
+} // namespace OPENVDB_VERSION_NAME
+} // namespace openvdb
+
+#endif // OPENVDB_IO_CODECS_VALUEMASKCODEC_HAS_BEEN_INCLUDED
diff --git a/openvdb/openvdb/codecs/impl/ScalarLeafCodec.h b/openvdb/openvdb/codecs/impl/ScalarLeafCodec.h
new file mode 100644
index 0000000000..2288f8a5a2
--- /dev/null
+++ b/openvdb/openvdb/codecs/impl/ScalarLeafCodec.h
@@ -0,0 +1,125 @@
+// Copyright Contributors to the OpenVDB Project
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENVDB_IO_CODECS_IMPL_SCALARLEAFCODEC_HAS_BEEN_INCLUDED
+#define OPENVDB_IO_CODECS_IMPL_SCALARLEAFCODEC_HAS_BEEN_INCLUDED
+
+#include
+
+namespace openvdb {
+OPENVDB_USE_VERSION_NAMESPACE
+namespace OPENVDB_VERSION_NAME {
+namespace codecs {
+namespace internal {
+
+template
+void writeScalarLeafBuffers(const LeafT& leaf, std::ostream& os, bool saveFloatAsHalf,
+ const typename LeafT::ValueType* background = nullptr)
+{
+ using NodeMaskT = typename LeafT::NodeMaskType;
+
+ // Write out the value mask.
+ leaf.getValueMask().save(os);
+
+ leaf.buffer().data(); // load values
+
+ io::writeCompressedValues(os, leaf.buffer().data(), LeafT::SIZE,
+ 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 typename StorageLeafT::ValueType* storageBackground = nullptr)
+{
+ using ValueT = typename LeafT::ValueType;
+ using NodeMaskT = typename LeafT::NodeMaskType;
+ using StorageBufferT = typename StorageLeafT::Buffer;
+ using StorageValueT = typename StorageLeafT::ValueType;
+
+ constexpr Index SIZE = LeafT::SIZE;
+
+ SharedPtr meta = io::getStreamMetadataPtr(is);
+ const bool seekable = meta && meta->seekable();
+
+ auto& valueMask = leaf.getValueMask();
+
+ // Load or seek the value mask
+ if (seekable) valueMask.seek(is);
+ else valueMask.load(is);
+
+ // Pre-node-mask-compression format stored the origin and buffer count
+ // inline after the value mask.
+ int8_t numBuffers = 1;
+ if (io::getFormatVersion(is) < OPENVDB_FILE_VERSION_NODE_MASK_COMPRESSION) {
+ Coord origin;
+ is.read(reinterpret_cast(&origin), sizeof(Coord::ValueType) * 3);
+ leaf.setOrigin(origin);
+
+ is.read(reinterpret_cast(&numBuffers), sizeof(int8_t));
+
+ if (numBuffers > 1) {
+ OPENVDB_THROW(IoError, "Old file format 221 (FLOAT_FRUSTUM_BBOX) with multiple buffers is not supported");
+ }
+ }
+
+ if (skip) {
+ if (seekable) {
+ io::readCompressedValues