From 740a69f46e4f1df0df5779ed163f50b3ced0f974 Mon Sep 17 00:00:00 2001 From: Jeremie Dumas Date: Mon, 7 Aug 2023 12:30:15 -0700 Subject: [PATCH 01/22] Don't update timestamp when regenerating instantation files. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérémie Dumas --- openvdb/openvdb/CMakeLists.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openvdb/openvdb/CMakeLists.txt b/openvdb/openvdb/CMakeLists.txt index 1b48e84a70..d4a3bd0a5c 100644 --- a/openvdb/openvdb/CMakeLists.txt +++ b/openvdb/openvdb/CMakeLists.txt @@ -582,14 +582,20 @@ if(USE_EXPLICIT_INSTANTIATION) # Define the source file location - "/instantiations/Activate.cc" set(INSTANTIATE_SOURCE "${CMAKE_CURRENT_BINARY_DIR}/instantiations/${NAME}.cc") + set(INSTANTIATE_INPUT "${CMAKE_CURRENT_BINARY_DIR}/instantiations.in/${NAME}.cc") # The auto-generated file holds the instantiate define and the header # which contains all the functions to be explicitly instantiated. - file(WRITE ${INSTANTIATE_SOURCE} + file(WRITE ${INSTANTIATE_INPUT} "#define OPENVDB_INSTANTIATE_${UPPER_NAME}\n" "#include \n" ) + # Update the actual source file only if the content has been modified. + # Without this step, rerunning CMake would update the timestamp of the + # auto-generated file systematically, retriggering compilation. + configure_file(${INSTANTIATE_INPUT} ${INSTANTIATE_SOURCE} COPYONLY) + # Add to the list of library source files # (prepend as instantiations tend to be slow to compile). list(PREPEND OPENVDB_LIBRARY_SOURCE_FILES ${INSTANTIATE_SOURCE}) From 663948efd666fa7e389c4858935934bd36b18c7c Mon Sep 17 00:00:00 2001 From: Edward Lam Date: Tue, 10 Oct 2023 11:14:36 -0400 Subject: [PATCH 02/22] Fix nullptr dereferences found by gcc 11.4 Signed-off-by: Edward Lam --- openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Convert.cc | 2 +- openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Extrapolate.cc | 3 ++- openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fracture.cc | 2 +- openvdb_houdini/openvdb_houdini/SOP_OpenVDB_To_Polygons.cc | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Convert.cc b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Convert.cc index 702e87b688..888983a40d 100644 --- a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Convert.cc +++ b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Convert.cc @@ -1206,7 +1206,7 @@ SOP_OpenVDB_Convert::Cache::referenceMeshing( typename GridType::ConstPtr grid = openvdb::gridConstPtrCast(*it); if (!grid) { - badTypeList.push_back(grid->getName()); + badTypeList.push_back((*it)->getName()); continue; } diff --git a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Extrapolate.cc b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Extrapolate.cc index 193f008ebd..7d65d87211 100644 --- a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Extrapolate.cc +++ b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Extrapolate.cc @@ -583,7 +583,8 @@ SOP_OpenVDB_Extrapolate::Cache::process( if (parms.mNeedExt) { typename ExtGridT::ConstPtr extGrid = openvdb::gridConstPtrCast(exPrim->getConstGridPtr()); if (!extGrid) { - std::string msg = "Extension grid (" + extGrid->getName() + ") cannot be converted " + + auto grid = exPrim->getConstGridPtr(); + std::string msg = "Extension grid (" + grid->getName() + ") cannot be converted " + "to the explicit type specified."; throw std::runtime_error(msg); } diff --git a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fracture.cc b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fracture.cc index 8e0fbe59de..6e922aff63 100644 --- a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fracture.cc +++ b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Fracture.cc @@ -531,7 +531,7 @@ SOP_OpenVDB_Fracture::Cache::process( residuals.push_back(residual); } else { - badTypeList.push_back(residual->getName()); + badTypeList.push_back((*it)->getName()); continue; } diff --git a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_To_Polygons.cc b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_To_Polygons.cc index 3af1612937..cff8714837 100644 --- a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_To_Polygons.cc +++ b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_To_Polygons.cc @@ -896,7 +896,7 @@ SOP_OpenVDB_To_Polygons::Cache::referenceMeshing( typename GridType::ConstPtr grid = openvdb::gridConstPtrCast(*it); if (!grid) { - badTypeList.push_back(grid->getName()); + badTypeList.push_back((*it)->getName()); continue; } From d71a6d04c42c8a9514efca75eae9cd33165ddb1e Mon Sep 17 00:00:00 2001 From: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> Date: Thu, 12 Oct 2023 11:53:56 +1300 Subject: [PATCH 03/22] Fixed changenotes Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> --- CHANGES | 91 +++++++++++++++++++++++++------------------------ doc/changes.txt | 88 +++++++++++++++++++++++------------------------ 2 files changed, 88 insertions(+), 91 deletions(-) diff --git a/CHANGES b/CHANGES index 1fd526b9bb..de6d6a0763 100644 --- a/CHANGES +++ b/CHANGES @@ -2,62 +2,63 @@ OpenVDB Version History ======================= Version 10.1.0 - October 11, 2023 + + Highlights: + - OpenVDB Python bindings are now implemented using pybind11 instead of + Boost.Python. + [Contributed by Matthew Cong] + OpenVDB: New features: - Added points::replicate() for the replication of PointDataGrid points and attributes. Improvements: - - OpenVDB Python bindings are now implemented using pybind11 instead of Boost.Python. - [Contributed by Matthew Cong] - - Fix int-in-bool-context GCC9+ warnings by switching to use constexpr if. - Upgraded OpenVDBs internal half representation to IMath 3.1.6. Brings conversion support using F16C instructions (if enabled using -mf16c) and the removal of the exponent lookup table in favor of bit shifting. - - LevelSetRebuild includes disabled code that uses this for - resampling level sets - the original grid is used as the true - sign value. However, due to differences between - polygonalization and trilinear interpolation, it cannot be used - directly. The code is provided so we can learn from this - mistake. - - boost::uuid removed from Archive, instead std::random_device is used + - OpenVDBs copy of Half.h is no longer built with an internal lookup table, + but explicitly selects the non-LUT version and disables the creation of + the LUT. This is required to avoid symbol conflicts with different + namespaced OpenVDB builds. + - Removed boost::uuid from Archive, instead std::random_device is used directly to generate UUID-like objects. - Moved all cases of file static/global variables which relied on non-trivial construction into function scopes as static locals. These would previously initialize themselves on start-up in a non-deterministic, compiler-dictated order(static-initialization-order-fiasco). This order is now defined by the program's execution. - - Introduced openvdb::make_index_sequence to solve clang compilations - issues with compiler built-in index_sequence implementations. + - Fixed the constants used in openvdb::math::Coord::hash() and + nanovdb::Coord::hash() to correctly be prime numbers (note that this + changes the result of these methods). + [Contributed by Benedikt Mersch] + - Updated tools::meshToVolume to take two new optional arguments to provide + an interior test oracle and an interior testing method. These allow the + default outside-flood-fill to be replaced if the actual sidedness can be + known. + [Contributed by Tomas Skrivan] + - LevelSetRebuild now includes example code that demonstrates the intended + use of the new meshToVolume interior testing parameters for the + resampling of level sets, where the original grid is used as the true + sign value. However, due to differences between polygonalization and + trilinear interpolation, this behaviour is disabled and exists as a + reference. - Introduced openvdb::TupleList to wrap std::tuple and provide interface interop methods with openvdb::TypeList. - Added OPENVDB_FORCE_INLINE, OPENVDB_LIKELY and OPENVDB_UNLIKELY macros. + - Introduced openvdb::make_index_sequence to solve clang compilations + issues with compiler built-in index_sequence implementations. API changes: - - openvdb/tools/MeshToVolume's meshToVolume takes two - new optional arguments to provide an interior test oracle and an - interior testing methods. These allow the default - outside-flood-fill to be replaced if the actual sidedness can be - known. - [Contributed by Tomas Skrivan] - - PagedArray iterators no longer derive from std::iterator - (but remains standard compliant). - Significant infrastructural change to the ValueAccessor header and implementation. All ValueAccessor specializations have been consolidated into a single class which supports all possible ValueAccessor configurations using index_sequences. Backward compatible declarations have been provided. The new ValueAccessor implementation is marked as final. + - PagedArray iterators no longer derive from std::iterator + (but remains standard compliant). Bug Fixes: - - Fixed a build issue where Boost was not being pulled in when - OPENVDB_USE_DELAYED_LOADING was set to OFF. - - Fixed the constants used in openvdb::math::Coord::hash() and nanovdb::Coord::hash() - to correctly be prime numbers (note that this changes the result of these methods). - [Contributed by Benedikt Mersch] - - Improved support for compiling with C++20. - [Contributed by Denys Maletskyy and Jérémie Dumas] - - OpenVDB's CMake no longer modifies the BUILD_SHARED_LIBS variable. - [Reported by Maksim Shabunin] - Internal counters in tree::RangeIterator were limited to 32bit precision. They are now extended to size_t. [Reported by SpaceX] @@ -70,20 +71,6 @@ Version 10.1.0 - October 11, 2023 - Fixed a bug with LeafNodeBool Topology constructor with designated on/off values which wouldn't apply them correctly. [Reported by @hozhaoea] - - Fixed a compilation error that would be encountered when attempting to - enable the SSE4.2 or AVX SIMD options on non-x86 based platforms. - - The Half.h is no longer built with an internal lookup table, but - explicitly selects the non-LUT version and disables the creation of - a LUT. This is required to avoid symbol conflicts with - different namespaced OpenVDB builds. - - Fixed a compilation issue with the min() and max() methods on Stencils - in openvdb/math/Stencils.h. - [Reported by Samuel Mauch] - - Introduced openvdb::make_index_sequence to solve clang compilations - issues with compiler builtin index_sequence implementations. - - Introduced openvdb::TupleList to wrap std::tuple and provide interface - interop methods with openvdb::TypeList. - - Added OPENVDB_FORCE_INLINE, OPENVDB_LIKELY and OPENVDB_UNLIKELY macros. OpenVDB AX: Improvements: @@ -92,8 +79,6 @@ Version 10.1.0 - October 11, 2023 Bug Fixes: - Fixed a bug in AX on older X86 hardware which could cause a crash when accessing point attributes with half compression (bug introduced in 9.1.0). - - Fixed a build issue with AX on 32-bit platforms. - [Reported by Mathieu Malaterre] - Fixed an incorrect option in the `vdb_ax` command line tool where the default optimization level was set to NONE instead of O3 (issue introduced in 10.0.0). @@ -106,6 +91,22 @@ Version 10.1.0 - October 11, 2023 - Fix a bug in the projection mode of the Advect Points SOP that was causing a segfault. + Build: + - Fixed a build issue where Boost was not being pulled in when + OPENVDB_USE_DELAYED_LOADING was set to OFF. + - Fixed a build issue with AX on 32-bit platforms. + [Reported by Mathieu Malaterre] + - Fixed a compilation issue with the min() and max() methods on Stencils + in openvdb/math/Stencils.h. + [Reported by Samuel Mauch] + - Fixed a compilation error that would be encountered when attempting to + enable the SSE4.2 or AVX SIMD options on non-x86 based platforms. + - Improved support for compiling with C++20. + [Contributed by Denys Maletskyy and Jérémie Dumas] + - OpenVDB's CMake no longer modifies the BUILD_SHARED_LIBS variable. + [Reported by Maksim Shabunin] + - Fix int-in-bool-context GCC9+ warnings by switching to use constexpr if. + Version 10.0.1 - November 30, 2022 Bug Fixes: diff --git a/doc/changes.txt b/doc/changes.txt index c3c7675a1a..857f386906 100644 --- a/doc/changes.txt +++ b/doc/changes.txt @@ -18,56 +18,51 @@ OpenVDB: and attributes. - Improvements: - - OpenVDB Python bindings are now implemented using pybind11 instead of Boost.Python. - [Contributed by Matthew Cong] - - Fix int-in-bool-context GCC9+ warnings by switching to use constexpr if. - Upgraded OpenVDBs internal half representation to IMath 3.1.6. Brings conversion support using F16C instructions (if enabled using -mf16c) and the removal of the exponent lookup table in favor of bit shifting. - - LevelSetRebuild includes disabled code that uses this for - resampling level sets - the original grid is used as the true - sign value. However, due to differences between - polygonalization and trilinear interpolation, it cannot be used - directly. The code is provided so we can learn from this - mistake. - - boost::uuid removed from Archive, instead std::random_device is used + - OpenVDBs copy of Half.h is no longer built with an internal lookup table, + but explicitly selects the non-LUT version and disables the creation of + the LUT. This is required to avoid symbol conflicts with different + namespaced OpenVDB builds. + - Removed boost::uuid from Archive, instead std::random_device is used directly to generate UUID-like objects. - Moved all cases of file static/global variables which relied on non-trivial construction into function scopes as static locals. These would previously - initialize themselves on start-up in a non-deterministic, compiler dictated + initialize themselves on start-up in a non-deterministic, compiler-dictated order(static-initialization-order-fiasco). This order is now defined by the program's execution. - - Introduced openvdb::make_index_sequence to solve clang compilations - issues with compiler built-in index_sequence implementations. + - Fixed the constants used in openvdb::math::Coord::hash() and + nanovdb::Coord::hash() to correctly be prime numbers (note that this + changes the result of these methods). + [Contributed by Benedikt Mersch] + - Updated tools::meshToVolume to take two new optional arguments to provide + an interior test oracle and an interior testing method. These allow the + default outside-flood-fill to be replaced if the actual sidedness can be + known. + [Contributed by Tomas Skrivan] + - LevelSetRebuild now includes example code that demonstrates the intended + use of the new meshToVolume interior testing parameters for the + resampling of level sets, where the original grid is used as the true + sign value. However, due to differences between polygonalization and + trilinear interpolation, this behaviour is disabled and exists as a + reference. - Introduced openvdb::TupleList to wrap std::tuple and provide interface interop methods with openvdb::TypeList. - Added OPENVDB_FORCE_INLINE, OPENVDB_LIKELY and OPENVDB_UNLIKELY macros. + - Introduced openvdb::make_index_sequence to solve clang compilations + issues with compiler built-in index_sequence implementations. - API changes: - - openvdb/tools/MeshToVolume's meshToVolume takes two - new optional arguments to provide an interior test oracle and an - interior testing methods. These allow the default - outside-flood-fill to be replaced if the actual sidedness can be - known. - [Contributed by Tomas Skrivan] - - PagedArray iterators no longer derive from std::iterator - (but remains standard compliant). - Significant infrastructural change to the ValueAccessor header and implementation. All ValueAccessor specializations have been consolidated into a single class which supports all possible ValueAccessor configurations using index_sequences. Backward compatible declarations have been provided. The new ValueAccessor implementation is marked as final. + - PagedArray iterators no longer derive from std::iterator + (but remains standard compliant). - Bug Fixes: - - Fixed a build issue where Boost was not being pulled in when - OPENVDB_USE_DELAYED_LOADING was set to OFF. - - Fixed the constants used in openvdb::math::Coord::hash() and nanovdb::Coord::hash() - to correctly be prime numbers (note that this changes the result of these methods). - [Contributed by Benedikt Mersch] - - Improved support for compiling with C++20. - [Contributed by Denys Maletskyy and Jérémie Dumas] - - OpenVDB's CMake no longer modifies the BUILD_SHARED_LIBS variable. - [Reported by Maksim Shabunin] - Internal counters in tree::RangeIterator were limited to 32bit precision. They are now extended to size_t. [Reported by SpaceX] @@ -80,20 +75,6 @@ OpenVDB: - Fixed a bug with LeafNodeBool Topology constructor with designated on/off values which wouldn't apply them correctly. [Reported by @hozhaoea] - - Fixed a compilation error that would be encountered when attempting to - enable the SSE4.2 or AVX SIMD options on non-x86 based platforms. - - The Half.h is no longer built with an internal lookup table, but - explicitly selects the non-LUT version and disables the creation of - a LUT. This is required to avoid symbol conflicts with - different namespaced OpenVDB builds. - - Fixed a compilation issue with the min() and max() methods on Stencils - in openvdb/math/Stencils.h. - [Reported by Samuel Mauch] - - Introduced openvdb::make_index_sequence to solve clang compilations - issues with compiler builtin index_sequence implementations. - - Introduced openvdb::TupleList to wrap std::tuple and provide interface - interop methods with openvdb::TypeList. - - Added OPENVDB_FORCE_INLINE, OPENVDB_LIKELY and OPENVDB_UNLIKELY macros. @par OpenVDB AX: @@ -103,8 +84,6 @@ OpenVDB AX: - Bug Fixes: - Fixed a bug in AX on older X86 hardware which could cause a crash when accessing point attributes with half compression (bug introduced in 9.1.0). - - Fixed a build issue with AX on 32-bit platforms. - [Reported by Mathieu Malaterre] - Fixed an incorrect option in the `vdb_ax` command line tool where the default optimization level was set to NONE instead of O3 (issue introduced in 10.0.0). @@ -118,6 +97,23 @@ OpenVDB Houdini: - Fix a bug in the projection mode of the Advect Points SOP that was causing a segfault. +@par +Build: +- Fixed a build issue where Boost was not being pulled in when + OPENVDB_USE_DELAYED_LOADING was set to OFF. +- Fixed a build issue with AX on 32-bit platforms. + [Reported by Mathieu Malaterre] +- Fixed a compilation issue with the min() and max() methods on Stencils + in openvdb/math/Stencils.h. + [Reported by Samuel Mauch] +- Fixed a compilation error that would be encountered when attempting to + enable the SSE4.2 or AVX SIMD options on non-x86 based platforms. +- Improved support for compiling with C++20. + [Contributed by Denys Maletskyy and Jérémie Dumas] +- OpenVDB's CMake no longer modifies the BUILD_SHARED_LIBS variable. + [Reported by Maksim Shabunin] +- Fix int-in-bool-context GCC9+ warnings by switching to use constexpr if. + @par @htmlonly @endhtmlonly @par From 9876cdf56e9f8cf7391ace419289f4e16de97b03 Mon Sep 17 00:00:00 2001 From: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> Date: Thu, 5 Oct 2023 23:09:35 +1300 Subject: [PATCH 04/22] Infrastructure changes for VDB 11 Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> --- .github/workflows/ax.yml | 20 +- .github/workflows/build.yml | 25 +- .github/workflows/docs.yml | 4 +- .github/workflows/houdini.yml | 24 +- .github/workflows/weekly.yml | 17 +- CHANGES | 12 + CMakeLists.txt | 11 +- README.md | 134 +++--- ci/build.sh | 2 +- ci/install_cmake.sh | 15 - ci/install_gtest.sh | 18 - ci/install_macos.sh | 40 +- ci/install_macos_ax.sh | 32 -- cmake/FindIlmBase.cmake | 447 ------------------ cmake/FindOpenVDB.cmake | 18 +- cmake/OpenVDBGLFW3Setup.cmake | 7 + cmake/OpenVDBHoudiniSetup.cmake | 21 - cmake/config/OpenVDBVersions.cmake | 46 +- cmake/scripts/ubsan.supp | 3 - doc/build.txt | 21 +- doc/dependencies.txt | 56 +-- doc/img/banner.png | Bin 0 -> 1075494 bytes doc/python.txt | 6 +- openvdb/openvdb/CMakeLists.txt | 35 +- openvdb/openvdb/Platform.h | 6 - openvdb/openvdb/openvdb.cc | 22 +- openvdb/openvdb/points/AttributeArray.h | 2 - openvdb/openvdb/tree/InternalNode.h | 12 - openvdb/openvdb/tree/LeafBuffer.h | 15 +- openvdb/openvdb/tree/LeafNode.h | 12 - openvdb/openvdb/tree/LeafNodeBool.h | 19 - openvdb/openvdb/tree/LeafNodeMask.h | 14 - openvdb/openvdb/tree/RootNode.h | 12 - openvdb/openvdb/unittest/TestLeaf.cc | 2 - openvdb/openvdb/unittest/TestLeafBool.cc | 2 - openvdb/openvdb/unittest/TestLeafMask.cc | 2 - openvdb/openvdb/unittest/TestTree.cc | 4 - openvdb/openvdb/version.h.in | 18 +- .../openvdb_ax/compiler/PointExecutable.cc | 19 - openvdb_cmd/vdb_render/CMakeLists.txt | 34 +- .../openvdb_houdini/SOP_OpenVDB_Merge.cc | 10 - pendingchanges/vdb11.txt | 2 + 42 files changed, 233 insertions(+), 988 deletions(-) delete mode 100755 ci/install_cmake.sh delete mode 100755 ci/install_gtest.sh delete mode 100755 ci/install_macos_ax.sh delete mode 100644 cmake/FindIlmBase.cmake create mode 100644 doc/img/banner.png create mode 100644 pendingchanges/vdb11.txt diff --git a/.github/workflows/ax.yml b/.github/workflows/ax.yml index 083bf70d53..0b8c99463f 100644 --- a/.github/workflows/ax.yml +++ b/.github/workflows/ax.yml @@ -43,6 +43,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +defaults: + run: + shell: bash + jobs: linux-ax: if: | @@ -63,7 +67,7 @@ jobs: config: - { image: '2023-clang15', cxx: 'clang++', build: 'Release', cmake: '' } - { image: '2023-clang15', cxx: 'g++', build: 'Release', cmake: '' } - - { image: '2022-clang11', cxx: 'clang++', build: 'Debug', cmake: '' } + - { image: '2023-clang15', cxx: 'clang++', build: 'Debug', cmake: '' } - { image: '2022-clang11', cxx: 'clang++', build: 'Release', cmake: '' } - { image: '2022-clang11', cxx: 'g++', build: 'Release', cmake: '' } - { image: '2021-clang10', cxx: 'clang++', build: 'Release', cmake: '-DDISABLE_DEPENDENCY_VERSION_CHECKS=ON' } @@ -76,13 +80,12 @@ jobs: run: ./ci/install_pybind11.sh 2.10.0 - name: timestamp id: timestamp - shell: bash - run: echo "::set-output name=timestamp::`date -u +'%Y-%m-%dT%H:%M:%SZ'`" + run: echo "timestamp=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT - name: ccache # don't use ccache for debug builds if: matrix.config.build == 'Release' id: ccache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: /tmp/ccache key: linux-ax${{ matrix.config.image }}-${{ matrix.config.cxx }}-${{ steps.timestamp.outputs.timestamp }} @@ -105,7 +108,6 @@ jobs: # Keep ccache light by stripping out any caches not accessed in the last day - name: ccache_clean if: matrix.config.build == 'Release' - shell: bash run: ccache --evict-older-than 1d macos-ax: @@ -121,14 +123,14 @@ jobs: matrix: config: #@note llvm10 never got its own brew formula... - - { runner: 'macos-11', cxx: 'clang++', build: 'Release', llvm: '11' } - - { runner: 'macos-11', cxx: 'clang++', build: 'Release', llvm: '12' } - - { runner: 'macos-11', cxx: 'clang++', build: 'Release', llvm: '13' } + - { runner: 'macos-latest', cxx: 'clang++', build: 'Release', llvm: '11' } + - { runner: 'macos-latest', cxx: 'clang++', build: 'Release', llvm: '12' } + - { runner: 'macos-latest', cxx: 'clang++', build: 'Release', llvm: '13' } fail-fast: false steps: - uses: actions/checkout@v3 - name: install_deps - run: ./ci/install_macos_ax.sh ${{ matrix.config.llvm }} + run: ./ci/install_macos.sh ${{ matrix.config.llvm }} - name: build run: > ./ci/build.sh -v diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c468c6b4d2..c9b36595c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,15 +77,13 @@ jobs: # @note we specifically use clang15.0 (not clang15) here as the newest # versions of the clang15.X containers have some issues with the GLFW # installation - - { cxx: clang++, image: '2023-clang15.0', abi: '10', build: 'Release', cmake: '' } - - { cxx: g++, image: '2023-clang15.0', abi: '10', build: 'Release', cmake: '' } + - { cxx: clang++, image: '2023-clang15.0', abi: '11', build: 'Release', cmake: '' } + - { cxx: g++, image: '2023-clang15.0', abi: '11', build: 'Release', cmake: '' } + - { cxx: clang++, image: '2022-clang11', abi: '11', build: 'Debug', cmake: '' } - { cxx: clang++, image: '2022-clang11', abi: '10', build: 'Release', cmake: '' } - - { cxx: clang++, image: '2022-clang11', abi: '10', build: 'Debug' , cmake: '' } - { cxx: g++, image: '2022-clang11', abi: '10', build: 'Release', cmake: '' } - - { cxx: clang++, image: '2022-clang11', abi: '9', build: 'Release', cmake: '' } - - { cxx: g++, image: '2022-clang11', abi: '9', build: 'Release', cmake: '' } - - { cxx: clang++, image: '2021', abi: '8', build: 'Release', cmake: '-DDISABLE_DEPENDENCY_VERSION_CHECKS=ON' } - - { cxx: g++, image: '2021', abi: '8', build: 'Release', cmake: '-DDISABLE_DEPENDENCY_VERSION_CHECKS=ON' } + - { cxx: clang++, image: '2022-clang11', abi: '9', build: 'Release', cmake: '-DDISABLE_DEPENDENCY_VERSION_CHECKS=ON' } + - { cxx: g++, image: '2022-clang11', abi: '9', build: 'Release', cmake: '-DDISABLE_DEPENDENCY_VERSION_CHECKS=ON' } fail-fast: false steps: - uses: actions/checkout@v3 @@ -94,12 +92,12 @@ jobs: run: ./ci/install_pybind11.sh 2.10.0 - name: timestamp id: timestamp - run: echo "::set-output name=timestamp::`date -u +'%Y-%m-%dT%H:%M:%SZ'`" + run: echo "timestamp=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT - name: ccache # don't use ccache for debug builds if: matrix.config.build == 'Release' id: ccache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: /tmp/ccache key: linux-vfx${{ matrix.config.image }}-abi${{ matrix.config.abi }}-${{ matrix.config.cxx }}-${{ steps.timestamp.outputs.timestamp }} @@ -188,24 +186,17 @@ jobs: github.event_name != 'workflow_dispatch' || github.event.inputs.type == 'all' || github.event.inputs.type == 'mac' - runs-on: macos-11 + runs-on: macos-latest env: CXX: clang++ steps: - uses: actions/checkout@v3 - name: install - # brew boost-python3 installs a "Keg-only" version of python which is - # not installed to PATH. We must manually provide the location of the - # required python installation to CMake through a hint variable which - # is exported in install_macos.sh run: ./ci/install_macos.sh - name: build - # Also need to disable compiler warnings for ABI 6 and above due to - # the version of clang installed run: > ./ci/build.sh -v --build-type=Release --components=\"core,python,bin,view,render,test\" - --cargs=\"-DOPENVDB_CXX_STRICT=OFF -DOPENVDB_ABI_VERSION_NUMBER=10 -DOPENVDB_SIMD=SSE42\" - name: test run: cd build && ctest -V diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 24a63e5dc3..d3febe0d76 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -105,13 +105,11 @@ jobs: github.event.inputs.deploy == 'coverage' runs-on: ubuntu-latest container: - image: aswf/ci-openvdb:2022 + image: aswf/ci-openvdb:2023 env: CXX: g++ steps: - uses: actions/checkout@v3 - - name: install_gtest - run: ./ci/install_gtest.sh 1.10.0 - name: install_gcovr run: pip install gcovr - name: build diff --git a/.github/workflows/houdini.yml b/.github/workflows/houdini.yml index 43fb6ed441..3f6b539ece 100644 --- a/.github/workflows/houdini.yml +++ b/.github/workflows/houdini.yml @@ -36,6 +36,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +defaults: + run: + shell: bash + jobs: checksecret: # Check that valid github secrets have been set. This isn't needed to retrieve @@ -49,7 +53,7 @@ jobs: env: HOUDINI_CLIENT_ID: ${{ secrets.HOUDINI_CLIENT_ID }} HOUDINI_SECRET_KEY: ${{ secrets.HOUDINI_SECRET_KEY }} - run: echo "::set-output name=HOUDINI_SECRETS::${{ env.HOUDINI_CLIENT_ID != '' && env.HOUDINI_SECRET_KEY != '' }}" + run: echo "HOUDINI_SECRETS=${{ env.HOUDINI_CLIENT_ID != '' && env.HOUDINI_SECRET_KEY != '' }}" >> $GITHUB_OUTPUT - name: Skip Next Jobs if: steps.check.outputs.HOUDINI_SECRETS != 'true' run: echo "HOUDINI_CLIENT_ID and HOUDINI_SECRET_KEY GitHub Action Secrets needs to be set to install Houdini builds" @@ -69,11 +73,10 @@ jobs: strategy: matrix: config: - - { cxx: clang++, image: '2021', hou: '19_5', j: '8', build: 'Release', components: 'core,hou,bin,view,render,python,test,axcore,axbin,axtest', disable_checks: 'OFF' } - # Houdini 19 is technically on VFX 2020, but we need 2021 dependencies for VDB 10 - - { cxx: clang++, image: '2021', hou: '19_0', j: '8', build: 'Release', components: 'core,hou,bin,view,render,python,test,axcore,axbin,axtest', disable_checks: 'ON' } - - { cxx: clang++, image: '2021', hou: '19_5', j: '8', build: 'Debug', components: 'core,hou', disable_checks: 'OFF' } - - { cxx: g++, image: '2021', hou: '19_5', j: '8', build: 'Release', components: 'core,hou', disable_checks: 'OFF' } + #- { cxx: clang++, image: '2022', hou: '20_0', build: 'Release', components: 'core,hou,bin,view,render,python,test,axcore,axbin,axtest' } + - { cxx: clang++, image: '2021', hou: '19_5', build: 'Release', components: 'core,hou,bin,view,render,python,test,axcore,axbin,axtest' } + #- { cxx: clang++, image: '2022', hou: '20_0', build: 'Debug', components: 'core,hou' } + #- { cxx: g++, image: '2022', hou: '20_0', build: 'Release', components: 'core,hou' } fail-fast: false steps: - uses: actions/checkout@v3 @@ -82,19 +85,18 @@ jobs: run: ./ci/install_pybind11.sh 2.10.0 - name: timestamp id: timestamp - shell: bash - run: echo "::set-output name=timestamp::`date -u +'%Y-%m-%dT%H:%M:%SZ'`" + run: echo "timestamp=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT - name: ccache # don't use ccache for debug builds if: matrix.config.build == 'Release' id: ccache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: /tmp/ccache key: linux-vfx-hou${{ matrix.config.hou }}-${{ matrix.config.image }}-${{ matrix.config.cxx }}-${{ steps.timestamp.outputs.timestamp }} restore-keys: linux-vfx-hou${{ matrix.config.hou }}-${{ matrix.config.image }}-${{ matrix.config.cxx }}- - name: fetch_houdini - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: hou key: dummy-houdini${{ matrix.config.hou }}-${{ steps.timestamp.outputs.timestamp }} @@ -110,7 +112,6 @@ jobs: cp hou/hou.tar.gz $HOME/houdini_install/hou.tar.gz cd $HOME/houdini_install && tar -xzf hou.tar.gz && cd - - name: build - shell: bash run: | export HFS="$HOME/houdini_install/hou" export HDSO="${HFS}/dsolib" @@ -121,7 +122,6 @@ jobs: # Keep ccache light by stripping out any caches not accessed in the last day - name: ccache_clean if: matrix.config.build == 'Release' - shell: bash run: ccache --evict-older-than 1d # Delete the houdini tarball so that this dummy cache occupies no space - name: delete_hou diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index 2644e45a14..c7e9a8333f 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -42,7 +42,7 @@ jobs: env: HOUDINI_CLIENT_ID: ${{ secrets.HOUDINI_CLIENT_ID }} HOUDINI_SECRET_KEY: ${{ secrets.HOUDINI_SECRET_KEY }} - run: echo "::set-output name=HOUDINI_SECRETS::${{ env.HOUDINI_CLIENT_ID != '' && env.HOUDINI_SECRET_KEY != '' }}" + run: echo "HOUDINI_SECRETS=${{ env.HOUDINI_CLIENT_ID != '' && env.HOUDINI_SECRET_KEY != '' }}" >> $GITHUB_OUTPUT - name: Skip Next Jobs if: steps.check.outputs.HOUDINI_SECRETS != 'true' run: echo "HOUDINI_CLIENT_ID and HOUDINI_SECRET_KEY GitHub Action Secrets needs to be set to install Houdini builds" @@ -67,8 +67,8 @@ jobs: strategy: matrix: config: - - { houdini_version: '19.0', houdini_version_str: '19_0' } - { houdini_version: '19.5', houdini_version_str: '19_5' } + #- { houdini_version: '20.0', houdini_version_str: '20_0' } fail-fast: false container: image: aswf/ci-base:2023 @@ -76,7 +76,7 @@ jobs: - uses: actions/checkout@v3 - name: timestamp id: timestamp - run: echo "::set-output name=timestamp::`date -u +'%Y-%m-%dT%H:%M:%SZ'`" + run: echo "timestamp=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT - name: download_houdini run: ./ci/download_houdini.sh ${{ matrix.config.houdini_version }} ON - name: install_houdini @@ -85,7 +85,7 @@ jobs: cp hou/hou.tar.gz $HOME/houdini_install/hou.tar.gz cd $HOME/houdini_install && tar -xzf hou.tar.gz && cd - - name: write_houdini_cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: hou key: vdb-v5-houdini${{ matrix.config.houdini_version_str }}-${{ steps.timestamp.outputs.timestamp }} @@ -104,7 +104,7 @@ jobs: runs-on: ${{ (github.repository_owner == 'AcademySoftwareFoundation' && 'ubuntu-20.04-8c-32g-300h') || 'ubuntu-latest' }} name: linux-extra:${{ matrix.config.name }} container: - image: aswf/ci-openvdb:2022-clang14 + image: aswf/ci-openvdb:2023-clang15 env: CXX: clang++ strategy: @@ -158,8 +158,7 @@ jobs: if [ "$RUNNER_OS" == "Linux" ]; then sudo apt-get -q install -y libboost-dev libboost-iostreams-dev libtbb-dev libblosc-dev llvm-dev libgtest-dev libcppunit-dev pybind11-dev elif [ "$RUNNER_OS" == "macOS" ]; then - ./ci/install_macos_ax.sh 15 - brew install googletest + ./ci/install_macos.sh 15 else echo "$RUNNER_OS not supported"; exit 1 fi @@ -243,7 +242,7 @@ jobs: github.event_name != 'workflow_dispatch' || github.event.inputs.type == 'all' || github.event.inputs.type == 'ax' - runs-on: macos-11 + runs-on: macos-latest name: macos-cxx:${{ matrix.config.cxx }}-llvm:${{ matrix.config.llvm }}-${{ matrix.config.build }} env: CXX: ${{ matrix.config.cxx }} @@ -257,7 +256,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: install_deps - run: ./ci/install_macos_ax.sh ${{ matrix.config.llvm }} + run: ./ci/install_macos.sh ${{ matrix.config.llvm }} - name: build run: > ./ci/build.sh -v diff --git a/CHANGES b/CHANGES index de6d6a0763..7fd5c5b343 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,13 @@ OpenVDB Version History ======================= +Version 11.0.0 - In Progress + + This version introduces ABI changes relative to older major releases, + so to preserve ABI compatibility it might be necessary to define the + macro OPENVDB_ABI_VERSION_NUMBER=N, where, for example, N is 9 for + Houdini 19.5 and 10 for Houdini 20.0. + Version 10.1.0 - October 11, 2023 Highlights: @@ -118,6 +125,11 @@ Version 10.0.1 - November 30, 2022 Version 10.0.0 - October 27, 2022 + This version introduces ABI changes relative to older major releases, + so to preserve ABI compatibility it might be necessary to define the + macro OPENVDB_ABI_VERSION_NUMBER=N, where, for example, N is 8 for + Houdini 19.0 and 9 for Houdini 19.5. + Highlights: - Introducing OpenVDBLink, which provides a Mathematica interface to OpenVDB. This link ports over access to various grid containers including diff --git a/CMakeLists.txt b/CMakeLists.txt index ba033ea643..8329dda947 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,8 +52,8 @@ endif() ###### Version -set(OpenVDB_MAJOR_VERSION 10) -set(OpenVDB_MINOR_VERSION 1) +set(OpenVDB_MAJOR_VERSION 11) +set(OpenVDB_MINOR_VERSION 0) set(OpenVDB_PATCH_VERSION 0) set(OpenVDB_VERSION "${OpenVDB_MAJOR_VERSION}.${OpenVDB_MINOR_VERSION}.${OpenVDB_PATCH_VERSION}") @@ -89,8 +89,8 @@ cmake_dependent_option(OPENVDB_INSTALL_CMAKE_MODULES option(USE_HOUDINI [=[ Build the library against a Houdini installation. Turns on automatically if OPENVDB_BUILD_HOUDINI_PLUGIN is enabled. -When enabled, you do not need to provide dependency locations for TBB, Blosc, IlmBase and OpenEXR. Boost must be -provided. IlmBase/OpenEXR can optionally be provided if Houdini Version >= 17.5.]=] OFF) +When enabled, you do not need to provide dependency locations for TBB, Blosc, Imath and OpenEXR. Boost must be +provided. Imath/OpenEXR can optionally be provided.]=] OFF) option(USE_MAYA [=[ Build the library against a Maya installation. Turns on automatically if OPENVDB_BUILD_MAYA_PLUGIN is enabled. When enabled, you do not need to provide dependency locations for TBB. All other dependencies must be provided.]=] OFF) @@ -281,13 +281,11 @@ enable_testing() list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") # Add cmake modules to installation command -# @todo fix our glew cmake module if(OPENVDB_INSTALL_CMAKE_MODULES) set(OPENVDB_CMAKE_MODULES cmake/FindBlosc.cmake cmake/FindJemalloc.cmake - cmake/FindIlmBase.cmake cmake/FindLog4cplus.cmake cmake/FindOpenEXR.cmake cmake/FindOpenVDB.cmake @@ -406,7 +404,6 @@ endif() if(USE_STATIC_DEPENDENCIES) set(BLOSC_USE_STATIC_LIBS ON) set(OPENEXR_USE_STATIC_LIBS ON) - set(ILMBASE_USE_STATIC_LIBS ON) set(TBB_USE_STATIC_LIBS ON) set(LOG4CPLUS_USE_STATIC_LIBS ON) set(JEMALLOC_USE_STATIC_LIBS ON) diff --git a/README.md b/README.md index 33e524ad8d..b70edb4064 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,74 @@ -![OpenVDB](https://www.openvdb.org/images/openvdb_logo.png) +![OpenVDB](doc/img/banner.png) -[![License](https://img.shields.io/github/license/AcademySoftwareFoundation/openvdb)](LICENSE) -[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/2774/badge)](https://bestpractices.coreinfrastructure.org/projects/2774) -[![Slack](https://slack.aswf.io/badge.svg)](https://slack.aswf.io/) +| OpenVDB | AX | Nano | Houdini | License | CII | +| :----: | :----: | :----: | :----: | :-----: | :-: | +| [![core](https://github.com/AcademySoftwareFoundation/openvdb/actions/workflows/build.yml/badge.svg)](https://github.com/AcademySoftwareFoundation/openvdb/actions/workflows/build.yml) | [![ax](https://github.com/AcademySoftwareFoundation/openvdb/actions/workflows/ax.yml/badge.svg)](https://github.com/AcademySoftwareFoundation/openvdb/actions/workflows/ax.yml) | [![nano](https://github.com/AcademySoftwareFoundation/openvdb/actions/workflows/nanovdb.yml/badge.svg)](https://github.com/AcademySoftwareFoundation/openvdb/actions/workflows/nanovdb.yml) | [![hou](https://github.com/AcademySoftwareFoundation/openvdb/actions/workflows/houdini.yml/badge.svg)](https://github.com/AcademySoftwareFoundation/openvdb/actions/workflows/houdini.yml) | [![License](https://img.shields.io/github/license/AcademySoftwareFoundation/openvdb)](LICENSE) | [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/2774/badge)](https://bestpractices.coreinfrastructure.org/projects/2774) | - -| OpenVDB | AX | Nano | Houdini | -| :----: | :----: | :----: | :----: | -| [![core](https://github.com/AcademySoftwareFoundation/openvdb/actions/workflows/build.yml/badge.svg)](https://github.com/AcademySoftwareFoundation/openvdb/actions/workflows/build.yml) | [![ax](https://github.com/AcademySoftwareFoundation/openvdb/actions/workflows/ax.yml/badge.svg)](https://github.com/AcademySoftwareFoundation/openvdb/actions/workflows/ax.yml) | [![nano](https://github.com/AcademySoftwareFoundation/openvdb/actions/workflows/nanovdb.yml/badge.svg)](https://github.com/AcademySoftwareFoundation/openvdb/actions/workflows/nanovdb.yml) | [![hou](https://github.com/AcademySoftwareFoundation/openvdb/actions/workflows/houdini.yml/badge.svg)](https://github.com/AcademySoftwareFoundation/openvdb/actions/workflows/houdini.yml) | +------------------------------------------------------------------------------- [Website](https://www.openvdb.org) | [Discussion Forum](https://github.com/AcademySoftwareFoundation/openvdb/discussions) | -[Documentation](https://www.openvdb.org/documentation/doxygen) - -OpenVDB is an open source C++ library comprising a novel hierarchical data structure and a large suite of tools for the efficient storage and manipulation of sparse volumetric data discretized on three-dimensional grids. It was developed by DreamWorks Animation for use in volumetric applications typically encountered in feature film production. +[Documentation](https://www.openvdb.org/documentation/doxygen) | +[Releases](https://github.com/AcademySoftwareFoundation/openvdb/releases) | +[License](https://www.mozilla.org/MPL/2.0) | +[Slack](https://slack.aswf.io/) +OpenVDB is an open source C++ library comprising a novel hierarchical data +structure and a large suite of tools for the efficient storage and manipulation +of sparse volumetric data discretized on three-dimensional grids. It was +developed by DreamWorks Animation for use in volumetric applications typically +encountered in feature film production. ### Development Repository -This GitHub repository hosts the trunk of the OpenVDB development. This implies that it is the newest public version with the latest features and bug fixes. However, it also means that it has not undergone a lot of testing and is generally less stable than the [production releases](https://github.com/AcademySoftwareFoundation/openvdb/releases). - +This GitHub repository hosts the trunk of the OpenVDB development. This implies +that it is the newest public version with the latest features and bug fixes. +However, it also means that it has not undergone a lot of testing and is +generally less stable than the [production releases](https://github.com/AcademySoftwareFoundation/openvdb/releases). ### License -OpenVDB is released under the [Mozilla Public License Version 2.0](https://www.mozilla.org/MPL/2.0/), which is a free, open source software license developed and maintained by the Mozilla Foundation. +OpenVDB is released under the [Mozilla Public License Version 2.0](https://www.mozilla.org/MPL/2.0/), +which is a free, open source software license developed and maintained by the +Mozilla Foundation. -The trademarks of any contributor to this project may not be used in association with the project without the contributor's express permission. +The trademarks of any contributor to this project may not be used in +association with the project without the contributor's express permission. ### Contributing -OpenVDB welcomes contributions to the OpenVDB project. Please refer to the [contribution guidelines](CONTRIBUTING.md) for details on how to make a contribution. +OpenVDB welcomes contributions to the OpenVDB project. Please refer to the +[contribution guidelines](CONTRIBUTING.md) for details on how to make a +contribution. + +------------------------------------------------------------------------------- ### Developer Quick Start -The following provides basic installation examples for the core OpenVDB library. Other components, such as the python module, OpenVDB AX, NanoVDB and various executables, may require additional dependencies. See the [build documentation](https://www.openvdb.org/documentation/doxygen/build.html) for help with installations. +The following provides basic installation examples for the core OpenVDB library. +Other components, such as the python module, OpenVDB AX, NanoVDB and various +executables, may require additional dependencies. See the +[build documentation](https://www.openvdb.org/documentation/doxygen/build.html) +for help with installations. -#### Linux -##### Installing Dependencies (Boost, TBB, Blosc) +##### Linux/MacOS ```bash +# Linux # @note If your distribution does not have required versions, consider using # apt pinning. See the dependency documentation for more details. apt-get install -y libboost-iostreams-dev apt-get install -y libtbb-dev apt-get install -y libblosc-dev -``` -##### Building OpenVDB -```bash -git clone git@github.com:AcademySoftwareFoundation/openvdb.git -cd openvdb -mkdir build -cd build -cmake .. -make -j4 && make install -``` -#### macOS -##### Installing Dependencies (Boost, TBB, Blosc) -```bash +# MacOS +# @note We are using homebrew in this example to install requried dependencies +# https://brew.sh/ brew install boost brew install tbb brew install c-blosc ``` -##### Building OpenVDB + ```bash git clone git@github.com:AcademySoftwareFoundation/openvdb.git cd openvdb @@ -71,14 +77,16 @@ cd build cmake .. make -j4 && make install ``` -#### Windows -##### Installing Dependencies (Boost, TBB, Blosc) + +##### Windows Note that the following commands have only been tested for 64bit systems/libraries. It is recommended to set the `VCPKG_DEFAULT_TRIPLET` environment variable to `x64-windows` to use 64-bit libraries by default. You will also require -[Git](https://git-scm.com/downloads), [vcpkg](https://github.com/microsoft/vcpkg) -and [CMake](https://cmake.org/download/) to be installed. +[Visual Studio](https://visualstudio.microsoft.com/downloads/) (for the MSVC C++ +runtime and compiler toolchains), [CMake](https://cmake.org/download/) and optionally +[vcpkg](https://github.com/microsoft/vcpkg) for the installation of OpenVDB's +dependencies. ```bash vcpkg install zlib:x64-windows @@ -87,10 +95,9 @@ vcpkg install tbb:x64-windows vcpkg install boost-iostreams:x64-windows vcpkg install boost-any:x64-windows vcpkg install boost-algorithm:x64-windows -vcpkg install boost-uuid:x64-windows vcpkg install boost-interprocess:x64-windows ``` -##### Building OpenVDB + ```bash git clone git@github.com:AcademySoftwareFoundation/openvdb.git cd openvdb @@ -100,42 +107,19 @@ cmake -DCMAKE_TOOLCHAIN_FILE=\scripts\buildsystems\vcpkg.cmake -D cmake --build . --parallel 4 --config Release --target install ``` -#### Building OpenVDB AX +#### Building OpenVDB AX and NanoVDB -OpenVDB AX depends on the core OpenVDB library. See the [build documentation](https://www.openvdb.org/documentation/doxygen/build.html) for all available AX component options: +OpenVDB AX depends on the core OpenVDB library. NanoVDB can be built with and +without OpenVDB support. Note that NanoVDB has its own build instructuins, see +the [NanoVDB build documentation](https://www.openvdb.org/documentation/doxygen/NanoVDB_HowToBuild.html) +for details. -```bash -git clone git@github.com:AcademySoftwareFoundation/openvdb.git -cd openvdb -mkdir build -cd build -cmake -DOPENVDB_BUILD_AX=ON .. -make -j4 && make install -``` - -#### Building NanoVDB +The following variables can be passed to the `cmake` configure command. There +are more optional VDB components, see the [build documentation](https://www.openvdb.org/documentation/doxygen/build.html) +for a complete list. -NanoVDB can be built with and without OpenVDB support. To see full build instructions -see the [NanoVDB build documentation](https://www.openvdb.org/documentation/doxygen/NanoVDB_HowToBuild.html) - -#### Building Without OpenVDB Support - -```bash -git clone git@github.com:AcademySoftwareFoundation/openvdb.git -cd openvdb/nanovdb/nanovdb # Build from the subdirectory -mkdir build -cd build -cmake .. -make -j4 && make install -``` - -#### Building With OpenVDB Support - -```bash -git clone git@github.com:AcademySoftwareFoundation/openvdb.git -cd openvdb -mkdir build -cd build -cmake -DOPENVDB_BUILD_NANOVDB=ON .. -make -j4 && make install -``` +| Option | Details | +| :---- | :----- | +| `-D OPENVDB_BUILD_AX=ON` | to enable OpenVDB AX | +| `-D OPENVDB_BUILD_NANOVDB=ON` | to enable NanoVDB | +| `-D NANOVDB_USE_OPENVDB=ON` | to use OpenVDB in NanoVDB | diff --git a/ci/build.sh b/ci/build.sh index 68bb493903..7e35797a14 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -176,8 +176,8 @@ set -x # - always enabled the python tests with OPENVDB_BUILD_PYTHON_UNITTESTS if the python module is in use, # regardless of the 'test' component being enabled or not (see the OPENVDB_BUILD_PYTHON_UNITTESTS option). cmake \ - -DOPENVDB_USE_DEPRECATED_ABI_8=ON \ -DOPENVDB_USE_DEPRECATED_ABI_9=ON \ + -DOPENVDB_USE_DEPRECATED_ABI_10=ON \ -DOPENVDB_BUILD_VDB_PRINT=ON \ -DOPENVDB_BUILD_VDB_LOD=ON \ -DOPENVDB_BUILD_VDB_TOOL=ON \ diff --git a/ci/install_cmake.sh b/ci/install_cmake.sh deleted file mode 100755 index 9fc6607b6e..0000000000 --- a/ci/install_cmake.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -CMAKE_VERSION="$1" -CMAKE_PACKAGE=cmake-${CMAKE_VERSION}-Linux-x86_64 - -wget https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/${CMAKE_PACKAGE}.tar.gz - -tar -xf ${CMAKE_PACKAGE}.tar.gz -cp ${CMAKE_PACKAGE}/bin/* /usr/local/bin/ -cp -r ${CMAKE_PACKAGE}/share/* /usr/local/share/ - -rm -rf ${CMAKE_PACKAGE} -rm -rf ${CMAKE_PACKAGE}.tar.gz diff --git a/ci/install_gtest.sh b/ci/install_gtest.sh deleted file mode 100755 index d9059b2a7e..0000000000 --- a/ci/install_gtest.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -GTEST_VERSION="$1" - -git clone https://github.com/google/googletest.git -cd googletest - -if [ "$GTEST_VERSION" != "latest" ]; then - git checkout release-${GTEST_VERSION} -b v${GTEST_VERSION} -fi - -mkdir build -cd build -cmake ../. -make -j8 -make install diff --git a/ci/install_macos.sh b/ci/install_macos.sh index 77d5bfd55b..20a2d6f4ad 100755 --- a/ci/install_macos.sh +++ b/ci/install_macos.sh @@ -3,27 +3,17 @@ set -x brew update - -if [ ! -z $2 ]; then - if [[ $2 == "gcc"* || $2 == "llvm"* ]]; then - brew install $2 - else - # don't silently succeed - echo "Unknown compiler type/version for second argument to install_macos.sh: $2" - exit -1 - fi -fi - brew install bash gnu-getopt # for CI scripts -brew install cmake brew install boost -brew install pybind11 # also installs the dependent python version -brew install zlib +brew install c-blosc +brew install cmake brew install glfw brew install googletest -brew install c-blosc brew install jq # for trivial parsing of brew json +brew install openexr +brew install pybind11 # also installs the dependent python version brew install tbb +brew install zlib # Alias python version installed by pybind11 to path py_version=$(brew info pybind11 --json | \ @@ -36,15 +26,13 @@ echo "/usr/local/opt/$py_version/bin" >> $GITHUB_PATH # use gnu-getopt echo "/usr/local/opt/gnu-getopt/bin" >> $GITHUB_PATH -LATEST=$1 -if [ "$LATEST" == "latest" ]; then - brew install openexr -else - brew install ilmbase - brew install openexr@2 - - # Export OpenEXR paths which are no longer installed to /usr/local (as v2.x is deprecated) - echo "IlmBase_ROOT=/usr/local/opt/ilmbase" >> $GITHUB_ENV - echo "OpenEXR_ROOT=/usr/local/opt/openexr@2" >> $GITHUB_ENV - echo "/usr/local/opt/openexr@2/bin" >> $GITHUB_PATH +LLVM_VERSION=$1 +if [ ! -z "$LLVM_VERSION" ]; then + if [ "$LLVM_VERSION" == "latest" ]; then + brew install llvm + brew install cppunit + else + brew install llvm@$LLVM_VERSION + brew install cppunit + fi fi diff --git a/ci/install_macos_ax.sh b/ci/install_macos_ax.sh deleted file mode 100755 index 00c41b466d..0000000000 --- a/ci/install_macos_ax.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash - -# Download and install deps from homebrew on macos - -brew update -brew install bash gnu-getopt # for CI scripts -brew install cmake -brew install boost -brew install pybind11 # also installs the dependent python version -brew install cppunit -brew install c-blosc -brew install zlib -brew install jq # for trivial parsing of brew json -brew install tbb - -# Alias python version installed by pybind11 to path -py_version=$(brew info pybind11 --json | \ - jq -cr '.[].dependencies[] | select(. | startswith("python"))') -echo "Using python $py_version" -# export for subsequent action steps (note, not exported for this env) -echo "Python_ROOT_DIR=/usr/local/opt/$py_version" >> $GITHUB_ENV -echo "/usr/local/opt/$py_version/bin" >> $GITHUB_PATH - -# use gnu-getopt -echo "/usr/local/opt/gnu-getopt/bin" >> $GITHUB_PATH - -LLVM_VERSION=$1 -if [ "$LLVM_VERSION" == "latest" ]; then - brew install llvm -else - brew install llvm@$LLVM_VERSION -fi diff --git a/cmake/FindIlmBase.cmake b/cmake/FindIlmBase.cmake deleted file mode 100644 index 8237ac76ea..0000000000 --- a/cmake/FindIlmBase.cmake +++ /dev/null @@ -1,447 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: MPL-2.0 -# -#[=======================================================================[.rst: - -FindIlmBase ------------ - -Find IlmBase include dirs and libraries - -Use this module by invoking find_package with the form:: - - find_package(IlmBase - [version] [EXACT] # Minimum or EXACT version - [REQUIRED] # Fail with error if IlmBase is not found - [COMPONENTS ...] # IlmBase libraries by their canonical name - # e.g. "Half" for "libHalf" - ) - -IMPORTED Targets -^^^^^^^^^^^^^^^^ - -``IlmBase::Half`` - The Half library target. -``IlmBase::Iex`` - The Iex library target. -``IlmBase::IexMath`` - The IexMath library target. -``IlmBase::IlmThread`` - The IlmThread library target. -``IlmBase::Imath`` - The Imath library target. - -Result Variables -^^^^^^^^^^^^^^^^ - -This will define the following variables: - -``IlmBase_FOUND`` - True if the system has the IlmBase library. -``IlmBase_VERSION`` - The version of the IlmBase library which was found. -``IlmBase_INCLUDE_DIRS`` - Include directories needed to use IlmBase. -``IlmBase_RELEASE_LIBRARIES`` - Libraries needed to link to the release version of IlmBase. -``IlmBase_RELEASE_LIBRARY_DIRS`` - IlmBase release library directories. -``IlmBase_DEBUG_LIBRARIES`` - Libraries needed to link to the debug version of IlmBase. -``IlmBase_DEBUG_LIBRARY_DIRS`` - IlmBase debug library directories. -``IlmBase_{COMPONENT}_FOUND`` - True if the system has the named IlmBase component. - -Deprecated - use [RELEASE|DEBUG] variants: - -``IlmBase_LIBRARIES`` - Libraries needed to link to IlmBase. -``IlmBase_LIBRARY_DIRS`` - IlmBase library directories. - -Cache Variables -^^^^^^^^^^^^^^^ - -The following cache variables may also be set: - -``IlmBase_INCLUDE_DIR`` - The directory containing ``IlmBase/config-auto.h``. -``IlmBase_{COMPONENT}_LIBRARY`` - Individual component libraries for IlmBase. may include target_link_libraries() debug/optimized keywords. -``IlmBase_{COMPONENT}_LIBRARY_RELEASE`` - Individual component libraries for IlmBase release -``IlmBase_{COMPONENT}_LIBRARY_DEBUG`` - Individual component libraries for IlmBase debug - -Hints -^^^^^ - -Instead of explicitly setting the cache variables, the following variables -may be provided to tell this module where to look. - -``IlmBase_ROOT`` - Preferred installation prefix. -``ILMBASE_INCLUDEDIR`` - Preferred include directory e.g. /include -``ILMBASE_LIBRARYDIR`` - Preferred library directory e.g. /lib -``ILMBASE_DEBUG_SUFFIX`` - Suffix of the debug version of ilmbase libs. Defaults to "_d". -``SYSTEM_LIBRARY_PATHS`` - Global list of library paths intended to be searched by and find_xxx call -``ILMBASE_USE_STATIC_LIBS`` - Only search for static ilmbase libraries -``DISABLE_CMAKE_SEARCH_PATHS`` - Disable CMakes default search paths for find_xxx calls in this module - -#]=======================================================================] - -cmake_minimum_required(VERSION 3.18) -include(GNUInstallDirs) - - -mark_as_advanced( - IlmBase_INCLUDE_DIR - IlmBase_LIBRARY -) - -set(_FIND_ILMBASE_ADDITIONAL_OPTIONS "") -if(DISABLE_CMAKE_SEARCH_PATHS) - set(_FIND_ILMBASE_ADDITIONAL_OPTIONS NO_DEFAULT_PATH) -endif() - -set(_ILMBASE_COMPONENT_LIST - Half - Iex - IexMath - IlmThread - Imath -) - -if(IlmBase_FIND_COMPONENTS) - set(ILMBASE_COMPONENTS_PROVIDED TRUE) - set(_IGNORED_COMPONENTS "") - foreach(COMPONENT ${IlmBase_FIND_COMPONENTS}) - if(NOT ${COMPONENT} IN_LIST _ILMBASE_COMPONENT_LIST) - list(APPEND _IGNORED_COMPONENTS ${COMPONENT}) - endif() - endforeach() - - if(_IGNORED_COMPONENTS) - message(STATUS "Ignoring unknown components of IlmBase:") - foreach(COMPONENT ${_IGNORED_COMPONENTS}) - message(STATUS " ${COMPONENT}") - endforeach() - list(REMOVE_ITEM IlmBase_FIND_COMPONENTS ${_IGNORED_COMPONENTS}) - endif() -else() - set(ILMBASE_COMPONENTS_PROVIDED FALSE) - set(IlmBase_FIND_COMPONENTS ${_ILMBASE_COMPONENT_LIST}) -endif() - -# Set _ILMBASE_ROOT based on a user provided root var. Xxx_ROOT and ENV{Xxx_ROOT} -# are prioritised over the legacy capitalized XXX_ROOT variables for matching -# CMake 3.12 behaviour -# @todo deprecate -D and ENV ILMBASE_ROOT from CMake 3.12 -if(IlmBase_ROOT) - set(_ILMBASE_ROOT ${IlmBase_ROOT}) -elseif(DEFINED ENV{IlmBase_ROOT}) - set(_ILMBASE_ROOT $ENV{IlmBase_ROOT}) -elseif(ILMBASE_ROOT) - set(_ILMBASE_ROOT ${ILMBASE_ROOT}) -elseif(DEFINED ENV{ILMBASE_ROOT}) - set(_ILMBASE_ROOT $ENV{ILMBASE_ROOT}) -endif() - -# Additionally try and use pkconfig to find IlmBase -if(USE_PKGCONFIG) - if(NOT DEFINED PKG_CONFIG_FOUND) - find_package(PkgConfig) - endif() - if(PKG_CONFIG_FOUND) - pkg_check_modules(PC_IlmBase QUIET ilmbase) - endif() -endif() - -# ------------------------------------------------------------------------ -# Search for IlmBase include DIR -# ------------------------------------------------------------------------ - -set(_ILMBASE_INCLUDE_SEARCH_DIRS "") -list(APPEND _ILMBASE_INCLUDE_SEARCH_DIRS - ${ILMBASE_INCLUDEDIR} - ${_ILMBASE_ROOT} - ${PC_IlmBase_INCLUDEDIR} - ${SYSTEM_LIBRARY_PATHS} -) - -# Look for a standard IlmBase header file. -find_path(IlmBase_INCLUDE_DIR IlmBaseConfig.h - ${_FIND_ILMBASE_ADDITIONAL_OPTIONS} - PATHS ${_ILMBASE_INCLUDE_SEARCH_DIRS} - PATH_SUFFIXES ${CMAKE_INSTALL_INCLUDEDIR}/OpenEXR include/OpenEXR OpenEXR -) - -if(EXISTS "${IlmBase_INCLUDE_DIR}/IlmBaseConfig.h") - # Get the ILMBASE version information from the config header - file(STRINGS "${IlmBase_INCLUDE_DIR}/IlmBaseConfig.h" - _ilmbase_version_major_string REGEX "#define ILMBASE_VERSION_MAJOR " - ) - string(REGEX REPLACE "#define ILMBASE_VERSION_MAJOR" "" - _ilmbase_version_major_string "${_ilmbase_version_major_string}" - ) - string(STRIP "${_ilmbase_version_major_string}" IlmBase_VERSION_MAJOR) - - file(STRINGS "${IlmBase_INCLUDE_DIR}/IlmBaseConfig.h" - _ilmbase_version_minor_string REGEX "#define ILMBASE_VERSION_MINOR " - ) - string(REGEX REPLACE "#define ILMBASE_VERSION_MINOR" "" - _ilmbase_version_minor_string "${_ilmbase_version_minor_string}" - ) - string(STRIP "${_ilmbase_version_minor_string}" IlmBase_VERSION_MINOR) - - unset(_ilmbase_version_major_string) - unset(_ilmbase_version_minor_string) - - set(IlmBase_VERSION ${IlmBase_VERSION_MAJOR}.${IlmBase_VERSION_MINOR}) -endif() - -# ------------------------------------------------------------------------ -# Search for ILMBASE lib DIR -# ------------------------------------------------------------------------ - -if(NOT DEFINED ILMBASE_DEBUG_SUFFIX) - set(ILMBASE_DEBUG_SUFFIX _d) -endif() - -set(_ILMBASE_LIBRARYDIR_SEARCH_DIRS "") - -# Append to _ILMBASE_LIBRARYDIR_SEARCH_DIRS in priority order - -list(APPEND _ILMBASE_LIBRARYDIR_SEARCH_DIRS - ${ILMBASE_LIBRARYDIR} - ${_ILMBASE_ROOT} - ${PC_IlmBase_LIBDIR} - ${SYSTEM_LIBRARY_PATHS} -) - -set(IlmBase_LIB_COMPONENTS "") -list(APPEND ILM_BUILD_TYPES RELEASE DEBUG) - -foreach(COMPONENT ${IlmBase_FIND_COMPONENTS}) - foreach(BUILD_TYPE ${ILM_BUILD_TYPES}) - - set(_TMP_SUFFIX "") - if(BUILD_TYPE STREQUAL DEBUG) - set(_TMP_SUFFIX ${ILMBASE_DEBUG_SUFFIX}) - endif() - - set(_IlmBase_Version_Suffix "-${IlmBase_VERSION_MAJOR}_${IlmBase_VERSION_MINOR}") - set(_ILMBASE_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES}) - - if(WIN32) - if(ILMBASE_USE_STATIC_LIBS) - set(CMAKE_FIND_LIBRARY_SUFFIXES "${_TMP_SUFFIX}.lib") - endif() - list(APPEND CMAKE_FIND_LIBRARY_SUFFIXES "${_IlmBase_Version_Suffix}${_TMP_SUFFIX}.lib") - else() - if(ILMBASE_USE_STATIC_LIBS) - set(CMAKE_FIND_LIBRARY_SUFFIXES "${_TMP_SUFFIX}.a") - else() - if(APPLE) - list(APPEND CMAKE_FIND_LIBRARY_SUFFIXES "${_IlmBase_Version_Suffix}${_TMP_SUFFIX}.dylib") - else() - list(APPEND CMAKE_FIND_LIBRARY_SUFFIXES "${_IlmBase_Version_Suffix}${_TMP_SUFFIX}.so") - endif() - endif() - list(APPEND CMAKE_FIND_LIBRARY_SUFFIXES "${_IlmBase_Version_Suffix}${_TMP_SUFFIX}.a") - endif() - - # Find the lib - find_library(IlmBase_${COMPONENT}_LIBRARY_${BUILD_TYPE} ${COMPONENT} - ${_FIND_ILMBASE_ADDITIONAL_OPTIONS} - PATHS ${_ILMBASE_LIBRARYDIR_SEARCH_DIRS} - PATH_SUFFIXES ${CMAKE_INSTALL_LIBDIR} lib64 lib - ) - - if(EXISTS ${IlmBase_${COMPONENT}_LIBRARY_${BUILD_TYPE}}) - list(APPEND IlmBase_LIB_COMPONENTS ${IlmBase_${COMPONENT}_LIBRARY_${BUILD_TYPE}}) - list(APPEND IlmBase_LIB_COMPONENTS_${BUILD_TYPE} ${IlmBase_${COMPONENT}_LIBRARY_${BUILD_TYPE}}) - endif() - - # Reset library suffix - set(CMAKE_FIND_LIBRARY_SUFFIXES ${_ILMBASE_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES}) - unset(_ILMBASE_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES) - unset(_IlmBase_Version_Suffix) - unset(_TMP_SUFFIX) - endforeach() - - if(IlmBase_${COMPONENT}_LIBRARY_DEBUG AND IlmBase_${COMPONENT}_LIBRARY_RELEASE) - # if the generator is multi-config or if CMAKE_BUILD_TYPE is set for - # single-config generators, set optimized and debug libraries - get_property(_isMultiConfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) - if(_isMultiConfig OR CMAKE_BUILD_TYPE) - set(IlmBase_${COMPONENT}_LIBRARY optimized ${IlmBase_${COMPONENT}_LIBRARY_RELEASE} debug ${IlmBase_${COMPONENT}_LIBRARY_DEBUG}) - else() - # For single-config generators where CMAKE_BUILD_TYPE has no value, - # just use the release libraries - set(IlmBase_${COMPONENT}_LIBRARY ${IlmBase_${COMPONENT}_LIBRARY_RELEASE}) - endif() - # FIXME: This probably should be set for both cases - set(IlmBase_${COMPONENT}_LIBRARIES optimized ${IlmBase_${COMPONENT}_LIBRARY_RELEASE} debug ${IlmBase_${COMPONENT}_LIBRARY_DEBUG}) - endif() - - # if only the release version was found, set the debug variable also to the release version - if(IlmBase_${COMPONENT}_LIBRARY_RELEASE AND NOT IlmBase_${COMPONENT}_LIBRARY_DEBUG) - set(IlmBase_${COMPONENT}_LIBRARY_DEBUG ${IlmBase_${COMPONENT}_LIBRARY_RELEASE}) - set(IlmBase_${COMPONENT}_LIBRARY ${IlmBase_${COMPONENT}_LIBRARY_RELEASE}) - set(IlmBase_${COMPONENT}_LIBRARIES ${IlmBase_${COMPONENT}_LIBRARY_RELEASE}) - endif() - - # if only the debug version was found, set the release variable also to the debug version - if(IlmBase_${COMPONENT}_LIBRARY_DEBUG AND NOT IlmBase_${COMPONENT}_LIBRARY_RELEASE) - set(IlmBase_${COMPONENT}_LIBRARY_RELEASE ${IlmBase_${COMPONENT}_LIBRARY_DEBUG}) - set(IlmBase_${COMPONENT}_LIBRARY ${IlmBase_${COMPONENT}_LIBRARY_DEBUG}) - set(IlmBase_${COMPONENT}_LIBRARIES ${IlmBase_${COMPONENT}_LIBRARY_DEBUG}) - endif() - - # If the debug & release library ends up being the same, omit the keywords - if("${IlmBase_${COMPONENT}_LIBRARY_RELEASE}" STREQUAL "${IlmBase_${COMPONENT}_LIBRARY_DEBUG}") - set(IlmBase_${COMPONENT}_LIBRARY ${IlmBase_${COMPONENT}_LIBRARY_RELEASE} ) - set(IlmBase_${COMPONENT}_LIBRARIES ${IlmBase_${COMPONENT}_LIBRARY_RELEASE} ) - endif() - - if(IlmBase_${COMPONENT}_LIBRARY) - set(IlmBase_${COMPONENT}_FOUND TRUE) - else() - set(IlmBase_${COMPONENT}_FOUND FALSE) - endif() -endforeach() - -# ------------------------------------------------------------------------ -# Cache and set ILMBASE_FOUND -# ------------------------------------------------------------------------ - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(IlmBase - FOUND_VAR IlmBase_FOUND - REQUIRED_VARS - IlmBase_INCLUDE_DIR - IlmBase_LIB_COMPONENTS - VERSION_VAR IlmBase_VERSION - HANDLE_COMPONENTS -) - -if(NOT IlmBase_FOUND) - if(IlmBase_FIND_REQUIRED) - message(FATAL_ERROR "Unable to find IlmBase") - endif() - return() -endif() - -# Partition release/debug lib vars - -set(IlmBase_RELEASE_LIBRARIES "") -set(IlmBase_RELEASE_LIBRARY_DIRS "") -set(IlmBase_DEBUG_LIBRARIES "") -set(IlmBase_DEBUG_LIBRARY_DIRS "") -foreach(LIB ${IlmBase_LIB_COMPONENTS_RELEASE}) - get_filename_component(_ILM_LIBDIR ${LIB} DIRECTORY) - list(APPEND IlmBase_RELEASE_LIBRARIES ${LIB}) - list(APPEND IlmBase_RELEASE_LIBRARY_DIRS ${_ILM_LIBDIR}) -endforeach() - -foreach(LIB ${IlmBase_LIB_COMPONENTS_DEBUG}) - get_filename_component(_ILM_LIBDIR ${LIB} DIRECTORY) - list(APPEND IlmBase_DEBUG_LIBRARIES ${LIB}) - list(APPEND IlmBase_DEBUG_LIBRARY_DIRS ${_ILM_LIBDIR}) -endforeach() - -list(REMOVE_DUPLICATES IlmBase_RELEASE_LIBRARY_DIRS) -list(REMOVE_DUPLICATES IlmBase_DEBUG_LIBRARY_DIRS) - -set(IlmBase_LIBRARIES ${IlmBase_RELEASE_LIBRARIES}) -set(IlmBase_LIBRARY_DIRS ${IlmBase_RELEASE_LIBRARY_DIRS}) - -# We have to add both include and include/OpenEXR to the include -# path in case OpenEXR and IlmBase are installed separately. -# -# Make sure we get the absolute path to avoid issues where -# /usr/include/OpenEXR/../ is picked up and passed to gcc from cmake -# which won't correctly compute /usr/include as an implicit system -# dir if the path is relative: -# -# https://github.com/AcademySoftwareFoundation/openvdb/issues/632 -# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=70129 - -set(_IlmBase_Parent_Dir "") -get_filename_component(_IlmBase_Parent_Dir - ${IlmBase_INCLUDE_DIR}/../ ABSOLUTE) - -set(IlmBase_INCLUDE_DIRS) -list(APPEND IlmBase_INCLUDE_DIRS - ${_IlmBase_Parent_Dir} - ${IlmBase_INCLUDE_DIR} -) -unset(_IlmBase_Parent_Dir) - -# Configure imported targets - -foreach(COMPONENT ${IlmBase_FIND_COMPONENTS}) - # Configure lib type. If XXX_USE_STATIC_LIBS, we always assume a static - # lib is in use. If win32, we can't mark the import .libs as shared, so - # these are always marked as UNKNOWN. Otherwise, infer from extension. - set(ILMBASE_${COMPONENT}_LIB_TYPE UNKNOWN) - if(ILMBASE_USE_STATIC_LIBS) - set(ILMBASE_${COMPONENT}_LIB_TYPE STATIC) - elseif(UNIX) - get_filename_component(_ILMBASE_${COMPONENT}_EXT ${IlmBase_${COMPONENT}_LIBRARY_RELEASE} EXT) - if(${_ILMBASE_${COMPONENT}_EXT} STREQUAL ".a") - set(ILMBASE_${COMPONENT}_LIB_TYPE STATIC) - elseif(${_ILMBASE_${COMPONENT}_EXT} STREQUAL ".so" OR - ${_ILMBASE_${COMPONENT}_EXT} STREQUAL ".dylib") - set(ILMBASE_${COMPONENT}_LIB_TYPE SHARED) - endif() - endif() - - set(IlmBase_${COMPONENT}_DEFINITIONS) - - # Add the OPENEXR_DLL define if the library is not static on WIN32 - if(WIN32) - if(NOT ILMBASE_${COMPONENT}_LIB_TYPE STREQUAL STATIC) - list(APPEND IlmBase_${COMPONENT}_DEFINITIONS OPENEXR_DLL) - endif() - endif() - - if(NOT TARGET IlmBase::${COMPONENT}) - add_library(IlmBase::${COMPONENT} ${ILMBASE_${COMPONENT}_LIB_TYPE} IMPORTED) - set_target_properties(IlmBase::${COMPONENT} PROPERTIES - INTERFACE_COMPILE_OPTIONS "${PC_IlmBase_CFLAGS_OTHER}" - INTERFACE_COMPILE_DEFINITIONS "${IlmBase_${COMPONENT}_DEFINITIONS}" - INTERFACE_INCLUDE_DIRECTORIES "${IlmBase_INCLUDE_DIRS}") - - # Standard location - set_target_properties(IlmBase::${COMPONENT} PROPERTIES - IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" - IMPORTED_LOCATION "${IlmBase_${COMPONENT}_LIBRARY}") - - # Release location - if(EXISTS "${IlmBase_${COMPONENT}_LIBRARY_RELEASE}") - set_property(TARGET IlmBase::${COMPONENT} APPEND PROPERTY - IMPORTED_CONFIGURATIONS RELEASE) - set_target_properties(IlmBase::${COMPONENT} PROPERTIES - IMPORTED_LINK_INTERFACE_LANGUAGES_RELEASE "CXX" - IMPORTED_LOCATION_RELEASE "${IlmBase_${COMPONENT}_LIBRARY_RELEASE}") - endif() - - # Debug location - if(EXISTS "${IlmBase_${COMPONENT}_LIBRARY_DEBUG}") - set_property(TARGET IlmBase::${COMPONENT} APPEND PROPERTY - IMPORTED_CONFIGURATIONS DEBUG) - set_target_properties(IlmBase::${COMPONENT} PROPERTIES - IMPORTED_LINK_INTERFACE_LANGUAGES_DEBUG "CXX" - IMPORTED_LOCATION_DEBUG "${IlmBase_${COMPONENT}_LIBRARY_DEBUG}") - endif() - endif() -endforeach() diff --git a/cmake/FindOpenVDB.cmake b/cmake/FindOpenVDB.cmake index d7acaad1d0..c4213c853a 100644 --- a/cmake/FindOpenVDB.cmake +++ b/cmake/FindOpenVDB.cmake @@ -653,19 +653,7 @@ if(OpenVDB_USES_LOG4CPLUS) endif() if(OpenVDB_USES_IMATH_HALF) - find_package(Imath CONFIG) - if (NOT TARGET Imath::Imath) - find_package(IlmBase REQUIRED COMPONENTS Half) - endif() - - if(WIN32) - # @note OPENVDB_OPENEXR_STATICLIB is old functionality and should be removed - if(OPENEXR_USE_STATIC_LIBS OR - ("${ILMBASE_LIB_TYPE}" STREQUAL "STATIC_LIBRARY") OR - ("${IMATH_LIB_TYPE}" STREQUAL "STATIC_LIBRARY")) - list(APPEND OpenVDB_DEFINITIONS OPENVDB_OPENEXR_STATICLIB) - endif() - endif() + find_package(Imath REQUIRED CONFIG) endif() if(UNIX) @@ -673,7 +661,7 @@ if(UNIX) endif() # Set deps. Note that the order here is important. If we're building against -# Houdini 17.5 we must include IlmBase deps first to ensure the users chosen +# Houdini we must include Imath deps first to ensure the users chosen # namespaced headers are correctly prioritized. Otherwise other include paths # from shared installs (including houdini) may pull in the wrong headers @@ -685,7 +673,7 @@ if(OpenVDB_USES_DELAYED_LOADING) endif() if(OpenVDB_USES_IMATH_HALF) - list(APPEND _OPENVDB_VISIBLE_DEPENDENCIES $ $) + list(APPEND _OPENVDB_VISIBLE_DEPENDENCIES Imath::Imath) endif() if(OpenVDB_USES_LOG4CPLUS) diff --git a/cmake/OpenVDBGLFW3Setup.cmake b/cmake/OpenVDBGLFW3Setup.cmake index 24a7aea0d1..c0fac4c716 100644 --- a/cmake/OpenVDBGLFW3Setup.cmake +++ b/cmake/OpenVDBGLFW3Setup.cmake @@ -121,6 +121,13 @@ if(${CMAKE_VERSION} VERSION_LESS 3.19) ) endif() +if(OPENVDB_FUTURE_DEPRECATION AND FUTURE_MINIMUM_GLFW_VERSION) + if(glfw3_VERSION VERSION_LESS ${FUTURE_MINIMUM_GLFW_VERSION}) + message(DEPRECATION "Support for GLFW versions < ${FUTURE_MINIMUM_GLFW_VERSION} " + "is deprecated and will be removed.") + endif() +endif() + unset(glfw3_FIND_VERSION) # GLFW 3.1 does not export INTERFACE_LINK_LIBRARIES so detect this diff --git a/cmake/OpenVDBHoudiniSetup.cmake b/cmake/OpenVDBHoudiniSetup.cmake index e92c3b648c..2de38b53f6 100644 --- a/cmake/OpenVDBHoudiniSetup.cmake +++ b/cmake/OpenVDBHoudiniSetup.cmake @@ -49,8 +49,6 @@ overwrite user provided values. ``ZLIB_LIBRARY`` ``OPENEXR_INCLUDEDIR`` ``OPENEXR_LIBRARYDIR`` -``ILMBASE_INCLUDEDIR`` -``ILMBASE_LIBRARYDIR`` ``TBB_INCLUDEDIR`` ``TBB_LIBRARYDIR`` ``BLOSC_INCLUDEDIR`` @@ -147,14 +145,6 @@ elseif(MINIMUM_HOUDINI_VERSION) endif() endif() -# Temporary change to support Houdini 19 which deploys with Blosc 1.5. -# This allows VDB to continue to build using Blosc 1.5. This support -# will be dropped in VDB 11 -if(Houdini_VERSION VERSION_LESS 19.5) - message(DEPRECATION "Setting MINIMUM_BLOSC_VERSION to 1.5.0 for Houdini 19.0 compatibility.") - set(MINIMUM_BLOSC_VERSION 1.5.0) -endif() - set(Houdini_VERSION_MAJOR_MINOR "${Houdini_VERSION_MAJOR}.${Houdini_VERSION_MINOR}") find_package(PackageHandleStandardArgs) @@ -311,21 +301,10 @@ if(NOT OPENEXR_LIBRARYDIR) set(OPENEXR_LIBRARYDIR ${HOUDINI_LIB_DIR}) endif() -# IlmBase - -if(NOT ILMBASE_INCLUDEDIR) - set(ILMBASE_INCLUDEDIR ${HOUDINI_INCLUDE_DIR}) -endif() -if(NOT ILMBASE_LIBRARYDIR) - set(ILMBASE_LIBRARYDIR ${HOUDINI_LIB_DIR}) -endif() - # Boost - currently must be provided as VDB is not fully configured to # use Houdini's namespaced hboost -# Versions of Houdini >= 17.5 have some namespaced libraries (IlmBase/OpenEXR). # Add the required suffix as part of the cmake lib suffix searches - if(APPLE) list(APPEND CMAKE_FIND_LIBRARY_SUFFIXES "_sidefx.dylib") elseif(UNIX) diff --git a/cmake/config/OpenVDBVersions.cmake b/cmake/config/OpenVDBVersions.cmake index df94e73a17..ac36f582e7 100644 --- a/cmake/config/OpenVDBVersions.cmake +++ b/cmake/config/OpenVDBVersions.cmake @@ -27,7 +27,7 @@ set(OPENVDB_DEPRECATED_ABI_LIST ${MINIMUM_OPENVDB_ABI_VERSION} ${_PREV_ABI}) unset(_PREV_ABI) if(NOT DISABLE_DEPENDENCY_VERSION_CHECKS) - # @note Currently tracking CY2021 of the VFX platform where available + # @note Currently tracking CY2022 of the VFX platform where available # @Note Compiler versions are not really a hard and fast rule, you just need # a compiler with complete support for our MINIMUM_CXX_STANDARD (currently 17). @@ -37,46 +37,48 @@ if(NOT DISABLE_DEPENDENCY_VERSION_CHECKS) set(MINIMUM_GCC_VERSION 9.3.1) set(MINIMUM_CLANG_VERSION 5.0) set(MINIMUM_ICC_VERSION 19) - set(MINIMUM_MSVC_VERSION 19.10) # 1910 (Visual Studio 2017 15.0) + set(MINIMUM_MSVC_VERSION 19.28) # 1928 (Visual Studio 2019 Version 16.8 + 16.9) - set(MINIMUM_BOOST_VERSION 1.73) + set(MINIMUM_BOOST_VERSION 1.76) set(MINIMUM_PYBIND_VERSION 2.9.1) - set(MINIMUM_ILMBASE_VERSION 2.4) - set(MINIMUM_OPENEXR_VERSION 2.4) + set(MINIMUM_IMATH_VERSION 3.1) + set(MINIMUM_OPENEXR_VERSION 3.1) set(MINIMUM_ZLIB_VERSION 1.2.7) - set(MINIMUM_TBB_VERSION 2020.2) + set(MINIMUM_TBB_VERSION 2020.3) set(MINIMUM_LLVM_VERSION 10.0.0) set(MINIMUM_BLOSC_VERSION 1.17.0) + set(MINIMUM_GLFW_VERSION 3.1) - set(MINIMUM_PYTHON_VERSION 3.7) - set(MINIMUM_NUMPY_VERSION 1.19.0) + set(MINIMUM_PYTHON_VERSION 3.9.1) + set(MINIMUM_NUMPY_VERSION 1.20.0) set(MINIMUM_GOOGLETEST_VERSION 1.10) - set(MINIMUM_GLFW_VERSION 3.1) set(MINIMUM_LOG4CPLUS_VERSION 1.1.2) - set(MINIMUM_HOUDINI_VERSION 19.0) + set(MINIMUM_HOUDINI_VERSION 19.5) # These always promote warnings rather than errors set(MINIMUM_MAYA_VERSION 2017) set(MINIMUM_DOXYGEN_VERSION 1.8.8) endif() -# VFX 22 deprecations to transition to MINIMUM_* variables in OpenVDB 11.0.0 +# VFX 23 deprecations to transition to MINIMUM_* variables in OpenVDB 12.0.0 # @note At the time of writing, any variables that are commented out don't # have target transitional versions. set(FUTURE_MINIMUM_GCC_VERSION 11.2.1) -set(FUTURE_MINIMUM_MSVC_VERSION 19.28) # 1928 (Visual Studio 2019 Version 16.8 + 16.9) +set(FUTURE_MINIMUM_MSVC_VERSION 19.30) # 1930 (Visual Studio 2022) # set(FUTURE_MINIMUM_ICC_VERSION 19) -# set(FUTURE_MINIMUM_CXX_STANDARD 17) -# set(FUTURE_MINIMUM_CMAKE_VERSION 3.18) -set(FUTURE_MINIMUM_ILMBASE_VERSION 3.1) -set(FUTURE_MINIMUM_OPENEXR_VERSION 3.1) -set(FUTURE_MINIMUM_BOOST_VERSION 1.76) +# set(FUTURE_MINIMUM_CXX_STANDARD 20) +set(FUTURE_MINIMUM_CMAKE_VERSION 3.20) +# set(FUTURE_MINIMUM_OPENEXR_VERSION 3.1) +set(FUTURE_MINIMUM_BOOST_VERSION 1.80) +set(FUTURE_MINIMUM_GLFW_VERSION 3.3) +set(FUTURE_MINIMUM_LOG4CPLUS_VERSION 2.0) + # set(FUTURE_MINIMUM_BLOSC_VERSION 1.17.0) -set(FUTURE_MINIMUM_TBB_VERSION 2020.3) -set(FUTURE_MINIMUM_PYTHON_VERSION 3.9.1) -set(FUTURE_MINIMUM_NUMPY_VERSION 1.20.0) -set(FUTURE_MINIMUM_HOUDINI_VERSION 19.5) -# set(FUTURE_MINIMUM_LLVM_VERSION 10.0.0) +# set(FUTURE_MINIMUM_TBB_VERSION 2020.3) +set(FUTURE_MINIMUM_PYTHON_VERSION 3.10) +set(FUTURE_MINIMUM_NUMPY_VERSION 1.23.0) +# set(FUTURE_MINIMUM_HOUDINI_VERSION 20.0) +set(FUTURE_MINIMUM_LLVM_VERSION 13.0.0) diff --git a/cmake/scripts/ubsan.supp b/cmake/scripts/ubsan.supp index ae5d34ed40..c0b6a6ca92 100644 --- a/cmake/scripts/ubsan.supp +++ b/cmake/scripts/ubsan.supp @@ -16,9 +16,6 @@ vptr:tbb/task.h ##### OpenVDB ##### -# The sOn/sOff static bool data held on the LeaffBuff objects can be -# initialised to arbitrary true/false values. See the note in LeafBuffer.h -enum:openvdb/tools/Activate.h # Some 2s complement tests, ignore the negative shifts # @todo Should address these shift-base:TestMultiResGrid.cc diff --git a/doc/build.txt b/doc/build.txt index dbaf03d6f5..3dbfb48d57 100644 --- a/doc/build.txt +++ b/doc/build.txt @@ -87,7 +87,7 @@ given FindModule e.g. `FindBlosc.cmake`: - @b Xxx_ROOT - Preferred installation prefix. The given dependency is expected to follow a folder structure `Xxx_ROOT/include` and `Xxx_ROOT/lib` exist. Note that unlike the above, this is the case matching name of the - find package .i.e. Blosc_ROOT, IlmBase_ROOT, TBB_ROOT etc. + find package .i.e. Blosc_ROOT, TBB_ROOT etc. - @b XXX_INCLUDEDIR / @b XXX_LIBRARYDIR - Preferred include and library directories - @b SYSTEM_LIBRARY_PATHS - A list of paths appended to all include and lib @@ -238,8 +238,8 @@ it's a good idea to read the above section on DCC | Supported Version | OpenVDB ABI | -------- | ----------------- | ----------- | -Houdini | 19.0 | 8 | Houdini | 19.5 | 9 | +Houdini | 20.0 | 10 | Maya | 2018 | Any | Maya | 2019 | Any | @@ -355,7 +355,7 @@ CppUnit | A unit testing framework module for C++ GLFW | Simple API for OpenGL development | OpenVDB View | Doxygen | Documentation generation from C++ | Documentation | OpenEXR | EXR serialization support | OpenVDB Render | -IlmBase | Used half precision floating points and EXR serialization support | Optional (All) | +Imath | Used half precision floating points and EXR serialization support | Optional (All) | Log4cplus | An optional dependency for improved OpenVDB Logging | Optional (All) | PyBind11 | C++/python bindings | Optional (Python) | NumPy | Scientific computing with Python | Optional (Python) | @@ -394,14 +394,12 @@ Optional: install the Maya plugin to. Defaults to the value of `${CMAKE_INSTALL_PREFIX}/maya${Maya_VERSION}` - @b BOOST_ROOT =`/path/to/boost/install` # Path to boost. May not be required, CMake may find it automatically - - @b IlmBase_ROOT =`/path/to/ilmbase/install` # Path to ilmbase. For example on MacOS and where the build folder has been created inside the OpenVDB source root: @code cmake -D BOOST_ROOT=/path/to/boost/install \ - -D IlmBase_ROOT=/path/to/ilmbase/install \ -D Maya_ROOT=/Applications/Autodesk/maya2019/ \ -D USE_MAYA=ON \ ../ @@ -609,8 +607,17 @@ OpenVDB uses [imported targets](https://cmake.org/cmake/help/latest/command/add_ for all its dependencies. For imported Boost compatibility, the following versions of CMake are required: - - Boost 1.68, 1.69 require CMake 3.13 or newer. - - Boost 1.70 requires CMake 3.14 or newer. + - Boost 1.73 requires CMake 3.17.2 or newer. + - Boost 1.74 requires CMake 3.19 or newer. + - Boost 1.75 requires CMake 3.19.5 or newer. + - Boost 1.76 requires CMake 3.20.3 or newer. + - Boost 1.77 requires CMake 3.21.3 or newer. + - Boost 1.78 requires CMake 3.22.2 or newer. + - Boost 1.79 requires CMake 3.23.2 or newer. + - Boost 1.80 requires CMake 3.24.2 or newer. + - Boost 1.81 requires CMake 3.25.2 or newer. + - Boost 1.82 requires CMake 3.27.0 or newer. + - Boost 1.83 requires CMake 3.27.4 or newer. Use the above list to identify the version of CMake you require for your version of Boost. Note that this may be implemented into the OpenVDB CMake in the future. diff --git a/doc/dependencies.txt b/doc/dependencies.txt index 113c20a003..f8c730a015 100644 --- a/doc/dependencies.txt +++ b/doc/dependencies.txt @@ -9,7 +9,6 @@ - @ref depInstallingDependencies - @ref depUsingAptGet - @ref depUsingHomebrew - - @ref depManInstall ------------------------------------------------------------------------------ @@ -37,10 +36,10 @@ Reference Platform, but for those that do, their specified versions are Component | Requirements | Optional ----------------------- | ----------------------------------------------- | -------- -OpenVDB Core Library | CMake, C++17 compiler, TBB::tbb, Boost::headers | Blosc, ZLib, Log4cplus, IlmBase::Half, Boost::iostream +OpenVDB Core Library | CMake, C++17 compiler, TBB::tbb, Boost::headers | Blosc, ZLib, Log4cplus, Imath::Imath, Boost::iostream OpenVDB Print | Core Library dependencies | - OpenVDB LOD | Core Library dependencies | - -OpenVDB Render | Core Library dependencies | OpenEXR, IlmBase, libpng +OpenVDB Render | Core Library dependencies | OpenEXR, Imath::Imath, libpng OpenVDB View | Core Library dependencies, OpenGL, GLFW3, GLEW* | - OpenVDB Python | Core Library dependencies, Python, PyBind11 | NumPy OpenVDB AX | Core Library dependencies, LLVM | Bison, Flex @@ -58,27 +57,27 @@ OpenVDB Documentation | Doxygen | - Package | Minimum | Recommended | Description | apt-get | Homebrew | Source -------------- | ------- | ----------- | ----------------------------------------------------------------- | ------- | -------- | ------ CMake | 3.18 | Latest | Cross-platform family of tools designed to help build software | Y | Y | https://cmake.org -GCC | 9.3.1 | 9.3.1 | C++ 17 Compiler: The GNU Compiler Collection | Y | Y | https://www.gnu.org/software/gcc +GCC | 9.3.1 | 11.2.1 | C++ 17 Compiler: The GNU Compiler Collection | Y | Y | https://www.gnu.org/software/gcc Clang | 5.0 | Latest | C++ 17 Compiler: A C language family frontend for LLVM | Y | Y | https://clang.llvm.org Intel ICC | 19 | Latest | C++ 17 Compiler: Intels C++ Compiler | Y | Y | https://software.intel.com/en-us/c-compilers -MSVC | 19.28 | Latest | C++ 17 Compiler: Microsoft Visual C++ Compiler | Y | Y | https://visualstudio.microsoft.com/vs -IlmBase | 2.4 | Removed | Used half precision floating points and EXR serialization support | Y | Y | http://www.openexr.com -OpenEXR | 2.4 | 3.1 | EXR serialization support | Y | Y | http://www.openexr.com +MSVC | 19.28 | 19.30 | C++ 17 Compiler: Microsoft Visual C++ Compiler | Y | Y | https://visualstudio.microsoft.com/vs +Imath | 3.1 | Latest | Half precision floating points | Y | Y | http://www.openexr.com +OpenEXR | 3.1 | Latest | EXR serialization support | Y | Y | http://www.openexr.com TBB | 2020.2 | 2020.3 | Threading Building Blocks - template library for task parallelism | Y | Y | https://www.threadingbuildingblocks.org ZLIB | 1.2.7 | Latest | Compression library for disk serialization compression | Y | Y | https://www.zlib.net -Boost | 1.73 | 1.76 | Components: headers, iostreams | Y | Y | https://www.boost.org -LLVM | 10.0.0 | 10.0.0* | Target-independent code generation | Y | Y | https://llvm.org/ +Boost | 1.76 | 1.80 | Components: headers, iostreams | Y | Y | https://www.boost.org +LLVM | 10.0.0 | 13.0.0* | Target-independent code generation | Y | Y | https://llvm.org/ Bison | 3.0.0 | 3.7.0 | General-purpose parser generator | Y | Y | https://www.gnu.org/software/gcc Flex | 2.6.0 | 2.6.4 | Fast lexical analyzer generator | Y | Y | https://github.com/westes/flex -Python | 3.7 | 3.7 | The python interpreter and libraries | Y | Y | https://www.python.org +Python | 3.9.1 | 3.10 | The python interpreter and libraries | Y | Y | https://www.python.org PyBind11 | 2.9.1 | Latest | C++/python bindings | Y | Y | https://pybind11.readthedocs.io -NumPy | 1.19.0 | 1.20.0 | Scientific computing with Python | Y | Y | http://www.numpy.org +NumPy | 1.20.0 | 1.23.0 | Scientific computing with Python | Y | Y | http://www.numpy.org GoogleTest | 1.10 | Latest | A unit testing framework module for C++ | Y | Y | https://github.com/google/googletest CppUnit | 1.10 | Latest | A unit testing framework module for C++ | N | Y | https://freedesktop.org/wiki/Software/cppunit Blosc | 1.17.0* | 1.17.0 | Recommended dependency for improved disk compression | Y | Y | https://github.com/Blosc/c-blosc/releases Log4cplus | 1.1.2 | Latest | An optional dependency for improved OpenVDB Logging | Y | Y | https://github.com/log4cplus/log4cplus libpng | - | Latest | Library for manipulating PNG images | Y | Y | http://www.libpng.org/pub/png/libpng.html -GLFW | 3.1 | Latest | Simple API for OpenGL development | Y | Y | https://www.glfw.org +GLFW | 3.1 | > 3.3 | Simple API for OpenGL development | Y | Y | https://www.glfw.org OpenGL | 3.2 | Latest | Environment for developing portable graphics applications | Y | Y | https://www.opengl.org GLEW | 1.0.0 | Latest | A cross-platform OpenGL extension loading library. | Y | Y | http://glew.sourceforge.net CUDA | - | Latest | Parallel computing platform for graphical processing units. | Y | N | https://developer.nvidia.com/cuda-downloads @@ -97,8 +96,8 @@ Doxygen | 1.8.8 | 1.8.11 | Documentation generation from C++ segfault on closure. Fixed in GLFW 3.3 - @b Blosc: OpenVDB has historically used an old version of blosc (1.5.0) to - serialize .vdb files. OpenVDB has recently moved to a new blosc version minimum - of 1.17.0. We have tested a range of blosc versions and found that the following + serialize .vdb files. OpenVDB has moved to a new blosc version minimum of + 1.17.0. We have tested a range of blosc versions and found that the following versions are NOT compatible with OpenVDB caches written using blosc 1.5.0. Additionally these versions of blosc may generally not be compatible with OpenVDB itself. Avoid using these versions and upgrade to the new minimum of 1.17.0 where @@ -137,9 +136,6 @@ Pin-Priority: 990 Alternatively you can install any missing dependencies manually or through other package managers. -@b Note:CppUnit is unavailable using apt-get. See the other package manager -methods or the [manual installation](@ref depManInstall) options. - @code{.sh} #!/bin/bash # Core library @@ -149,17 +145,17 @@ apt-get install zlibc # zlib apt-get install libboost-iostreams-dev # Boost::iostream apt-get install libblosc-dev # Blosc # AX -apt-get install llvm-10-dev # LLVM +apt-get install llvm-10-dev # LLVM # Python apt-get install pybind11-dev # Python apt-get install python-dev # Python apt-get install python-numpy # NumPy # Optional apt-get install libpng-dev # libpng -apt-get install libilmbase-dev # IlmBase apt-get install libopenexr-dev # OpenEXR apt-get install liblog4cplus-dev # Log4cplus apt-get install googletest # GoogleTest +apt-get install libcppunit-dev # CppUnit # vdb_view apt-get install libglfw3-dev # GLFW # Documentation @@ -193,32 +189,10 @@ brew install cppunit # CppUnit # vdb_view brew install glfw # GLFW # vdb_render -brew install ilmbase # IlmBase brew install openexr # OpenEXR brew install libpng # libpng # Documentation brew install doxygen # Doxygen @endcode -@subsection depManInstall UNIX Manual Installations - -Some dependencies may be unavailable using the above package manager methods. -Below are manual installation methods for these packages. Note that you will -have to provide your chosen installation locations to the OpenVDB CMake build -system when building OpenVDB. - -@b CppUnit (for AX Unit Tests) - -@code{.sh} -#!/bin/bash -wget http://dev-www.libreoffice.org/src/cppunit-1.13.2.tar.gz -tar -xvzf cppunit-1.13.2.tar.gz -cd cppunit-1.13.2 -# Install to users home directory - you may customise this location -mkdir -p $HOME/cppunit -./configure --prefix=$HOME/cppunit -make -make install -@endcode - */ diff --git a/doc/img/banner.png b/doc/img/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..95378aae2d612f75b462c64599f6f1b311c01be8 GIT binary patch literal 1075494 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�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%0Dw?G)&ob4^Xa(c5}k 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%0Ar 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 euxPWn~|0yJ|3%Xx4YV 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

t?Eef$Gc(3K$z64{tT}+Uj43 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⋙w&wi}j41?s?>@O4-sYhZu9Us;q6W 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%0A%^RLet5t)=+0ZRQ}Lm=n)WvQ6v@*=5WG zK~ght5$X6#Xm4-AcF!_e?)?7lB5(Izf1mH-Ph}S6A#t20ru3l6edzX{WB6lAkqSb{ zP$vMS8EPv|0cu%tgj#@F?$)lN@bWS#LXW*zWO*REQj+~Gug=Em3^~lp4S!sOGW#1) z!mW6i*rVU6f4c}8+h~>&L(X>PgpluM5iyp%ASh@1K{uUlM8^(e-f>Baq*?;1%qnO& z380kF;@-t!!>P}T%4PH0-&X5{EfNeliMvty`n zrbG)Mj)+W1p5Y!o9$(8S40oCi+sR=MSk$vCOi7b2i&&*O!GhzQ940=_9F+jOMsb2| z8~1280VQ&kV3gc@mcYo4mhdoK_H`~iu8f0xUKteN60%2p`o&o%H7z zs|v52I)$%(?*}+}?wkn1?{W};mAb}4N?1qO;WRfi`ixyNfzp#Sq=P%Cu{-67}aOnb;mi9uTL@RmM3;-+H07Nu0 z<;m!O4XK@-!aRkF*b0<2DE#o+)wgl^(&>Nn`R(6#I{?CXyoQsfU&8wORjjXH!`k}Q zEgcE}rH;03u(o~`YwK4#-wy^OEG@2JadA6VmUdxbVOf4~nIM?2l?VAgM0s=h*@+Z- zdqd?sF2+CSpnfg;y_&Yc+ZWGZJlVwNWE11*_&*yg{!`2| zE7-BLg2nmyum=OY^eA++P+rWORi0jR=ACQd}eQ@tFG1F(_sLi8}Sg zO+?PwIEcw0MGh^=GFtM&cXv?HIRvF}cSs$0pn%Ng;bQ^RF(Gm%kp>%N`OFDgq>Qz~ zsu`?x>JIOsf3W{p&hxTrKCAG2<)4;yYwBOQuTry4y%k$00B*7^t4+jGW&)}=uw_`5 zJ_N1XZlvE5Ob#?Thzk$#5|)M|_I#a8d`7FZ*jjc!BqsG-7LFjf}1F zumTmb$+K4yAw35OnKW^a=I%bS=d)#d*fRs3Id1Bh9MgTSMP?1th0B`0SGs)1pQbXM zIRkwAnygMn6ztvv%Va>@2ZB73HP$2$rb4H*o<5nv%Poi{r9O&N68W!Au&`I7bATvh zFi^Q(GSbLl=o~`FtR^{$)F<|a!xD_DnF8E`bL(YRt*0C3EZ=9z8ZhTht>w$e0-BOI zGAGlFdQt+KOeZkIey+{vPMUdRCBT4&7(_XyGz;FbnDugIkC4yVdTZF;(QF^%|G5O` znIXPJ&Ig)gfHJY5o9ow zYeNrACB{BKZ#GOpbf++Mv#%Bj*X~ins-!sfcbOLAjo4l0MSo(HEYQqisj>?^&DJcn zse8{J%-{g-i{Xj|W^~i@0F=({?zGTrZ5ZY#G!o~6CyG%>Hww4#0Bka6M$o5Rl8EBy zR&%)N5wGPfzWZ4>j4Zc*Upuz)-u!*XhOqG}H(0_-1~nO^&4#U?0KCz#62;081L&0Y zF-cB6W@be13I+&cD6{emEk+0Tpv1ihUl1duJf7H8- zV?@@|gG>`b1xI9al7G*+Z_1|r;GYgh*AFcwx ztcF`b;~?w>Aqk!`t7;{gU<-JJ0VQxs09CRj)p6Y-Mvg`PWQnBGFphMLUXRt+Qs?bL zLr_MPA2}5xz?(zEoC41EV8X499}4Nrtt_oYjRWv{m`KudBFkRXpMPm?2Eri{k!03SaJAuW$JCBW{%qG0>G(@MS0#%JT-i@`jHEe81Hp1oAi#U8_1Tz;VPDrFp{m5v& z&8!TWjV~18)TyAC^a^xZjGP7H7cZOy01SsiOePalRfYZg_hVyY1FNg6n46o!`uaKm zU}b3-i^h@MKn!=ix)4Vswx~ld>APuoH=vGVKo{pVtM-k010aw z*YU>jAH6H=tg+wX;v%+h-;Snfv1|7(9653X3-b%0Bqf0s7Z;^p(V)hz-FvXHv5vLt z*8u=~_w5Izge#XXp_xvxxU>!H>uVSchN!9vo12^1+}yxqJi+-37qGT=0~_lbc{!{09V`2=ZQBmVSG8?97m`D1pV5#GV0s8v z001BWNkl+VBlcvRVGRF1m z*Kz66C7d{M0%uO2o+-VzKHk9kcmr==I*a+yJofF_gFV}Ki}P6byQ8Q%#EDb|FR&|# zKZe`TaZ9;6N(`zb`jY^p9M&NiQq7oHR1WftaL&Q_LPgH40Of7a#+C$RhV(pvo>gS0 zMC2uF5(5j^ExtaMmmX?5ZK`DWbJ>$TdQMDix~(mNR$lpD!x;3RWyS#(~@!bm`>r0j;3 ze*}PvEaFxcUh0~g9LdN{i`-<9TBt!)6NX$NrT-+C(`^G%^vPQlpmfCo$e@=9bd~c9b<))g86E?nKEOJTpl%`Ie@rC_}8y zb~lyl1)KFAT94aB1vS#SoTC^o38kUIsTfVI#EY0UvePWn-y)uHmJ+WF2|hupfGhzU zOeL_3G=kf*5;Aiz?v=f3O`U<(ohBZ3#6~4criO@cmJz4f4V1N>Jaqp->X7T+Vq9wEW;MWC={b|4lA(K$>%`ETBv24kYz(Z>vbmTsF=Z4v$et@VHk~keMu5&bR3kv3 zK_%-w7*JjQbU~nqjt0MMVKStfQFTH}s+u`f5re+@3M{aVfz&cU87*3o)iVQCMpf5H zHBfWH02Kz@qT)vOiqSv@a?_$|B(olMEjng&c!RkQ;Xn*uJuHKyk6iie{`g z8CvB+!!y?#g6^(+!kX(zB^^ac!x^rTQG=}KM3P>1zUYSU#`g%FAm*GUJ3ogYb}LI{ zHWpUeVPy}Ej&7A%;7$w+I@V`O#()b=U4g4aEgNCb!FTitDuz{NAWIY*x8Yjr9ObFJ zCp~7sWZoIm^QWiR&FE9B=g``j7m3X0%Em)ia$n(xVkZm)ZzZP-CIRP7#Xn#k;BV4Wp50#2x(UafR{0JBGS76_3=<3HU>wA@*@gN zTj3;;4Z`5SS!%IQTaBz?35rk|TmhtI$be#m`1prU*@>QTZy0m2RAv6eGcA}A1fq=PvbnRAKCW@j-vwjg{& zc$fi?&5$S0W2&51l!i{ifHF&JcAMGDiIiI7nmwS(PQ)KFn#jc|C+lomSLfrnRlrpY z(%ge{H#%3hfy00;dK2W$a*8(_j+62dm{uScyzS76Ji#$Q3LB~Co`^gex?{sKK}Dk( z453^=U`viN4bSFVXr-;GLC6GIa^w`jBy*X&6D()QVUu%YfWiYx%6M@Km~vuxF1Tu0 z4+0-aJGOLYGe&mtNfNw#5R@5V?STzS%9+hE|=+`nT5i6mO>osBULEzDtg zP@`eSOP8@s6O=rAkwB^TOoGX@kw)uN#-!1- zM9n~>N=qA5R195Z>v&d+MSxjpK37uA=xyXY zIk@*X8NcfosLYUN%D>U@LjNO8GNVCP3i5&KbL<-SXNEy^VIP0e_vMv+sOuq`W`eb~ zHT>0=zk=nJmCm{2@fhdNpTjM;+zJ4=as4{}`b+--pZ~&dVQzjN0Pyrv-@z@n9>L*T zZUF$SuC8J*7~qfo@DFhQ{5h=MxPtMt#b7Wh_)Ha&mfkQWHQ4wIj*+XvN^g7eG07d{ zp0VT7#kT;6@%VrG<3Gl>rDg2ey&Llj3;6DnPhf5B27cjlk7M`lJ^1F=zlL|-ej9ha z=N^3cp^xD8*Ivc6fCKyYkbvORH>aVx%Xii<{9Y!078-MjtS@-w?;j2S5A}PP};>mo7@S%)^g-4CBcdKYsRy0Dy&s1#DYd!s?aF<@r^@ zV16D4_8-9JEt|OS-g}W!!uB0Iabs-_r%#>4v(G+@lW)D{*KCY8aN_JqoH~C72Y2ts z;oS!?s4JNJ#3t|C)Qsf{iy-dl&ql{J>E22maFMI*p0-!qpl9|h(;w_J$Mq|VL3STC zx`NeUYO+Y6Wg^$L+FJmyvMNSi*#-YwUsc8iJ~@dQedgX45kDtGn74d&!SaLo)cr2TqUv_w>l7&mFtQ$vy(L{ zx$GDjv+6fi%7V;k3^Z*9C4qBQRTxl()Kq9lf+N~Cg>{qiC^=8TSt5+B^eu6g^~vVg zkY^{fV9Jt-N+xfLOe!mGb+@VTDjj#!1x$|W=rG1+lh!`Z0wOnCSzr;8;*XZPwGxUh z8_RPYzk@vQ`i!nq!K3VUEq1&`k(nTY7hpwG*79}g+>tdf{YcxqGD3C`$p2H_CO2h6TT1wF%jP75vB*mb~bxUI31JP z@rp=Vwt7+{eOBjAn~^k~V|1ha*M(EtN^RS=HMMPbs;O;bs;M=d+O}6Jo97MfLo!)PwX`Bn zE{aG0IUFGy5VACQeq$R{v!=JMS~j~CNhEkrx@qtOL!LPz+j+Y(@#XsS?AJ-!GvVlH9g6fheAkXLh%S}R8*cI4ZxFak5+_=rBO3jz0wNg*N;Xy2ubG%8w zg_$TRS-LeFV=Nhu57t-e!<`rMaUiwliI-kCWTYTg8=GJA{eI;pnWe10})qnr&iFY{@0 zf1|x=%;P<~{LloGGxr(bzg;q83b(2F%2H^B0+^~Ns?Uq&u#_kBK>xHrV)4Hp#($3a zE*xei>~^Gr?Wt7k6D zuAY>)$a)4N)QH7+N~|}x{&_W7h+21>1pGJchwB(qM6U(SD(o(}}!xget;@-sd0CC4RY8>bH1K6dH<)?9ZSgXzQ-MK>@nAj zYxM3B0B1Q|^S*vu*I6I@-GBf5Gu!X&DjRp^_Lhw(;NAH`*aKaXmEEx(;dsF*7F84I?UGM0R7k@)~-tPip+N*C0Py!%}+IW4R z_>}@PoM*b8cKx1&1Hay8J~08cjSl#qJ1C#1GoL$e&x0ev@pGF5nl+VsZ`)r&xdETN zc@yUCQ43d{@$6O!X2;`(1gk#yN5?G=ZOC7>kxt|Ik)u zg7*l!Y`iq($s*2?5ydU4qoF|&o1sf=y_f%hz@}W*G*B_@7FTKVH|x=sdiz`E(ia1r zUP(^dAT`}K)pJ}JmqpT@+MX$on-m5NIX7v2eq<$IvN=2w4Axn@m_gQlWiNCfLl2v%6eUPc~nQ^*O$mr2i7BN1GZbtiN98F;Y+Kb3$;H)|~)9ySY0_C*++Zec zrMCnGIBAis-tRv_R?!$>DUDZJ$c<%>O(`;Wq3Rl?bmwj0#l+240Og;6B8utOa!u;q zKo)|NWeR8Dq7+7vbPJWb4k3i0P5*@_wCGd=$9xGFJpa5h_{UkhH$vF8OW{<* zli=d%sROwUoRYIN^(;9C)iT)$swuD93)JliVUW`r7?M`H070A`O znklm~VlNl5D28e5$Q*>3?AMOdf981S{@b#D&4Z-U1FQG3towm21yAXDeJlyp zJ3N!~Ku)I3$WKn!UG%FFk`hc@DwpLJ)jAw^iKM99RhBYDN?3juKT1dA&j7L>^7$f6 zG{sZ;MOgj%@RPild4ZZpMm(&vd+Ov;Hh8ux4>!7T=8#@<&Dic~>~^WrRWoi=p%;t1&lyZM=uFn zWNZD>w~~#tyu|#9FRS^)-?(sKm7|K4Fk0pH$H2;QRsJcP1FlQl^AqnyHtZ|yC;f6U zblEP7+@Ml3YyU1&Sq-`gn@q2DB8xDAv38WWv7akMz-@8Oo_UStbM7u`Plp}1P>UxQZT?q1jGg6@f8sHrp&Jc3_$HzX*OdW zDUN{7_?}F!rGl}0+_*WbyJ4Emex%xe?0(T})$)C0f_wQIM~i8HtU_z<>`d@s9`60% zdE@!G=Y?g2bjWk2?BGm=ceIzV^F~#UlKpU!iw-RfKVWeZDg? zqwyKNy$9cHeOlyG2ReGW9e}4ixHl6GLg8ACUceb6*ZZ_(85DiF*JF4t^O)A>3)XNr zqPVqiKUNXKi1NT)_mnA^Vw`|^R8aoX!Lhtp_d;MYHXi1XF(#un|LV zv=Kvpxc~a}Jm32T&yOZ8=hHpOSnd&!#*m)nP_y4?MgsB0ZhS*y$<#OFb!OgIl$Dhi zuG(vOEmgo+&E}e&xD~?z&(5ef;&*^l5n8U-E3H0H01=A+HHgq-?}_{#aCgoVa7l24 zgME$P^RnUjQj7xzz&}54^?gBCm0NA_s7VoSZ;$SI8{giU?zkL&{I}tJG-=~|b$VsW zgSYG9fA!>iahUFOb|4Fyk|2hQ|92O5oi8_a^uMN)fr!wcDTVb0cEDRIGX!AE;|s$2 zkD7juJ5qwcEsOsb$``8Dbao$i&&`j{>-Rgpo>=(`D`%Sr0{4GBf#PphgT_2hD7wj? zyFN-pJ8UwfHc&=at^4i@C29iq*W^5|ubBU}`GD)u5uu=Lz5hNGI3V-Skf*?-px6HE zkN3BpcLdPKvfu6|+#iGd?`8s=uKccsnhz~J$PlH;1)hUY_HtTgo^`ujE)Kt)79ab+ zlM8!zdEw$P>-R#=zwL;l_(2LkZ?VfnU9Xl1gMg7;?wgAS;lZ!BrkWrfZ>r9lM?D^o z7w}6VU;JifrYIG@I_{PfU{`gghPiTyy}#6Q<~0>QmiP98_I(}+(K#NZSV;?W(rWQh z11?q0Uj6dw3d|q#hn=87TwYvaND6}Bsm}{l#?#2ku`W2>Ljq0YC`a6AmYPapHu;Rq z_}Jo9;djAAV^y6^16sU{Z?8I@fh@|br8ZRft$GE$AXDD}F?orxJjEa$^QCs!*f>7O z34$g-V>l4FEUxp2|kKyxPbE-FhNgK-Y(e zXy6g;>#A9OI<6y0P4ep-Nzx~;-d-RN69&cT;P@Ks+Y+M2zMj*ZN`&=v&@K1qz1k1J zYk;V`tB*lnKzbHm1AABENA{j2O06k9b5S#LO{+2fgynWRv7+T(Jq)j{#UU;$05N|) zdviH_T*Fxn{)5>L?Ks)zj`>nDW@iNnB{Z(IG`%uZ^%uU5_ya?{wkvX}Yx!YpS+%Q1 zQz;#5qP%8D8gDPu8=NG9=@MbaeXE{y^6H<+AqVfweHBY|?6S3DOo zrxMV711&!T|A&}{R&hb&eqij_jT*#~%Z975M3*RpS!GLzEBOj;@D}PdJisoo@FSgeul_#EhzuiR;zDT7l>VC8Zr*P~aO2~N8M8>yr~{*9o~F)h z3w#JwPJn$#WChCnyA+kwRgIL4d^+EkYbcps_mthuOReib!#Mn#4KYM(JPahH_8<@E zv%3Pd;<$63sMSvIs^L4F+eoEHr%WeHbfyTElrCW!ESv{TGO5^=!-jY0^nMg~;d*Y; zXol;WcxZ>4WUf-lxC;zV?9vgj>6^wwlX^vp88i|KbL7r2pp)GvouoxsXkzh_uJQf4md4p-fBnkRQ^y9S|{Gxq&jY1oG$ zqB!U(A6~Q-!X)3a%5zjtERxhqEuiz!#4^N>;$q9IDIOZCF`%0`K7NbI z*#WQW7qf_WrBpl*duM)J5DIDp96A|husm#$p&-lvAc%%+rj_uc z=ND*VPQ3Jxw&NZaQ@Qp|hgkC$m(>1-s0dUtt2~h!%7G6vx4Vc3NhAnw7(`lDM@1Oa zb-Dm(mqy$AVuby`KvVZ$d3n+c9M2s!pGf|vTK-ct8F_hEm2cESFSLsxV8i>0pf(b@ zg@`x!0|t3-ekcYcBEIj25POflp;cCQBkixukt)m4pX&3F*!M*;9?uGrWe3LKC;vnHEOZ$u0=l`>aB5HSChTnf48-Uy*szc-W} zz3pc{rflJe8KKV!Z9|z$<6^Z9Ls(_Ng+0gA&jG1r(}liz#({_S|5<=)!y(WZVRvKu9ol93c{YMrOc1zZ zXY>L83-`aNKCxX794-YrI>%YEbr7g|_>)31$)tO2Q z93VVH+92Jq+I#=a*C~oS<`$1f~)5`9K)A<&JsAYp)SPLanXN9;; z^wN|^PYMPjn{rYdg9sImVJtN?xE-Yk0yAroTtSVy2fWcQ-ZvcYo&jxsh`lDZj zMCNmmn54$Vb}4Aeuz|t6@Rb26H$TLd@g5b(2EZ8vA6rv1w;_}C#AQXI>Qw!yek5|t z=NJs_53(sdeN(Qh;axLjRve`GmMoX$Cl@X@(JCQ0=O^9L5I z8yqw8Hv=+SZtHNRA5IiM6=J^i##E6)SW^a;PtmnlxWhzB!g5!Q&VcJO8feHuwia}7 zC^W0)QM3~mI^sN~r=_Up%-vfizeJ198aUhrU<Ipuj<0p>3~bE@AAo-<=xlI|rG&&dM3;!iL@ z4e&Dx3sd?5^5O!WRB=A5+HOW-#kNUP{|-w9#Y2 z)(YVjOk!t5>!_+|Q7nF8woFaRg0>$y${@UvGv&UGO|$N6bkh~OTq^c6yWntmlN?ec z`?Sn9$?e;49&ZE@loY-9tLOuBPwiF0#J#<^GTSNoc`AoA#Jw%wWT&=}b612w|DDxt6MDl4;)xX!=D$ni0k+9??g<-AOFKGju zA0dv95bwKaTik8k6{_KO?SR!=o!^tq2Uo_OevwIn5xm58TH(983 z1AOMAZ`$jm*d^6|{XKuYtPQhdlz@eurs!SBB*d|N6gOFiLcDE}mMa35$U*vvK-NPK zV0GFg_f?dK_MLA}!`#1`rjX2ZlL(He>HV|8H2xlJ^USdIJqq1E6FaV3sYyb1!`&{; zgM4_uwSSY~Oh2|eJ{@#6)JD|jzkM+tD|+B?0<<4;Y!iOY8uNlNC)I>^lvA!Nvt2JU zNkS+ayz+2q@T-DtE*#`=ua!-~Mj{`EX{%W8Auc+rA>K!zk6Q!>r5<@d4)!22Jve<1)!(imHIO)1jcD&Q{c#s^&p`ugy$Kr#E6*=2&VT}K?WVL@&y%2%>SW>?+mMBE zk%wRKEmVFVt~eXxo%Kp59pFfJ@x`JdE{$nfG*)kcBqV<|nG@?Ff zabavjSN6uDfFYTz1nNL!`YCOh&+>}Uaw^0|fyK>D)H_%4SSj(zIA}c1Le5i&Ej^pM zY+OY%G}1HW-|)Yq?Hr4Dos#vmmO~BkFl6G3tNW5U9TbZzbvJQ>79@t%3lHEO?lXs% zSIAO4PG?7IxgC@|whS*lD7xj z+8XD?|)f=Ty87H zKB$zHHC3i&`*fLq`UpL?{`3FCQ_wHl-#`P)F;7HP6VRNy^c?R$OW>;W4jGw10Ftc# zRE_#((j%|G>#0jCYXlVd0=8^<;_pXR@e@FHoe zympj!LsX*1_x#|b?YZlPK-uZ-3u%2ARRPssNMxdRC=k%NTIWjL{kT+9NVNETvELJ` zuJQc3@`+udl-EB%{K1o!mIgA}yJP;?I2|;DsFVL@=slmwUs&l)H`h5H!oj=Na~9)M zQ|F$?Ia;j3!U-FWJ%pf!Uq!#GJsLzvGB7djzU00C4@QV55RA52*x^_5MX&>zO)uN; zj4W22*PBXCp#Ht{$?t05@~_YJdPNNgU^qCj_cqth=jJa6gc$P-h7rCN z1&N}auw(yVgyWh0PZ;7szpD@4cbT}wySqCcr)$tEiQ-*0K&Rg;Cf71ZNL@2(Yisw` zr5p7Z$KW`hClx&pQWFm;fdotP7^Fn6mH&;ac8@b*g3v1sG7(>>mLV?{E}HSCw%*X$ z(w(-xcApc=nJn{%p!#Fy*asEP8(OF(nb-zbYyMQk6= zvvMP!lWP+eb}HR=Yc`_&(ImZ_(-EQp{O_GX_$xIbVEXOuPz%j{TmL%rKkNMPvaqoI zjop6F9!~#ZlH=uTN0R2P0{)G5PS0in!xqF(|6OUM)e>!2B_RDE&rTxcrCYc-h5k47 zc*<$HpdgyTpzE76%*ia&J_c48k(iJY=laNv+t31$((ZM-h1Niv3pR@wU6G`_6zYDi zh46CI_|K+M?80j89`dMp#Z(|c(ZDZX<%=Rr(iy}Al}h8)t^vA#f&q1!Di<5imLmLiGsdUoWR&gBQqU%vNydn$+f(27?JcR zXrVNMlUJ% zJ-rv2L)Gdw36eLa(m6pKt4sGB9*CIck%PbB>mDST`$gAc79|52lPs164p*(3zWY6q zsWJeE&VV@lJUF4f-7Kw3CXJmQdDA;7))Q8j)0Fz!c*a9ji*>_TV1uO=0Z$IRfmQy6 z9$$j`N`kNsep8CwC};?2P*izEPRZ|^E(XuNPJeXgH3~(UYf}P+7YB;jI!ikr<|l&Z zsSI)ynrulSK41TUuNW z1vsB$Y2feBaNG?Th>I-zpbY1WbtHzxrgW0m&teVxMn-qL=H4?2L<;)#O3&_Ur;T}V zI&^9wKqb6b{C{hG=r_yeVgq6$!^zND3(#?#RjON>vq^vZ&ZE1lADm!g!?BLSIl~9! z@F1x2HLZH{u}czo;g6&sCZ~{C2qFRWlB`Wf_B)Kz5ciBpyqR>1w6gBOX?V7-F;qN@ zWLaE%q)jT~o_Qwq!eV*yK}AEA>;cRHDbOuP+LufI+-6JI@YW_ zek&DFJ-0s1zR#HyzxKfGH;9NeoR(L_S{yq`hDiQJA0B@I4RFI$%h4$D{RZ#Eh@7!! zT`MyWpPz_kc|~~0gg4!O-zj{~wtPz#xp04bs5YvTse1_`L_m>&ySMje=^0SzO#Qb3 z1^}5B^MfA#V1bLy+$FuBMi??CM*hDBo)w7f{Bmm{@Fvcxnjj^=+P$B6?cf>@*$No$ zus4Sm4_|@YwpHZ6r3GP+_@H2c;st1*LCdB$WWcl17c8iGr!e$-v0z|k2Gw5Szy)fh zz5B9cP{_Y!+nM?oE)G-nNUoqyU1ulKNPJi5)21skNZ1hR_xK|jeXVbsn%Ka7z4g3s z0|MLH99lo76P{ghK@bKK$WG7zIsLwS`+2=~HQm7|zR#qaU%fEbf2s2ZU+#`sfQz1A z5dZV`fq3Ng70>?z zp(pI_cv==NYYYe?B5^>03uh23ak(h`vJ9sz2Z9c%msgf=ZsnQxhU4&`C#{5GEQk5O z_r%?J#y|HWO|tS`GLrh%fV`c4kyqL40^(UTtuGtJ!=Fmpwix`50u5o zX*TF_pUMFTV17dau@%?P%pYu#up`@^5CA6-qDrM0(#7x2M@au|fT$~QOzGs`4=LEW zCo4Q5?K~=1eYaqN?4#0#7b~JLE|*;a!7;iM7{hAz6aH^a4pnj0*stjdG!0(Roq^MB zk$bnX5|@UJV6Qwe`$W5c3ye2eMoalH(v@PeX+9F8SzD7WD%rG{Qn+r-n)Qb2S#L5q4pk3lJaKvlC~ZE#BVbEK32T`)7Bwf!%)_J ztl3t_&d=#3L@d=p%d6_AdUI#fI#zmiamE7V&t!5I;6WdWaA)>a*Rk8%fUw(B8w%p1 zB;lrq(#gW^_7_1cjC$fpLBcnLzuwE{(S5MPPRRagywN-NJG*T}KWANoD<-uyz8zLZ zE-p$}-cwQjL7KKjlr}X^qLGWtw#;{s@y5iZrfpzzS$2w73r@kLRrYpUq7mt#JS%Y9 zQ^t~=j|%x&L-1|E%TgzN)6%48)!4H|GnN!haM?X1Sy@I;!Z2OE_t*0a1H!lwD>`H@ z?N$pI)0p-!TOQpN{XTkatb{oh@e4nX_*yNq8(-(oaT0k56&`TjtKmlVC(S`9N*|ov z&e=Wq*z$MKdLVLN)Hs7L?2??F&V9eqh9jPS3VXOb`#2VuV0;B_qak9f&Vq=9c_?gp;e)T`Edw`yk#N_)llsS{{8=oU zi&-L$F5R<0uxnYPKEypFomYSrdH|<*kBeTCxHrA`1a;q$acncP1%?$C=BTAQi;`uA z-1i;>`6@7YKSHDi@^z>WJAi5p+2%&{u1R$!{o^etK67#fmfpE%3w3NgmMU73{zy|) zwD)@Nf9;Q~x$LYtWdGQ4x;E-!SEY$IWE9nCh*zW*Esf9F-?;j6;9*ZuC6qufx|~U; z4K>EyduNOpuO%kc^$kUeMxpWz$cLu)o)+_{4y|X`sHMm~QX8$mrm7F`|NFE2Fcl=4 zo>$d+lbdc8tTCoVRL&uL`Ca)pNkmLSBVV)-lW&(2<4Tsl@0Epxn5vf)7*Cqg04KC@ zn>2HVUUz$Y&n$oOt{BB2BucccG#7E^XeJOGX({-5%eGBKrP4OPH znVr#Bdb{JeX)VB(?U%2`uQ`ODq{Xhg^fC6ot0czK-85?uHl^x4XOl@#OBD~conU`H z{rtxejWhcop}f3tJ?-hx=^PmcE``LR5k6#>7mO|*zsf@5!%3tkG8#lLiBiAp6K3iO zv43)1EUE)cftqEE1X~Ch;0Y_4wPD*`iZaxH&FuB5&-Ud*S63Efjt&@>@7yvW1lNb8 z%tj{e~CYZM(Hwzj(3tKMoAMFX1fj&}z-;Puf$#4on(UHsz5DS^SNGtY-fwJ&q z2c4cL&4L5eAOq4Zwkh+_?7X#}qxnj*B=@EtV62!7KF0uQHRN8EmAO@xDcFHCbh`l- zThGY&3G(rBS@Tv+l_Q}e4Wp-CohDDLozSGBZ)8$n!~+?aKBiLJ5|5lV=`G);iJ()E zWq!${JcBzyu~IFw42YwF$MLb_HX;6Cg3q?fjXqg-(hh%cv{B)y$Yl|^@S;?pD3k$< z(G(OnB9CZ7{`%YV+2iEkNc6*uC1b_O-Wc$;u+;@*!6vS%u6PQtL<+a%eO-eEBo_LU z%+xJ1vTQh-$tTBxb91dfuT2slBR`RAv&$ji3VwC06)#0%YODe8!bm$B3xyJ)v6yqchdjAGWW-PzC`Uj1}}N%?s@ zgyDIX8v-P>FIZrlJH@v4kavTRsD_C~r|B%Prj`y5c5e~sP;;X3W{xU(-~=nt8GEvu zEyd@+2PMQT@Xqv-8&cWxIi}yJ!LVm0=$*4Jk@KnRs?6?L@U2n%QP=8o zM9BQin%`g~lIaTT+EjW61IB&qnJ>}2O9tTHYnDDGrGgZVuv%+M(}Tjvy@`Zy)l6lU zJ>dN6$9|l-_C7?-wYb#8Ra4GcJ7Z?Y3-giTQGI+P2qoKMG{8~$4CBh20@LsEMh_gh zzvlo!0H1)54+y>|{y*-fp5~Btb`{!&y2H(5|E!x{S^W0-x?kdsoj@SuqAjQA)y-qp zcVl~B0&#Km*v^`zh-03dcvs41CBIlKYD^W*lv4tP^k?*FzzHZEHswm+7+o%gytZVuS<`ufBp zZuahkhYu5k`me8HABhDb>C>4^ylmibggYa-xAXgZk~h8&oUE#!1j4MJ)3^G6aKy5< zeto_fSAjUhxBrBIR8depL*4f;u+GPEdYWi(k8Ka)Kkk?P2?5uBpi&Jta@mbT%Y}6AnTM)_#4JB;dHb}Yrn=8Wc1dLbJJIbQrp>y2+-Cs=v{wB4-coB ztuRIwfLB=cIDN<;M9UTr2a`LzJ5>@6%E(#(0)c}EkN;YIPtlJot!B^82dsqNc=g-w z(b_?a*`GY0lSXRm>x0{Pqc2X}J3wzbuzp6DIYo7JeC)LQ&I>eidg^*I0i8RDaChf4 zy<0aT9Ms({6bF5BbtP?NM2vlO45r*?#R{+e$0G&-;d;x=+zk@~A_ULEIJU2OZ0z^y zO@F_@f3e@9k@&Jcd*{?xc#HvZROx8#f{N#0i?3e^?8AxZ@)b|8+n561_hqJg?SA`~ zbTaECkIMW*yVDJtf%olT%a=i1xn-^VRPwa8RheE3J&wOA;pu^VMjg`7NULX2Pv|{Y z=!HR5*P%`wCC>n;WWjcZWMd2j#s#noi$X&k4_n&aw)3ln{X=zUp&#eO-)Jl2TxC+B@6B zJlf}p78j)>Xz;lBShk#O5{u`!nQ@39@$jT)O0ar>S{zVe@mxArFmq&YJUVM_Zq3Dj zhr|WH7?@3tSjnkN!M|JsOEk?kfh%k+7Q#fw(;Uqt;iNGfwr<9k^D&m*>?a9ZP@Sxu zNe++!+s6iw<0Zj*N_Z-h{S6`VRem4Q{I^O&qOnfTze4gEM?+(RQKrbB`&ky5l{#<+ zSKjinu%kPe``+Ghr&qsRw^trNZvrSfSD-fe=L`_`jO;Ku9 zEtQ2O!K55h5i*GFGDJa9Dd1l0x%yidsw>79v{>$KYut71N*}E!T|Z8&d}R4J3wfo< z3?_83iLw2{CrnYG%6BW1Z}*X~7(dvFJo&)N`bGJGUCg-YD2u-f5#*2$#{<>3C;eRW z^}V2W;1lYdXy6kZ(_LI}pduXG1GNVCB|A+T*UST;#7J@OUFrw-q*IXjAMWF;rAxw8 zlv2M1W3B-cWofD;V9imB|%yGaHM6*)AF zrs6>S5a(x7+Bh~+mt|GY!ZJL>-zcZQ%Hf9kUnLVU*n3<`decy#&2Hyw@sJW5R-}ga zCn`5FfPBGzrpUW;t@Q|3@CO+tW+LHKJ{jES_Z@vU){8X8Wcz`O6oEl^oV^E?zp)SN z7G#3(R_IGtf>rW0W)gK00&c&5fy>*S0|JHYZr18p-$QcvRk)$27}Sb-Q=5KpIQ)rz zS+P8>+wH{0%sqFjy(AbuJNVchXS27u`h2Jl<%H&6qq&}2=d)Zluy^1xMr7%l>wAlt zSmmFkMnFLbxsfs;HNFxdhPE{i{BdLv;7pL*0Ty4A+$5DHKK422Ytq_Wvw^*bKdTvP#K$aRY6ARCib@5Y~m2 zMV1bx?}s3CKk(^$_`)9dr@qiB_6&(8GWpA88w&Iuq>19YesoFjGr_(n#lET#5Deu3 z2u=2iW-S^Ex&Lr_I8Yw_J;z9$9QK6Yp=Vw5UI8V!NC8TL!gp{uWst{&oq>6KaCn#v zR8NRJuSG95n-GW%c@jWTKSX2|lN&p&n%(!dhSeq?6hMAW4>5UB5fGPf=#_7`c;l)S zLtK0SR68NEg*~uAb#q`_;OR$YO-<2)&cNby(db?H{N0w=;EjIN4c#JG`;zbINE>>I zuqV-UDyUjL$8A3&z3=#J_s!0t3cU;_gSdjB3g$1w1#ZV3P^ALv-TB+H4KCt!u(GnE zQn6%=SRCo~^sj}5jW4?6vJK{IqvzF#g_|1&z~l6+!jbi1QqyZ}W(LJwohJpVZSEJp z`ma7p0s;|^r*|K;j6@sO`b&g9D5?#*qFt?KjJ>?zo`kPS@E)K+l^+SP%E}oATK{0? z;)1S|TeN1s7e@Sr3eruU?`ldUGi;qtm;Apn+M>)n{>J^=C~-Qvrv>!RUbDoqI$1=e z9$H-d%Iwej-Wgs<<8-}4ssO5(+=f0_pqk3#z4G@zob-CD<4U=_uz+;)saO2@WKXhj z1D_6Oa(XiVuOsN~cKcskTiqY%{`_$V38w*`mrpo>heb6Y5$=-dhX+Q1K8{iba%V zLlnxl?u52Aor|pooQ|i(sS3#i+o2X; z4)W{UoAZ<4(;g^Y3jvs9W;XRSM*XM~DmF@c5u@?{d8*(=%yb894UhXR!Wp3m5ZL=Y zb2NE<-MTFpr-_aojekbH%Nl8Pm73N$(br95AEEAKx-&%4x7cg@#q8NNTEUpf+7y8UAJz#TprJfQbw$vAF+I|Y|CXb#ex~bEeA`0Y2t#ycu z*eHdyMI(Wm(2w}U$EZ-dsdt-wqg}%t`;0-tJ#91eCRnaV$I2fLnmzuVE4l8;C&}lS zwht`X| z!6qR^Rbv>Zp%UwM*)F>k~P4ipsA#fMa&leZ62-dRFr;1PE#zBVT~;lnhD za9k1?Sut`sIR(^ zs-z(P_7|KBw1-iPi>{c26v3|Npyw1yv@m4glA|Du$!9dEj3Cw5%3E4GC?HAN~lwEGuTqBayQ z)~Kdpwyg57TQPI&ViXF6In29j3nS_;c_NXhV$2SqN@RFAy9oVZT(A6_3uY{(mjPyU z;T@&?a}5On%IkKVVTk)-k<-7U_s7vb7OcDkN$(kO^&Ya0nEEo%a>X8${pl>(P(KKd zv{E)nRmq7O6}QrXQ04JN^DWU@Rfh6ZDPblUm;YFiVHMpdkB((d*Q~j2R?Uc|yA1#9 z99`m}fw;JRpcP89|J-k;H@=(L+Y^|X?XwK;;2V9}fy&O<*grtv`AUt6pC3|(`&C~U z0*=!oqL&T|qGMn0&N6BS*}mc{0rHuv#&Z=QP8HhN|C6oS(2E%0bic~F`BF#T>i>`K z6$ZN2Zv+`=SJ&Jg?FX{`ZA_9DJ_G_22zjFyhsrG?{BKzk-m)rf1I z&4FL9GoKy+kp|94h)k)-Nj-xQ*RP*{$PwYWqbGwCMuxKYQSD7#aj%glkq5m$`Pjf?h9mDktX! zLePAU9Tb^UjyeAmPXFBTQ3Re*A1^i{VsMxMBq*NK{aJ6J0QJ+R|f!? z?eA+J%YX+K0-;~uYOSt>2!ub00LJdlU}}2J1-UDU78z2=gRaTH%aXSXQ=qh);S8%O zS2V~zQ;xM>hKvI>DIq!Gqx{tj5ma=Mt~j@w$M^7{Fr_7KZ0Wv#-wR7v&g>0*)mMRM zV+WSxamJpfe1p%0Ff%hD!Rpd|IMVPX@(7N5bt+R4*|sv>Vi}dz8EzYJbzvBFnNm(;UA&u!u$VRwa7~aKQh_l>Ru-2)EZO1IZeu+g$6CU#7<}Wp8Ciq zyfINILhxZR3q(vr2A#>vpxjy1n-fGk|HGV5G~p_R`mVmQV5LTuJH=+f#gaY-zy}2I zkF#tBgEEugjy}yv8>ot4$+EbDxxq;rZLTBCOY}(S`bGNKpw~DOo&#@81Y<1lmk)wf zS3BUABVyc3kYT&u36uVRS4nOYw2L~pSva;xdCWU%yD_k&_j;;LX0X{3TIGFmu&U(r z`bK>0a^DG9p{Cn{Y&^e2MyCq05Pdu6o0y%Cb7DdZhd8+5;oj===@hEbuq ztmuyE3M9kwXD}ZtZ7_S}9haIVB;Wn&la^sH?=9otmJ8CfI_#ZqI!@i5_%z(1Lnt32 zJTzH79%nTMOA42kQ+0~JVrEo_pOTG#OD8wH*WqRhscC~mgKGW(_JIqM%O|*Se^`Vx zH0xM5R~=JKe+o$s%NBfd2~Qz5l&@Q)69ym4?>me75=z5itba{LjydrlvCS}rM}7XM znlBMvHE?e|Q-rcPK-(|bZ6J}Ye)WM=o03nGDl9u@f60OadHq<5sDKIqT1%1o*rMeBF>j{u-Db+6TyI#r~LC{La8> zDQ{VI($GI-QQ{8D22vO-hOG?}n^lq_HQ@R|d~`7MALY&-d|V;QFBC)k>sIrVRIfNDbo@sBdair7Jj+Y z?_1$;Cyh4ce{~p9_wum82H{ks+i=I~$g`ObqAS(n&Jfv_yC8$-tOv`xZaj#p##jyB z3_8YVzm-IsRup&djlbdsTXUD=(%{}}`(zt7(O+~&Y2E(&VWYMhii5r{WmDPXFwcI=J{z3|*$&_oL6kzyoJXpxEtxc%jc5 z&F#~fhSg1&IrnLcpJ?q=3{$z1zV^3X&K!4K074(M=4NId-9}h{LPcG7{RA3*dEE@U z1`Lil)19j=cLrWY$eB#+!n`3s=7I6Tnc#aE6o>U?H}V7wsHQx)Vi^_SFb1&4gg@2E z@Cm-OeNe@?Buo+VK#-T`owVV(wAR0;{CBz@^XUxY53H&i_U*3=e4dZk*&cgZfTRy* z{b{^`4S4xqzp@sazY|q{Yl>`IA~;!T(}`F&)r$TUL)>-Oer?B}E;j zbYP!>0<-4%9_WC4?OeLc4Y^tzP`01F+$MVkX|U>d(+S z=>nJmGjp>#HoLMaw4R=x)b#(ta@ud8S@qik2T0#w&)WBeTAycffW_X}07#1Px%%)8 zD0LVa83CSAA`QcHN=h;t4^Q++ict42G@(wvfRJj>JJN5B4y3i5oN6w23muUo02A{4 zI}WG_Y}LTam87VZoZ9Xvj{H16Hoi4o6axaG2|wZkY63RlPZ8@6tLr^F_U&PFb6@#1 zj2sVXrGjZ=_v^tcdYAcXMi>sGS8MQS)IogeSv|YAAy!%nLip1 zCxnw4=$~9$oZK2iKqVE+-q+mbCjy|M#YH|`+`aGEBO~&JNg$CZX!eQ4Ll*!Q5mor| zWqa?2OHQMkKc=E!d+H~-t@fa}-sW|b>~t)=Bh7`hs8D3vs-!aQ7MwEPnEAO}MMgn1 zXS@`x>lS?-src;D z1Z7_mfTXDCJh6m~H0v9I!r-)OBWBu~<|cD7S( zr1SeRn4eHmiX|c*m7UT_pb8zO=Ze1MWU4s@!w3KFPrSV3ga(&K(wCZRvE?Cq|MmOy z>I@Eh5AxC?|BkYl_U%=keJ)=K#g$BKG%<%!^B0`Je359UP&)B@ej7uOE7q-StnsWc*?-bW zON*nUOPtSRZ1G<@${Og|pWGMA1vWG8Hd1)OHco3)jG(rqmR~Wn!*}?k=CIw4o?ja| zf$ir^`&`!PFlBQ>E+*j`XE!U%KH`TqN96F{tTQH(R+osS@n+tU9=q~_f`naK7q=BQ zn`O3sQ744p`E-sP_fGh~L%9Q(WOnkc0>eh-UJ(glj1nA7%47p}tW{&koFK?EFmyoV z!+mAIF(0-q2dc}m4L-d*5+_e&Y^}_p?I6><+a+usX^6t&rucp2BA+RWXZfq)S{Zfe zBm3oBD{FF(Xf1QdAASRt)=iEfT1r873N8R*S%rpzX3V-C$`R8H5L=;DkS))3WW5@{ zf)ebPqf_=r=xaozAbuRyP2SR@SQi$gLpqDR-oO%1q!<}8K!-eAdQs0_5~tn(vWH*! z9|WYXB4wkXHiazd#$%!e={wzTIZ(qUYO*iJ8x_aRBs9nIVa?R$75Dg8a*DAMTtv6H z^|qhi(l&d7vBiz>bD@nb^Zu-;w7+?b<=;WLOpkeN)W$ zlp54k5p5HxV`c2#fQ?@FC6UtNmrib+X{rqmKw4e~(J+5U21o&rQj}j2UX>v+(AT3A*(`!@`VOv4#Wb9zu7Nudn=O_Y3U)$a~66jC=ULjsxG1n>629J=t zP25lT{ltRDp>D2Qeq_YL^Edy+{L+Zq!QVDbd-)2em}!E<@mzWal5~+E{QS^|WDgAZ z@9e16hUB$eDQTC9k3(YOzJ2tuVJdq6IyStN!$Y@Wy?WJZl*x??L|82Y8}GjsDE9ja z?3b6DTks4{9x{iJUE@-nKbmy zDv7Fcuj18Is3Y7^#H=aA7r6wJ2#%4t6 zAL66((ao}iHz~TVqD}>)PAT zDaj>yKmDT`aR&JOJiV$i<&CU{wcf2Q=)dQwVioE=VcX}M6})SmS!AD}LG;bKTp29S zW6t^^cciQ1gs!JeAJYnsj;Ia`*ji9Ch~P~0V}EQNs!kNp|JS%3{fVvK7$G?&#c@IK zE}vOGQ{X}93Niqw0)QzV)0Ia7j)^n?Fa_bKA^FtI%w8BYp-Anv=yUHUokphzj_jZR zYVnDQdpqv;-xQJtpU{NDJ=(q`XnRXF1}}3-rU|A z+J46=Fy3~1DEXMTWMv2hN~SK>zzwW>BV0Q>yY2iLa0L7S?+gLRH!e>3{WJkc7&197r@T^CFHQ$C&ino{ps9M+R}#qI||)H_6t*-pKo(F?@tPia|zdJw}=Y71^hh z#pJZII6>$giyWw^aqWnFOi;K1jQ3RT8(Js z+kH@@j&N~qP_fDYooDP`%H02A6pCKQyz~<7Nx3p*qrD}IzE9IRgwJ(wM_$zhmwYSz z8U+p9zMgpUJ~NfGhaT3EJ31=jcvm|7Z_0rFZPhio4u!%y>Q);R&9y;3-Cq)T5fBUI z+LO$}sUvd8rhGIjLgXz)6z8@Ji@_+IHPvU&;Si1c?!X_B2-SxtNUzI){74jMMUcE~d|3Kf66Cpo6=U^=@V|Gn!_**hF zXHI1%;YqWf`Hj;f>-C2WM57MvYWSdScn6NU?1I(-^R1HEy3^giQ~Z;*e3jhb?`q)2b@^TqeDEE&06Kfgkl+e}yk4|Zr`s^8ArzT3a&M-q zL=Qtg?M@HpmWeXxz~jEMpkQ0cRsNsZ1^p5-EW|ZK%fz5R)zH6{oEnn$f~O$h`gl0! zOMVhk8CjVji@kSvB+16S9qI%nC+Yh699x6hyjNt*GS?EMuL>X;P%&po*yc_fYkYcKOUF9K85 zkds4oV7dE3uaxm=H`px(4qtJBO_?}cbnN1rBnFIBP&T6YVRk5+j=uuce;ZV^((}^6 zd#Xu!BC*_)iK5(Q_}1~qN`V4QArfea&LVnn+Fx?}fB6#@u2j_0DMyJ%Sf4bhjWnTz z;a6wsvro{QMsl<91W1o};}F1=(yJENBWa_^DbL#MxSf^QZz0&n;ho2hT}W?ZxtSS# zTd@4Q+GUZr>?5t8fO99Z{E0=VRU4bHy*hHp42B?tzk^LMfTW_B5C>$CueGpl93=p7 z1P*K4qgRXoLjeBs<4u0X`NG%ZNngjlhcLkKb#<8YWJAWkK3`&0`{cwv&yGcrm9^kz z_NN(moTX}Ob~?ORjBw2PuvZ?xtr zhf1_Hz>#7X?b`=KC6rQHUv8~Ves;$x!80}BzjwGw;vu~6DzzXEPB*eLYt z!;x20uIj4kr-ER6=s%)wn->r}yTt%L?=O;=?6iAkTb}pcg1_Pqbn{yLgg=cB{deQ4#M_J^*oMcU$N}4(J6{= z?RXTB@IG~RcBlbcp6OLMYs+HIE{Jff%c4zT%bp_q=K~@OwOb4KLR&LO1e&lH$-K5r zMrQBKgcp2>eGAI|cvfG?{>0`b5TOX{?R^mwZ}{)Sw&m^w$VwQv=o-2Ma+*ReNcp^; zPU*v0$8$bK{JC`G%J{6Xp4#8>)bIumnzTVNyjy$X_3prjCyWC2Ur3-dsVDedkcap4 zS={Ra;MZSH_8k^h?>3&+A9&u;`mQdD70>k-z8{ZV$;tGRWB-0J@a?lXU(-H5J~=vj z5So9<=|2U`qX3+-u`_gc3E&$JMLl>O&l&Gjg_)V1(RWigG{Gx+KuZC@YZ#uSN>b~3c?SesMUr&rsa6a?JJ36wsI7uHoLX0xGupZ;l7JW#xu-PW9jLrA` z$~#fgPq*a`Al5Sly$J#Apz+NQ!Es}f0#G`R+)t;pM>ec`WT@icXliHo99;Y*{rU0b z=87DogssaBUU5Db)YQ?H)Jncz+3)bpr)P6c$S0)zYvVV@F}gwx zh)Aol9}Q;H>dd$z2H}(CuTXytrx%eRBVU~9p_)a)&biWII5Z;0-zn;y$)3_YvCL@& zN;TYBdpMFs9g@S`C&qSqQunczMhVeU$^9&B#p0GQsS5Dw!WWh4CdAsRVr$^plCwZI zCL*Dfu$;*`7G6n+bX4b|R9>=q3O65lnqwgpl1lZ_`P{w*+(+0n&BTJT<)LtDLw1|3 zl7}ozd-B-29OT$VCB%zPx%m>258iMz9wB_trdn)B78Puh+DK^efBXjB!wJcI5*Pll zEEH5T>o8P&Z4z^O@cbHW20yAwq`1(%uH`T|$g&VI%x6n2ZR}ypH7%2GIEaotEj4dV zu#g^4zY!S)OYmmP6zo?W4bPQh%25AAFCg|We9%7cg0n{hdzGA9EI&;{oB)(GQDi3R zFpo}Gt>&IOtfQRK8h22cN|(qT8hOi3p2~>#yXDJ6k>+Rmm-UlZKHI7rjL=;XveP^A z33*zWk^UBPjeYHN|9=LMh)}l>wt-7L?z0cq4if6_txRPg zV*EkWqi$bc#;)x_6j(<1Fj8#5TJsk*m)Im(Q5z@Y{w`1W8iu(D)6~-mxrM}Ev_@+F zx){Z#7Z5)~s~b>PtzO6~cQ~!mIzGfNTd?b|s6dN`Xwx2aX34JDn;?7V{y6RnN$bL2 z@qVn`uYC}ZSkPLs&R@R@vrag83Ues=#H#0@-^6 z$uw!j?5Is5k*u^Wn)h8fqr+j`%OfYnHUHJ7f204UBD>{wgvb3JPY9dz*V%fbE*Wx? z5Lb?9%CHTO4?FnYBHA#P*|+tJ>f9~r3bk{0D2vwd(Y?=QYyy9H8>5$(#;q=N?V+c? zQJ={5J;!uC=8Q4pdHj>HHvK*?QE=FRbuL3HWIy>Px@74Xf_b&+d_hj*8^YYQ!UefewP5{6pne2(YAL=uu2t2lexssbG&QAN zNl5J6Qn&jg_=lT)fP?x{f`k`}q>;onBB@=p^2T02369kEC1JwGla7ZsrLGxSJ)Mie zOgi+`Kx*h%J;l03ws|5)ze={6AJ1MrbY^qpU?O{Uzq1LbDBzdl@7$H(Sczf0!`Vh- zDlh2l{S_qDGs7%DPKoDZt>9^bPV=EmnLocHc+D60UG-#iZY;j4onX1)T;B~Na zg!`hPc+uj!5p=<8&G=mBL<})r%Kqz82(2TiZKG7l%1It zs6_0T z!v@!S*!K72WNWdI5DBST#yi=??QBRH&#(>;_U=m4 zrE1!q-Rpq#A3VLSSW1w~-36~+>!+SWH@dZHdQdHY#&#ZE*&;ak%14lBlK(XC|BhI(ky!{(N0dWFURdWEWg#dUybG&w$?#W&+h5J`ln0fvyYjnBg`ghh`@WT-2%I;5nK|&=s zd_Jf39$H;pgH3=ZSi7P)4hoQDxLpA1!1A|yd6u*;+sVPBK$D`b|DFd}2 zA75Zp)OVK_U64rIl!(55j#O4}PEryMW^MT1UeTa*l zP_Dg5q?z#PoWkQ7jN#wx68LStUZkLli*2Cw`bqaAPmn&);LIobPm*E}Wtoa;ILV<9 z@ze9MM9M{@teF*_{P3Nn&BsD=*4;U}CyYmp!&aqcE znVP844k75WdeHD&@aVonDb&33@GJk(Tb;9Ska7Pw5ioZzlhT@I}$?H9m zmb~2|NJ{ElpM&@<#Ulo)5pKZq(@;N zS|e&4?c9gYo^jQb_7BdLV);q+M7>0n{506xKfoK;=}g@Yen*a`Y-TlyxWdZ@9jkb$a84upDa^(*b;H+@rJ%BBpIF ztN1@94`z=2w@pdEtBQtcm9LU;>EB&GasKRc_&flEpzd-^s^Jd-5vpUDM#QUWpdCl*rB>QZ!rf#>7@c(!S`4+7jEirKJ2zKVHuZzsn zj^6Y@@oBUr5&{h58B9zUpx5W1g-f)b*R{SwO;Q0H@UT@Z(eQ|QdkU0GFlfv+$gK6a zY+Ky>5U(Lm^PV+>9j38Wop6bkl3R)l)7JzaAG`r zpv}T5(j8rek-^94i@S{5s@U)~z-EoA%PH586W%^gd_d16xm^!%h*m))wGS$>=MDje zd~{Nh);q=xfZ3M1epmzRJ@@x~v<>GV5P*JDgMFX+Nnt?>Hffzek$^!(QR@R0)qMpZ zm@DNabUV*#a6CC+_nuoBl{{H~lbKx&bl;fCJ7@RKUp6*l?c5(7Lwhf3JnLJ$=X;O! zY5yB6E&R#{L=FKpj(-1}`T*Vy;PElrz((u#GN%D3-TWtfE*JjP)rA)INpduM#@yC( z0~v}v_tI|{G4=Y}SWof%SVs6gfA2_-kBNY$NhCn})=5R?Lu{(NagsK3f6?tu-2Q_4 z{15BxgP!+;d86aDfZa-C^!okjdLKN~QANFleVya>f7~Z4O~}VY$G(5=*M77|)wq<- z5v8VO0nmHO6bn*$d3o-fRom&ea}T3nv*hpB1*Ms> zS+ws1J{J^n-JuT-mcFRXX+;DrHd!-~dOl;homIV2%>!PDWEwLwGqIxt<{q?;J6&J~ zE1a$bZEhOA<1-Uuu-WeaobO+MbVLciki9Xy-gmq{9k>Xj>bLrJ`6)+6*@!=XxX^I~ z=S2)jS@LAO^A~^EgiC^b--9%Puqxo!bRdBaTt1~X@kc@44_2Dim-TP=DHaXOi|xh; z086v;kL2F|b(754&h9K(5ovA$pluv(4wQH53B&F1`teUz8_4kZDlkEi%N

%>m)* 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

+This version introduces ABI changes relative to older major releases, so to +preserve ABI compatibility it might be necessary to define the macro +OPENVDB_ABI_VERSION_NUMBER=N. +
+ +@par +
+OpenEXR 2 and Python 2 are no longer supported. +
+ +
+Boost is now an optional OpenVDB dependency. +
+ +@par +OpenVDB: + Improvements: + - Removed last traces of Boost when OPENVDB_USE_DELAYED_LOADING is OFF + [Contributed by Brian McKinnon] + +@par +NanoVDB: + API Changes: + - Minor version changed from 4 to 6 (major version is unchanged since the ABI + is preserved) + - @c nanovdb::Vec3R is deprecated. Use @c nanovdb::Vec3d instead. + - nanoToOpenVDB now accepts the index of the NanoVDB grid contained + inside of a GridHandle to be converted to OpenVDB. + - DataTypes are now public in all node types. + - GridMetaData can be copied. + - Major improvements to GridBuilder.h, which allows users to + construct grids with random access on the host + - The move constructor now requires the GridBuffer to actually contain a valid + grid + - Moved CudaDeviceBuffer.h to cuda/CudaDeviceBuffer.h + - Added @c nanovdb::pi() + - Introduced a new extendable API for acceleration of custom random-access + get/set methods on ValueAccessor, e.g. @c getValue(ijk), @c isActive + (ijk), @c probeValue(ijk, val). + - Added @c nanovdb::BitFlags. + - Added @c minComponentAtomic and @c maxComponentAtomic methods on the GPU + to @c nanovdb::Vec2 and @c nanovdb::Vec3. Added @c expandAtomic and @c + intersectAtomic on @c nanovdb::BBox, and @c expandAtomic in @c + nanovdb::Coord. + - Added @c nanovdb::Map constructors. + - Mask:: DenseIterator, Mask:: setOnAtomic,Mask:: + setOffAtomic. + - Improved and renamed device function that converts voxels into a grid - @c + nanovdb::cudaVoxelsToGrid + +@par + New Features: + - Added a new grid class called IndexGrid in 4 flavors (Index, OnIndex, + IndexMask, OnIndexMask). + - Several new ways to construct and modify NanoVDB grids on the GPU. See + CreateNanoGrid.h and CudaPointsToGrid.cuh. + - New device function to convert points into a compact grid - + nanovdb::cudaPointsToGrid + - Added cuda/CudaUtils.h and cuda/GpuTimer.h with cuda + utility functions + - Added cuda/CudaPointToGrids.cuh that constructs device grids from + points or voxels + - Added cuda/CudaIndexToGrid.cuh that converts IndexGrids and values + into regular Grids + - Added cuda/CudaSignedFloodFill.cuh that performs signed-flood + filing on SDF on the GPU + - Added cuda/CudaAddBlindData.cuh that adds blind data to an existing + grid on the GPU + - Added cuda/CudaGridChecksum.cuh that computes CRC32 + checksums of grids on the GPU + - Added cuda/CudaGridHandle.cuh that handles grids on the GPU + - Added cuda/CudaNodeManager.cuh that constructs a NodeManager on the + GPU + - Introduced new (dummy) build-type @c nanovdb::Points and @c + nanovdb::GridType::PointIndex + - Introduced new types @c nanovdb::GridType::Vec3u16 and @c + nanovdb::GridType::Vec3u8 used for compressed representations of point + coordinates as blind data + - Added PrefixSum.h for concurrent computation of prefix sum on the + host + - Primitives.h can now create grids on the CPU with SDF, FOG, and + point or torus + +@par + Improvements: + - Transition from C++11 to C++17 in NanoVDB.h and + its tools + - Improve NanoVDB Build Traits. + - CreateNanoGrid.h is replacing GridBuilder.h, + IndexGridBuilder.h and OpenToNanoVDB.h. + - Syncing PNanoVDB.h with NanoVDB.h. + +@par +Build: +- Support for OpenEXR 2.X has been removed. +- Boost is no longer required if OPENVDB_USE_DELAYED_LOADING is + OFF +- Better support for building with external package configurations with + CMAKE_FIND_PACKAGE_PREFER_CONFIG=ON. + +Python: +- Removed Python 2 support. +[Contributed by Matthew Cong] +- Removed explicit bindings for Math types. +[Contributed by Matthew Cong] +- Improved type casting for TypedMetadata. +[Contributed by Matthew Cong] + + + @htmlonly @endhtmlonly @par Version 10.1.0 - October 11, 2023 diff --git a/pendingchanges/nanovdb.txt b/pendingchanges/nanovdb.txt deleted file mode 100644 index 239c3f5e93..0000000000 --- a/pendingchanges/nanovdb.txt +++ /dev/null @@ -1,31 +0,0 @@ - - -NanoVDB: - - Minor version changed from 4 to 6 (major version is unchanged since the ABI is preserved) - - Transition from C++11 to C++17 in NanoVDB.h and its tools - - New (backwards compatible ) file format that allows serialized grids to with streamed directly to file without headers - - Several new ways to construct and modify NanoVDB grids on the GPU - - New device function to convert points into a compact grid: nanovdb::cudaPointsToGrid - - Improved and renamed device function that converts voxels into a grid: nanovdb::cudaVoxelsToGrid - - Introduced a new extendable API for acceleration of custom random-access methods, e.g. getValue(ijk) - - Index grids in 4 flavors (Index, OnIndex, IndexMask, and OnIndexMask) - - Introduced new (dummy) build-type nanovdb::Points and nanovdb::GridType::PointIndex - - Introduced new types nanovdb::GridType::Vec3u16 and nanovdb::GridType::Vec3u8 used for compressed representations of point coordinates as blind data - - CreateNanoGrid.h is replacing GridBuilder.h, IndexGridBuilder.h and OpenToNanoVDB.h - - Moved CudaDeviceBuffer.h to cuda/CudaDeviceBuffer.h - - Added cuda/CudaUtils.h and cuda/GpuTimer.h with cuda utility functions - - Added cuda/CudaPointToGrids.cuh that constructs device grids from lists of points or voxels - - Added cuda/CudaIndexToGrid.cuh that converts IndexGrids and values into regular Grids - - Added cuda/CudaSignedFloodFill.cuh that performs signed-flood filing on SDF on the GPU - - Added cuda/CudaAddBlindData.cuh that adds bind data to an existing grid on the GPU - - Added cuda/CudaGridChecksum.cuh that computes CRC32 checksums of grids on the GPU - - Added cuda/CudaGridHandle.cuh that handles grids on the GPU - - Added cuda/CudaNodeManager.cuh that constructs a NodeManager on the GPU - - Added cuda/CudaGridStats.cuh that computes grid statistics on the GPU - - The move constructor in GridHandle now requires the GridBuffer to actually contain a valid grid - - Added new types: Ve4f, Ve4d, ValueIndex, ValueOnIndex, ValueIndexMask, and ValueOnIndexMask - - Major improvements to GridBuilder.h, which allows user to construct grids with random access on the host - - Numerous improvements in NanoVDB.h: e.g. Customizable get/set methods on ValueAccessor, BitFlags, transform(Map), expandAtomic(BBox), expandAtomic(Coord), intersectAtomic(BBox), pi(), BuildTraits, more documentation, Mask:: DenseIterator, Mask:: setOnAtomic,Mask:: setOffAtomic, Map constructors, DataType are now public vs private in all node types, GridMetaData can now be copied - - PNanoVDB.h is now in sync with NanoVDB.h - - Added PrefixSum.h for concurrent computation of prefix sum on the host - - Primitives.h can now create grids on the CPU with SDF, FOG and point of torus \ No newline at end of file diff --git a/pendingchanges/vdb11.txt b/pendingchanges/vdb11.txt deleted file mode 100644 index 6448dd04a5..0000000000 --- a/pendingchanges/vdb11.txt +++ /dev/null @@ -1,10 +0,0 @@ -OpenVDB: - Improvements: - - Removed last traces of Boost when OPENVDB_USE_DELAYED_LOADING is OFF - [Contributed by Brian McKinnon] - -Build: - - Support for OpenEXR 2.X has been removed. - - Boost is no longer required if OPENVDB_USE_DELAYED_LOADING is OFF - - Better support for building with external package configurations - with CMAKE_FIND_PACKAGE_PREFER_CONFIG=ON. From b5bba2cd0d6dd904fcdf836116d0947ab73d04e5 Mon Sep 17 00:00:00 2001 From: Jeff Lait Date: Tue, 31 Oct 2023 16:09:59 -0400 Subject: [PATCH 19/22] Fix for potential crash when reading invalid .vdb files. Original fix thanks to Matthias Ueberheide. Signed-off-by: Jeff Lait --- openvdb/openvdb/io/Compression.cc | 27 +++++++++++++++++---------- pendingchanges/iofix.txt | 3 +++ 2 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 pendingchanges/iofix.txt diff --git a/openvdb/openvdb/io/Compression.cc b/openvdb/openvdb/io/Compression.cc index e7f5e7a618..bdfba7b5cf 100644 --- a/openvdb/openvdb/io/Compression.cc +++ b/openvdb/openvdb/io/Compression.cc @@ -123,20 +123,23 @@ unzipFromStream(std::istream& is, char* data, size_t numBytes) { // Read the size of the compressed data. // A negative size indicates uncompressed data. - Int64 numZippedBytes; + Int64 numZippedBytes{0}; is.read(reinterpret_cast(&numZippedBytes), 8); + if (!is.good()) + OPENVDB_THROW(RuntimeError, "Stream failure reading the size of a zip chunk"); if (numZippedBytes <= 0) { + // Check for an error + if (size_t(-numZippedBytes) != numBytes) { + OPENVDB_THROW(RuntimeError, "Expected to read a " << numBytes + << "-byte chunk, got a " << -numZippedBytes << "-byte chunk"); + } // Read the uncompressed data. if (data == nullptr) { is.seekg(-numZippedBytes, std::ios_base::cur); } else { is.read(data, -numZippedBytes); } - if (size_t(-numZippedBytes) != numBytes) { - OPENVDB_THROW(RuntimeError, "Expected to read a " << numBytes - << "-byte chunk, got a " << -numZippedBytes << "-byte chunk"); - } } else { if (data == nullptr) { // Seek over the compressed data. @@ -268,20 +271,24 @@ bloscFromStream(std::istream& is, char* data, size_t numBytes) { // Read the size of the compressed data. // A negative size indicates uncompressed data. - Int64 numCompressedBytes; + Int64 numCompressedBytes{0}; is.read(reinterpret_cast(&numCompressedBytes), 8); + if (!is.good()) + OPENVDB_THROW(RuntimeError, "Stream failure reading the size of a blosc chunk"); + if (numCompressedBytes <= 0) { + // Check for an error + if (size_t(-numCompressedBytes) != numBytes) { + OPENVDB_THROW(RuntimeError, "Expected to read a " << numBytes + << "-byte uncompressed chunk, got a " << -numCompressedBytes << "-byte chunk"); + } // Read the uncompressed data. if (data == nullptr) { is.seekg(-numCompressedBytes, std::ios_base::cur); } else { is.read(data, -numCompressedBytes); } - if (size_t(-numCompressedBytes) != numBytes) { - OPENVDB_THROW(RuntimeError, "Expected to read a " << numBytes - << "-byte uncompressed chunk, got a " << -numCompressedBytes << "-byte chunk"); - } } else { if (data == nullptr) { // Seek over the compressed data. diff --git a/pendingchanges/iofix.txt b/pendingchanges/iofix.txt new file mode 100644 index 0000000000..6d095d7a02 --- /dev/null +++ b/pendingchanges/iofix.txt @@ -0,0 +1,3 @@ +Bug Fixes: + - Fix potential crash reading corrupt .vdb files with invalid + blosc or zip chunks. [Fix thanks to Matthias Ueberheide] From 996b2971359559cf6273fb359c32f7f29f52c469 Mon Sep 17 00:00:00 2001 From: Dan Bailey Date: Wed, 1 Nov 2023 00:08:08 -0700 Subject: [PATCH 20/22] Revert "Replace boost conversion_traits with std::common_type" This reverts commit fd4c370b22f983fd89fa7eac8d6a63ee493be45f. Signed-off-by: Dan Bailey --- openvdb/openvdb/math/Math.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openvdb/openvdb/math/Math.h b/openvdb/openvdb/math/Math.h index 1406774620..8f2e705973 100644 --- a/openvdb/openvdb/math/Math.h +++ b/openvdb/openvdb/math/Math.h @@ -10,6 +10,7 @@ #include #include +#include #include // for std::max() #include #include // for std::ceil(), std::fabs(), std::pow(), std::sqrt(), etc. @@ -915,9 +916,10 @@ enum RotationOrder { ZXZ_ROTATION }; -template && std::is_arithmetic_v>> + +template struct promote { - using type = typename std::common_type_t; + using type = typename boost::numeric::conversion_traits::supertype; }; /// @brief Return the index [0,1,2] of the smallest value in a 3D vector. From 1d6f632edfc463aa28a1ae3e3dbbe6b89cdffc19 Mon Sep 17 00:00:00 2001 From: Dan Bailey Date: Wed, 1 Nov 2023 00:12:48 -0700 Subject: [PATCH 21/22] Revert "Remove boost from the build system when OPENVDB_USE_DELAYED_LOADING is OFF" This reverts commit 828bea2b75c6538a71cb8ee9d787e028e6693bb0. Signed-off-by: Dan Bailey --- cmake/FindOpenVDB.cmake | 32 +++++++++++++++++++++----- doc/build.txt | 2 +- doc/dependencies.txt | 4 ++-- openvdb/openvdb/CMakeLists.txt | 41 +++++++++++++++++++--------------- 4 files changed, 53 insertions(+), 26 deletions(-) diff --git a/cmake/FindOpenVDB.cmake b/cmake/FindOpenVDB.cmake index 5536e70a30..c4213c853a 100644 --- a/cmake/FindOpenVDB.cmake +++ b/cmake/FindOpenVDB.cmake @@ -491,11 +491,37 @@ endif() # Add standard dependencies find_package(TBB REQUIRED COMPONENTS tbb) +find_package(Boost REQUIRED COMPONENTS iostreams) # Add deps for pyopenvdb +# @todo track for numpy if(pyopenvdb IN_LIST OpenVDB_FIND_COMPONENTS) find_package(Python REQUIRED) + + # Boost python handling - try and find both python and pythonXx (version suffixed). + # Prioritize the version suffixed library, failing if neither exist. + + find_package(Boost ${MINIMUM_BOOST_VERSION} + QUIET COMPONENTS python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR} + ) + + if(TARGET Boost::python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR}) + set(BOOST_PYTHON_LIB "python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR}") + message(STATUS "Found boost_python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR}") + else() + find_package(Boost ${MINIMUM_BOOST_VERSION} QUIET COMPONENTS python) + if(TARGET Boost::python) + set(BOOST_PYTHON_LIB "python") + message(STATUS "Found non-suffixed boost_python, assuming to be python version " + "\"${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}\" compatible" + ) + else() + message(FATAL_ERROR "Unable to find boost_python or " + "boost_python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR}." + ) + endif() + endif() endif() # Add deps for openvdb_ax @@ -630,10 +656,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() @@ -744,7 +766,7 @@ if(OpenVDB_pyopenvdb_LIBRARY) set_target_properties(OpenVDB::pyopenvdb PROPERTIES IMPORTED_LOCATION "${OpenVDB_pyopenvdb_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES "${OpenVDB_pyopenvdb_INCLUDE_DIR};${PYTHON_INCLUDE_DIR}" - INTERFACE_LINK_LIBRARIES "OpenVDB::openvdb;${PYTHON_LIBRARIES}" + INTERFACE_LINK_LIBRARIES "OpenVDB::openvdb;Boost::${BOOST_PYTHON_LIB};${PYTHON_LIBRARIES}" INTERFACE_COMPILE_FEATURES cxx_std_17 ) endif() diff --git a/doc/build.txt b/doc/build.txt index ecdb748a96..3dbfb48d57 100644 --- a/doc/build.txt +++ b/doc/build.txt @@ -267,7 +267,7 @@ PyBind11 | C++/python bindings NumPy | Scientific computing with Python | Optional (Python) | LLVM | Target-independent code generation | OpenVDB AX | -At a minimum a matching C++17 compiler and CMake will be required. See +At a minimum, boost, a matching C++17 compiler and CMake will be required. See the full [dependency list](@ref dependencies) for help with downloading and installing the above software. Note that as Blosc and ZLib are provided as part of the Houdini installation `USE_BLOSC` and `USE_ZLIB` should be left `ON`. diff --git a/doc/dependencies.txt b/doc/dependencies.txt index db8945b090..16d3884fd0 100644 --- a/doc/dependencies.txt +++ b/doc/dependencies.txt @@ -36,7 +36,7 @@ Reference Platform, but for those that do, their specified versions are Component | Requirements | Optional ----------------------- | ----------------------------------------------- | -------- -OpenVDB Core Library | CMake, C++17 compiler, TBB::tbb | Blosc, ZLib, Log4cplus, Imath::Imath, Boost::iostream +OpenVDB Core Library | CMake, C++17 compiler, TBB::tbb, Boost::headers | Blosc, ZLib, Log4cplus, Imath::Imath, Boost::iostream OpenVDB Print | Core Library dependencies | - OpenVDB LOD | Core Library dependencies | - OpenVDB Render | Core Library dependencies | OpenEXR, Imath::Imath, libpng @@ -65,7 +65,7 @@ Imath | 3.1 | Latest | Half precision floating points OpenEXR | 3.1 | Latest | EXR serialization support | Y | Y | http://www.openexr.com TBB | 2020.2 | 2020.3 | Threading Building Blocks - template library for task parallelism | Y | Y | https://www.threadingbuildingblocks.org ZLIB | 1.2.7 | Latest | Compression library for disk serialization compression | Y | Y | https://www.zlib.net -Boost | 1.73 | 1.80 | Components: iostreams | Y | Y | https://www.boost.org +Boost | 1.73 | 1.80 | Components: headers, iostreams | Y | Y | https://www.boost.org LLVM | 10.0.0 | 13.0.0* | Target-independent code generation | Y | Y | https://llvm.org/ Bison | 3.0.0 | 3.7.0 | General-purpose parser generator | Y | Y | https://www.gnu.org/software/gcc Flex | 2.6.0 | 2.6.4 | Fast lexical analyzer generator | Y | Y | https://github.com/westes/flex diff --git a/openvdb/openvdb/CMakeLists.txt b/openvdb/openvdb/CMakeLists.txt index f56fdc198c..33d4e5cc85 100644 --- a/openvdb/openvdb/CMakeLists.txt +++ b/openvdb/openvdb/CMakeLists.txt @@ -110,14 +110,16 @@ endif() if(OPENVDB_USE_DELAYED_LOADING) find_package(Boost ${MINIMUM_BOOST_VERSION} REQUIRED COMPONENTS iostreams) +else() + find_package(Boost ${MINIMUM_BOOST_VERSION} REQUIRED COMPONENTS headers) +endif() - 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() +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() @@ -244,17 +246,20 @@ endif() 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() +else() + list(APPEND OPENVDB_CORE_DEPENDENT_LIBS Boost::headers) +endif() + +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() From 9ac0553af1d4257996bbb09e58437ad75c0536a3 Mon Sep 17 00:00:00 2001 From: Dan Bailey Date: Wed, 1 Nov 2023 13:23:40 -0700 Subject: [PATCH 22/22] Update release notes Signed-off-by: Dan Bailey --- CHANGES | 108 ++++++++++++++----------------- doc/changes.txt | 135 +++++++++++++++++---------------------- pendingchanges/iofix.txt | 3 - 3 files changed, 107 insertions(+), 139 deletions(-) delete mode 100644 pendingchanges/iofix.txt diff --git a/CHANGES b/CHANGES index 9486a712c3..c51d115b50 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,7 @@ OpenVDB Version History ======================= -Version 11.0.0 - October 31, 2023 +Version 11.0.0 - November 1, 2023 This version introduces ABI changes relative to older major releases, so to preserve ABI compatibility it might be necessary to define the @@ -9,78 +9,66 @@ Version 11.0.0 - October 31, 2023 Houdini 19.5 and 10 for Houdini 20.0. OpenEXR 2 and Python 2 are no longer supported. - Boost is now an optional OpenVDB dependency. OpenVDB: Improvements: - - Removed last traces of Boost when OPENVDB_USE_DELAYED_LOADING is OFF + - Removed use of boost::any in favor of std::any. [Contributed by Brian McKinnon] + Bug Fixes: + - Fix potential crash reading corrupt .vdb files with invalid + blosc or zip chunks. + [Contributed by Matthias Ueberheide] + NanoVDB: - API Changes: - - Minor version changed from 4 to 6 (major version is unchanged since the ABI is - preserved) - - nanovdb::Vec3R is deprecated. Use nanovdb::Vec3d instead. - - nanoToOpenVDB now accepts the index of the NanoVDB grid contained inside of a - GridHandle to be converted to OpenVDB. - - DataTypes are now public in all node types. - - GridMetaData can be copied. - - Major improvements to GridBuilder.h, which allows users to construct grids - with random access on the host - - The move constructor now requires the GridBuffer to actually contain a - valid grid - - Moved CudaDeviceBuffer.h to cuda/CudaDeviceBuffer.h - - Added nanovdb::pi() - - Introduced a new extendable API for acceleration of custom random-access - get/set methods on ValueAccessor, e.g. getValue(ijk), isActive - (ijk), probeValue(ijk, val). - - Added nanovdb::BitFlags. - - Added minComponentAtomic and maxComponentAtomic methods on the GPU to - nanovdb::Vec2 and nanovdb::Vec3. Added expandAtomic and intersectAtomic on - nanovdb::BBox, and expandAtomic in nanovdb::Coord. - - Added nanovdb::Map constructors. - - Mask:: DenseIterator, Mask:: setOnAtomic,Mask:: setOffAtomic. - - Improved and renamed device function that converts voxels into a grid - - nanovdb::cudaVoxelsToGrid + Highlights: + - Several new tools to generate and modify NanoVDB grids on the GPU. + - New file format that supports streaming of raw grid buffers. New Features: - - Added a new grid class called IndexGrid in 4 flavors (Index, OnIndex, - IndexMask, OnIndexMask). - - Several new ways to construct and modify NanoVDB grids on the GPU. See - CreateNanoGrid.h and CudaPointsToGrid.cuh. - - New device function to convert points into a compact grid - - nanovdb::cudaPointsToGrid - - Added cuda/CudaUtils.h and cuda/GpuTimer.h with cuda utility functions - - Added cuda/CudaPointToGrids.cuh that constructs device grids from points or - voxels - - Added cuda/CudaIndexToGrid.cuh that converts IndexGrids and values into - regular Grids - - Added cuda/CudaSignedFloodFill.cuh that performs signed-flood filing on SDF on - the GPU - - Added cuda/CudaAddBlindData.cuh that adds blind data to an existing grid on the - GPU - - Added cuda/CudaGridChecksum.cuh that computes CRC32 checksums of grids on the - GPU - - Added cuda/CudaGridHandle.cuh that handles grids on the GPU - - Added cuda/CudaNodeManager.cuh that constructs a NodeManager on the GPU - - Introduced new (dummy) build-type nanovdb::Points and - nanovdb::GridType::PointIndex - - Introduced new types nanovdb::GridType::Vec3u16 and nanovdb::GridType::Vec3u8 - used for compressed representations of point coordinates as blind data - - Added PrefixSum.h for concurrent computation of prefix sum on the host - - Primitives.h can now create grids on the CPU with SDF, FOG, and point or - torus + - New memory efficient GridClass::IndexGrid that separates values from tree + - 4 new GridTypes (Index, OnIndex, IndexMask, OnIndexMask) used by IndexGrid + - Added createNanoGrid that replaces older conversion methods in GridBuilder.h, + IndexGridBuilder.h and OpenToNanoVDB.h + - Added cudaPointsToGrid that constructs a point device grid from a list of + points. + - Added cudaVoxelsToGrid that constructs a voxel device grid from a list of + voxels. + - Added cuda/CudaUtils.h with several cuda utility functions. + - Added GpuTimer for timing of kernels in a specific cuda stream. + - Added cudaIndexToGrid that converts IndexGrids into regular Grids. + - Added cudaSignedFloodFill that performs signed-flood filling on the GPU. + - Added cudaAddBlindData that adds blind data to an existing grid on the GPU. + - Added cudaGridChecksum that computes checksums of device grids. + - Added cudaGridHandle that handles grids on the GPU. + - Added cudaNodeManager that constructs a NodeManager on the GPU. + - Added build type Points and GridType::PointIndex for point grids. + - Added GridType::Vec3u16 and GridType::Vec3u8 for compressed coordinates. + - Added PrefixSum.h for concurrent computation of prefix sum on the CPU. - Improvements: - - Transition from C++11 to C++17 in NanoVDB.h and its tools - - Improve NanoVDB Build Traits. - - CreateNanoGrid.h is replacing GridBuilder.h, IndexGridBuilder.h and - OpenToNanoVDB.h. + API Changes: + - Version 32.6.0 (ABI is unchanged). + - Transition from C++11 to C++17 + - Vec3R is deprecated, please use Vec3d instead. + - nanoToOpenVDB now takes the index of a NanoVDB in a GridHandle. + - GridData, InternalData and LeafData are now public. + - GridMetaData can be copied. + - Improvements to GridBuilder.h that allows construction of grids on CPU. + - GridHandle's move c-tor now requires the GridBuffer to contain a valid grid. + - Moved CudaDeviceBuffer.h to cuda/CudaDeviceBuffer.h. + - New API for acceleration of custom random-access with ValueAccessors. + - Added BitFlags class for convenient bit-mask operations. + - Added Vec2/3::min/maxComponentAtomic GPU methods. + - Added BBox::expandAtomic and BBox::intersectAtomic. + - Added Coord::expandAtomic. + - Added Map constructors. + - Added Mask::DenseIterator, Mask::setOnAtomic, and Mask::setOffAtomic. + - InternalNode::ChildIterator is now const-correct. + - Added several new NanoVDB Build Traits. - Syncing PNanoVDB.h with NanoVDB.h. Build: - Support for OpenEXR 2.X has been removed. - - Boost is no longer required if OPENVDB_USE_DELAYED_LOADING is OFF - Better support for building with external package configurations with CMAKE_FIND_PACKAGE_PREFER_CONFIG=ON. diff --git a/doc/changes.txt b/doc/changes.txt index a06d65ee17..b7435d86d2 100644 --- a/doc/changes.txt +++ b/doc/changes.txt @@ -4,7 +4,7 @@ @htmlonly @endhtmlonly @par -Version 11.0.0 - October 31, 2023 +Version 11.0.0 - November 1, 2023 @par
@@ -18,96 +18,79 @@ preserve ABI compatibility it might be necessary to define the macro OpenEXR 2 and Python 2 are no longer supported.
-
-Boost is now an optional OpenVDB dependency. -
- @par OpenVDB: - Improvements: - - Removed last traces of Boost when OPENVDB_USE_DELAYED_LOADING is OFF +- Improvements: + - Removed use of @c boost::any in favor of @c std::any. [Contributed by Brian McKinnon] +- Bug Fixes: + - Fix potential crash reading corrupt .vdb files with invalid blosc or zip + chunks. + [Contributed by Matthias Ueberheide] + @par NanoVDB: - API Changes: - - Minor version changed from 4 to 6 (major version is unchanged since the ABI - is preserved) - - @c nanovdb::Vec3R is deprecated. Use @c nanovdb::Vec3d instead. - - nanoToOpenVDB now accepts the index of the NanoVDB grid contained - inside of a GridHandle to be converted to OpenVDB. - - DataTypes are now public in all node types. - - GridMetaData can be copied. - - Major improvements to GridBuilder.h, which allows users to - construct grids with random access on the host - - The move constructor now requires the GridBuffer to actually contain a valid - grid - - Moved CudaDeviceBuffer.h to cuda/CudaDeviceBuffer.h - - Added @c nanovdb::pi() - - Introduced a new extendable API for acceleration of custom random-access - get/set methods on ValueAccessor, e.g. @c getValue(ijk), @c isActive - (ijk), @c probeValue(ijk, val). - - Added @c nanovdb::BitFlags. - - Added @c minComponentAtomic and @c maxComponentAtomic methods on the GPU - to @c nanovdb::Vec2 and @c nanovdb::Vec3. Added @c expandAtomic and @c - intersectAtomic on @c nanovdb::BBox, and @c expandAtomic in @c - nanovdb::Coord. - - Added @c nanovdb::Map constructors. - - Mask:: DenseIterator, Mask:: setOnAtomic,Mask:: - setOffAtomic. - - Improved and renamed device function that converts voxels into a grid - @c - nanovdb::cudaVoxelsToGrid - -@par - New Features: - - Added a new grid class called IndexGrid in 4 flavors (Index, OnIndex, - IndexMask, OnIndexMask). - - Several new ways to construct and modify NanoVDB grids on the GPU. See - CreateNanoGrid.h and CudaPointsToGrid.cuh. - - New device function to convert points into a compact grid - - nanovdb::cudaPointsToGrid - - Added cuda/CudaUtils.h and cuda/GpuTimer.h with cuda - utility functions - - Added cuda/CudaPointToGrids.cuh that constructs device grids from - points or voxels - - Added cuda/CudaIndexToGrid.cuh that converts IndexGrids and values - into regular Grids - - Added cuda/CudaSignedFloodFill.cuh that performs signed-flood - filing on SDF on the GPU - - Added cuda/CudaAddBlindData.cuh that adds blind data to an existing - grid on the GPU - - Added cuda/CudaGridChecksum.cuh that computes CRC32 - checksums of grids on the GPU - - Added cuda/CudaGridHandle.cuh that handles grids on the GPU - - Added cuda/CudaNodeManager.cuh that constructs a NodeManager on the - GPU - - Introduced new (dummy) build-type @c nanovdb::Points and @c - nanovdb::GridType::PointIndex - - Introduced new types @c nanovdb::GridType::Vec3u16 and @c - nanovdb::GridType::Vec3u8 used for compressed representations of point - coordinates as blind data - - Added PrefixSum.h for concurrent computation of prefix sum on the - host - - Primitives.h can now create grids on the CPU with SDF, FOG, and - point or torus +- Highlights: + - Several new tools to generate and modify NanoVDB grids on the GPU. + - New file format that supports streaming of raw grid buffers. -@par - Improvements: - - Transition from C++11 to C++17 in NanoVDB.h and - its tools - - Improve NanoVDB Build Traits. - - CreateNanoGrid.h is replacing GridBuilder.h, - IndexGridBuilder.h and OpenToNanoVDB.h. +- New Features: + - New memory efficient @c GridClass::IndexGrid that separates values from tree + - 4 new GridTypes (@c Index, @c OnIndex, @c IndexMask, @c OnIndexMask) used by + IndexGrid + - Added @c createNanoGrid that replaces older conversion methods in + GridBuilder.h, IndexGridBuilder.h and + OpenToNanoVDB.h + - Added @c cudaPointsToGrid that constructs a point device grid from a list of + points. + - Added @c cudaVoxelsToGrid that constructs a voxel device grid from a list of + voxels. + - Added cuda/CudaUtils.h with several cuda utility functions. + - Added @c GpuTimer for timing of kernels in a specific cuda stream. + - Added @c cudaIndexToGrid that converts IndexGrids into regular Grids. + - Added @c cudaSignedFloodFill that performs signed-flood filling on the GPU. + - Added @c cudaAddBlindData that adds blind data to an existing grid on the + GPU. + - Added @c cudaGridChecksum that computes checksums of device grids. + - Added @c cudaGridHandle that handles grids on the GPU. + - Added @c cudaNodeManager that constructs a NodeManager on the GPU. + - Added build type @c Points and @c GridType::PointIndex for point grids. + - Added @c GridType::Vec3u16 and @c GridType::Vec3u8 for compressed + coordinates. + - Added PrefixSum.h for concurrent computation of prefix sum on the + CPU. + +- API Changes: + - Version 32.6.0 (ABI is unchanged). + - Transition from C++11 to C++17 + - Vec3R is deprecated, please use Vec3d instead. + - nanoToOpenVDB now takes the index of a NanoVDB in a GridHandle. + - GridData, InternalData and LeafData are now public. + - GridMetaData can be copied. + - Improvements to GridBuilder.h that allows construction of grids on + CPU. + - GridHandle's move c-tor now requires the GridBuffer to contain a valid grid. + - Moved CudaDeviceBuffer.h to cuda/CudaDeviceBuffer.h. + - New API for acceleration of custom random-access with ValueAccessors. + - Added BitFlags class for convenient bit-mask operations. + - Added Vec2/3 min/maxComponentAtomic GPU methods. + - Added @c BBox::expandAtomic and @c BBox::intersectAtomic. + - Added @c Coord::expandAtomic. + - Added Map constructors. + - Added @c Mask::DenseIterator, @c Mask::setOnAtomic, and + @c Mask::setOffAtomic. + - @c InternalNode::ChildIterator is now const-correct. + - Added several new NanoVDB Build Traits. - Syncing PNanoVDB.h with NanoVDB.h. @par Build: - Support for OpenEXR 2.X has been removed. -- Boost is no longer required if OPENVDB_USE_DELAYED_LOADING is - OFF - Better support for building with external package configurations with CMAKE_FIND_PACKAGE_PREFER_CONFIG=ON. +@par Python: - Removed Python 2 support. [Contributed by Matthew Cong] diff --git a/pendingchanges/iofix.txt b/pendingchanges/iofix.txt deleted file mode 100644 index 6d095d7a02..0000000000 --- a/pendingchanges/iofix.txt +++ /dev/null @@ -1,3 +0,0 @@ -Bug Fixes: - - Fix potential crash reading corrupt .vdb files with invalid - blosc or zip chunks. [Fix thanks to Matthias Ueberheide]

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 KMNUMnLSTX+7o-3H literal 0 HcmV?d00001 diff --git a/doc/python.txt b/doc/python.txt index 304f4ca579..043d3e3c24 100644 --- a/doc/python.txt +++ b/doc/python.txt @@ -4,10 +4,8 @@ This section describes the OpenVDB Python module and includes Python code snippets and some complete programs that illustrate how to perform common tasks. -(An API reference is also available, -if Epydoc is installed.) -As of OpenVDB 2.0, the Python module exposes most of the functionality -of the C++ @vdblink::Grid Grid@endlink class, including I/O, metadata +The Python module exposes most of the functionality of the C++ +@vdblink::Grid Grid@endlink class, including I/O, metadata management, voxel access and iteration, but almost none of the many @ref secToolUtils "tools". We expect to add support for tools in forthcoming releases. diff --git a/openvdb/openvdb/CMakeLists.txt b/openvdb/openvdb/CMakeLists.txt index 71d15d1c79..2af2759bfb 100644 --- a/openvdb/openvdb/CMakeLists.txt +++ b/openvdb/openvdb/CMakeLists.txt @@ -132,23 +132,22 @@ if(OPENVDB_FUTURE_DEPRECATION AND FUTURE_MINIMUM_TBB_VERSION) endif() if(USE_IMATH_HALF) - find_package(Imath CONFIG) - if (NOT TARGET Imath::Imath) - find_package(IlmBase ${MINIMUM_ILMBASE_VERSION} REQUIRED COMPONENTS Half) - if(OPENVDB_FUTURE_DEPRECATION AND FUTURE_MINIMUM_ILMBASE_VERSION) - if(${IlmBase_VERSION} VERSION_LESS FUTURE_MINIMUM_ILMBASE_VERSION) - message(DEPRECATION "Support for IlmBase versions < ${FUTURE_MINIMUM_ILMBASE_VERSION} " - "is deprecated and will be removed.") - endif() - endif() - else() - message(STATUS "Found Imath ${Imath_VERSION}") - endif() + find_package(Imath ${MINIMUM_IMATH_VERSION} REQUIRED CONFIG) + get_target_property(openvdb_imath_includes Imath::Imath INTERFACE_INCLUDE_DIRECTORIES) + message(STATUS "Found Imath: ${openvdb_imath_includes} (found suitable version \"${Imath_VERSION}\", " + "minimum required is \"${MINIMUM_IMATH_VERSION}\")") + unset(openvdb_imath_includes) endif() if(USE_LOG4CPLUS) # Find Log4CPlus libraries find_package(Log4cplus ${MINIMUM_LOG4CPLUS_VERSION} REQUIRED) + if(OPENVDB_FUTURE_DEPRECATION AND FUTURE_MINIMUM_LOG4CPLUS_VERSION) + if(${Log4cplus_VERSION} VERSION_LESS FUTURE_MINIMUM_LOG4CPLUS_VERSION) + message(DEPRECATION "Support for Log4cplus versions < ${FUTURE_MINIMUM_LOG4CPLUS_VERSION} " + "is deprecated and will be removed.") + endif() + endif() endif() if(USE_BLOSC) @@ -208,8 +207,7 @@ endif() set(OPENVDB_CORE_DEPENDENT_LIBS "") if(USE_IMATH_HALF) - list(APPEND OPENVDB_CORE_DEPENDENT_LIBS - $ $) + list(APPEND OPENVDB_CORE_DEPENDENT_LIBS Imath::Imath) endif() # We then choose to pull in TBB. If building aganst Houdini, TBB should @@ -636,15 +634,6 @@ if(MINGW) list(APPEND OPENVDB_CORE_PUBLIC_DEFINES -D_USE_MATH_DEFINES) endif() -if(WIN32) - if(USE_IMATH_HALF) - # @note OPENVDB_OPENEXR_STATICLIB is old functionality and should be removed - if(ILMBASE_USE_STATIC_LIBS OR ("${ILMBASE_Half_LIB_TYPE}" STREQUAL STATIC)) - list(APPEND OPENVDB_CORE_PUBLIC_DEFINES -DOPENVDB_OPENEXR_STATICLIB) - endif() - endif() -endif() - if(USE_LOG4CPLUS) list(APPEND OPENVDB_CORE_PUBLIC_DEFINES -DOPENVDB_USE_LOG4CPLUS) endif() diff --git a/openvdb/openvdb/Platform.h b/openvdb/openvdb/Platform.h index b6b0d24c7b..b8a8f1cb6f 100644 --- a/openvdb/openvdb/Platform.h +++ b/openvdb/openvdb/Platform.h @@ -67,12 +67,6 @@ #if defined(_DLL) && !defined(OPENVDB_STATICLIB) && !defined(OPENVDB_DLL) #define OPENVDB_DLL #endif - - // By default, assume that we're dynamically linking OpenEXR, unless - // OPENVDB_OPENEXR_STATICLIB is defined. - #if !defined(OPENVDB_OPENEXR_STATICLIB) && !defined(OPENEXR_DLL) - #define OPENEXR_DLL - #endif #endif /// Macros to suppress undefined behaviour sanitizer warnings. Should be used diff --git a/openvdb/openvdb/openvdb.cc b/openvdb/openvdb/openvdb.cc index 4a9d08d635..c776409dfc 100644 --- a/openvdb/openvdb/openvdb.cc +++ b/openvdb/openvdb/openvdb.cc @@ -14,31 +14,31 @@ #include #endif -#if OPENVDB_ABI_VERSION_NUMBER <= 7 - #error ABI <= 7 is no longer supported +#if OPENVDB_ABI_VERSION_NUMBER <= 8 + #error ABI <= 8 is no longer supported #endif // If using an OPENVDB_ABI_VERSION_NUMBER that has been deprecated, issue an // error directive. This can be optionally suppressed by defining: // OPENVDB_USE_DEPRECATED_ABI_=ON. -#ifndef OPENVDB_USE_DEPRECATED_ABI_8 - #if OPENVDB_ABI_VERSION_NUMBER == 8 - #error ABI = 8 is deprecated, CMake option OPENVDB_USE_DEPRECATED_ABI_8 suppresses this error - #endif -#endif #ifndef OPENVDB_USE_DEPRECATED_ABI_9 #if OPENVDB_ABI_VERSION_NUMBER == 9 #error ABI = 9 is deprecated, CMake option OPENVDB_USE_DEPRECATED_ABI_9 suppresses this error #endif #endif +#ifndef OPENVDB_USE_DEPRECATED_ABI_10 + #if OPENVDB_ABI_VERSION_NUMBER == 10 + #error ABI = 10 is deprecated, CMake option OPENVDB_USE_DEPRECATED_ABI_10 suppresses this error + #endif +#endif // If using a future OPENVDB_ABI_VERSION_NUMBER, issue an error directive. // This can be optionally suppressed by defining: // OPENVDB_USE_FUTURE_ABI_=ON. -#ifndef OPENVDB_USE_FUTURE_ABI_11 - #if OPENVDB_ABI_VERSION_NUMBER == 11 - #error ABI = 11 is still in active development and has not been finalized, \ -CMake option OPENVDB_USE_FUTURE_ABI_11 suppresses this error +#ifndef OPENVDB_USE_FUTURE_ABI_12 + #if OPENVDB_ABI_VERSION_NUMBER == 12 + #error ABI = 12 is still in active development and has not been finalized, \ +CMake option OPENVDB_USE_FUTURE_ABI_12 suppresses this error #endif #endif diff --git a/openvdb/openvdb/points/AttributeArray.h b/openvdb/openvdb/points/AttributeArray.h index f363a6d8b1..cbbab2d955 100644 --- a/openvdb/openvdb/points/AttributeArray.h +++ b/openvdb/openvdb/points/AttributeArray.h @@ -767,10 +767,8 @@ class TypedAttributeArray final: public AttributeArray /// Return @c true if all data has been loaded bool isDataLoaded() const override; -#if OPENVDB_ABI_VERSION_NUMBER >= 9 /// Return the raw data buffer inline const StorageType* constData() const { return this->data(); } -#endif protected: AccessorBasePtr getAccessor() const override; diff --git a/openvdb/openvdb/tree/InternalNode.h b/openvdb/openvdb/tree/InternalNode.h index 9e4e1589da..2d1cf52e7a 100644 --- a/openvdb/openvdb/tree/InternalNode.h +++ b/openvdb/openvdb/tree/InternalNode.h @@ -268,12 +268,10 @@ class InternalNode /// Set the grid index coordinates of this node's local origin. void setOrigin(const Coord& origin) { mOrigin = origin; } -#if OPENVDB_ABI_VERSION_NUMBER >= 9 /// Return the transient data value. Index32 transientData() const { return mTransientData; } /// Set the transient data value. void setTransientData(Index32 transientData) { mTransientData = transientData; } -#endif Index32 leafCount() const; void nodeCount(std::vector &vec) const; @@ -795,10 +793,8 @@ class InternalNode NodeMaskType mChildMask, mValueMask; /// Global grid index coordinates (x,y,z) of the local origin of this node Coord mOrigin; -#if OPENVDB_ABI_VERSION_NUMBER >= 9 /// Transient data (not serialized) Index32 mTransientData = 0; -#endif }; // class InternalNode @@ -882,9 +878,7 @@ InternalNode::InternalNode(const InternalNode& other) : mChildMask(other.mChildMask) , mValueMask(other.mValueMask) , mOrigin(other.mOrigin) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { DeepCopy > tmp(&other, this); } @@ -898,9 +892,7 @@ InternalNode::InternalNode(const InternalNode= 9 , mTransientData(other.mTransientData) -#endif { DeepCopy > tmp(&other, this); } @@ -937,9 +929,7 @@ InternalNode::InternalNode(const InternalNode= 9 , mTransientData(other.mTransientData) -#endif { TopologyCopy1 > tmp(&other, this, background); } @@ -977,9 +967,7 @@ InternalNode::InternalNode(const InternalNode= 9 , mTransientData(other.mTransientData) -#endif { TopologyCopy2 > tmp(&other, this, offValue, onValue); } diff --git a/openvdb/openvdb/tree/LeafBuffer.h b/openvdb/openvdb/tree/LeafBuffer.h index e685b733a3..ce33101685 100644 --- a/openvdb/openvdb/tree/LeafBuffer.h +++ b/openvdb/openvdb/tree/LeafBuffer.h @@ -475,9 +475,8 @@ class LeafBuffer static const Index WORD_COUNT = NodeMaskType::WORD_COUNT; static const Index SIZE = 1 << 3 * Log2Dim; - // These static declarations must be on separate lines to avoid VC9 compiler errors. - static const bool sOn; - static const bool sOff; + static inline const bool sOn = true; + static inline const bool sOff = false; LeafBuffer() {} LeafBuffer(bool on): mData(on) {} @@ -521,16 +520,6 @@ class LeafBuffer NodeMaskType mData; }; // class LeafBuffer - -/// @internal For consistency with other nodes and with iterators, methods like -/// LeafNode::getValue() return a reference to a value. Since it's not possible -/// to return a reference to a bit in a node mask, we return a reference to one -/// of the following static values instead. -/// -/// @todo Make these static inline with C++17 -template const bool LeafBuffer::sOn = true; -template const bool LeafBuffer::sOff = false; - } // namespace tree } // namespace OPENVDB_VERSION_NAME } // namespace openvdb diff --git a/openvdb/openvdb/tree/LeafNode.h b/openvdb/openvdb/tree/LeafNode.h index e520aa7a0b..33ec3a56ae 100644 --- a/openvdb/openvdb/tree/LeafNode.h +++ b/openvdb/openvdb/tree/LeafNode.h @@ -183,12 +183,10 @@ class LeafNode /// Return the global coordinates for a linear table offset. Coord offsetToGlobalCoord(Index n) const; -#if OPENVDB_ABI_VERSION_NUMBER >= 9 /// Return the transient data value. Index32 transientData() const { return mTransientData; } /// Set the transient data value. void setTransientData(Index32 transientData) { mTransientData = transientData; } -#endif /// Return a string representation of this node. std::string str() const; @@ -877,10 +875,8 @@ class LeafNode NodeMaskType mValueMask; /// Global grid index coordinates (x,y,z) of the local origin of this node Coord mOrigin; -#if OPENVDB_ABI_VERSION_NUMBER >= 9 /// Transient data (not serialized) Index32 mTransientData = 0; -#endif }; // end of LeafNode class @@ -936,9 +932,7 @@ LeafNode::LeafNode(const LeafNode& other) : mBuffer(other.mBuffer) , mValueMask(other.valueMask()) , mOrigin(other.mOrigin) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { } @@ -950,9 +944,7 @@ inline LeafNode::LeafNode(const LeafNode& other) : mValueMask(other.valueMask()) , mOrigin(other.mOrigin) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { struct Local { /// @todo Consider using a value conversion functor passed as an argument instead. @@ -973,9 +965,7 @@ LeafNode::LeafNode(const LeafNode& other, : mBuffer(background) , mValueMask(other.valueMask()) , mOrigin(other.mOrigin) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { } @@ -987,9 +977,7 @@ LeafNode::LeafNode(const LeafNode& other, const ValueType& offValue, const ValueType& onValue, TopologyCopy) : mValueMask(other.valueMask()) , mOrigin(other.mOrigin) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { for (Index i = 0; i < SIZE; ++i) { mBuffer[i] = (mValueMask.isOn(i) ? onValue : offValue); diff --git a/openvdb/openvdb/tree/LeafNodeBool.h b/openvdb/openvdb/tree/LeafNodeBool.h index 840a1de15b..02f3cb4c99 100644 --- a/openvdb/openvdb/tree/LeafNodeBool.h +++ b/openvdb/openvdb/tree/LeafNodeBool.h @@ -187,12 +187,10 @@ class LeafNode /// Return the global coordinates for a linear table offset. Coord offsetToGlobalCoord(Index n) const; -#if OPENVDB_ABI_VERSION_NUMBER >= 9 /// Return the transient data value. Index32 transientData() const { return mTransientData; } /// Set the transient data value. void setTransientData(Index32 transientData) { mTransientData = transientData; } -#endif /// Return a string representation of this node. std::string str() const; @@ -728,10 +726,8 @@ class LeafNode Buffer mBuffer; /// Global grid index coordinates (x,y,z) of the local origin of this node Coord mOrigin; -#if OPENVDB_ABI_VERSION_NUMBER >= 9 /// Transient data (not serialized) Index32 mTransientData = 0; -#endif private: /// @brief During topology-only construction, access is needed @@ -796,9 +792,7 @@ LeafNode::LeafNode(const LeafNode &other) : mValueMask(other.valueMask()) , mBuffer(other.mBuffer) , mOrigin(other.mOrigin) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { } @@ -810,9 +804,7 @@ inline LeafNode::LeafNode(const LeafNode& other) : mValueMask(other.valueMask()) , mOrigin(other.origin()) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { struct Local { /// @todo Consider using a value conversion functor passed as an argument instead. @@ -833,9 +825,7 @@ LeafNode::LeafNode(const LeafNode& other, : mValueMask(other.valueMask()) , mBuffer(background) , mOrigin(other.origin()) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { } @@ -847,9 +837,7 @@ LeafNode::LeafNode(const LeafNode& other, Topolo : mValueMask(other.valueMask()) , mBuffer(other.valueMask())// value = active state , mOrigin(other.origin()) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { } @@ -858,16 +846,11 @@ inline LeafNode::LeafNode(const Coord& xyz, const NodeMaskType& mask, const Buffer& buff, -#if OPENVDB_ABI_VERSION_NUMBER < 9 - [[maybe_unused]] -#endif const Index32 trans) : mValueMask(mask) , mBuffer(buff) , mOrigin(xyz & (~(DIM - 1))) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(trans) -#endif { } @@ -879,9 +862,7 @@ LeafNode::LeafNode(const LeafNode& other, : mValueMask(other.valueMask()) , mBuffer(offValue) , mOrigin(other.origin()) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { for (Index i = 0; i < SIZE; ++i) { if (mValueMask.isOn(i)) { diff --git a/openvdb/openvdb/tree/LeafNodeMask.h b/openvdb/openvdb/tree/LeafNodeMask.h index 132157ead4..a8e31ec9e7 100644 --- a/openvdb/openvdb/tree/LeafNodeMask.h +++ b/openvdb/openvdb/tree/LeafNodeMask.h @@ -169,12 +169,10 @@ class LeafNode /// Return the global coordinates for a linear table offset. Coord offsetToGlobalCoord(Index n) const; -#if OPENVDB_ABI_VERSION_NUMBER >= 9 /// Return the transient data value. Index32 transientData() const { return mTransientData; } /// Set the transient data value. void setTransientData(Index32 transientData) { mTransientData = transientData; } -#endif /// Return a string representation of this node. std::string str() const; @@ -712,10 +710,8 @@ class LeafNode Buffer mBuffer; /// Global grid index coordinates (x,y,z) of the local origin of this node Coord mOrigin; -#if OPENVDB_ABI_VERSION_NUMBER >= 9 /// Transient data (not serialized) Index32 mTransientData = 0; -#endif private: /// @brief During topology-only construction, access is needed @@ -775,9 +771,7 @@ inline LeafNode::LeafNode(const LeafNode &other) : mBuffer(other.mBuffer) , mOrigin(other.mOrigin) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { } @@ -789,9 +783,7 @@ inline LeafNode::LeafNode(const LeafNode& other) : mBuffer(other.valueMask()) , mOrigin(other.origin()) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { } @@ -803,9 +795,7 @@ LeafNode::LeafNode(const LeafNode& other, bool, TopologyCopy) : mBuffer(other.valueMask())// value = active state , mOrigin(other.origin()) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { } @@ -816,9 +806,7 @@ inline LeafNode::LeafNode(const LeafNode& other, TopologyCopy) : mBuffer(other.valueMask())// value = active state , mOrigin(other.origin()) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { } @@ -830,9 +818,7 @@ LeafNode::LeafNode(const LeafNode& other, bool offValue, bool onValue, TopologyCopy) : mBuffer(other.valueMask()) , mOrigin(other.origin()) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { if (offValue==true) { if (onValue==false) { diff --git a/openvdb/openvdb/tree/RootNode.h b/openvdb/openvdb/tree/RootNode.h index 468d1252cb..c9abc2e965 100644 --- a/openvdb/openvdb/tree/RootNode.h +++ b/openvdb/openvdb/tree/RootNode.h @@ -405,12 +405,10 @@ class RootNode /// Return the bounding box of this RootNode, i.e., an infinite bounding box. static CoordBBox getNodeBoundingBox() { return CoordBBox::inf(); } -#if OPENVDB_ABI_VERSION_NUMBER >= 9 /// Return the transient data value. Index32 transientData() const { return mTransientData; } /// Set the transient data value. void setTransientData(Index32 transientData) { mTransientData = transientData; } -#endif /// @brief Change inactive tiles or voxels with a value equal to +/- the /// old background to the specified value (with the same sign). Active values @@ -967,10 +965,8 @@ class RootNode #if OPENVDB_ABI_VERSION_NUMBER >= 10 Coord mOrigin; #endif -#if OPENVDB_ABI_VERSION_NUMBER >= 9 /// Transient Data (not serialized) Index32 mTransientData = 0; -#endif }; // end of RootNode class @@ -1064,9 +1060,7 @@ RootNode::RootNode(const RootNode& other, #if OPENVDB_ABI_VERSION_NUMBER >= 10 , mOrigin(other.mOrigin) #endif -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { using OtherRootT = RootNode; @@ -1098,9 +1092,7 @@ RootNode::RootNode(const RootNode& other, #if OPENVDB_ABI_VERSION_NUMBER >= 10 , mOrigin(other.mOrigin) #endif -#if OPENVDB_ABI_VERSION_NUMBER >= 9 , mTransientData(other.mTransientData) -#endif { using OtherRootT = RootNode; @@ -1171,9 +1163,7 @@ struct RootNodeCopyHelper } self.mOrigin = other.mOrigin; #endif -#if OPENVDB_ABI_VERSION_NUMBER >= 9 self.mTransientData = other.mTransientData; -#endif self.clear(); self.initTable(); @@ -1206,9 +1196,7 @@ RootNode::operator=(const RootNode& other) OPENVDB_THROW(ValueError, "RootNode::operator=: non-zero offsets are currently not supported"); } #endif -#if OPENVDB_ABI_VERSION_NUMBER >= 9 mTransientData = other.mTransientData; -#endif this->clear(); this->initTable(); diff --git a/openvdb/openvdb/unittest/TestLeaf.cc b/openvdb/openvdb/unittest/TestLeaf.cc index 19ddfb411c..96efa1de71 100644 --- a/openvdb/openvdb/unittest/TestLeaf.cc +++ b/openvdb/openvdb/unittest/TestLeaf.cc @@ -518,7 +518,6 @@ TEST_F(TestLeaf, testCount) EXPECT_EQ(Index(3), dims[0]); } -#if OPENVDB_ABI_VERSION_NUMBER >= 9 TEST_F(TestLeaf, testTransientData) { using namespace openvdb; @@ -534,4 +533,3 @@ TEST_F(TestLeaf, testTransientData) LeafT leaf3 = leaf; EXPECT_EQ(Index32(5), leaf3.transientData()); } -#endif diff --git a/openvdb/openvdb/unittest/TestLeafBool.cc b/openvdb/openvdb/unittest/TestLeafBool.cc index 2ecfc75a33..f215dc5219 100644 --- a/openvdb/openvdb/unittest/TestLeafBool.cc +++ b/openvdb/openvdb/unittest/TestLeafBool.cc @@ -641,7 +641,6 @@ TEST_F(TestLeafBool, testMedian) // EXPECT_TRUE(tree->hasSameTopology(*copyOfTree)); // } -#if OPENVDB_ABI_VERSION_NUMBER >= 9 TEST_F(TestLeafBool, testTransientData) { LeafType leaf(openvdb::Coord(0, 0, 0), /*background=*/false); @@ -654,4 +653,3 @@ TEST_F(TestLeafBool, testTransientData) LeafType leaf3 = leaf; EXPECT_EQ(openvdb::Index32(5), leaf3.transientData()); } -#endif diff --git a/openvdb/openvdb/unittest/TestLeafMask.cc b/openvdb/openvdb/unittest/TestLeafMask.cc index 0868a21bc0..fa7eadba6c 100644 --- a/openvdb/openvdb/unittest/TestLeafMask.cc +++ b/openvdb/openvdb/unittest/TestLeafMask.cc @@ -562,7 +562,6 @@ TEST_F(TestLeafMask, testMedian) // EXPECT_TRUE(tree->hasSameTopology(*copyOfTree)); // } -#if OPENVDB_ABI_VERSION_NUMBER >= 9 TEST_F(TestLeafMask, testTransientData) { LeafType leaf(openvdb::Coord(0, 0, 0), /*background=*/false); @@ -575,4 +574,3 @@ TEST_F(TestLeafMask, testTransientData) LeafType leaf3 = leaf; EXPECT_EQ(openvdb::Index32(5), leaf3.transientData()); } -#endif diff --git a/openvdb/openvdb/unittest/TestTree.cc b/openvdb/openvdb/unittest/TestTree.cc index 59e6dfff93..74ec5d63ab 100644 --- a/openvdb/openvdb/unittest/TestTree.cc +++ b/openvdb/openvdb/unittest/TestTree.cc @@ -2840,7 +2840,6 @@ TEST_F(TestTree, testRootNode) } } -#if OPENVDB_ABI_VERSION_NUMBER >= 9 { // test transient data RootNodeType rootNode(0.0f); EXPECT_EQ(openvdb::Index32(0), rootNode.transientData()); @@ -2851,7 +2850,6 @@ TEST_F(TestTree, testRootNode) RootNodeType rootNode3 = rootNode; EXPECT_EQ(openvdb::Index32(5), rootNode3.transientData()); } -#endif } TEST_F(TestTree, testInternalNode) @@ -2947,7 +2945,6 @@ TEST_F(TestTree, testInternalNode) delete child; } -#if OPENVDB_ABI_VERSION_NUMBER >= 9 { // test transient data InternalNodeType internalNode(c1, 0.0f); EXPECT_EQ(openvdb::Index32(0), internalNode.transientData()); @@ -2958,5 +2955,4 @@ TEST_F(TestTree, testInternalNode) InternalNodeType internalNode3 = internalNode; EXPECT_EQ(openvdb::Index32(5), internalNode3.transientData()); } -#endif } diff --git a/openvdb/openvdb/version.h.in b/openvdb/openvdb/version.h.in index 897d649bac..9a95b8d3a5 100644 --- a/openvdb/openvdb/version.h.in +++ b/openvdb/openvdb/version.h.in @@ -169,9 +169,9 @@ // This can be suppressed by defining OPENVDB_USE_FUTURE_ABI_=ON. // Note that, whilst the VDB CMake does not allow this option to be hit, // it exists to propagate this message to downstream targets - #if OPENVDB_ABI_VERSION_NUMBER == 11 - #ifndef OPENVDB_USE_FUTURE_ABI_11 - PRAGMA(message("NOTE: ABI = 11 is still in active development and has not been finalized, " + #if OPENVDB_ABI_VERSION_NUMBER == 12 + #ifndef OPENVDB_USE_FUTURE_ABI_12 + PRAGMA(message("NOTE: ABI = 12 is still in active development and has not been finalized, " "define OPENVDB_USE_FUTURE_ABI_11 to suppress this message")) #endif #else @@ -183,15 +183,15 @@ // directive. This can be suppressed by defining OPENVDB_USE_DEPRECATED_ABI_. // Note that, whilst the VDB CMake does not allow this option to be hit, // it exists to propagate this message to downstream targets -#ifndef OPENVDB_USE_DEPRECATED_ABI_8 - #if OPENVDB_ABI_VERSION_NUMBER == 8 - PRAGMA(message("NOTE: ABI = 8 is deprecated, define OPENVDB_USE_DEPRECATED_ABI_7 " +#ifndef OPENVDB_USE_DEPRECATED_ABI_9 + #if OPENVDB_ABI_VERSION_NUMBER == 9 + PRAGMA(message("NOTE: ABI = 9 is deprecated, define OPENVDB_USE_DEPRECATED_ABI_9 " "to suppress this message")) #endif #endif -#ifndef OPENVDB_USE_DEPRECATED_ABI_9 - #if OPENVDB_ABI_VERSION_NUMBER == 9 - PRAGMA(message("NOTE: ABI = 9 is deprecated, define OPENVDB_USE_DEPRECATED_ABI_8 " +#ifndef OPENVDB_USE_DEPRECATED_ABI_10 + #if OPENVDB_ABI_VERSION_NUMBER == 10 + PRAGMA(message("NOTE: ABI = 10 is deprecated, define OPENVDB_USE_DEPRECATED_ABI_10 " "to suppress this message")) #endif #endif diff --git a/openvdb_ax/openvdb_ax/compiler/PointExecutable.cc b/openvdb_ax/openvdb_ax/compiler/PointExecutable.cc index 88675fe474..c84a12c9b3 100644 --- a/openvdb_ax/openvdb_ax/compiler/PointExecutable.cc +++ b/openvdb_ax/openvdb_ax/compiler/PointExecutable.cc @@ -358,7 +358,6 @@ struct PointFunctionArguments // @todo if the array is shared we should probably make it unique? -#if OPENVDB_ABI_VERSION_NUMBER >= 9 if (mData.mUseBufferKernel) { const_cast(array).loadData(); const char* data = array.constDataAsByteArray(); @@ -373,12 +372,6 @@ struct PointFunctionArguments mHandlesOrBuffers.emplace_back(handle->mHandle.get()); mAttributeHandles.emplace_back(std::move(handle)); } -#else - assert(!mData.mUseBufferKernel); - typename ReadHandle::UniquePtr handle(new ReadHandle(leaf, Index(pos))); - mHandlesOrBuffers.emplace_back(handle->mHandle.get()); - mAttributeHandles.emplace_back(std::move(handle)); -#endif mFlags.emplace_back(flag); } @@ -390,7 +383,6 @@ struct PointFunctionArguments points::AttributeArray& array = leaf.attributeArray(pos); array.expand(); -#if OPENVDB_ABI_VERSION_NUMBER >= 9 if (mData.mUseBufferKernel) { array.loadData(); const char* data = array.constDataAsByteArray(); @@ -406,12 +398,6 @@ struct PointFunctionArguments mHandlesOrBuffers.emplace_back(handle->mHandle.get()); mAttributeHandles.emplace_back(std::move(handle)); } -#else - assert(!mData.mUseBufferKernel); - typename WriteHandle::UniquePtr handle(new WriteHandle(leaf, Index(pos))); - mHandlesOrBuffers.emplace_back(handle->mHandle.get()); - mAttributeHandles.emplace_back(std::move(handle)); -#endif mFlags.emplace_back(flag); } @@ -866,7 +852,6 @@ void PointExecutable::execute(openvdb::points::PointDataGrid& grid) const // Compute whether we can use the accelerated kernel // @note Assumes attributes are valid (i.e. has errored out if they are not) -#if OPENVDB_ABI_VERSION_NUMBER >= 9 if (!usingGroup) { const auto& desc = leafIter->attributeSet().descriptor(); data.mUseBufferKernel = checkCodecs(desc, *mAttributeRegistry, @@ -877,10 +862,6 @@ void PointExecutable::execute(openvdb::points::PointDataGrid& grid) const // if a group has been specified we can't use the buffer range yet data.mUseBufferKernel = false; } -#else - // can't access data buffers until ABI >= 9 - data.mUseBufferKernel = false; -#endif // execute diff --git a/openvdb_cmd/vdb_render/CMakeLists.txt b/openvdb_cmd/vdb_render/CMakeLists.txt index 1fd9e161e4..403aa8bd7f 100644 --- a/openvdb_cmd/vdb_render/CMakeLists.txt +++ b/openvdb_cmd/vdb_render/CMakeLists.txt @@ -17,22 +17,11 @@ if(USE_PNG) endif() if(USE_IMATH_HALF) - find_package(Imath CONFIG) - if (NOT TARGET Imath::Imath) - if(USE_EXR) - find_package(IlmBase ${MINIMUM_ILMBASE_VERSION} REQUIRED COMPONENTS Half Iex IlmThread Imath) - else() - find_package(IlmBase ${MINIMUM_ILMBASE_VERSION} REQUIRED COMPONENTS Half) - endif() - endif() + find_package(Imath CONFIG REQUIRED) endif() if(USE_EXR) - if (NOT TARGET Imath::Imath) - find_package(OpenEXR ${MINIMUM_OPENEXR_VERSION} REQUIRED COMPONENTS IlmImf) - else() - find_package(OpenEXR CONFIG REQUIRED) - endif() + find_package(OpenEXR CONFIG REQUIRED) endif() set(SOURCE_FILES main.cc) @@ -58,27 +47,8 @@ target_link_libraries(vdb_render $ $ $ - # For IlmBase/OpenEXR v2.X - $ - $ - $ - $ - $ - $ ${OPENVDB_BINARIES_DEPENDENT_LIBS} $ ) -if(WIN32) - # @note OPENVDB_OPENEXR_STATICLIB is old functionality and should be removed - if (TARGET Imath::Imath) - get_target_property(ILMBASE_LIB_TYPE Imath::Imath TYPE) - else() - get_target_property(ILMBASE_LIB_TYPE IlmBase::Half TYPE) - endif() - if(OPENEXR_USE_STATIC_LIBS OR (${ILMBASE_LIB_TYPE} STREQUAL STATIC_LIBRARY)) - target_compile_definitions(vdb_render PUBLIC -DOPENVDB_OPENEXR_STATICLIB) - endif() -endif() - install(TARGETS vdb_render RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Merge.cc b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Merge.cc index 6c379f2bfe..90ccc27ed2 100644 --- a/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Merge.cc +++ b/openvdb_houdini/openvdb_houdini/SOP_OpenVDB_Merge.cc @@ -391,12 +391,7 @@ struct MergeOp auto hasUniqueTree = [&](GU_PrimVDB* vdbPrim) { -#if OPENVDB_ABI_VERSION_NUMBER >= 8 return vdbPrim->getConstGridPtr()->isTreeUnique(); -#else - const TreeBase::ConstPtr treeBaseConstPtr = vdbPrim->getConstGridPtr()->constBaseTreePtr(); - return treeBaseConstPtr.use_count() <= 2; -#endif }; auto stealTree = [&](auto& gridBase, GU_PrimVDB* vdbPrim = nullptr) @@ -480,12 +475,7 @@ struct MergeOp auto hasUniqueTree = [&](GU_PrimVDB* vdbPrim) { -#if OPENVDB_ABI_VERSION_NUMBER >= 8 return vdbPrim->getConstGridPtr()->isTreeUnique(); -#else - const TreeBase::ConstPtr treeBaseConstPtr = vdbPrim->getConstGridPtr()->constBaseTreePtr(); - return treeBaseConstPtr.use_count() <= 2; -#endif }; auto stealTree = [&](auto& gridBase, GU_PrimVDB* vdbPrim = nullptr) diff --git a/pendingchanges/vdb11.txt b/pendingchanges/vdb11.txt new file mode 100644 index 0000000000..6bada32357 --- /dev/null +++ b/pendingchanges/vdb11.txt @@ -0,0 +1,2 @@ +Build: + - Support for OpenEXR 2.X has been removed. From fd4c370b22f983fd89fa7eac8d6a63ee493be45f Mon Sep 17 00:00:00 2001 From: Brian McKinnon Date: Sun, 15 Oct 2023 19:14:36 -0500 Subject: [PATCH 05/22] Replace boost conversion_traits with std::common_type Signed-off-by: Brian McKinnon --- openvdb/openvdb/math/Math.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openvdb/openvdb/math/Math.h b/openvdb/openvdb/math/Math.h index 8f2e705973..1406774620 100644 --- a/openvdb/openvdb/math/Math.h +++ b/openvdb/openvdb/math/Math.h @@ -10,7 +10,6 @@ #include #include -#include #include // for std::max() #include #include // for std::ceil(), std::fabs(), std::pow(), std::sqrt(), etc. @@ -916,10 +915,9 @@ enum RotationOrder { ZXZ_ROTATION }; - -template +template && std::is_arithmetic_v>> struct promote { - using type = typename boost::numeric::conversion_traits::supertype; + using type = typename std::common_type_t; }; /// @brief Return the index [0,1,2] of the smallest value in a 3D vector. From c00edd56130dd7b0d900fa79f0841be14143362a Mon Sep 17 00:00:00 2001 From: Brian McKinnon Date: Mon, 16 Oct 2023 08:26:08 -0500 Subject: [PATCH 06/22] Replace boost::any with std::any Replace BOOST_STATIC_ASSERT with static_assert Signed-off-by: Brian McKinnon --- openvdb/openvdb/io/Archive.cc | 8 ++++---- openvdb/openvdb/io/io.h | 4 ++-- openvdb/openvdb/points/PointDataGrid.h | 16 ++++++++-------- openvdb/openvdb/tools/VelocityFields.h | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/openvdb/openvdb/io/Archive.cc b/openvdb/openvdb/io/Archive.cc index cedcecffa3..9b1374c9cc 100644 --- a/openvdb/openvdb/io/Archive.cc +++ b/openvdb/openvdb/io/Archive.cc @@ -330,10 +330,10 @@ namespace { template inline bool -writeAsType(std::ostream& os, const boost::any& val) +writeAsType(std::ostream& os, const std::any& val) { if (val.type() == typeid(T)) { - os << boost::any_cast(val); + os << std::any_cast(val); return true; } return false; @@ -415,8 +415,8 @@ operator<<(std::ostream& os, const StreamMetadata::AuxDataMap& auxData) it != end; ++it) { os << it->first << ": "; - // Note: boost::any doesn't support serialization. - const boost::any& val = it->second; + // Note: std::any doesn't support serialization. + const std::any& val = it->second; if (!writeAsType(os, val) && !writeAsType(os, val) && !writeAsType(os, val) diff --git a/openvdb/openvdb/io/io.h b/openvdb/openvdb/io/io.h index 56fed8fa8d..f63afd35ac 100644 --- a/openvdb/openvdb/io/io.h +++ b/openvdb/openvdb/io/io.h @@ -7,7 +7,7 @@ #include #include // for SharedPtr #include -#include +#include #include #include // for std::ios_base #include @@ -89,7 +89,7 @@ class OPENVDB_API StreamMetadata const MetaMap& gridMetadata() const; //@} - using AuxDataMap = std::map; + using AuxDataMap = std::map; //@{ /// @brief Return a map that can be populated with arbitrary user data. AuxDataMap& auxData(); diff --git a/openvdb/openvdb/points/PointDataGrid.h b/openvdb/openvdb/points/PointDataGrid.h index 386ea249fd..bd560acf39 100644 --- a/openvdb/openvdb/points/PointDataGrid.h +++ b/openvdb/openvdb/points/PointDataGrid.h @@ -1172,7 +1172,7 @@ PointDataLeafNode::readBuffers(std::istream& is, const CoordBBox& /* std::string key("paged:" + std::to_string(index)); auto it = auxData.find(key); if (it != auxData.end()) { - return *(boost::any_cast(it->second)); + return *(std::any_cast(it->second)); } else { compression::PagedInputStream::Ptr pagedStream = std::make_shared(); @@ -1216,7 +1216,7 @@ PointDataLeafNode::readBuffers(std::istream& is, const CoordBBox& /* std::string descriptorKey("descriptorPtr"); auto itDescriptor = auxData.find(descriptorKey); assert(itDescriptor != auxData.end()); - const Descriptor::Ptr descriptor = boost::any_cast(itDescriptor->second); + const Descriptor::Ptr descriptor = std::any_cast(itDescriptor->second); return descriptor; } }; @@ -1338,7 +1338,7 @@ PointDataLeafNode::writeBuffers(std::ostream& os, bool toHalf) const std::string key("paged:" + std::to_string(index)); auto it = auxData.find(key); if (it != auxData.end()) { - compression::PagedOutputStream& stream = *(boost::any_cast(it->second)); + compression::PagedOutputStream& stream = *(std::any_cast(it->second)); stream.flush(); (const_cast(auxData)).erase(it); } @@ -1350,7 +1350,7 @@ PointDataLeafNode::writeBuffers(std::ostream& os, bool toHalf) const std::string key("paged:" + std::to_string(index)); auto it = auxData.find(key); if (it != auxData.end()) { - return *(boost::any_cast(it->second)); + return *(std::any_cast(it->second)); } else { compression::PagedOutputStream::Ptr pagedStream = std::make_shared(); @@ -1374,12 +1374,12 @@ PointDataLeafNode::writeBuffers(std::ostream& os, bool toHalf) const } else { // if matching bool is found and is false, early exit (a previous descriptor did not match) - bool matching = boost::any_cast(itMatching->second); + bool matching = std::any_cast(itMatching->second); if (!matching) return; assert(itDescriptor != auxData.end()); // if matching bool is true, check whether the existing descriptor matches the current one and set // matching bool to false if not - const Descriptor::Ptr existingDescriptor = boost::any_cast(itDescriptor->second); + const Descriptor::Ptr existingDescriptor = std::any_cast(itDescriptor->second); if (*existingDescriptor != *descriptor) { (const_cast(auxData))[matchingKey] = false; } @@ -1393,7 +1393,7 @@ PointDataLeafNode::writeBuffers(std::ostream& os, bool toHalf) const // if matching key is not found, no matching descriptor if (itMatching == auxData.end()) return false; // if matching key is found and is false, no matching descriptor - if (!boost::any_cast(itMatching->second)) return false; + if (!std::any_cast(itMatching->second)) return false; return true; } @@ -1404,7 +1404,7 @@ PointDataLeafNode::writeBuffers(std::ostream& os, bool toHalf) const // if matching key is true, however descriptor is not found, it has already been retrieved if (itDescriptor == auxData.end()) return nullptr; // otherwise remove it and return it - const Descriptor::Ptr descriptor = boost::any_cast(itDescriptor->second); + const Descriptor::Ptr descriptor = std::any_cast(itDescriptor->second); (const_cast(auxData)).erase(itDescriptor); return descriptor; } diff --git a/openvdb/openvdb/tools/VelocityFields.h b/openvdb/openvdb/tools/VelocityFields.h index cacc20f3b3..d48fe4b279 100644 --- a/openvdb/openvdb/tools/VelocityFields.h +++ b/openvdb/openvdb/tools/VelocityFields.h @@ -229,7 +229,7 @@ class VelocityIntegrator template inline void rungeKutta(const ElementType dt, LocationType& world) const { - BOOST_STATIC_ASSERT(OrderRK <= 4); + static_assert(OrderRK <= 4); VecType P(static_cast(world[0]), static_cast(world[1]), static_cast(world[2])); From fb58dceff4f5c10681912293037b8584a6e02415 Mon Sep 17 00:00:00 2001 From: Jeff Lait Date: Tue, 17 Oct 2023 16:56:11 -0400 Subject: [PATCH 07/22] Houdini 20 requires RenderContext rather than Render *. Signed-off-by: Jeff Lait --- .../openvdb_houdini/GR_PrimVDBPoints.cc | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/openvdb_houdini/openvdb_houdini/GR_PrimVDBPoints.cc b/openvdb_houdini/openvdb_houdini/GR_PrimVDBPoints.cc index b394835bdc..229c95b41f 100644 --- a/openvdb_houdini/openvdb_houdini/GR_PrimVDBPoints.cc +++ b/openvdb_houdini/openvdb_houdini/GR_PrimVDBPoints.cc @@ -39,6 +39,10 @@ #include #include +#if UT_VERSION_INT < 0x14000000 // Below 20.0, there is no RE_RenderContext +#define RE_RenderContext RE_Render * +#endif + //////////////////////////////////////// static RE_ShaderHandle theMarkerDecorShader("decor/GL32/point_marker.prog"); @@ -112,7 +116,7 @@ class GR_PrimVDBPoints : public GR_Primitive /// Called whenever the parent detail is changed, draw modes are changed, /// selection is changed, or certain volatile display options are changed /// (such as level of detail). - void update(RE_Render*, const GT_PrimitiveHandle&, const GR_UpdateParms&) override; + void update(RE_RenderContext, const GT_PrimitiveHandle&, const GR_UpdateParms&) override; /// return true if the primitive is in or overlaps the view frustum. /// always returning true will effectively disable frustum culling. @@ -126,12 +130,12 @@ class GR_PrimVDBPoints : public GR_Primitive /// than one time per viewport redraw (beauty, shadow passes, wireframe-over) /// It also may be called outside of a viewport redraw to do picking of the /// geometry. - void render(RE_Render*, GR_RenderMode, GR_RenderFlags, GR_DrawParms) override; + void render(RE_RenderContext, GR_RenderMode, GR_RenderFlags, GR_DrawParms) override; - int renderPick(RE_Render*, const GR_DisplayOption*, unsigned int, + int renderPick(RE_RenderContext, const GR_DisplayOption*, unsigned int, GR_PickStyle, bool) override { return 0; } - void renderDecoration(RE_Render*, GR_Decoration, const GR_DecorationParms&) override; + void renderDecoration(RE_RenderContext, GR_Decoration, const GR_DecorationParms&) override; protected: void computeCentroid(const openvdb::points::PointDataGrid& grid); @@ -711,7 +715,7 @@ GR_PrimVDBPoints::updateWireBuffer(RE_Render *r, } void -GR_PrimVDBPoints::update(RE_Render *r, +GR_PrimVDBPoints::update(RE_RenderContext r, const GT_PrimitiveHandle &primh, const GR_UpdateParms &p) { @@ -861,7 +865,7 @@ GR_PrimVDBPoints::removeBuffer(const std::string& name) } void -GR_PrimVDBPoints::render(RE_Render *r, GR_RenderMode, GR_RenderFlags, GR_DrawParms dp) +GR_PrimVDBPoints::render(RE_RenderContext r, GR_RenderMode, GR_RenderFlags, GR_DrawParms dp) { if (!myGeo && !myWire) return; @@ -934,7 +938,7 @@ GR_PrimVDBPoints::render(RE_Render *r, GR_RenderMode, GR_RenderFlags, GR_DrawPar void -GR_PrimVDBPoints::renderDecoration(RE_Render* r, GR_Decoration decor, const GR_DecorationParms& p) +GR_PrimVDBPoints::renderDecoration(RE_RenderContext r, GR_Decoration decor, const GR_DecorationParms& p) { // just render native GR_Primitive decorations if position not available From e6664ec62cfd9e2d43eb32f0bc5dd7bdf68add90 Mon Sep 17 00:00:00 2001 From: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> Date: Wed, 18 Oct 2023 11:32:40 +1300 Subject: [PATCH 08/22] Fix houdini CI caching Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> --- .github/workflows/houdini.yml | 2 +- .github/workflows/weekly.yml | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/houdini.yml b/.github/workflows/houdini.yml index 3f6b539ece..81f9709e7d 100644 --- a/.github/workflows/houdini.yml +++ b/.github/workflows/houdini.yml @@ -96,7 +96,7 @@ jobs: key: linux-vfx-hou${{ matrix.config.hou }}-${{ matrix.config.image }}-${{ matrix.config.cxx }}-${{ steps.timestamp.outputs.timestamp }} restore-keys: linux-vfx-hou${{ matrix.config.hou }}-${{ matrix.config.image }}-${{ matrix.config.cxx }}- - name: fetch_houdini - uses: actions/cache@v3 + uses: actions/cache/restore@v3 with: path: hou key: dummy-houdini${{ matrix.config.hou }}-${{ steps.timestamp.outputs.timestamp }} diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index c7e9a8333f..0dc2cd8ae4 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -74,6 +74,16 @@ jobs: image: aswf/ci-base:2023 steps: - uses: actions/checkout@v3 + # We bumped from the 2021 CI image to 2023 here to fix some OpenSSL issues + # with the Houdini download script. In so doing we broke some of the caching + # between this job and the jobs in houdini.yml which _don't_ use the 2023 + # image yet. The issue is that the cache action will use zstd if it's + # available to zip the cache and this causes it to be inserted with a unique + # hash which images without zstd (i.e. the 2021/2022 images don't have + # access to). For now, uninstall zstd here instead of installing it + # everywhere and ask the LF to add zstd to the older base images. + - name: remove zstd + run: yum -y remove zstd - name: timestamp id: timestamp run: echo "timestamp=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT @@ -85,7 +95,7 @@ jobs: cp hou/hou.tar.gz $HOME/houdini_install/hou.tar.gz cd $HOME/houdini_install && tar -xzf hou.tar.gz && cd - - name: write_houdini_cache - uses: actions/cache@v3 + uses: actions/cache/save@v3 with: path: hou key: vdb-v5-houdini${{ matrix.config.houdini_version_str }}-${{ steps.timestamp.outputs.timestamp }} From 35808213e663612bb3a8d9c8420e4d7ecb5f79c1 Mon Sep 17 00:00:00 2001 From: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> Date: Thu, 19 Oct 2023 17:04:12 +1300 Subject: [PATCH 09/22] Minor spelling fix in README Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b70edb4064..f16e03c451 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ cmake --build . --parallel 4 --config Release --target install #### Building OpenVDB AX and NanoVDB OpenVDB AX depends on the core OpenVDB library. NanoVDB can be built with and -without OpenVDB support. Note that NanoVDB has its own build instructuins, see +without OpenVDB support. Note that NanoVDB has its own build instructions, see the [NanoVDB build documentation](https://www.openvdb.org/documentation/doxygen/NanoVDB_HowToBuild.html) for details. From 4845be45537f23725819c48881bbcfafa9e345b7 Mon Sep 17 00:00:00 2001 From: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> Date: Tue, 24 Oct 2023 17:20:26 +1300 Subject: [PATCH 10/22] Various CMake fixes to allow VDB to be built with CMAKE_FIND_PACKAGE_PREFER_CONFIG Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> --- cmake/FindTBB.cmake | 4 +++- openvdb/openvdb/CMakeLists.txt | 2 +- openvdb/openvdb/unittest/CMakeLists.txt | 15 ++++++++++----- pendingchanges/vdb11.txt | 2 ++ 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/cmake/FindTBB.cmake b/cmake/FindTBB.cmake index f8cfdfc899..8588aa0482 100644 --- a/cmake/FindTBB.cmake +++ b/cmake/FindTBB.cmake @@ -34,7 +34,7 @@ This will define the following variables: ``Tbb_FOUND`` True if the system has the Tbb library. -``Tbb_VERSION`` +``Tbb_VERSION`` ``TBB_VERSION`` The version of the Tbb library which was found. ``Tbb_INCLUDE_DIRS`` Include directories needed to use Tbb. @@ -220,7 +220,9 @@ if(EXISTS ${_tbb_version_file}) unset(_tbb_version_minor_string) unset(_tbb_binary_version_string) + # Set both for compatibility reasons, TBB's CONFIG files only set the latter set(Tbb_VERSION ${Tbb_VERSION_MAJOR}.${Tbb_VERSION_MINOR}) + set(TBB_VERSION ${Tbb_VERSION}) endif() unset(_tbb_version_file) diff --git a/openvdb/openvdb/CMakeLists.txt b/openvdb/openvdb/CMakeLists.txt index 2af2759bfb..d34d5c184d 100644 --- a/openvdb/openvdb/CMakeLists.txt +++ b/openvdb/openvdb/CMakeLists.txt @@ -125,7 +125,7 @@ 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) + if(${TBB_VERSION} VERSION_LESS FUTURE_MINIMUM_TBB_VERSION) message(DEPRECATION "Support for TBB versions < ${FUTURE_MINIMUM_TBB_VERSION} " "is deprecated and will be removed.") endif() diff --git a/openvdb/openvdb/unittest/CMakeLists.txt b/openvdb/openvdb/unittest/CMakeLists.txt index 33e4431a3c..d9b011f4f3 100644 --- a/openvdb/openvdb/unittest/CMakeLists.txt +++ b/openvdb/openvdb/unittest/CMakeLists.txt @@ -37,13 +37,18 @@ else() set(OPENVDB_LIB openvdb) endif() +set(OPENVDB_TEST_DEPENDENT_LIBS ${OPENVDB_LIB}) + find_package(GTest ${MINIMUM_GOOGLETEST_VERSION} REQUIRED) -set(OPENVDB_TEST_DEPENDENT_LIBS - ${OPENVDB_LIB} - GTest::GTest - GTest::Main -) +if(TARGET GTest::gtest_main) + # New gtest targets as of CMake 3.20. Defined by both the Config and Module + # find modes + list(APPEND OPENVDB_TEST_DEPENDENT_LIBS GTest::gtest GTest::gtest_main) +else() + # Older targets, only defined by the Module find_package calls + list(APPEND OPENVDB_TEST_DEPENDENT_LIBS GTest::GTest GTest::Main) +endif() if(CONCURRENT_MALLOC STREQUAL "Jemalloc") find_package(Jemalloc REQUIRED) diff --git a/pendingchanges/vdb11.txt b/pendingchanges/vdb11.txt index 6bada32357..6a537c9357 100644 --- a/pendingchanges/vdb11.txt +++ b/pendingchanges/vdb11.txt @@ -1,2 +1,4 @@ Build: - Support for OpenEXR 2.X has been removed. + - Better support for building with external package configurations + with CMAKE_FIND_PACKAGE_PREFER_CONFIG=ON. From 828bea2b75c6538a71cb8ee9d787e028e6693bb0 Mon Sep 17 00:00:00 2001 From: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> Date: Wed, 25 Oct 2023 08:35:31 +1300 Subject: [PATCH 11/22] Remove boost from the build system when OPENVDB_USE_DELAYED_LOADING is OFF Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> --- cmake/FindOpenVDB.cmake | 32 +++------------ cmake/config/OpenVDBVersions.cmake | 3 +- doc/build.txt | 2 +- doc/dependencies.txt | 4 +- openvdb/openvdb/CMakeLists.txt | 41 ++++++++----------- openvdb_maya/openvdb_maya/OpenVDBCopyNode.cc | 2 - .../openvdb_maya/OpenVDBTransformNode.cc | 5 +-- openvdb_maya/openvdb_maya/OpenVDBUtil.cc | 28 ++++++++----- 8 files changed, 48 insertions(+), 69 deletions(-) diff --git a/cmake/FindOpenVDB.cmake b/cmake/FindOpenVDB.cmake index c4213c853a..5536e70a30 100644 --- a/cmake/FindOpenVDB.cmake +++ b/cmake/FindOpenVDB.cmake @@ -491,37 +491,11 @@ endif() # Add standard dependencies find_package(TBB REQUIRED COMPONENTS tbb) -find_package(Boost REQUIRED COMPONENTS iostreams) # Add deps for pyopenvdb -# @todo track for numpy if(pyopenvdb IN_LIST OpenVDB_FIND_COMPONENTS) find_package(Python REQUIRED) - - # Boost python handling - try and find both python and pythonXx (version suffixed). - # Prioritize the version suffixed library, failing if neither exist. - - find_package(Boost ${MINIMUM_BOOST_VERSION} - QUIET COMPONENTS python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR} - ) - - if(TARGET Boost::python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR}) - set(BOOST_PYTHON_LIB "python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR}") - message(STATUS "Found boost_python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR}") - else() - find_package(Boost ${MINIMUM_BOOST_VERSION} QUIET COMPONENTS python) - if(TARGET Boost::python) - set(BOOST_PYTHON_LIB "python") - message(STATUS "Found non-suffixed boost_python, assuming to be python version " - "\"${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}\" compatible" - ) - else() - message(FATAL_ERROR "Unable to find boost_python or " - "boost_python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR}." - ) - endif() - endif() endif() # Add deps for openvdb_ax @@ -656,6 +630,10 @@ 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() @@ -766,7 +744,7 @@ if(OpenVDB_pyopenvdb_LIBRARY) set_target_properties(OpenVDB::pyopenvdb PROPERTIES IMPORTED_LOCATION "${OpenVDB_pyopenvdb_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES "${OpenVDB_pyopenvdb_INCLUDE_DIR};${PYTHON_INCLUDE_DIR}" - INTERFACE_LINK_LIBRARIES "OpenVDB::openvdb;Boost::${BOOST_PYTHON_LIB};${PYTHON_LIBRARIES}" + INTERFACE_LINK_LIBRARIES "OpenVDB::openvdb;${PYTHON_LIBRARIES}" INTERFACE_COMPILE_FEATURES cxx_std_17 ) endif() diff --git a/cmake/config/OpenVDBVersions.cmake b/cmake/config/OpenVDBVersions.cmake index ac36f582e7..f956b8fdbe 100644 --- a/cmake/config/OpenVDBVersions.cmake +++ b/cmake/config/OpenVDBVersions.cmake @@ -39,7 +39,8 @@ if(NOT DISABLE_DEPENDENCY_VERSION_CHECKS) set(MINIMUM_ICC_VERSION 19) set(MINIMUM_MSVC_VERSION 19.28) # 1928 (Visual Studio 2019 Version 16.8 + 16.9) - set(MINIMUM_BOOST_VERSION 1.76) + # Should be 1.76 for VFX 22, but only version in apt is 1.73 + set(MINIMUM_BOOST_VERSION 1.73) set(MINIMUM_PYBIND_VERSION 2.9.1) set(MINIMUM_IMATH_VERSION 3.1) set(MINIMUM_OPENEXR_VERSION 3.1) diff --git a/doc/build.txt b/doc/build.txt index 3dbfb48d57..ecdb748a96 100644 --- a/doc/build.txt +++ b/doc/build.txt @@ -267,7 +267,7 @@ PyBind11 | C++/python bindings NumPy | Scientific computing with Python | Optional (Python) | LLVM | Target-independent code generation | OpenVDB AX | -At a minimum, boost, a matching C++17 compiler and CMake will be required. See +At a minimum a matching C++17 compiler and CMake will be required. See the full [dependency list](@ref dependencies) for help with downloading and installing the above software. Note that as Blosc and ZLib are provided as part of the Houdini installation `USE_BLOSC` and `USE_ZLIB` should be left `ON`. diff --git a/doc/dependencies.txt b/doc/dependencies.txt index f8c730a015..db8945b090 100644 --- a/doc/dependencies.txt +++ b/doc/dependencies.txt @@ -36,7 +36,7 @@ Reference Platform, but for those that do, their specified versions are Component | Requirements | Optional ----------------------- | ----------------------------------------------- | -------- -OpenVDB Core Library | CMake, C++17 compiler, TBB::tbb, Boost::headers | Blosc, ZLib, Log4cplus, Imath::Imath, Boost::iostream +OpenVDB Core Library | CMake, C++17 compiler, TBB::tbb | Blosc, ZLib, Log4cplus, Imath::Imath, Boost::iostream OpenVDB Print | Core Library dependencies | - OpenVDB LOD | Core Library dependencies | - OpenVDB Render | Core Library dependencies | OpenEXR, Imath::Imath, libpng @@ -65,7 +65,7 @@ Imath | 3.1 | Latest | Half precision floating points OpenEXR | 3.1 | Latest | EXR serialization support | Y | Y | http://www.openexr.com TBB | 2020.2 | 2020.3 | Threading Building Blocks - template library for task parallelism | Y | Y | https://www.threadingbuildingblocks.org ZLIB | 1.2.7 | Latest | Compression library for disk serialization compression | Y | Y | https://www.zlib.net -Boost | 1.76 | 1.80 | Components: headers, iostreams | Y | Y | https://www.boost.org +Boost | 1.73 | 1.80 | Components: iostreams | Y | Y | https://www.boost.org LLVM | 10.0.0 | 13.0.0* | Target-independent code generation | Y | Y | https://llvm.org/ Bison | 3.0.0 | 3.7.0 | General-purpose parser generator | Y | Y | https://www.gnu.org/software/gcc Flex | 2.6.0 | 2.6.4 | Fast lexical analyzer generator | Y | Y | https://github.com/westes/flex diff --git a/openvdb/openvdb/CMakeLists.txt b/openvdb/openvdb/CMakeLists.txt index d34d5c184d..f64fb4256c 100644 --- a/openvdb/openvdb/CMakeLists.txt +++ b/openvdb/openvdb/CMakeLists.txt @@ -110,16 +110,14 @@ endif() if(OPENVDB_USE_DELAYED_LOADING) find_package(Boost ${MINIMUM_BOOST_VERSION} REQUIRED COMPONENTS iostreams) -else() - find_package(Boost ${MINIMUM_BOOST_VERSION} REQUIRED COMPONENTS headers) -endif() -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.") + 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() @@ -246,20 +244,17 @@ endif() if(OPENVDB_USE_DELAYED_LOADING) list(APPEND OPENVDB_CORE_DEPENDENT_LIBS Boost::iostreams) -else() - list(APPEND OPENVDB_CORE_DEPENDENT_LIBS Boost::headers) -endif() - -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 - ) + 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() diff --git a/openvdb_maya/openvdb_maya/OpenVDBCopyNode.cc b/openvdb_maya/openvdb_maya/OpenVDBCopyNode.cc index efe690e74f..09fc45db7f 100644 --- a/openvdb_maya/openvdb_maya/OpenVDBCopyNode.cc +++ b/openvdb_maya/openvdb_maya/OpenVDBCopyNode.cc @@ -14,8 +14,6 @@ #include #include -#include // boost::math::constants::pi - namespace mvdb = openvdb_maya; diff --git a/openvdb_maya/openvdb_maya/OpenVDBTransformNode.cc b/openvdb_maya/openvdb_maya/OpenVDBTransformNode.cc index a7e215b885..5850b21079 100644 --- a/openvdb_maya/openvdb_maya/OpenVDBTransformNode.cc +++ b/openvdb_maya/openvdb_maya/OpenVDBTransformNode.cc @@ -5,6 +5,7 @@ /// @author FX R&D OpenVDB team #include "OpenVDBPlugin.h" +#include #include #include @@ -15,8 +16,6 @@ #include #include -#include // boost::math::constants::pi - namespace mvdb = openvdb_maya; @@ -216,7 +215,7 @@ MStatus OpenVDBTransformNode::compute(const MPlug& plug, MDataBlock& data) mat.preTranslate(openvdb::Vec3R(p[0], p[1], p[2])); - const double deg2rad = boost::math::constants::pi() / 180.0; + const double deg2rad = openvdb::math::pi() / 180.0; mat.preRotate(openvdb::math::X_AXIS, deg2rad*r[0]); mat.preRotate(openvdb::math::Y_AXIS, deg2rad*r[1]); mat.preRotate(openvdb::math::Z_AXIS, deg2rad*r[2]); diff --git a/openvdb_maya/openvdb_maya/OpenVDBUtil.cc b/openvdb_maya/openvdb_maya/OpenVDBUtil.cc index 5ed1f93477..5b6c88a2d0 100644 --- a/openvdb_maya/openvdb_maya/OpenVDBUtil.cc +++ b/openvdb_maya/openvdb_maya/OpenVDBUtil.cc @@ -7,11 +7,10 @@ #include "OpenVDBUtil.h" #include +#include #include -#include - #include // std::setw, std::setfill, std::left #include // std::stringstream #include // std::string, std::getline @@ -64,12 +63,13 @@ void getGrids(std::vector& grids, std::string getGridNames(const OpenVDBData& vdb) { - std::vector names; + std::string names; for (size_t n = 0, N = vdb.numberOfGrids(); n < N; ++n) { - names.push_back(vdb.grid(n).getName()); + names += std::string(vdb.grid(n).getName()); + names += " "; } - - return boost::algorithm::join(names, " "); + if (!names.empty()) names.pop_back(); + return names; } @@ -111,8 +111,12 @@ getSelectedGrids(GridCPtrVec& grids, const std::string& selection, { grids.clear(); - std::vector selectionList; - boost::split(selectionList, selection, boost::is_any_of(" ")); + std::string input(selection); + size_t pos = 0; + while ((pos = input.find(" ")) != std::string::npos) { + selectionList.emplace_back(input.substr(0, pos)); + input.erase(0, pos + 1); + } for (size_t n = 0, N = inputVdb.numberOfGrids(); n < N; ++n) { @@ -135,8 +139,12 @@ getSelectedGrids(GridCPtrVec& grids, const std::string& selection, { grids.clear(); - std::vector selectionList; - boost::split(selectionList, selection, boost::is_any_of(" ")); + std::string input(selection); + size_t pos = 0; + while ((pos = input.find(" ")) != std::string::npos) { + selectionList.emplace_back(input.substr(0, pos)); + input.erase(0, pos + 1); + } for (size_t n = 0, N = inputVdb.numberOfGrids(); n < N; ++n) { From 4edd7ff18371b7229258121dfb0de228b7464e07 Mon Sep 17 00:00:00 2001 From: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> Date: Tue, 24 Oct 2023 17:19:51 +1300 Subject: [PATCH 12/22] Weekly CI fix for 2024 containers and added CMake CONFIG test Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> --- .github/workflows/weekly.yml | 26 +++++++++++++++----------- openvdb/openvdb/unittest/TestCount.cc | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index 0dc2cd8ae4..867e440398 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -114,21 +114,25 @@ jobs: runs-on: ${{ (github.repository_owner == 'AcademySoftwareFoundation' && 'ubuntu-20.04-8c-32g-300h') || 'ubuntu-latest' }} name: linux-extra:${{ matrix.config.name }} container: - image: aswf/ci-openvdb:2023-clang15 + # @note we specifically use clang15.0 (not clang15) here as the newest + # versions of the clang15.X containers have some issues with the GLFW + # installation + image: aswf/ci-openvdb:2023-clang15.0 env: CXX: clang++ strategy: matrix: config: - - { name: 'all', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=ON -DUSE_ZLIB=ON -DUSE_EXR=ON -DUSE_PNG=ON' } - - { name: 'lite', 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: 'half', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=OFF -DUSE_IMATH_HALF=ON' } - - { name: 'sse', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DOPENVDB_SIMD=SSE42' } - - { name: 'avx', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DOPENVDB_SIMD=AVX' } - - { name: 'numpy', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_NUMPY=ON -DOPENVDB_PYTHON_WRAP_ALL_GRID_TYPES=ON' } - - { name: 'asan', build: 'asan', components: 'core,test,axcore,axtest', cmake: '-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', build: 'ubsan', components: 'core,test,axcore,axtest', cmake: '' } - - { name: 'c++20', build: 'Release', components: 'core,test,axcore,axtest', cmake: '-DCMAKE_CXX_STANDARD=20' } + - { name: 'all', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=ON -DUSE_ZLIB=ON -DUSE_EXR=ON -DUSE_PNG=ON' } + - { name: 'lite', 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: 'half', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=OFF -DUSE_IMATH_HALF=ON' } + - { name: 'sse', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DOPENVDB_SIMD=SSE42' } + - { name: 'avx', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DOPENVDB_SIMD=AVX' } + - { name: 'numpy', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_NUMPY=ON -DOPENVDB_PYTHON_WRAP_ALL_GRID_TYPES=ON' } + - { name: 'asan', build: 'asan', components: 'core,test,axcore,axtest', cmake: '-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', build: 'ubsan', components: 'core,test,axcore,axtest', cmake: '' } + - { name: 'c++20', build: 'Release', components: 'core,test,axcore,axtest', cmake: '-DCMAKE_CXX_STANDARD=20' } + - { name: 'conf', build: 'Release', components: 'core,python,bin,view,render,test,axcore,axtest', cmake: '-DCMAKE_FIND_PACKAGE_PREFER_CONFIG=ON' } fail-fast: false steps: - uses: actions/checkout@v3 @@ -352,7 +356,7 @@ jobs: runs-on: ${{ (github.repository_owner == 'AcademySoftwareFoundation' && 'ubuntu-20.04-8c-32g-300h') || 'ubuntu-latest' }} name: linux-blosc:${{ matrix.blosc }} container: - image: aswf/ci-base:2021 + image: aswf/ci-base:2023 strategy: matrix: blosc: ['1.18.0','1.19.0','1.20.0','1.21.0'] diff --git a/openvdb/openvdb/unittest/TestCount.cc b/openvdb/openvdb/unittest/TestCount.cc index e32fdb59d6..0c82efcbbd 100644 --- a/openvdb/openvdb/unittest/TestCount.cc +++ b/openvdb/openvdb/unittest/TestCount.cc @@ -209,7 +209,7 @@ TEST_F(TestCount, testMemUsage) Index64 internalNodeMemUsage(0); Index64 expectedMaxMem(sizeof(tree) + sizeof(root)); - Index64 leafCount(0); + [[maybe_unused]] Index64 leafCount(0); for (auto internal1Iter = root.cbeginChildOn(); internal1Iter; ++internal1Iter) { internalNodeMemUsage += Internal1NodeT::NUM_VALUES * sizeof(Internal1NodeT::UnionType); From 1e6f47adde38172c1b2feb164f08a8f839a7030f Mon Sep 17 00:00:00 2001 From: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> Date: Wed, 25 Oct 2023 09:35:36 +1300 Subject: [PATCH 13/22] Updated pendingchanges Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> --- pendingchanges/vdb11.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pendingchanges/vdb11.txt b/pendingchanges/vdb11.txt index 6a537c9357..6448dd04a5 100644 --- a/pendingchanges/vdb11.txt +++ b/pendingchanges/vdb11.txt @@ -1,4 +1,10 @@ +OpenVDB: + Improvements: + - Removed last traces of Boost when OPENVDB_USE_DELAYED_LOADING is OFF + [Contributed by Brian McKinnon] + Build: - Support for OpenEXR 2.X has been removed. + - Boost is no longer required if OPENVDB_USE_DELAYED_LOADING is OFF - Better support for building with external package configurations with CMAKE_FIND_PACKAGE_PREFER_CONFIG=ON. From 9ecdc8f02a97ba04c17eb392192f86c2c08a8fa9 Mon Sep 17 00:00:00 2001 From: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> Date: Wed, 25 Oct 2023 10:23:39 +1300 Subject: [PATCH 14/22] Added a warning when attempting to use the unmaintained maya plugin Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com> --- openvdb_maya/openvdb_maya/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openvdb_maya/openvdb_maya/CMakeLists.txt b/openvdb_maya/openvdb_maya/CMakeLists.txt index 3e5b691700..855210174b 100644 --- a/openvdb_maya/openvdb_maya/CMakeLists.txt +++ b/openvdb_maya/openvdb_maya/CMakeLists.txt @@ -10,6 +10,8 @@ cmake_minimum_required(VERSION 3.18) project(OpenVDBMaya LANGUAGES CXX) +message(WARNING "The OpenVDB Maya plugin is currently unmaintained. The plugin " + "exists as a reference and may not compile or run correctly.") ###### OpenVDB Maya Options From e36bedb8816340947304ed886620942f8fe5c3b8 Mon Sep 17 00:00:00 2001 From: apradhana Date: Thu, 26 Oct 2023 16:04:20 -0700 Subject: [PATCH 15/22] Fix nanovdb CI and use the correct MSVC_RUNTIME library tag explicitly. Signed-off-by: apradhana --- .github/workflows/nanovdb.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/nanovdb.yml b/.github/workflows/nanovdb.yml index 0cf2079117..35c25010e4 100644 --- a/.github/workflows/nanovdb.yml +++ b/.github/workflows/nanovdb.yml @@ -53,16 +53,16 @@ jobs: name: > linux-nanovdb:cxx:${{ matrix.config.cxx }}-${{ matrix.config.build }} container: - image: aswf/ci-base:2022 + image: aswf/ci-openvdb:${{ matrix.config.image }} env: CXX: ${{ matrix.config.cxx }} strategy: matrix: config: - - { cxx: g++, build: 'Release' } - - { cxx: g++, build: 'Debug' } - - { cxx: clang++, build: 'Release' } - - { cxx: clang++, build: 'Debug' } + - { cxx: g++, image: '2022-clang11', build: 'Release' } + - { cxx: g++, image: '2022-clang11', build: 'Debug' } + - { cxx: clang++, image: '2022-clang11', build: 'Release' } + - { cxx: clang++, image: '2022-clang11', build: 'Debug' } fail-fast: false steps: - uses: actions/checkout@v3 @@ -71,9 +71,9 @@ jobs: yum -y install yum-utils yum-config-manager --add-repo http://developer.download.nvidia.com/compute/cuda/repos/rhel7/x86_64/cuda-rhel7.repo echo "Installing cuda toolkit" - yum --enablerepo=epel -y install cuda-toolkit-11-0 - echo "/usr/local/cuda-11.0/bin" >> $GITHUB_PATH - echo "LD_LIBRARY_PATH=/usr/local/cuda-11.0/lib64:$LD_LIBRARY_PATH" >> $GITHUB_ENV + yum --enablerepo=epel -y install cuda-toolkit-11-6 + echo "/usr/local/cuda-11.6/bin" >> $GITHUB_PATH + echo "LD_LIBRARY_PATH=/usr/local/cuda-11.6/lib64:$LD_LIBRARY_PATH" >> $GITHUB_ENV - name: build run: > ./ci/build.sh -v @@ -87,7 +87,7 @@ jobs: -DUSE_BLOSC=OFF \' - name: test - run: cd build && sudo ctest -V + run: cd build && sudo ctest -V -E ".*cuda.*" windows-nanovdb: if: | @@ -105,8 +105,8 @@ jobs: # static build of blosc from vcpkg does not build internal sources. # USE_STATIC_DEPENDENCIES is required for IlmBase/OpenEXR defines and # Boost as both shared and static libs are installed. - - { vc: 'x64-windows-static', build: 'Release', cmake: '-A x64 -G \"Visual Studio 17 2022\" -DOPENVDB_CORE_SHARED=OFF -DUSE_STATIC_DEPENDENCIES=ON -DBLOSC_USE_EXTERNAL_SOURCES=ON' } - - { vc: 'x64-windows-static', build: 'Debug', cmake: '-A x64 -G \"Visual Studio 17 2022\" -DOPENVDB_CORE_SHARED=OFF -DUSE_STATIC_DEPENDENCIES=ON -DBLOSC_USE_EXTERNAL_SOURCES=ON' } + - { vc: 'x64-windows-static', build: 'Release', cmake: '-A x64 -G \"Visual Studio 17 2022\" -DOPENVDB_CORE_SHARED=OFF -DUSE_STATIC_DEPENDENCIES=ON -DBLOSC_USE_EXTERNAL_SOURCES=ON -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded' } + - { vc: 'x64-windows-static', build: 'Debug', cmake: '-A x64 -G \"Visual Studio 17 2022\" -DOPENVDB_CORE_SHARED=OFF -DUSE_STATIC_DEPENDENCIES=ON -DBLOSC_USE_EXTERNAL_SOURCES=ON -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebug' } - { vc: 'x64-windows', build: 'Release', cmake: '-A x64 -G \"Visual Studio 17 2022\" -DOPENVDB_CORE_STATIC=OFF' } - { vc: 'x64-windows', build: 'Debug', cmake: '-A x64 -G \"Visual Studio 17 2022\" -DOPENVDB_CORE_STATIC=OFF' } fail-fast: false @@ -142,7 +142,7 @@ jobs: \' - name: test shell: bash - run: cd build && ctest -V + run: cd build && ctest -V -E ".*cuda.*" macos-nanovdb: if: | @@ -169,7 +169,7 @@ jobs: --components=core,nano,nanotest,nanoexam,nanobench,nanotool --cargs=\'-DUSE_EXPLICIT_INSTANTIATION=OFF -DNANOVDB_USE_CUDA=OFF -DNANOVDB_USE_OPENVDB=ON\' - name: test - run: cd build && ctest -V + run: cd build && ctest -V -E ".*cuda.*" nanovdb-lite: if: | From 77f28d16114949dfbe2937aa2dae1379f6d2b653 Mon Sep 17 00:00:00 2001 From: Ken Museth Date: Fri, 27 Oct 2023 14:03:17 -0700 Subject: [PATCH 16/22] Massive improvements to NanoVDB (#1651) --- doc/nanovdb/HelloWorld.md | 6 +- doc/nanovdb/SourceTree.md | 247 +- nanovdb/nanovdb/CMakeLists.txt | 22 +- nanovdb/nanovdb/CNanoVDB.h | 2 +- nanovdb/nanovdb/NanoVDB.h | 5136 ++++++++++---- nanovdb/nanovdb/PNanoVDB.h | 5983 ++++++++++------- .../nanovdb/cmd/convert/nanovdb_convert.cc | 58 +- nanovdb/nanovdb/cmd/print/nanovdb_print.cc | 10 +- .../nanovdb/cmd/validate/nanovdb_validate.cc | 2 +- nanovdb/nanovdb/examples/CMakeLists.txt | 8 +- .../examples/benchmark/BenchKernels_dense.cu | 108 - .../examples/benchmark/BenchKernels_nano.cu | 112 - .../nanovdb/examples/benchmark/Benchmark.cc | 702 -- .../examples/benchmark/Benchmark_dense.cc | 106 - .../examples/benchmark/Benchmark_nano.cc | 113 - .../nanovdb/examples/benchmark/CMakeLists.txt | 74 - nanovdb/nanovdb/examples/benchmark/Camera.h | 72 - .../nanovdb/examples/benchmark/DenseGrid.h | 487 -- nanovdb/nanovdb/examples/benchmark/Image.h | 159 - .../ex_bump_pool_buffer/bump_pool_buffer.cc | 4 +- .../examples/ex_collide_level_set/common.h | 1 + .../examples/ex_collide_level_set/main.cc | 4 +- .../examples/ex_collide_level_set/nanovdb.cu | 7 +- .../examples/ex_collide_level_set/openvdb.cc | 7 +- .../ex_index_grid_cuda/index_grid_cuda.cc | 24 +- ...grid_cuda.cu => index_grid_cuda_kernel.cu} | 15 +- .../make_custom_nanovdb.cc | 11 +- .../make_custom_nanovdb_cuda.cc | 47 + .../make_custom_nanovdb_cuda_kernel.cu | 36 + .../make_funny_nanovdb.cc | 15 +- .../ex_make_typed_grids/make_typed_grids.cc | 17 +- .../ex_map_pool_buffer/map_pool_buffer.cc | 4 +- .../modify_nanovdb_thrust.cc | 43 + .../modify_nanovdb_thrust.cu | 39 +- .../ex_nodemanager_cuda/nodemanager_cuda.cc | 23 +- ...ger_cuda.cu => nodemanager_cuda_kernel.cu} | 9 + .../openvdb_to_nanovdb.cc | 6 +- .../openvdb_to_nanovdb_accessor.cc | 4 +- .../openvdb_to_nanovdb_cuda.cc | 11 +- ...a.cu => openvdb_to_nanovdb_cuda_kernel.cu} | 1 + .../examples/ex_raytrace_fog_volume/common.h | 1 + .../examples/ex_raytrace_fog_volume/main.cc | 4 +- .../ex_raytrace_fog_volume/nanovdb.cu | 13 +- .../ex_raytrace_fog_volume/openvdb.cc | 5 +- .../examples/ex_raytrace_level_set/common.h | 1 + .../examples/ex_raytrace_level_set/main.cc | 4 +- .../examples/ex_raytrace_level_set/nanovdb.cu | 16 +- .../examples/ex_raytrace_level_set/openvdb.cc | 5 +- .../read_nanovdb_sphere_accessor_cuda.cc | 40 - .../read_nanovdb_sphere_accessor_cuda.cu | 61 +- ...ead_nanovdb_sphere_accessor_cuda_kernel.cu | 36 + nanovdb/nanovdb/examples/ex_util/CpuTimer.h | 52 - .../examples/ex_vox_to_nanovdb/VoxToNanoVDB.h | 12 +- .../ex_voxels_to_grid_cuda.cu | 53 + .../write_nanovdb_grids.cc | 2 +- nanovdb/nanovdb/unittest/CMakeLists.txt | 8 + nanovdb/nanovdb/unittest/TestNanoVDB.cc | 2758 ++++++-- nanovdb/nanovdb/unittest/TestNanoVDB.cu | 2693 ++++++++ nanovdb/nanovdb/unittest/TestOpenVDB.cc | 451 +- .../unittest/pnanovdb_validate_strides.h | 22 +- nanovdb/nanovdb/util/CpuTimer.h | 83 + nanovdb/nanovdb/util/CreateNanoGrid.h | 2075 ++++++ nanovdb/nanovdb/util/CudaDeviceBuffer.h | 197 - nanovdb/nanovdb/util/DitherLUT.h | 2 +- nanovdb/nanovdb/util/GridBuilder.h | 3290 +++++---- nanovdb/nanovdb/util/GridChecksum.h | 424 +- nanovdb/nanovdb/util/GridHandle.h | 522 +- nanovdb/nanovdb/util/GridStats.h | 300 +- nanovdb/nanovdb/util/GridValidator.h | 20 +- nanovdb/nanovdb/util/HostBuffer.h | 22 +- nanovdb/nanovdb/util/IO.h | 860 +-- nanovdb/nanovdb/util/IndexGridBuilder.h | 652 -- nanovdb/nanovdb/util/Invoke.h | 2 +- nanovdb/nanovdb/util/NanoToOpenVDB.h | 32 +- nanovdb/nanovdb/util/NodeManager.h | 102 +- nanovdb/nanovdb/util/OpenToNanoVDB.h | 1489 +--- nanovdb/nanovdb/util/PrefixSum.h | 79 + nanovdb/nanovdb/util/Primitives.h | 1508 +++-- nanovdb/nanovdb/util/Ray.h | 18 +- nanovdb/nanovdb/util/Stencils.h | 2 +- .../nanovdb/util/cuda/CudaAddBlindData.cuh | 127 + nanovdb/nanovdb/util/cuda/CudaDeviceBuffer.h | 194 + .../nanovdb/util/cuda/CudaGridChecksum.cuh | 244 + nanovdb/nanovdb/util/cuda/CudaGridHandle.cuh | 134 + nanovdb/nanovdb/util/cuda/CudaGridStats.cuh | 250 + nanovdb/nanovdb/util/cuda/CudaIndexToGrid.cuh | 386 ++ nanovdb/nanovdb/util/cuda/CudaNodeManager.cuh | 90 + .../nanovdb/util/cuda/CudaPointsToGrid.cuh | 1179 ++++ .../nanovdb/util/cuda/CudaSignedFloodFill.cuh | 201 + nanovdb/nanovdb/util/cuda/CudaUtils.h | 136 + nanovdb/nanovdb/util/cuda/GpuTimer.h | 110 + openvdb/openvdb/points/AttributeArray.h | 4 +- openvdb_cmd/vdb_tool/include/Tool.h | 38 +- pendingchanges/nanovdb.txt | 31 + 94 files changed, 22575 insertions(+), 12200 deletions(-) delete mode 100644 nanovdb/nanovdb/examples/benchmark/BenchKernels_dense.cu delete mode 100644 nanovdb/nanovdb/examples/benchmark/BenchKernels_nano.cu delete mode 100644 nanovdb/nanovdb/examples/benchmark/Benchmark.cc delete mode 100644 nanovdb/nanovdb/examples/benchmark/Benchmark_dense.cc delete mode 100644 nanovdb/nanovdb/examples/benchmark/Benchmark_nano.cc delete mode 100644 nanovdb/nanovdb/examples/benchmark/CMakeLists.txt delete mode 100644 nanovdb/nanovdb/examples/benchmark/Camera.h delete mode 100644 nanovdb/nanovdb/examples/benchmark/DenseGrid.h delete mode 100644 nanovdb/nanovdb/examples/benchmark/Image.h rename nanovdb/nanovdb/examples/ex_index_grid_cuda/{index_grid_cuda.cu => index_grid_cuda_kernel.cu} (79%) create mode 100644 nanovdb/nanovdb/examples/ex_make_custom_nanovdb_cuda/make_custom_nanovdb_cuda.cc create mode 100644 nanovdb/nanovdb/examples/ex_make_custom_nanovdb_cuda/make_custom_nanovdb_cuda_kernel.cu create mode 100644 nanovdb/nanovdb/examples/ex_modify_nanovdb_thrust/modify_nanovdb_thrust.cc rename nanovdb/nanovdb/examples/ex_nodemanager_cuda/{nodemanager_cuda.cu => nodemanager_cuda_kernel.cu} (69%) rename nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_cuda/{openvdb_to_nanovdb_cuda.cu => openvdb_to_nanovdb_cuda_kernel.cu} (90%) delete mode 100644 nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda.cc create mode 100644 nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda_kernel.cu delete mode 100644 nanovdb/nanovdb/examples/ex_util/CpuTimer.h create mode 100644 nanovdb/nanovdb/examples/ex_voxels_to_grid_cuda/ex_voxels_to_grid_cuda.cu create mode 100644 nanovdb/nanovdb/unittest/TestNanoVDB.cu create mode 100644 nanovdb/nanovdb/util/CpuTimer.h create mode 100644 nanovdb/nanovdb/util/CreateNanoGrid.h delete mode 100644 nanovdb/nanovdb/util/CudaDeviceBuffer.h delete mode 100644 nanovdb/nanovdb/util/IndexGridBuilder.h create mode 100644 nanovdb/nanovdb/util/PrefixSum.h create mode 100644 nanovdb/nanovdb/util/cuda/CudaAddBlindData.cuh create mode 100644 nanovdb/nanovdb/util/cuda/CudaDeviceBuffer.h create mode 100644 nanovdb/nanovdb/util/cuda/CudaGridChecksum.cuh create mode 100644 nanovdb/nanovdb/util/cuda/CudaGridHandle.cuh create mode 100644 nanovdb/nanovdb/util/cuda/CudaGridStats.cuh create mode 100644 nanovdb/nanovdb/util/cuda/CudaIndexToGrid.cuh create mode 100644 nanovdb/nanovdb/util/cuda/CudaNodeManager.cuh create mode 100644 nanovdb/nanovdb/util/cuda/CudaPointsToGrid.cuh create mode 100644 nanovdb/nanovdb/util/cuda/CudaSignedFloodFill.cuh create mode 100644 nanovdb/nanovdb/util/cuda/CudaUtils.h create mode 100644 nanovdb/nanovdb/util/cuda/GpuTimer.h create mode 100644 pendingchanges/nanovdb.txt diff --git a/doc/nanovdb/HelloWorld.md b/doc/nanovdb/HelloWorld.md index bddc0a1e3d..2bc5d98328 100644 --- a/doc/nanovdb/HelloWorld.md +++ b/doc/nanovdb/HelloWorld.md @@ -4,7 +4,7 @@ ```cpp #include // replace with your own dependencies for generating the OpenVDB grid -#include // converter from OpenVDB to NanoVDB (includes NanoVDB.h and GridManager.h) +#include // converter from OpenVDB to NanoVDB (includes NanoVDB.h and GridManager.h) #include // Convert an openvdb level set sphere into a nanovdb, use accessors to print out multiple values from both @@ -17,7 +17,7 @@ int main() auto srcGrid = openvdb::tools::createLevelSetSphere(100.0f, openvdb::Vec3f(0.0f), 1.0f); // Convert the OpenVDB grid, srcGrid, into a NanoVDB grid handle. - auto handle = nanovdb::openToNanoVDB(*srcGrid); + auto handle = nanovdb::createNanoGrid(*srcGrid); // Define a (raw) pointer to the NanoVDB grid on the host. Note we match the value type of the srcGrid! auto* dstGrid = handle.grid(); @@ -77,7 +77,7 @@ int main() ```cpp #include // this is required to read (and write) NanoVDB files on the host -#include // required for CUDA memory management +#include // required for CUDA memory management extern "C" void launch_kernels(const nanovdb::NanoGrid*, const nanovdb::NanoGrid*, diff --git a/doc/nanovdb/SourceTree.md b/doc/nanovdb/SourceTree.md index 816e0f23eb..6eb0a1dcc9 100644 --- a/doc/nanovdb/SourceTree.md +++ b/doc/nanovdb/SourceTree.md @@ -15,120 +15,135 @@ ```bash foo@bar:~$ tree . -└── nanovdb - ├── CMakeLists.txt - ├── cmd - │ ├── CMakeLists.txt - │ ├── convert - │ │ └── nanovdb_convert.cc - │ ├── print - │ │ └── nanovdb_print.cc - │ └── validate - │ └── nanovdb_validate.cc - ├── CNanoVDB.h - ├── docs - │ ├── CMakeLists.txt - │ ├── codingstyle.txt - │ └── doxygen-config - ├── examples - │ ├── benchmark - │ │ ├── BenchKernels_dense.cu - │ │ ├── BenchKernels_nano.cu - │ │ ├── Benchmark.cc - │ │ ├── Benchmark_dense.cc - │ │ ├── Benchmark_nano.cc - │ │ ├── Camera.h - │ │ ├── CMakeLists.txt - │ │ ├── DenseGrid.h - │ │ └── Image.h - │ ├── CMakeLists.txt - │ ├── ex_bump_pool_buffer - │ │ └── bump_pool_buffer.cc - │ ├── ex_collide_level_set - │ │ ├── common.h - │ │ ├── main.cc - │ │ ├── nanovdb.cu - │ │ └── openvdb.cc - │ ├── ex_index_grid_cuda - │ │ ├── index_grid_cuda.cc - │ │ └── index_grid_cuda.cu - │ ├── ex_make_custom_nanovdb - │ │ └── make_custom_nanovdb.cc - │ ├── ex_make_funny_nanovdb - │ │ └── make_funny_nanovdb.cc - │ ├── ex_make_nanovdb_sphere - │ │ └── make_nanovdb_sphere.cc - │ ├── ex_make_typed_grids - │ │ └── make_typed_grids.cc - │ ├── ex_map_pool_buffer - │ │ └── map_pool_buffer.cc - │ ├── ex_modify_nanovdb_thrust - │ │ └── modify_nanovdb_thrust.cu - │ ├── ex_nodemanager_cuda - │ │ ├── nodemanager_cuda.cc - │ │ └── nodemanager_cuda.cu - │ ├── ex_openvdb_to_nanovdb - │ │ └── openvdb_to_nanovdb.cc - │ ├── ex_openvdb_to_nanovdb_accessor - │ │ └── openvdb_to_nanovdb_accessor.cc - │ ├── ex_openvdb_to_nanovdb_cuda - │ │ ├── openvdb_to_nanovdb_cuda.cc - │ │ └── openvdb_to_nanovdb_cuda.cu - │ ├── ex_raytrace_fog_volume - │ │ ├── common.h - │ │ ├── main.cc - │ │ ├── nanovdb.cu - │ │ └── openvdb.cc - │ ├── ex_raytrace_level_set - │ │ ├── common.h - │ │ ├── main.cc - │ │ ├── nanovdb.cu - │ │ └── openvdb.cc - │ ├── ex_read_nanovdb_sphere - │ │ └── read_nanovdb_sphere.cc - │ ├── ex_read_nanovdb_sphere_accessor - │ │ └── read_nanovdb_sphere_accessor.cc - │ ├── ex_read_nanovdb_sphere_accessor_cuda - │ │ ├── read_nanovdb_sphere_accessor_cuda.cc - │ │ └── read_nanovdb_sphere_accessor_cuda.cu - │ ├── ex_util - │ │ ├── ComputePrimitives.h - │ │ └── CpuTimer.h - │ ├── ex_vox_to_nanovdb - │ │ ├── vox_to_nanovdb.cc - │ │ └── VoxToNanoVDB.h - │ └── ex_write_nanovdb_grids - │ └── write_nanovdb_grids.cc - ├── NanoVDB.h - ├── PNanoVDB.h - ├── Readme.md - ├── unittest - │ ├── CMakeLists.txt - │ ├── pnanovdb_validate_strides.h - │ ├── TestNanoVDB.cc - │ └── TestOpenVDB.cc - └── util - ├── CSampleFromVoxels.h - ├── CudaDeviceBuffer.h - ├── DitherLUT.h - ├── ForEach.h - ├── GridBuilder.h - ├── GridChecksum.h - ├── GridHandle.h - ├── GridStats.h - ├── GridValidator.h - ├── HDDA.h - ├── HostBuffer.h - ├── IndexGridBuilder.h - ├── Invoke.h - ├── IO.h - ├── NanoToOpenVDB.h - ├── NodeManager.h - ├── OpenToNanoVDB.h - ├── Primitives.h - ├── Range.h - ├── Ray.h - ├── Reduce.h - ├── SampleFromVoxels.h - └── Stencils.h +├── CMakeLists.txt +├── cmd +│ ├── CMakeLists.txt +│ ├── convert +│ │ └── nanovdb_convert.cc +│ ├── print +│ │ └── nanovdb_print.cc +│ └── validate +│ └── nanovdb_validate.cc +├── CNanoVDB.h +├── docs +│ ├── CMakeLists.txt +│ ├── codingstyle.txt +│ └── doxygen-config +├── examples +│ ├── benchmark +│ │ ├── BenchKernels_dense.cu +│ │ ├── BenchKernels_nano.cu +│ │ ├── Benchmark_dense.cu +│ │ ├── Benchmark_nano.cu +│ │ ├── Camera.h +│ │ ├── CMakeLists.txt +│ │ ├── DenseGrid.h +│ │ ├── Image.h +│ │ ├── TestBenchmark.cc +│ │ └── TestBenchmark.cu +│ ├── CMakeLists.txt +│ ├── ex_bump_pool_buffer +│ │ └── bump_pool_buffer.cc +│ ├── ex_collide_level_set +│ │ ├── common.h +│ │ ├── main.cc +│ │ ├── nanovdb.cu +│ │ └── openvdb.cc +│ ├── ex_index_grid_cuda +│ │ ├── index_grid_cuda.cu +│ │ └── index_grid_cuda_kernel.cu +│ ├── ex_make_custom_nanovdb +│ │ └── make_custom_nanovdb.cc +│ ├── ex_make_custom_nanovdb_cuda +│ │ ├── make_custom_nanovdb_cuda.cc +│ │ └── make_custom_nanovdb_cuda_kernel.cu +│ ├── ex_make_funny_nanovdb +│ │ └── make_funny_nanovdb.cc +│ ├── ex_make_nanovdb_sphere +│ │ └── make_nanovdb_sphere.cc +│ ├── ex_make_typed_grids +│ │ └── make_typed_grids.cc +│ ├── ex_map_pool_buffer +│ │ └── map_pool_buffer.cc +│ ├── ex_modify_nanovdb_thrust +│ │ └── modify_nanovdb_thrust.cu +│ ├── ex_nodemanager_cuda +│ │ ├── nodemanager_cuda.cc +│ │ └── nodemanager_cuda_kernel.cu +│ ├── ex_openvdb_to_nanovdb +│ │ └── openvdb_to_nanovdb.cc +│ ├── ex_openvdb_to_nanovdb_accessor +│ │ └── openvdb_to_nanovdb_accessor.cc +│ ├── ex_openvdb_to_nanovdb_cuda +│ │ ├── openvdb_to_nanovdb_cuda.cc +│ │ └── openvdb_to_nanovdb_cuda_kernel.cu +│ ├── ex_raytrace_fog_volume +│ │ ├── common.h +│ │ ├── main.cc +│ │ ├── nanovdb.cu +│ │ └── openvdb.cc +│ ├── ex_raytrace_level_set +│ │ ├── common.h +│ │ ├── main.cc +│ │ ├── nanovdb.cu +│ │ └── openvdb.cc +│ ├── ex_read_nanovdb_sphere +│ │ └── read_nanovdb_sphere.cc +│ ├── ex_read_nanovdb_sphere_accessor +│ │ └── read_nanovdb_sphere_accessor.cc +│ ├── ex_read_nanovdb_sphere_accessor_cuda +│ │ ├── read_nanovdb_sphere_accessor_cuda.cu +│ │ └── read_nanovdb_sphere_accessor_cuda_kernel.cu +│ ├── ex_util +│ │ └── ComputePrimitives.h +│ ├── ex_voxels_to_grid_cuda +│ │ └── ex_voxels_to_grid_cuda.cu +│ ├── ex_vox_to_nanovdb +│ │ ├── vox_to_nanovdb.cc +│ │ └── VoxToNanoVDB.h +│ └── ex_write_nanovdb_grids +│ └── write_nanovdb_grids.cc +├── NanoVDB.h +├── PNanoVDB.h +├── Readme.md +├── unittest +│ ├── CMakeLists.txt +│ ├── pnanovdb_validate_strides.h +│ ├── TestNanoVDB.cc +│ ├── TestNanoVDB.cu +│ └── TestOpenVDB.cc +└── util + ├── CpuTimer.h + ├── CreateNanoGrid.h + ├── CSampleFromVoxels.h + ├── cuda + │ ├── CudaAddBlindData.cuh + │ ├── CudaDeviceBuffer.h + │ ├── CudaGridHandle.cuh + │ ├── CudaIndexToGrid.cuh + │ ├── CudaPointsToGrid.cuh + │ ├── CudaSignedFloodFill.cuh + │ ├── CudaUtils.h + │ └── GpuTimer.cuh + ├── DitherLUT.h + ├── ForEach.h + ├── GridBuilder.h + ├── GridChecksum.h + ├── GridHandle.h + ├── GridStats.h + ├── GridValidator.h + ├── HDDA.h + ├── HostBuffer.h + ├── Invoke.h + ├── IO.h + ├── NanoToOpenVDB.h + ├── NodeManager.h + ├── OpenToNanoVDB.h + ├── PrefixSum.h + ├── Primitives.h + ├── Range.h + ├── Ray.h + ├── Reduce.h + ├── SampleFromVoxels.h + └── Stencils.h ``` diff --git a/nanovdb/nanovdb/CMakeLists.txt b/nanovdb/nanovdb/CMakeLists.txt index d9e60ad5a0..7bb3ab862d 100644 --- a/nanovdb/nanovdb/CMakeLists.txt +++ b/nanovdb/nanovdb/CMakeLists.txt @@ -25,7 +25,7 @@ message(STATUS "----------------------------------------------------") option(NANOVDB_BUILD_TOOLS "Build command-line tools" ON) option(NANOVDB_BUILD_UNITTESTS "Build Unit tests" OFF) option(NANOVDB_BUILD_EXAMPLES "Build examples" OFF) -option(NANOVDB_BUILD_BENCHMARK "Build benchmark in examples" OFF) +#option(NANOVDB_BUILD_BENCHMARK "Build benchmark in examples" OFF) option(NANOVDB_USE_INTRINSICS "Build with hardware intrinsics support" OFF) option(NANOVDB_USE_CUDA "Build with CUDA support" OFF) @@ -71,12 +71,13 @@ if(UNIX) find_package(Threads REQUIRED) endif() -if(NANOVDB_BUILD_UNITTESTS OR NANOVDB_BUILD_BENCHMARK) +#if(NANOVDB_BUILD_UNITTESTS OR NANOVDB_BUILD_BENCHMARK) +if(NANOVDB_BUILD_UNITTESTS) find_package(GTest REQUIRED) endif() if(NANOVDB_USE_CUDA) - set(CMAKE_CUDA_STANDARD 11) + set(CMAKE_CUDA_STANDARD 17) set(CMAKE_CUDA_STANDARD_REQUIRED ON) if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.18) @@ -165,8 +166,20 @@ set(NANOVDB_INCLUDE_FILES # NanoVDB util header files set(NANOVDB_INCLUDE_UTILFILES + util/CpuTimer.h + util/CreateNanoGrid.h util/CSampleFromVoxels.h - util/CudaDeviceBuffer.h + util/cuda/CudaAddBlindData.cuh + util/cuda/CudaDeviceBuffer.h + util/cuda/CudaGridChecksum.cuh + util/cuda/CudaGridHandle.cuh + util/cuda/CudaGridStats.cuh + util/cuda/CudaIndexToGrid.cuh + util/cuda/CudaNodeManager.cuh + util/cuda/CudaPointsToGrid.cuh + util/cuda/CudaSignedFloodFill.cuh + util/cuda/CudaUtils.h + util/cuda/GpuTimer.h util/DitherLUT.h util/ForEach.h util/GridBuilder.h @@ -181,6 +194,7 @@ set(NANOVDB_INCLUDE_UTILFILES util/NanoToOpenVDB.h util/NodeManager.h util/OpenToNanoVDB.h + util/PrefixSum.h util/Primitives.h util/Range.h util/Ray.h diff --git a/nanovdb/nanovdb/CNanoVDB.h b/nanovdb/nanovdb/CNanoVDB.h index db3802a4aa..a3d8873e7e 100644 --- a/nanovdb/nanovdb/CNanoVDB.h +++ b/nanovdb/nanovdb/CNanoVDB.h @@ -687,7 +687,7 @@ cnanovdb_griddata_valid(const CNANOVDB_GLOBAL cnanovdb_griddata *RESTRICT grid) { if (!grid) return 0; - if (grid->mMagic != 0x304244566f6e614eUL) + if (grid->mMagic != 0x304244566f6e614eUL && grid->mMagic != 0x314244566f6e614eUL) return 0; return 1; } diff --git a/nanovdb/nanovdb/NanoVDB.h b/nanovdb/nanovdb/NanoVDB.h index b1fe3ee433..1b770e901e 100644 --- a/nanovdb/nanovdb/NanoVDB.h +++ b/nanovdb/nanovdb/NanoVDB.h @@ -29,7 +29,7 @@ structure can safely be ignored by most client codes)! - \warning NanoVDB grids can only be constructed via tools like openToNanoVDB + \warning NanoVDB grids can only be constructed via tools like createNanoGrid or the GridBuilder. This explains why none of the grid nodes defined below have public constructors or destructors. @@ -102,12 +102,12 @@ Notation: "]---[" implies it has optional padding, and "][" implies zero padding - [GridData(672B)][TreeData(64B)]---[RootData][N x Root::Tile]---[NodeData<5>]---[ModeData<4>]---[LeafData<3>]---[BLINDMETA...]---[BLIND0]---[BLIND1]---etc. - ^ ^ ^ ^ ^ ^ - | | | | | | - +-- Start of 32B aligned buffer | | | | +-- Node0::DataType* leafData - GridType::DataType* gridData | | | | - | | | +-- Node1::DataType* lowerData + [GridData(672B)][TreeData(64B)]---[RootData][N x Root::Tile]---[InternalData<5>]---[InternalData<4>]---[LeafData<3>]---[BLINDMETA...]---[BLIND0]---[BLIND1]---etc. + ^ ^ ^ ^ ^ ^ + | | | | | | + +-- Start of 32B aligned buffer | | | | +-- Node0::DataType* leafData + GridType::DataType* gridData | | | | + | | | +-- Node1::DataType* lowerData RootType::DataType* rootData --+ | | | +-- Node2::DataType* upperData | @@ -118,22 +118,39 @@ #ifndef NANOVDB_NANOVDB_H_HAS_BEEN_INCLUDED #define NANOVDB_NANOVDB_H_HAS_BEEN_INCLUDED +// NANOVDB_MAGIC_NUMBER is currently used for both grids and files (starting with v32.6.0) +// NANOVDB_MAGIC_GRID will soon be used exclusively for grids +// NANOVDB_MAGIC_FILE will soon be used exclusively for files +// NANOVDB_MAGIC_NODE will soon be used exclusively for NodeManager +// | : 0 in 30 corresponds to 0 in NanoVDB0 #define NANOVDB_MAGIC_NUMBER 0x304244566f6e614eUL // "NanoVDB0" in hex - little endian (uint64_t) +#define NANOVDB_MAGIC_GRID 0x314244566f6e614eUL // "NanoVDB1" in hex - little endian (uint64_t) +#define NANOVDB_MAGIC_FILE 0x324244566f6e614eUL // "NanoVDB2" in hex - little endian (uint64_t) +#define NANOVDB_MAGIC_NODE 0x334244566f6e614eUL // "NanoVDB3" in hex - little endian (uint64_t) +#define NANOVDB_MAGIC_MASK 0x00FFFFFFFFFFFFFFUL // use this mask to remove the number +//#define NANOVDB_USE_NEW_MAGIC_NUMBERS// used to enable use of the new magic numbers described above #define NANOVDB_MAJOR_VERSION_NUMBER 32 // reflects changes to the ABI and hence also the file format -#define NANOVDB_MINOR_VERSION_NUMBER 4 // reflects changes to the API but not ABI -#define NANOVDB_PATCH_VERSION_NUMBER 2 // reflects changes that does not affect the ABI or API +#define NANOVDB_MINOR_VERSION_NUMBER 6 // reflects changes to the API but not ABI +#define NANOVDB_PATCH_VERSION_NUMBER 0 // reflects changes that does not affect the ABI or API + +#define TBB_SUPPRESS_DEPRECATED_MESSAGES 1 // This replaces a Coord key at the root level with a single uint64_t -#define USE_SINGLE_ROOT_KEY +#define NANOVDB_USE_SINGLE_ROOT_KEY // This replaces three levels of Coord keys in the ReadAccessor with one Coord -//#define USE_SINGLE_ACCESSOR_KEY +//#define NANOVDB_USE_SINGLE_ACCESSOR_KEY +// Use this to switch between std::ofstream or FILE implementations //#define NANOVDB_USE_IOSTREAMS +// Use this to switch between old and new accessor methods +#define NANOVDB_NEW_ACCESSOR_METHODS + #define NANOVDB_FPN_BRANCHLESS +// Do not change this value! 32 byte alignment is fixed in NanoVDB #define NANOVDB_DATA_ALIGNMENT 32 #if !defined(NANOVDB_ALIGN) @@ -186,12 +203,27 @@ typedef unsigned long long uint64_t; #endif // __CUDACC_RTC__ #if defined(__CUDACC__) || defined(__HIP__) -// Only define __hostdev__ when using NVIDIA CUDA or HIP compiler -#define __hostdev__ __host__ __device__ +// Only define __hostdev__ when using NVIDIA CUDA or HIP compilers +#ifndef __hostdev__ +#define __hostdev__ __host__ __device__ // Runs on the CPU and GPU, called from the CPU or the GPU +#endif #else -#define __hostdev__ +// Dummy definitions of macros only defined by CUDA and HIP compilers +#ifndef __hostdev__ +#define __hostdev__ // Runs on the CPU and GPU, called from the CPU or the GPU +#endif +#ifndef __global__ +#define __global__ // Runs on the GPU, called from the CPU or the GPU +#endif +#ifndef __device__ +#define __device__ // Runs on the GPU, called from the GPU +#endif +#ifndef __host__ +#define __host__ // Runs on the CPU, called from the CPU #endif +#endif // if defined(__CUDACC__) || defined(__HIP__) + // The following macro will suppress annoying warnings when nvcc // compiles functions that call (host) intrinsics (which is perfectly valid) #if defined(_MSC_VER) && defined(__CUDACC__) @@ -202,6 +234,13 @@ typedef unsigned long long uint64_t; #define NANOVDB_HOSTDEV_DISABLE_WARNING #endif +// Define compiler warnings that work with all compilers +//#if defined(_MSC_VER) +//#define NANO_WARNING(msg) _pragma("message" #msg) +//#else +//#define NANO_WARNING(msg) _Pragma("message" #msg) +//#endif + // A portable implementation of offsetof - unfortunately it doesn't work with static_assert #define NANOVDB_OFFSETOF(CLASS, MEMBER) ((int)(size_t)((char*)&((CLASS*)0)->MEMBER - (char*)0)) @@ -210,25 +249,60 @@ namespace nanovdb { // --------------------------> Build types <------------------------------------ /// @brief Dummy type for a voxel whose value equals an offset into an external value array -class ValueIndex {}; +class ValueIndex +{ +}; + +/// @brief Dummy type for a voxel whose value equals an offset into an external value array of active values +class ValueOnIndex +{ +}; + +/// @brief Like @c ValueIndex but with a mutable mask +class ValueIndexMask +{ +}; + +/// @brief Like @c ValueOnIndex but with a mutable mask +class ValueOnIndexMask +{ +}; /// @brief Dummy type for a voxel whose value equals its binary active state -class ValueMask {}; +class ValueMask +{ +}; /// @brief Dummy type for a 16 bit floating point values -class Half {}; +class Half +{ +}; /// @brief Dummy type for a 4bit quantization of float point values -class Fp4 {}; +class Fp4 +{ +}; /// @brief Dummy type for a 8bit quantization of float point values -class Fp8 {}; +class Fp8 +{ +}; /// @brief Dummy type for a 16bit quantization of float point values -class Fp16 {}; +class Fp16 +{ +}; /// @brief Dummy type for a variable bit quantization of floating point values -class FpN {}; +class FpN +{ +}; + +/// @dummy type for indexing points into voxels +class Point +{ +}; +//using Points = Point;// for backwards compatibility // --------------------------> GridType <------------------------------------ @@ -240,62 +314,69 @@ class FpN {}; /// 3) Verify that the ConvertTrait in NanoToOpenVDB.h works correctly with the new type /// 4) Add the new type to mapToGridType (defined below) that maps NanoVDB types to GridType /// 5) Add the new type to toStr (defined below) -enum class GridType : uint32_t { Unknown = 0, - Float = 1,// single precision floating point value - Double = 2,// double precision floating point value - Int16 = 3,// half precision signed integer value - Int32 = 4,// single precision signed integer value - Int64 = 5,// double precision signed integer value - Vec3f = 6,// single precision floating 3D vector - Vec3d = 7,// double precision floating 3D vector - Mask = 8,// no value, just the active state - Half = 9,// half precision floating point value - UInt32 = 10,// single precision unsigned integer value - Boolean = 11,// boolean value, encoded in bit array - RGBA8 = 12,// RGBA packed into 32bit word in reverse-order. R in low bits. - Fp4 = 13,// 4bit quantization of float point value - Fp8 = 14,// 8bit quantization of float point value - Fp16 = 15,// 16bit quantization of float point value - FpN = 16,// variable bit quantization of floating point value - Vec4f = 17,// single precision floating 4D vector - Vec4d = 18,// double precision floating 4D vector - Index = 19,// index into an external array of values - End = 20 }; +enum class GridType : uint32_t { Unknown = 0, // unknown value type - should rarely be used + Float = 1, // single precision floating point value + Double = 2, // double precision floating point value + Int16 = 3, // half precision signed integer value + Int32 = 4, // single precision signed integer value + Int64 = 5, // double precision signed integer value + Vec3f = 6, // single precision floating 3D vector + Vec3d = 7, // double precision floating 3D vector + Mask = 8, // no value, just the active state + Half = 9, // half precision floating point value (placeholder for IEEE 754 Half) + UInt32 = 10, // single precision unsigned integer value + Boolean = 11, // boolean value, encoded in bit array + RGBA8 = 12, // RGBA packed into 32bit word in reverse-order, i.e. R is lowest byte. + Fp4 = 13, // 4bit quantization of floating point value + Fp8 = 14, // 8bit quantization of floating point value + Fp16 = 15, // 16bit quantization of floating point value + FpN = 16, // variable bit quantization of floating point value + Vec4f = 17, // single precision floating 4D vector + Vec4d = 18, // double precision floating 4D vector + Index = 19, // index into an external array of active and inactive values + OnIndex = 20, // index into an external array of active values + IndexMask = 21, // like Index but with a mutable mask + OnIndexMask = 22, // like OnIndex but with a mutable mask + PointIndex = 23, // voxels encode indices to co-located points + Vec3u8 = 24, // 8bit quantization of floating point 3D vector (only as blind data) + Vec3u16 = 25, // 16bit quantization of floating point 3D vector (only as blind data) + End = 26 }; // should never be used #ifndef __CUDACC_RTC__ -/// @brief Retuns a c-string used to describe a GridType +/// @brief Maps a GridType to a c-string +/// @param gridType GridType to be mapped to a string +/// @return Retuns a c-string used to describe a GridType inline const char* toStr(GridType gridType) { - static const char * LUT[] = { "?", "float", "double" , "int16", "int32", - "int64", "Vec3f", "Vec3d", "Mask", "Half", - "uint32", "bool", "RGBA8", "Float4", "Float8", - "Float16", "FloatN", "Vec4f", "Vec4d", "Index", "End" }; - static_assert( sizeof(LUT)/sizeof(char*) - 1 == int(GridType::End), "Unexpected size of LUT" ); + static const char* LUT[] = {"?", "float", "double", "int16", "int32", "int64", "Vec3f", "Vec3d", "Mask", "Half", + "uint32", "bool", "RGBA8", "Float4", "Float8", "Float16", "FloatN", "Vec4f", "Vec4d", + "Index", "OnIndex", "IndexMask", "OnIndexMask", "PointIndex", "Vec3u8", "Vec3u16", "End"}; + static_assert(sizeof(LUT) / sizeof(char*) - 1 == int(GridType::End), "Unexpected size of LUT"); return LUT[static_cast(gridType)]; } #endif // --------------------------> GridClass <------------------------------------ -/// @brief Classes (defined in OpenVDB) that are currently supported by NanoVDB +/// @brief Classes (superset of OpenVDB) that are currently supported by NanoVDB enum class GridClass : uint32_t { Unknown = 0, - LevelSet = 1, // narrow band level set, e.g. SDF - FogVolume = 2, // fog volume, e.g. density - Staggered = 3, // staggered MAC grid, e.g. velocity - PointIndex = 4, // point index grid - PointData = 5, // point data grid - Topology = 6, // grid with active states only (no values) - VoxelVolume = 7, // volume of geometric cubes, e.g. Minecraft - IndexGrid = 8,// grid whose values are offsets, e.g. into an external array - End = 9 }; + LevelSet = 1, // narrow band level set, e.g. SDF + FogVolume = 2, // fog volume, e.g. density + Staggered = 3, // staggered MAC grid, e.g. velocity + PointIndex = 4, // point index grid + PointData = 5, // point data grid + Topology = 6, // grid with active states only (no values) + VoxelVolume = 7, // volume of geometric cubes, e.g. colors cubes in Minecraft + IndexGrid = 8, // grid whose values are offsets, e.g. into an external array + TensorGrid = 9, // Index grid for indexing learnable tensor features + End = 10 }; #ifndef __CUDACC_RTC__ /// @brief Retuns a c-string used to describe a GridClass inline const char* toStr(GridClass gridClass) { - static const char * LUT[] = { "?", "SDF", "FOG" , "MAC", "PNTIDX", - "PNTDAT", "TOPO", "VOX", "INDEX", "END" }; - static_assert( sizeof(LUT)/sizeof(char*) - 1 == int(GridClass::End), "Unexpected size of LUT" ); + static const char* LUT[] = {"?", "SDF", "FOG", "MAC", "PNTIDX", "PNTDAT", "TOPO", "VOX", "INDEX", "TENSOR", "END"}; + static_assert(sizeof(LUT) / sizeof(char*) - 1 == int(GridClass::End), "Unexpected size of LUT"); return LUT[static_cast(gridClass)]; } #endif @@ -304,27 +385,27 @@ inline const char* toStr(GridClass gridClass) /// @brief Grid flags which indicate what extra information is present in the grid buffer. enum class GridFlags : uint32_t { - HasLongGridName = 1 << 0,// grid name is longer than 256 characters - HasBBox = 1 << 1,// nodes contain bounding-boxes of active values - HasMinMax = 1 << 2,// nodes contain min/max of active values - HasAverage = 1 << 3,// nodes contain averages of active values - HasStdDeviation = 1 << 4,// nodes contain standard deviations of active values - IsBreadthFirst = 1 << 5,// nodes are arranged breadth-first in memory - End = 1 << 6, + HasLongGridName = 1 << 0, // grid name is longer than 256 characters + HasBBox = 1 << 1, // nodes contain bounding-boxes of active values + HasMinMax = 1 << 2, // nodes contain min/max of active values + HasAverage = 1 << 3, // nodes contain averages of active values + HasStdDeviation = 1 << 4, // nodes contain standard deviations of active values + IsBreadthFirst = 1 << 5, // nodes are typically arranged breadth-first in memory + End = 1 << 6, // use End - 1 as a mask for the 5 lower bit flags }; #ifndef __CUDACC_RTC__ /// @brief Retuns a c-string used to describe a GridFlags inline const char* toStr(GridFlags gridFlags) { - static const char * LUT[] = { "has long grid name", - "has bbox", - "has min/max", - "has average", - "has standard deviation", - "is breadth-first", - "end" }; - static_assert( 1 << (sizeof(LUT)/sizeof(char*) - 1) == int(GridFlags::End), "Unexpected size of LUT" ); + static const char* LUT[] = {"has long grid name", + "has bbox", + "has min/max", + "has average", + "has standard deviation", + "is breadth-first", + "end"}; + static_assert(1 << (sizeof(LUT) / sizeof(char*) - 1) == int(GridFlags::End), "Unexpected size of LUT"); return LUT[static_cast(gridFlags)]; } #endif @@ -341,19 +422,29 @@ enum class GridBlindDataClass : uint32_t { Unknown = 0, /// @brief Blind-data Semantics that are currently understood by NanoVDB enum class GridBlindDataSemantic : uint32_t { Unknown = 0, - PointPosition = 1, + PointPosition = 1, // 3D coordinates in an unknown space PointColor = 2, PointNormal = 3, PointRadius = 4, PointVelocity = 5, PointId = 6, - End = 8 }; + WorldCoords = 7, // 3D coordinates in world space, e.g. (0.056, 0.8, 1,8) + GridCoords = 8, // 3D coordinates in grid space, e.g. (1.2, 4.0, 5.7), aka index-space + VoxelCoords = 9, // 3D coordinates in voxel space, e.g. (0.2, 0.0, 0.7) + End = 10 }; // --------------------------> is_same <------------------------------------ /// @brief C++11 implementation of std::is_same -template +/// @note When more than two arguments are provided value = T0==T1 || T0==T2 || ... +template struct is_same +{ + static constexpr bool value = is_same::value || is_same::value; +}; + +template +struct is_same { static constexpr bool value = false; }; @@ -364,6 +455,36 @@ struct is_same static constexpr bool value = true; }; +// --------------------------> is_floating_point <------------------------------------ + +/// @brief C++11 implementation of std::is_floating_point +template +struct is_floating_point +{ + static constexpr bool value = is_same::value; +}; + +// --------------------------> BuildTraits <------------------------------------ + +/// @brief Define static boolean tests for template build types +template +struct BuildTraits +{ + // check if T is an index type + static constexpr bool is_index = is_same::value; + static constexpr bool is_onindex = is_same::value; + static constexpr bool is_offindex = is_same::value; + static constexpr bool is_indexmask = is_same::value; + // check if T is a compressed float type with fixed bit precision + static constexpr bool is_FpX = is_same::value; + // check if T is a compressed float type with fixed or variable bit precision + static constexpr bool is_Fp = is_same::value; + // check if T is a POD float type, i.e float or double + static constexpr bool is_float = is_floating_point::value; + // check if T is a template specialization of LeafData, i.e. has T mValues[512] + static constexpr bool is_special = is_index || is_Fp || is_same::value; +}; // BuildTraits + // --------------------------> enable_if <------------------------------------ /// @brief C++11 implementation of std::enable_if @@ -378,6 +499,19 @@ struct enable_if using type = T; }; +// --------------------------> disable_if <------------------------------------ + +template +struct disable_if +{ + typedef T type; +}; + +template +struct disable_if +{ +}; + // --------------------------> is_const <------------------------------------ template @@ -392,27 +526,102 @@ struct is_const static constexpr bool value = true; }; +// --------------------------> is_pointer <------------------------------------ + +/// @brief Trait used to identify template parameter that are pointers +/// @tparam T Template parameter to be tested +template +struct is_pointer +{ + static constexpr bool value = false; +}; + +/// @brief Template specialization of non-const pointers +/// @tparam T Template parameter to be tested +template +struct is_pointer +{ + static constexpr bool value = true; +}; + +/// @brief Template specialization of const pointers +/// @tparam T Template parameter to be tested +template +struct is_pointer +{ + static constexpr bool value = true; +}; + // --------------------------> remove_const <------------------------------------ +/// @brief Trait use to const from type. Default implementation is just a pass-through +/// @tparam T Type +/// @details remove_pointer::type = float template struct remove_const { using type = T; }; +/// @brief Template specialization of trait class use to remove const qualifier type from a type +/// @tparam T Type of the const type +/// @details remove_pointer::type = float template struct remove_const { using type = T; }; -// --------------------------> is_floating_point <------------------------------------ +// --------------------------> remove_reference <------------------------------------ -/// @brief C++11 implementation of std::is_floating_point -template -struct is_floating_point +/// @brief Trait use to remove reference, i.e. "&", qualifier from a type. Default implementation is just a pass-through +/// @tparam T Type +/// @details remove_pointer::type = float +template +struct remove_reference {using type = T;}; + +/// @brief Template specialization of trait class use to remove reference, i.e. "&", qualifier from a type +/// @tparam T Type of the reference +/// @details remove_pointer::type = float +template +struct remove_reference {using type = T;}; + +// --------------------------> remove_pointer <------------------------------------ + +/// @brief Trait use to remove pointer, i.e. "*", qualifier from a type. Default implementation is just a pass-through +/// @tparam T Type +/// @details remove_pointer::type = float +template +struct remove_pointer {using type = T;}; + +/// @brief Template specialization of trait class use to to remove pointer, i.e. "*", qualifier from a type +/// @tparam T Type of the pointer +/// @details remove_pointer::type = float +template +struct remove_pointer {using type = T;}; + +// --------------------------> match_const <------------------------------------ + +/// @brief Trait used to transfer the const-ness of a reference type to another type +/// @tparam T Type whose const-ness needs to match the reference type +/// @tparam ReferenceT Reference type that is not const +/// @details match_const::type = int +/// match_const::type = int +template +struct match_const { - static const bool value = is_same::value || is_same::value; + using type = typename remove_const::type; +}; + +/// @brief Template specialization used to transfer the const-ness of a reference type to another type +/// @tparam T Type that will adopt the const-ness of the reference type +/// @tparam ReferenceT Reference type that is const +/// @details match_const::type = const int +/// match_const::type = const int +template +struct match_const +{ + using type = const typename remove_const::type; }; // --------------------------> is_specialization <------------------------------------ @@ -422,6 +631,8 @@ struct is_floating_point /// given in the second template parameter. /// /// @details is_specialization, Vec3>::value == true; +/// is_specialization::value == true; +/// is_specialization, std::vector>::value == true; template class TemplateType> struct is_specialization { @@ -433,10 +644,10 @@ struct is_specialization, TemplateType> static const bool value = true; }; -// --------------------------> Value Map <------------------------------------ +// --------------------------> BuildToValueMap <------------------------------------ /// @brief Maps one type (e.g. the build types above) to other (actual) types -template +template struct BuildToValueMap { using Type = T; @@ -450,6 +661,27 @@ struct BuildToValueMap using type = uint64_t; }; +template<> +struct BuildToValueMap +{ + using Type = uint64_t; + using type = uint64_t; +}; + +template<> +struct BuildToValueMap +{ + using Type = uint64_t; + using type = uint64_t; +}; + +template<> +struct BuildToValueMap +{ + using Type = uint64_t; + using type = uint64_t; +}; + template<> struct BuildToValueMap { @@ -492,6 +724,13 @@ struct BuildToValueMap using type = float; }; +template<> +struct BuildToValueMap +{ + using Type = uint64_t; + using type = uint64_t; +}; + // --------------------------> utility functions related to alignment <------------------------------------ /// @brief return true if the specified pointer is aligned @@ -529,92 +768,113 @@ __hostdev__ inline static const T* alignPtr(const T* p) return reinterpret_cast( (const uint8_t*)p + alignmentPadding(p) ); } -// --------------------------> PtrDiff PtrAdd <------------------------------------ +// --------------------------> PtrDiff <------------------------------------ -template +/// @brief Compute the distance, in bytes, between two pointers +/// @tparam T1 Type of the first pointer +/// @tparam T2 Type of the second pointer +/// @param p fist pointer, assumed to NOT be NULL +/// @param q second pointer, assumed to NOT be NULL +/// @return signed distance between pointer addresses in units of bytes +template __hostdev__ inline static int64_t PtrDiff(const T1* p, const T2* q) { NANOVDB_ASSERT(p && q); return reinterpret_cast(p) - reinterpret_cast(q); } -template -__hostdev__ inline static DstT* PtrAdd(SrcT *p, int64_t offset) +// --------------------------> PtrAdd <------------------------------------ + +/// @brief Adds a byte offset of a non-const pointer to produce another non-const pointer +/// @tparam DstT Type of the return pointer +/// @tparam SrcT Type of the input pointer +/// @param p non-const input pointer, assumed to NOT be NULL +/// @param offset signed byte offset +/// @return a non-const pointer defined as the offset of an input pointer +template +__hostdev__ inline static DstT* PtrAdd(SrcT* p, int64_t offset) { NANOVDB_ASSERT(p); return reinterpret_cast(reinterpret_cast(p) + offset); } -template -__hostdev__ inline static const DstT* PtrAdd(const SrcT *p, int64_t offset) +/// @brief Adds a byte offset of a const pointer to produce another const pointer +/// @tparam DstT Type of the return pointer +/// @tparam SrcT Type of the input pointer +/// @param p const input pointer, assumed to NOT be NULL +/// @param offset signed byte offset +/// @return a const pointer defined as the offset of a const input pointer +template +__hostdev__ inline static const DstT* PtrAdd(const SrcT* p, int64_t offset) { NANOVDB_ASSERT(p); return reinterpret_cast(reinterpret_cast(p) + offset); } -// --------------------------> Rgba8 <------------------------------------ +// --------------------------> isFloatingPoint(GridType) <------------------------------------ -/// @brief 8-bit red, green, blue, alpha packed into 32 bit unsigned int -class Rgba8 +/// @brief return true if the GridType maps to a floating point type +__hostdev__ inline bool isFloatingPoint(GridType gridType) { - union { - uint8_t c[4];// 4 color channels of red, green, blue and alpha components. - uint32_t packed;// 32 bit packed representation - } mData; -public: - static const int SIZE = 4; - using ValueType = uint8_t; + return gridType == GridType::Float || + gridType == GridType::Double || + gridType == GridType::Half || + gridType == GridType::Fp4 || + gridType == GridType::Fp8 || + gridType == GridType::Fp16 || + gridType == GridType::FpN; +} - Rgba8(const Rgba8&) = default; - Rgba8(Rgba8&&) = default; - Rgba8& operator=(Rgba8&&) = default; - Rgba8& operator=(const Rgba8&) = default; - __hostdev__ Rgba8() : mData{{0,0,0,0}} {static_assert(sizeof(uint32_t) == sizeof(Rgba8),"Unexpected sizeof");} - __hostdev__ Rgba8(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255u) : mData{{r, g, b, a}} {} - explicit __hostdev__ Rgba8(uint8_t v) : Rgba8(v,v,v,v) {} - __hostdev__ Rgba8(float r, float g, float b, float a = 1.0f) - : mData{{(uint8_t(0.5f + r * 255.0f)), // round to nearest - (uint8_t(0.5f + g * 255.0f)), // round to nearest - (uint8_t(0.5f + b * 255.0f)), // round to nearest - (uint8_t(0.5f + a * 255.0f))}}// round to nearest - { - } - __hostdev__ bool operator<(const Rgba8& rhs) const { return mData.packed < rhs.mData.packed; } - __hostdev__ bool operator==(const Rgba8& rhs) const { return mData.packed == rhs.mData.packed; } - __hostdev__ float lengthSqr() const - { - return 0.0000153787005f*(float(mData.c[0])*mData.c[0] + - float(mData.c[1])*mData.c[1] + - float(mData.c[2])*mData.c[2]);//1/255^2 - } - __hostdev__ float length() const { return sqrtf(this->lengthSqr() ); } - __hostdev__ const uint8_t& operator[](int n) const { return mData.c[n]; } - __hostdev__ uint8_t& operator[](int n) { return mData.c[n]; } - __hostdev__ const uint32_t& packed() const { return mData.packed; } - __hostdev__ uint32_t& packed() { return mData.packed; } - __hostdev__ const uint8_t& r() const { return mData.c[0]; } - __hostdev__ const uint8_t& g() const { return mData.c[1]; } - __hostdev__ const uint8_t& b() const { return mData.c[2]; } - __hostdev__ const uint8_t& a() const { return mData.c[3]; } - __hostdev__ uint8_t& r() { return mData.c[0]; } - __hostdev__ uint8_t& g() { return mData.c[1]; } - __hostdev__ uint8_t& b() { return mData.c[2]; } - __hostdev__ uint8_t& a() { return mData.c[3]; } -};// Rgba8 - -using PackedRGBA8 = Rgba8;// for backwards compatibility +// --------------------------> isFloatingPointVector(GridType) <------------------------------------ -// --------------------------> isValue(GridType, GridClass) <------------------------------------ +/// @brief return true if the GridType maps to a floating point vec3. +__hostdev__ inline bool isFloatingPointVector(GridType gridType) +{ + return gridType == GridType::Vec3f || + gridType == GridType::Vec3d || + gridType == GridType::Vec4f || + gridType == GridType::Vec4d; +} -/// @brief return true if the GridType maps to a floating point value. -__hostdev__ inline bool isFloatingPoint(GridType gridType) +// --------------------------> isInteger(GridType) <------------------------------------ + +/// @brief Return true if the GridType maps to a POD integer type. +/// @details These types are used to associate a voxel with a POD integer type +__hostdev__ inline bool isInteger(GridType gridType) { - return gridType == GridType::Float || - gridType == GridType::Double || - gridType == GridType::Fp4 || - gridType == GridType::Fp8 || - gridType == GridType::Fp16 || - gridType == GridType::FpN; + return gridType == GridType::Int16 || + gridType == GridType::Int32 || + gridType == GridType::Int64 || + gridType == GridType::UInt32; +} + +// --------------------------> isIndex(GridType) <------------------------------------ + +/// @brief Return true if the GridType maps to a special index type (not a POD integer type). +/// @details These types are used to index from a voxel into an external array of values, e.g. sidecar or blind data. +__hostdev__ inline bool isIndex(GridType gridType) +{ + return gridType == GridType::Index ||// index both active and inactive values + gridType == GridType::OnIndex ||// index active values only + gridType == GridType::IndexMask ||// as Index, but with an additional mask + gridType == GridType::OnIndexMask;// as OnIndex, but with an additional mask +} + +// --------------------------> memcpy64 <------------------------------------ + +/// @brief copy 64 bit words from @c src to @c dst +/// @param dst pointer to destination +/// @param src pointer to source +/// @param word_count number of 64 bit words to be copied +/// @return destination pointer +/// @warning @c src and @c dst cannot overlap and should both be 64 bit aligned +__hostdev__ inline static void* memcpy64(void *dst, const void *src, size_t word_count) +{ + NANOVDB_ASSERT(uint64_t(dst) % 8 == 0 && uint64_t(src) % 8 == 0); + auto *d = reinterpret_cast(dst), *e = d + word_count; + auto *s = reinterpret_cast(src); + while (d != e) *d++ = *s++; + return dst; } // --------------------------> isValue(GridType, GridClass) <------------------------------------ @@ -625,18 +885,58 @@ __hostdev__ inline bool isValid(GridType gridType, GridClass gridClass) if (gridClass == GridClass::LevelSet || gridClass == GridClass::FogVolume) { return isFloatingPoint(gridType); } else if (gridClass == GridClass::Staggered) { - return gridType == GridType::Vec3f || gridType == GridType::Vec3d || - gridType == GridType::Vec4f || gridType == GridType::Vec4d; - } else if (gridClass == GridClass::PointIndex || gridClass == GridClass::PointData) { - return gridType == GridType::UInt32; + return isFloatingPointVector(gridType); + } else if (gridClass == GridClass::PointIndex || gridClass == GridClass::PointData) { + return gridType == GridType::PointIndex || gridType == GridType::UInt32; } else if (gridClass == GridClass::Topology) { return gridType == GridType::Mask; } else if (gridClass == GridClass::IndexGrid) { - return gridType == GridType::Index; + return isIndex(gridType); } else if (gridClass == GridClass::VoxelVolume) { - return gridType == GridType::RGBA8 || gridType == GridType::Float || gridType == GridType::Double || gridType == GridType::Vec3f || gridType == GridType::Vec3d || gridType == GridType::UInt32; + return gridType == GridType::RGBA8 || gridType == GridType::Float || + gridType == GridType::Double || gridType == GridType::Vec3f || + gridType == GridType::Vec3d || gridType == GridType::UInt32; } - return gridClass < GridClass::End && gridType < GridType::End;// any valid combination + return gridClass < GridClass::End && gridType < GridType::End; // any valid combination +} + +// --------------------------> validation of blind data meta data <------------------------------------ + +/// @brief return true if the combination of GridBlindDataClass, GridBlindDataSemantic and GridType is valid. +__hostdev__ inline bool isValid(const GridBlindDataClass& blindClass, + const GridBlindDataSemantic& blindSemantics, + const GridType& blindType) +{ + bool test = false; + switch (blindClass) { + case GridBlindDataClass::IndexArray: + test = (blindSemantics == GridBlindDataSemantic::Unknown || + blindSemantics == GridBlindDataSemantic::PointId) && + isInteger(blindType); + break; + case GridBlindDataClass::AttributeArray: + if (blindSemantics == GridBlindDataSemantic::PointPosition || + blindSemantics == GridBlindDataSemantic::WorldCoords) { + test = blindType == GridType::Vec3f || blindType == GridType::Vec3d; + } else if (blindSemantics == GridBlindDataSemantic::GridCoords) { + test = blindType == GridType::Vec3f; + } else if (blindSemantics == GridBlindDataSemantic::VoxelCoords) { + test = blindType == GridType::Vec3f || blindType == GridType::Vec3u8 || blindType == GridType::Vec3u16; + } else { + test = blindSemantics != GridBlindDataSemantic::PointId; + } + break; + case GridBlindDataClass::GridName: + test = blindSemantics == GridBlindDataSemantic::Unknown && blindType == GridType::Unknown; + break; + default: // captures blindClass == Unknown and ChannelArray + test = blindClass < GridBlindDataClass::End && + blindSemantics < GridBlindDataSemantic::End && + blindType < GridType::End; // any valid combination + break; + } + //if (!test) printf("Invalid combination: GridBlindDataClass=%u, GridBlindDataSemantic=%u, GridType=%u\n",(uint32_t)blindClass, (uint32_t)blindSemantics, (uint32_t)blindType); + return test; } // ----------------------------> Version class <------------------------------------- @@ -646,42 +946,73 @@ __hostdev__ inline bool isValid(GridType gridType, GridClass gridClass) /// @details major is the top 11 bits, minor is the 11 middle bits and patch is the lower 10 bits class Version { - uint32_t mData;// 11 + 11 + 10 bit packing of major + minor + patch + uint32_t mData; // 11 + 11 + 10 bit packing of major + minor + patch public: - __hostdev__ Version() : mData( uint32_t(NANOVDB_MAJOR_VERSION_NUMBER) << 21 | - uint32_t(NANOVDB_MINOR_VERSION_NUMBER) << 10 | - uint32_t(NANOVDB_PATCH_VERSION_NUMBER) ) + __hostdev__ Version() + : mData(uint32_t(NANOVDB_MAJOR_VERSION_NUMBER) << 21 | + uint32_t(NANOVDB_MINOR_VERSION_NUMBER) << 10 | + uint32_t(NANOVDB_PATCH_VERSION_NUMBER)) { } + __hostdev__ Version(uint32_t data) : mData(data) {} __hostdev__ Version(uint32_t major, uint32_t minor, uint32_t patch) - : mData( major << 21 | minor << 10 | patch ) - { - NANOVDB_ASSERT(major < (1u << 11));// max value of major is 2047 - NANOVDB_ASSERT(minor < (1u << 11));// max value of minor is 2047 - NANOVDB_ASSERT(patch < (1u << 10));// max value of patch is 1023 - } - __hostdev__ bool operator==(const Version &rhs) const {return mData == rhs.mData;} - __hostdev__ bool operator< (const Version &rhs) const {return mData < rhs.mData;} - __hostdev__ bool operator<=(const Version &rhs) const {return mData <= rhs.mData;} - __hostdev__ bool operator> (const Version &rhs) const {return mData > rhs.mData;} - __hostdev__ bool operator>=(const Version &rhs) const {return mData >= rhs.mData;} - __hostdev__ uint32_t id() const { return mData; } - __hostdev__ uint32_t getMajor() const { return (mData >> 21) & ((1u << 11) - 1);} - __hostdev__ uint32_t getMinor() const { return (mData >> 10) & ((1u << 11) - 1);} - __hostdev__ uint32_t getPatch() const { return mData & ((1u << 10) - 1);} + : mData(major << 21 | minor << 10 | patch) + { + NANOVDB_ASSERT(major < (1u << 11)); // max value of major is 2047 + NANOVDB_ASSERT(minor < (1u << 11)); // max value of minor is 2047 + NANOVDB_ASSERT(patch < (1u << 10)); // max value of patch is 1023 + } + __hostdev__ bool operator==(const Version& rhs) const { return mData == rhs.mData; } + __hostdev__ bool operator<( const Version& rhs) const { return mData < rhs.mData; } + __hostdev__ bool operator<=(const Version& rhs) const { return mData <= rhs.mData; } + __hostdev__ bool operator>( const Version& rhs) const { return mData > rhs.mData; } + __hostdev__ bool operator>=(const Version& rhs) const { return mData >= rhs.mData; } + __hostdev__ uint32_t id() const { return mData; } + __hostdev__ uint32_t getMajor() const { return (mData >> 21) & ((1u << 11) - 1); } + __hostdev__ uint32_t getMinor() const { return (mData >> 10) & ((1u << 11) - 1); } + __hostdev__ uint32_t getPatch() const { return mData & ((1u << 10) - 1); } + __hostdev__ bool isCompatible() const { return this->getMajor() == uint32_t(NANOVDB_MAJOR_VERSION_NUMBER);} + /// @brief Check the major version of this instance relative to NANOVDB_MAJOR_VERSION_NUMBER + /// @return return 0 if the major version equals NANOVDB_MAJOR_VERSION_NUMBER, else a negative age if it is + /// older, i.e. smaller, and a positive age if it's newer, i.e.e larger. + __hostdev__ int age() const {return int(this->getMajor()) - int(NANOVDB_MAJOR_VERSION_NUMBER);} #ifndef __CUDACC_RTC__ const char* c_str() const { - char *buffer = (char*)malloc(4 + 1 + 4 + 1 + 4 + 1);// xxxx.xxxx.xxxx\0 - snprintf(buffer, 4 + 1 + 4 + 1 + 4 + 1, "%d.%d.%d", this->getMajor(), this->getMinor(), this->getPatch()); // Prevents overflows by enforcing a fixed size of buffer + char* buffer = (char*)malloc(4 + 1 + 4 + 1 + 4 + 1); // xxxx.xxxx.xxxx\0 + snprintf(buffer, 4 + 1 + 4 + 1 + 4 + 1, "%u.%u.%u", this->getMajor(), this->getMinor(), this->getPatch()); // Prevents overflows by enforcing a fixed size of buffer return buffer; } #endif -};// Version +}; // Version // ----------------------------> Various math functions <------------------------------------- +//@{ +/// @brief Pi constant taken from Boost to match old behaviour +template +inline __hostdev__ constexpr T pi() +{ + return 3.141592653589793238462643383279502884e+00; +} +template<> +inline __hostdev__ constexpr float pi() +{ + return 3.141592653589793238462643383279502884e+00F; +} +template<> +inline __hostdev__ constexpr double pi() +{ + return 3.141592653589793238462643383279502884e+00; +} +template<> +inline __hostdev__ constexpr long double pi() +{ + return 3.141592653589793238462643383279502884e+00L; +} +//@} + //@{ /// Tolerance for floating-point comparison template @@ -727,7 +1058,7 @@ struct Maximum template<> struct Maximum { - __hostdev__ static uint32_t value() { return 4294967295; } + __hostdev__ static uint32_t value() { return 4294967295u; } }; template<> struct Maximum @@ -859,7 +1190,7 @@ __hostdev__ inline T Abs(T x) template<> __hostdev__ inline float Abs(float x) { - return fabs(x); + return fabsf(x); } template<> @@ -910,8 +1241,11 @@ __hostdev__ inline double Sqrt(double x) //@} /// Return the sign of the given value as an integer (either -1, 0 or 1). -template -__hostdev__ inline T Sign(const T &x) { return ((T(0) < x)?T(1):T(0)) - ((x < T(0))?T(1):T(0)); } +template +__hostdev__ inline T Sign(const T& x) +{ + return ((T(0) < x) ? T(1) : T(0)) - ((x < T(0)) ? T(1) : T(0)); +} template __hostdev__ inline int MinIndex(const Vec3T& v) @@ -960,7 +1294,8 @@ __hostdev__ inline uint64_t AlignUp(uint64_t byteCount) // ------------------------------> Coord <-------------------------------------- // forward declaration so we can define Coord::asVec3s and Coord::asVec3d -template class Vec3; +template +class Vec3; /// @brief Signed (i, j, k) 32-bit integer coordinate class, similar to openvdb::math::Coord class Coord @@ -988,7 +1323,7 @@ class Coord { } - __hostdev__ Coord(ValueType *ptr) + __hostdev__ Coord(ValueType* ptr) : mVec{ptr[0], ptr[1], ptr[2]} { } @@ -1015,9 +1350,9 @@ class Coord /// @warning The argument is assumed to be 0, 1, or 2. __hostdev__ ValueType& operator[](IndexType i) { return mVec[i]; } - /// @brief Assignment operator that works with openvdb::Coord - template - __hostdev__ Coord& operator=(const CoordT &other) + /// @brief Assignment operator that works with openvdb::Coord + template + __hostdev__ Coord& operator=(const CoordT& other) { static_assert(sizeof(Coord) == sizeof(CoordT), "Mis-matched sizeof"); mVec[0] = other[0]; @@ -1038,12 +1373,26 @@ class Coord /// @brief Return true if this Coord is lexicographically less than the given Coord. __hostdev__ bool operator<(const Coord& rhs) const { - return mVec[0] < rhs[0] ? true : mVec[0] > rhs[0] ? false : mVec[1] < rhs[1] ? true : mVec[1] > rhs[1] ? false : mVec[2] < rhs[2] ? true : false; + return mVec[0] < rhs[0] ? true + : mVec[0] > rhs[0] ? false + : mVec[1] < rhs[1] ? true + : mVec[1] > rhs[1] ? false + : mVec[2] < rhs[2] ? true : false; + } + + /// @brief Return true if this Coord is lexicographically less or equal to the given Coord. + __hostdev__ bool operator<=(const Coord& rhs) const + { + return mVec[0] < rhs[0] ? true + : mVec[0] > rhs[0] ? false + : mVec[1] < rhs[1] ? true + : mVec[1] > rhs[1] ? false + : mVec[2] <=rhs[2] ? true : false; } // @brief Return true if the Coord components are identical. - __hostdev__ bool operator==(const Coord& rhs) const { return mVec[0] == rhs[0] && mVec[1] == rhs[1] && mVec[2] == rhs[2]; } - __hostdev__ bool operator!=(const Coord& rhs) const { return mVec[0] != rhs[0] || mVec[1] != rhs[1] || mVec[2] != rhs[2]; } + __hostdev__ bool operator==(const Coord& rhs) const { return mVec[0] == rhs[0] && mVec[1] == rhs[1] && mVec[2] == rhs[2]; } + __hostdev__ bool operator!=(const Coord& rhs) const { return mVec[0] != rhs[0] || mVec[1] != rhs[1] || mVec[2] != rhs[2]; } __hostdev__ Coord& operator&=(int n) { mVec[0] &= n; @@ -1072,8 +1421,9 @@ class Coord mVec[2] += n; return *this; } - __hostdev__ Coord operator+(const Coord& rhs) const { return Coord(mVec[0] + rhs[0], mVec[1] + rhs[1], mVec[2] + rhs[2]); } - __hostdev__ Coord operator-(const Coord& rhs) const { return Coord(mVec[0] - rhs[0], mVec[1] - rhs[1], mVec[2] - rhs[2]); } + __hostdev__ Coord operator+(const Coord& rhs) const { return Coord(mVec[0] + rhs[0], mVec[1] + rhs[1], mVec[2] + rhs[2]); } + __hostdev__ Coord operator-(const Coord& rhs) const { return Coord(mVec[0] - rhs[0], mVec[1] - rhs[1], mVec[2] - rhs[2]); } + __hostdev__ Coord operator-() const { return Coord(-mVec[0], -mVec[1], -mVec[2]); } __hostdev__ Coord& operator+=(const Coord& rhs) { mVec[0] += rhs[0]; @@ -1112,6 +1462,22 @@ class Coord mVec[2] = other[2]; return *this; } +#if defined(__CUDACC__) // the following functions only run on the GPU! + __device__ inline Coord& minComponentAtomic(const Coord& other) + { + atomicMin(&mVec[0], other[0]); + atomicMin(&mVec[1], other[1]); + atomicMin(&mVec[2], other[2]); + return *this; + } + __device__ inline Coord& maxComponentAtomic(const Coord& other) + { + atomicMax(&mVec[0], other[0]); + atomicMax(&mVec[1], other[1]); + atomicMax(&mVec[2], other[2]); + return *this; + } +#endif __hostdev__ Coord offsetBy(ValueType dx, ValueType dy, ValueType dz) const { @@ -1133,28 +1499,32 @@ class Coord __hostdev__ static Coord Floor(const Vec3T& xyz) { return Coord(nanovdb::Floor(xyz[0]), nanovdb::Floor(xyz[1]), nanovdb::Floor(xyz[2])); } /// @brief Return a hash key derived from the existing coordinates. - /// @details For details on this hash function please see the VDB paper. - /// The prime numbers are modified based on the ACM Transactions on Graphics paper: - /// "Real-time 3D reconstruction at scale using voxel hashing" + /// @details The hash function is originally taken from the SIGGRAPH paper: + /// "VDB: High-resolution sparse volumes with dynamic topology" + /// and the prime numbers are modified based on the ACM Transactions on Graphics paper: + /// "Real-time 3D reconstruction at scale using voxel hashing" (the second number had a typo!) template __hostdev__ uint32_t hash() const { return ((1 << Log2N) - 1) & (mVec[0] * 73856093 ^ mVec[1] * 19349669 ^ mVec[2] * 83492791); } /// @brief Return the octant of this Coord //__hostdev__ size_t octant() const { return (uint32_t(mVec[0])>>31) | ((uint32_t(mVec[1])>>31)<<1) | ((uint32_t(mVec[2])>>31)<<2); } - __hostdev__ uint8_t octant() const { return uint8_t((uint8_t(bool(mVec[0] & (1u << 31)))) | + __hostdev__ uint8_t octant() const { return (uint8_t(bool(mVec[0] & (1u << 31)))) | (uint8_t(bool(mVec[1] & (1u << 31))) << 1) | - (uint8_t(bool(mVec[2] & (1u << 31))) << 2)); } + (uint8_t(bool(mVec[2] & (1u << 31))) << 2); } /// @brief Return a single precision floating-point vector of this coordinate __hostdev__ inline Vec3 asVec3s() const; /// @brief Return a double precision floating-point vector of this coordinate __hostdev__ inline Vec3 asVec3d() const; + + // returns a copy of itself, so it mimics the behaviour of Vec3::round() + __hostdev__ inline Coord round() const { return *this; } }; // Coord class // ----------------------------> Vec3 <-------------------------------------- -/// @brief A simple vector class with three double components, similar to openvdb::math::Vec3 +/// @brief A simple vector class with three components, similar to openvdb::math::Vec3 template class Vec3 { @@ -1162,6 +1532,7 @@ class Vec3 public: static const int SIZE = 3; + static const int size = 3; // in openvdb::math::Tuple using ValueType = T; Vec3() = default; __hostdev__ explicit Vec3(T x) @@ -1172,6 +1543,12 @@ class Vec3 : mVec{x, y, z} { } + template class Vec3T, class T2> + __hostdev__ Vec3(const Vec3T& v) + : mVec{T(v[0]), T(v[1]), T(v[2])} + { + static_assert(Vec3T::size == size, "expected Vec3T::size==3!"); + } template __hostdev__ explicit Vec3(const Vec3& v) : mVec{T(v[0]), T(v[1]), T(v[2])} @@ -1183,16 +1560,17 @@ class Vec3 } __hostdev__ bool operator==(const Vec3& rhs) const { return mVec[0] == rhs[0] && mVec[1] == rhs[1] && mVec[2] == rhs[2]; } __hostdev__ bool operator!=(const Vec3& rhs) const { return mVec[0] != rhs[0] || mVec[1] != rhs[1] || mVec[2] != rhs[2]; } - template - __hostdev__ Vec3& operator=(const Vec3T& rhs) + template class Vec3T, class T2> + __hostdev__ Vec3& operator=(const Vec3T& rhs) { + static_assert(Vec3T::size == size, "expected Vec3T::size==3!"); mVec[0] = rhs[0]; mVec[1] = rhs[1]; mVec[2] = rhs[2]; return *this; } __hostdev__ const T& operator[](int i) const { return mVec[i]; } - __hostdev__ T& operator[](int i) { return mVec[i]; } + __hostdev__ T& operator[](int i) { return mVec[i]; } template __hostdev__ T dot(const Vec3T& v) const { return mVec[0] * v[0] + mVec[1] * v[1] + mVec[2] * v[2]; } template @@ -1206,14 +1584,16 @@ class Vec3 { return mVec[0] * mVec[0] + mVec[1] * mVec[1] + mVec[2] * mVec[2]; // 5 flops } - __hostdev__ T length() const { return Sqrt(this->lengthSqr()); } - __hostdev__ Vec3 operator-() const { return Vec3(-mVec[0], -mVec[1], -mVec[2]); } - __hostdev__ Vec3 operator*(const Vec3& v) const { return Vec3(mVec[0] * v[0], mVec[1] * v[1], mVec[2] * v[2]); } - __hostdev__ Vec3 operator/(const Vec3& v) const { return Vec3(mVec[0] / v[0], mVec[1] / v[1], mVec[2] / v[2]); } - __hostdev__ Vec3 operator+(const Vec3& v) const { return Vec3(mVec[0] + v[0], mVec[1] + v[1], mVec[2] + v[2]); } - __hostdev__ Vec3 operator-(const Vec3& v) const { return Vec3(mVec[0] - v[0], mVec[1] - v[1], mVec[2] - v[2]); } - __hostdev__ Vec3 operator*(const T& s) const { return Vec3(s * mVec[0], s * mVec[1], s * mVec[2]); } - __hostdev__ Vec3 operator/(const T& s) const { return (T(1) / s) * (*this); } + __hostdev__ T length() const { return Sqrt(this->lengthSqr()); } + __hostdev__ Vec3 operator-() const { return Vec3(-mVec[0], -mVec[1], -mVec[2]); } + __hostdev__ Vec3 operator*(const Vec3& v) const { return Vec3(mVec[0] * v[0], mVec[1] * v[1], mVec[2] * v[2]); } + __hostdev__ Vec3 operator/(const Vec3& v) const { return Vec3(mVec[0] / v[0], mVec[1] / v[1], mVec[2] / v[2]); } + __hostdev__ Vec3 operator+(const Vec3& v) const { return Vec3(mVec[0] + v[0], mVec[1] + v[1], mVec[2] + v[2]); } + __hostdev__ Vec3 operator-(const Vec3& v) const { return Vec3(mVec[0] - v[0], mVec[1] - v[1], mVec[2] - v[2]); } + __hostdev__ Vec3 operator+(const Coord& ijk) const { return Vec3(mVec[0] + ijk[0], mVec[1] + ijk[1], mVec[2] + ijk[2]); } + __hostdev__ Vec3 operator-(const Coord& ijk) const { return Vec3(mVec[0] - ijk[0], mVec[1] - ijk[1], mVec[2] - ijk[2]); } + __hostdev__ Vec3 operator*(const T& s) const { return Vec3(s * mVec[0], s * mVec[1], s * mVec[2]); } + __hostdev__ Vec3 operator/(const T& s) const { return (T(1) / s) * (*this); } __hostdev__ Vec3& operator+=(const Vec3& v) { mVec[0] += v[0]; @@ -1221,6 +1601,13 @@ class Vec3 mVec[2] += v[2]; return *this; } + __hostdev__ Vec3& operator+=(const Coord& ijk) + { + mVec[0] += T(ijk[0]); + mVec[1] += T(ijk[1]); + mVec[2] += T(ijk[2]); + return *this; + } __hostdev__ Vec3& operator-=(const Vec3& v) { mVec[0] -= v[0]; @@ -1228,6 +1615,13 @@ class Vec3 mVec[2] -= v[2]; return *this; } + __hostdev__ Vec3& operator-=(const Coord& ijk) + { + mVec[0] -= T(ijk[0]); + mVec[1] -= T(ijk[1]); + mVec[2] -= T(ijk[2]); + return *this; + } __hostdev__ Vec3& operator*=(const T& s) { mVec[0] *= s; @@ -1270,9 +1664,29 @@ class Vec3 { return mVec[0] > mVec[1] ? (mVec[0] > mVec[2] ? mVec[0] : mVec[2]) : (mVec[1] > mVec[2] ? mVec[1] : mVec[2]); } + /// @brief Round each component if this Vec up to its integer value + /// @return Return an integer Coord __hostdev__ Coord floor() const { return Coord(Floor(mVec[0]), Floor(mVec[1]), Floor(mVec[2])); } + /// @brief Round each component if this Vec down to its integer value + /// @return Return an integer Coord __hostdev__ Coord ceil() const { return Coord(Ceil(mVec[0]), Ceil(mVec[1]), Ceil(mVec[2])); } - __hostdev__ Coord round() const { return Coord(Floor(mVec[0] + 0.5), Floor(mVec[1] + 0.5), Floor(mVec[2] + 0.5)); } + /// @brief Round each component if this Vec to its closest integer value + /// @return Return an integer Coord + __hostdev__ Coord round() const + { + if constexpr(is_same::value) { + return Coord(Floor(mVec[0] + 0.5f), Floor(mVec[1] + 0.5f), Floor(mVec[2] + 0.5f)); + } else if constexpr(is_same::value) { + return Coord(mVec[0], mVec[1], mVec[2]); + } else { + return Coord(Floor(mVec[0] + 0.5), Floor(mVec[1] + 0.5), Floor(mVec[2] + 0.5)); + } + } + + /// @brief return a non-const raw constant pointer to array of three vector components + __hostdev__ T* asPointer() { return mVec; } + /// @brief return a const raw constant pointer to array of three vector components + __hostdev__ const T* asPointer() const { return mVec; } }; // Vec3 template @@ -1286,20 +1700,29 @@ __hostdev__ inline Vec3 operator/(T1 scalar, const Vec3& vec) return Vec3(scalar / vec[0], scalar / vec[1], scalar / vec[2]); } -using Vec3R = Vec3; +//using Vec3R = Vec3;// deprecated using Vec3d = Vec3; using Vec3f = Vec3; -using Vec3i = Vec3; +using Vec3i = Vec3; +using Vec3u = Vec3; +using Vec3u8 = Vec3; +using Vec3u16 = Vec3; /// @brief Return a single precision floating-point vector of this coordinate -__hostdev__ inline Vec3f Coord::asVec3s() const { return Vec3f(float(mVec[0]), float(mVec[1]), float(mVec[2])); } +__hostdev__ inline Vec3f Coord::asVec3s() const +{ + return Vec3f(float(mVec[0]), float(mVec[1]), float(mVec[2])); +} /// @brief Return a double precision floating-point vector of this coordinate -__hostdev__ inline Vec3d Coord::asVec3d() const { return Vec3d(double(mVec[0]), double(mVec[1]), double(mVec[2])); } +__hostdev__ inline Vec3d Coord::asVec3d() const +{ + return Vec3d(double(mVec[0]), double(mVec[1]), double(mVec[2])); +} // ----------------------------> Vec4 <-------------------------------------- -/// @brief A simple vector class with three double components, similar to openvdb::math::Vec4 +/// @brief A simple vector class with four components, similar to openvdb::math::Vec4 template class Vec4 { @@ -1307,6 +1730,7 @@ class Vec4 public: static const int SIZE = 4; + static const int size = 4; using ValueType = T; Vec4() = default; __hostdev__ explicit Vec4(T x) @@ -1322,33 +1746,41 @@ class Vec4 : mVec{T(v[0]), T(v[1]), T(v[2]), T(v[3])} { } + template class Vec4T, class T2> + __hostdev__ Vec4(const Vec4T& v) + : mVec{T(v[0]), T(v[1]), T(v[2]), T(v[3])} + { + static_assert(Vec4T::size == size, "expected Vec4T::size==4!"); + } __hostdev__ bool operator==(const Vec4& rhs) const { return mVec[0] == rhs[0] && mVec[1] == rhs[1] && mVec[2] == rhs[2] && mVec[3] == rhs[3]; } __hostdev__ bool operator!=(const Vec4& rhs) const { return mVec[0] != rhs[0] || mVec[1] != rhs[1] || mVec[2] != rhs[2] || mVec[3] != rhs[3]; } - template - __hostdev__ Vec4& operator=(const Vec4T& rhs) + template class Vec4T, class T2> + __hostdev__ Vec4& operator=(const Vec4T& rhs) { + static_assert(Vec4T::size == size, "expected Vec4T::size==4!"); mVec[0] = rhs[0]; mVec[1] = rhs[1]; mVec[2] = rhs[2]; mVec[3] = rhs[3]; return *this; } + __hostdev__ const T& operator[](int i) const { return mVec[i]; } - __hostdev__ T& operator[](int i) { return mVec[i]; } + __hostdev__ T& operator[](int i) { return mVec[i]; } template __hostdev__ T dot(const Vec4T& v) const { return mVec[0] * v[0] + mVec[1] * v[1] + mVec[2] * v[2] + mVec[3] * v[3]; } __hostdev__ T lengthSqr() const { return mVec[0] * mVec[0] + mVec[1] * mVec[1] + mVec[2] * mVec[2] + mVec[3] * mVec[3]; // 7 flops } - __hostdev__ T length() const { return Sqrt(this->lengthSqr()); } - __hostdev__ Vec4 operator-() const { return Vec4(-mVec[0], -mVec[1], -mVec[2], -mVec[3]); } - __hostdev__ Vec4 operator*(const Vec4& v) const { return Vec4(mVec[0] * v[0], mVec[1] * v[1], mVec[2] * v[2], mVec[3] * v[3]); } - __hostdev__ Vec4 operator/(const Vec4& v) const { return Vec4(mVec[0] / v[0], mVec[1] / v[1], mVec[2] / v[2], mVec[3] / v[3]); } - __hostdev__ Vec4 operator+(const Vec4& v) const { return Vec4(mVec[0] + v[0], mVec[1] + v[1], mVec[2] + v[2], mVec[3] + v[3]); } - __hostdev__ Vec4 operator-(const Vec4& v) const { return Vec4(mVec[0] - v[0], mVec[1] - v[1], mVec[2] - v[2], mVec[3] - v[3]); } - __hostdev__ Vec4 operator*(const T& s) const { return Vec4(s * mVec[0], s * mVec[1], s * mVec[2], s * mVec[3]); } - __hostdev__ Vec4 operator/(const T& s) const { return (T(1) / s) * (*this); } + __hostdev__ T length() const { return Sqrt(this->lengthSqr()); } + __hostdev__ Vec4 operator-() const { return Vec4(-mVec[0], -mVec[1], -mVec[2], -mVec[3]); } + __hostdev__ Vec4 operator*(const Vec4& v) const { return Vec4(mVec[0] * v[0], mVec[1] * v[1], mVec[2] * v[2], mVec[3] * v[3]); } + __hostdev__ Vec4 operator/(const Vec4& v) const { return Vec4(mVec[0] / v[0], mVec[1] / v[1], mVec[2] / v[2], mVec[3] / v[3]); } + __hostdev__ Vec4 operator+(const Vec4& v) const { return Vec4(mVec[0] + v[0], mVec[1] + v[1], mVec[2] + v[2], mVec[3] + v[3]); } + __hostdev__ Vec4 operator-(const Vec4& v) const { return Vec4(mVec[0] - v[0], mVec[1] - v[1], mVec[2] - v[2], mVec[3] - v[3]); } + __hostdev__ Vec4 operator*(const T& s) const { return Vec4(s * mVec[0], s * mVec[1], s * mVec[2], s * mVec[3]); } + __hostdev__ Vec4 operator/(const T& s) const { return (T(1) / s) * (*this); } __hostdev__ Vec4& operator+=(const Vec4& v) { mVec[0] += v[0]; @@ -1410,7 +1842,7 @@ __hostdev__ inline Vec4 operator*(T1 scalar, const Vec4& vec) return Vec4(scalar * vec[0], scalar * vec[1], scalar * vec[2], scalar * vec[3]); } template -__hostdev__ inline Vec4 operator/(T1 scalar, const Vec3& vec) +__hostdev__ inline Vec4 operator/(T1 scalar, const Vec4& vec) { return Vec4(scalar / vec[0], scalar / vec[1], scalar / vec[2], scalar / vec[3]); } @@ -1420,11 +1852,117 @@ using Vec4d = Vec4; using Vec4f = Vec4; using Vec4i = Vec4; + +// --------------------------> Rgba8 <------------------------------------ + +/// @brief 8-bit red, green, blue, alpha packed into 32 bit unsigned int +class Rgba8 +{ + union + { + uint8_t c[4]; // 4 integer color channels of red, green, blue and alpha components. + uint32_t packed; // 32 bit packed representation + } mData; + +public: + static const int SIZE = 4; + using ValueType = uint8_t; + + /// @brief Default copy constructor + Rgba8(const Rgba8&) = default; + + /// @brief Default move constructor + Rgba8(Rgba8&&) = default; + + /// @brief Default move assignment operator + /// @return non-const reference to this instance + Rgba8& operator=(Rgba8&&) = default; + + /// @brief Default copy assignment operator + /// @return non-const reference to this instance + Rgba8& operator=(const Rgba8&) = default; + + /// @brief Default ctor initializes all channels to zero + __hostdev__ Rgba8() + : mData{{0, 0, 0, 0}} + { + static_assert(sizeof(uint32_t) == sizeof(Rgba8), "Unexpected sizeof"); + } + + /// @brief integer r,g,b,a ctor where alpha channel defaults to opaque + /// @note all values should be in the range 0u to 255u + __hostdev__ Rgba8(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255u) + : mData{{r, g, b, a}} + { + } + + /// @brief @brief ctor where all channels are initialized to the same value + /// @note value should be in the range 0u to 255u + explicit __hostdev__ Rgba8(uint8_t v) + : mData{{v, v, v, v}} + { + } + + /// @brief floating-point r,g,b,a ctor where alpha channel defaults to opaque + /// @note all values should be in the range 0.0f to 1.0f + __hostdev__ Rgba8(float r, float g, float b, float a = 1.0f) + : mData{{static_cast(0.5f + r * 255.0f), // round floats to nearest integers + static_cast(0.5f + g * 255.0f), // double {{}} is needed due to union + static_cast(0.5f + b * 255.0f), + static_cast(0.5f + a * 255.0f)}} + { + } + + /// @brief Vec3f r,g,b ctor (alpha channel it set to 1) + /// @note all values should be in the range 0.0f to 1.0f + __hostdev__ Rgba8(const Vec3f& rgb) + : Rgba8(rgb[0], rgb[1], rgb[2]) + { + } + + /// @brief Vec4f r,g,b,a ctor + /// @note all values should be in the range 0.0f to 1.0f + __hostdev__ Rgba8(const Vec4f& rgba) + : Rgba8(rgba[0], rgba[1], rgba[2], rgba[3]) + { + } + + __hostdev__ bool operator< (const Rgba8& rhs) const { return mData.packed < rhs.mData.packed; } + __hostdev__ bool operator==(const Rgba8& rhs) const { return mData.packed == rhs.mData.packed; } + __hostdev__ float lengthSqr() const + { + return 0.0000153787005f * (float(mData.c[0]) * mData.c[0] + + float(mData.c[1]) * mData.c[1] + + float(mData.c[2]) * mData.c[2]); //1/255^2 + } + __hostdev__ float length() const { return sqrtf(this->lengthSqr()); } + /// @brief return n'th color channel as a float in the range 0 to 1 + __hostdev__ float asFloat(int n) const { return 0.003921569f*float(mData.c[n]); }// divide by 255 + __hostdev__ const uint8_t& operator[](int n) const { return mData.c[n]; } + __hostdev__ uint8_t& operator[](int n) { return mData.c[n]; } + __hostdev__ const uint32_t& packed() const { return mData.packed; } + __hostdev__ uint32_t& packed() { return mData.packed; } + __hostdev__ const uint8_t& r() const { return mData.c[0]; } + __hostdev__ const uint8_t& g() const { return mData.c[1]; } + __hostdev__ const uint8_t& b() const { return mData.c[2]; } + __hostdev__ const uint8_t& a() const { return mData.c[3]; } + __hostdev__ uint8_t& r() { return mData.c[0]; } + __hostdev__ uint8_t& g() { return mData.c[1]; } + __hostdev__ uint8_t& b() { return mData.c[2]; } + __hostdev__ uint8_t& a() { return mData.c[3]; } + __hostdev__ operator Vec3f() const { + return Vec3f(this->asFloat(0), this->asFloat(1), this->asFloat(2)); + } + __hostdev__ operator Vec4f() const { + return Vec4f(this->asFloat(0), this->asFloat(1), this->asFloat(2), this->asFloat(3)); + } +}; // Rgba8 + +using PackedRGBA8 = Rgba8; // for backwards compatibility + // ----------------------------> TensorTraits <-------------------------------------- -template::value || - is_specialization::value || - is_same::value) ? 1 : 0> +template::value || is_specialization::value || is_same::value) ? 1 : 0> struct TensorTraits; template @@ -1470,73 +2008,141 @@ struct FloatTraits }; template<> -struct FloatTraits// size of empty class in C++ is 1 byte and not 0 byte +struct FloatTraits // size of empty class in C++ is 1 byte and not 0 byte { using FloatType = uint64_t; }; template<> -struct FloatTraits// size of empty class in C++ is 1 byte and not 0 byte +struct FloatTraits // size of empty class in C++ is 1 byte and not 0 byte { - using FloatType = bool; + using FloatType = uint64_t; }; -// ----------------------------> mapping ValueType -> GridType <-------------------------------------- - -/// @brief Maps from a templated value type to a GridType enum -template +template<> +struct FloatTraits // size of empty class in C++ is 1 byte and not 0 byte +{ + using FloatType = uint64_t; +}; + +template<> +struct FloatTraits // size of empty class in C++ is 1 byte and not 0 byte +{ + using FloatType = uint64_t; +}; + +template<> +struct FloatTraits // size of empty class in C++ is 1 byte and not 0 byte +{ + using FloatType = bool; +}; + +template<> +struct FloatTraits // size of empty class in C++ is 1 byte and not 0 byte +{ + using FloatType = double; +}; + +// ----------------------------> mapping BuildType -> GridType <-------------------------------------- + +/// @brief Maps from a templated build type to a GridType enum +template __hostdev__ inline GridType mapToGridType() { - if (is_same::value) { // resolved at compile-time + if constexpr(is_same::value) { // resolved at compile-time return GridType::Float; - } else if (is_same::value) { + } else if constexpr(is_same::value) { return GridType::Double; - } else if (is_same::value) { + } else if constexpr(is_same::value) { return GridType::Int16; - } else if (is_same::value) { + } else if constexpr(is_same::value) { return GridType::Int32; - } else if (is_same::value) { + } else if constexpr(is_same::value) { return GridType::Int64; - } else if (is_same::value) { + } else if constexpr(is_same::value) { return GridType::Vec3f; - } else if (is_same::value) { + } else if constexpr(is_same::value) { return GridType::Vec3d; - } else if (is_same::value) { + } else if constexpr(is_same::value) { return GridType::UInt32; - } else if (is_same::value) { + } else if constexpr(is_same::value) { return GridType::Mask; - } else if (is_same::value) { + } else if constexpr(is_same::value) { + return GridType::Half; + } else if constexpr(is_same::value) { return GridType::Index; - } else if (is_same::value) { + } else if constexpr(is_same::value) { + return GridType::OnIndex; + } else if constexpr(is_same::value) { + return GridType::IndexMask; + } else if constexpr(is_same::value) { + return GridType::OnIndexMask; + } else if constexpr(is_same::value) { return GridType::Boolean; - } else if (is_same::value) { + } else if constexpr(is_same::value) { return GridType::RGBA8; } else if (is_same::value) { return GridType::Fp4; - } else if (is_same::value) { + } else if constexpr(is_same::value) { return GridType::Fp8; - } else if (is_same::value) { + } else if constexpr(is_same::value) { return GridType::Fp16; - } else if (is_same::value) { + } else if constexpr(is_same::value) { return GridType::FpN; - } else if (is_same::value) { + } else if constexpr(is_same::value) { return GridType::Vec4f; - } else if (is_same::value) { + } else if constexpr(is_same::value) { return GridType::Vec4d; + } else if (is_same::value) { + return GridType::PointIndex; + } else if constexpr(is_same::value) { + return GridType::Vec3u8; + } else if constexpr(is_same::value) { + return GridType::Vec3u16; } return GridType::Unknown; } +// ----------------------------> mapping BuildType -> GridClass <-------------------------------------- + +/// @brief Maps from a templated build type to a GridClass enum +template +__hostdev__ inline GridClass mapToGridClass(GridClass defaultClass = GridClass::Unknown) +{ + if (is_same::value) { + return GridClass::Topology; + } else if (BuildTraits::is_index) { + return GridClass::IndexGrid; + } else if (is_same::value) { + return GridClass::VoxelVolume; + } else if (is_same::value) { + return GridClass::PointIndex; + } + return defaultClass; +} + // ----------------------------> matMult <-------------------------------------- +/// @brief Multiply a 3x3 matrix and a 3d vector using 32bit floating point arithmetics +/// @note This corresponds to a linear mapping, e.g. scaling, rotation etc. +/// @tparam Vec3T Template type of the input and output 3d vectors +/// @param mat pointer to an array of floats with the 3x3 matrix +/// @param xyz input vector to be multiplied by the matrix +/// @return result of matrix-vector multiplication, i.e. mat x xyz template __hostdev__ inline Vec3T matMult(const float* mat, const Vec3T& xyz) { - return Vec3T(fmaf(xyz[0], mat[0], fmaf(xyz[1], mat[1], xyz[2] * mat[2])), - fmaf(xyz[0], mat[3], fmaf(xyz[1], mat[4], xyz[2] * mat[5])), - fmaf(xyz[0], mat[6], fmaf(xyz[1], mat[7], xyz[2] * mat[8]))); // 6 fmaf + 3 mult = 9 flops + return Vec3T(fmaf(static_cast(xyz[0]), mat[0], fmaf(static_cast(xyz[1]), mat[1], static_cast(xyz[2]) * mat[2])), + fmaf(static_cast(xyz[0]), mat[3], fmaf(static_cast(xyz[1]), mat[4], static_cast(xyz[2]) * mat[5])), + fmaf(static_cast(xyz[0]), mat[6], fmaf(static_cast(xyz[1]), mat[7], static_cast(xyz[2]) * mat[8]))); // 6 fmaf + 3 mult = 9 flops } +/// @brief Multiply a 3x3 matrix and a 3d vector using 64bit floating point arithmetics +/// @note This corresponds to a linear mapping, e.g. scaling, rotation etc. +/// @tparam Vec3T Template type of the input and output 3d vectors +/// @param mat pointer to an array of floats with the 3x3 matrix +/// @param xyz input vector to be multiplied by the matrix +/// @return result of matrix-vector multiplication, i.e. mat x xyz template __hostdev__ inline Vec3T matMult(const double* mat, const Vec3T& xyz) { @@ -1545,14 +2151,28 @@ __hostdev__ inline Vec3T matMult(const double* mat, const Vec3T& xyz) fma(static_cast(xyz[0]), mat[6], fma(static_cast(xyz[1]), mat[7], static_cast(xyz[2]) * mat[8]))); // 6 fmaf + 3 mult = 9 flops } +/// @brief Multiply a 3x3 matrix to a 3d vector and add another 3d vector using 32bit floating point arithmetics +/// @note This corresponds to an affine transformation, i.e a linear mapping followed by a translation. e.g. scale/rotation and translation +/// @tparam Vec3T Template type of the input and output 3d vectors +/// @param mat pointer to an array of floats with the 3x3 matrix +/// @param vec 3d vector to be added AFTER the matrix multiplication +/// @param xyz input vector to be multiplied by the matrix and a translated by @c vec +/// @return result of affine transformation, i.e. (mat x xyz) + vec template __hostdev__ inline Vec3T matMult(const float* mat, const float* vec, const Vec3T& xyz) { - return Vec3T(fmaf(xyz[0], mat[0], fmaf(xyz[1], mat[1], fmaf(xyz[2], mat[2], vec[0]))), - fmaf(xyz[0], mat[3], fmaf(xyz[1], mat[4], fmaf(xyz[2], mat[5], vec[1]))), - fmaf(xyz[0], mat[6], fmaf(xyz[1], mat[7], fmaf(xyz[2], mat[8], vec[2])))); // 9 fmaf = 9 flops + return Vec3T(fmaf(static_cast(xyz[0]), mat[0], fmaf(static_cast(xyz[1]), mat[1], fmaf(static_cast(xyz[2]), mat[2], vec[0]))), + fmaf(static_cast(xyz[0]), mat[3], fmaf(static_cast(xyz[1]), mat[4], fmaf(static_cast(xyz[2]), mat[5], vec[1]))), + fmaf(static_cast(xyz[0]), mat[6], fmaf(static_cast(xyz[1]), mat[7], fmaf(static_cast(xyz[2]), mat[8], vec[2])))); // 9 fmaf = 9 flops } +/// @brief Multiply a 3x3 matrix to a 3d vector and add another 3d vector using 64bit floating point arithmetics +/// @note This corresponds to an affine transformation, i.e a linear mapping followed by a translation. e.g. scale/rotation and translation +/// @tparam Vec3T Template type of the input and output 3d vectors +/// @param mat pointer to an array of floats with the 3x3 matrix +/// @param vec 3d vector to be added AFTER the matrix multiplication +/// @param xyz input vector to be multiplied by the matrix and a translated by @c vec +/// @return result of affine transformation, i.e. (mat x xyz) + vec template __hostdev__ inline Vec3T matMult(const double* mat, const double* vec, const Vec3T& xyz) { @@ -1561,16 +2181,26 @@ __hostdev__ inline Vec3T matMult(const double* mat, const double* vec, const Vec fma(static_cast(xyz[0]), mat[6], fma(static_cast(xyz[1]), mat[7], fma(static_cast(xyz[2]), mat[8], vec[2])))); // 9 fma = 9 flops } -// matMultT: Multiply with the transpose: - +/// @brief Multiply the transposed of a 3x3 matrix and a 3d vector using 32bit floating point arithmetics +/// @note This corresponds to an inverse linear mapping, e.g. inverse scaling, inverse rotation etc. +/// @tparam Vec3T Template type of the input and output 3d vectors +/// @param mat pointer to an array of floats with the 3x3 matrix +/// @param xyz input vector to be multiplied by the transposed matrix +/// @return result of matrix-vector multiplication, i.e. mat^T x xyz template __hostdev__ inline Vec3T matMultT(const float* mat, const Vec3T& xyz) { - return Vec3T(fmaf(xyz[0], mat[0], fmaf(xyz[1], mat[3], xyz[2] * mat[6])), - fmaf(xyz[0], mat[1], fmaf(xyz[1], mat[4], xyz[2] * mat[7])), - fmaf(xyz[0], mat[2], fmaf(xyz[1], mat[5], xyz[2] * mat[8]))); // 6 fmaf + 3 mult = 9 flops + return Vec3T(fmaf(static_cast(xyz[0]), mat[0], fmaf(static_cast(xyz[1]), mat[3], static_cast(xyz[2]) * mat[6])), + fmaf(static_cast(xyz[0]), mat[1], fmaf(static_cast(xyz[1]), mat[4], static_cast(xyz[2]) * mat[7])), + fmaf(static_cast(xyz[0]), mat[2], fmaf(static_cast(xyz[1]), mat[5], static_cast(xyz[2]) * mat[8]))); // 6 fmaf + 3 mult = 9 flops } +/// @brief Multiply the transposed of a 3x3 matrix and a 3d vector using 64bit floating point arithmetics +/// @note This corresponds to an inverse linear mapping, e.g. inverse scaling, inverse rotation etc. +/// @tparam Vec3T Template type of the input and output 3d vectors +/// @param mat pointer to an array of floats with the 3x3 matrix +/// @param xyz input vector to be multiplied by the transposed matrix +/// @return result of matrix-vector multiplication, i.e. mat^T x xyz template __hostdev__ inline Vec3T matMultT(const double* mat, const Vec3T& xyz) { @@ -1582,9 +2212,9 @@ __hostdev__ inline Vec3T matMultT(const double* mat, const Vec3T& xyz) template __hostdev__ inline Vec3T matMultT(const float* mat, const float* vec, const Vec3T& xyz) { - return Vec3T(fmaf(xyz[0], mat[0], fmaf(xyz[1], mat[3], fmaf(xyz[2], mat[6], vec[0]))), - fmaf(xyz[0], mat[1], fmaf(xyz[1], mat[4], fmaf(xyz[2], mat[7], vec[1]))), - fmaf(xyz[0], mat[2], fmaf(xyz[1], mat[5], fmaf(xyz[2], mat[8], vec[2])))); // 9 fmaf = 9 flops + return Vec3T(fmaf(static_cast(xyz[0]), mat[0], fmaf(static_cast(xyz[1]), mat[3], fmaf(static_cast(xyz[2]), mat[6], vec[0]))), + fmaf(static_cast(xyz[0]), mat[1], fmaf(static_cast(xyz[1]), mat[4], fmaf(static_cast(xyz[2]), mat[7], vec[1]))), + fmaf(static_cast(xyz[0]), mat[2], fmaf(static_cast(xyz[1]), mat[5], fmaf(static_cast(xyz[2]), mat[8], vec[2])))); // 9 fmaf = 9 flops } template @@ -1601,22 +2231,22 @@ __hostdev__ inline Vec3T matMultT(const double* mat, const double* vec, const Ve template struct BaseBBox { - Vec3T mCoord[2]; - __hostdev__ bool operator==(const BaseBBox& rhs) const { return mCoord[0] == rhs.mCoord[0] && mCoord[1] == rhs.mCoord[1]; }; - __hostdev__ bool operator!=(const BaseBBox& rhs) const { return mCoord[0] != rhs.mCoord[0] || mCoord[1] != rhs.mCoord[1]; }; + Vec3T mCoord[2]; + __hostdev__ bool operator==(const BaseBBox& rhs) const { return mCoord[0] == rhs.mCoord[0] && mCoord[1] == rhs.mCoord[1]; }; + __hostdev__ bool operator!=(const BaseBBox& rhs) const { return mCoord[0] != rhs.mCoord[0] || mCoord[1] != rhs.mCoord[1]; }; __hostdev__ const Vec3T& operator[](int i) const { return mCoord[i]; } - __hostdev__ Vec3T& operator[](int i) { return mCoord[i]; } - __hostdev__ Vec3T& min() { return mCoord[0]; } - __hostdev__ Vec3T& max() { return mCoord[1]; } + __hostdev__ Vec3T& operator[](int i) { return mCoord[i]; } + __hostdev__ Vec3T& min() { return mCoord[0]; } + __hostdev__ Vec3T& max() { return mCoord[1]; } __hostdev__ const Vec3T& min() const { return mCoord[0]; } __hostdev__ const Vec3T& max() const { return mCoord[1]; } - __hostdev__ Coord& translate(const Vec3T& xyz) + __hostdev__ BaseBBox& translate(const Vec3T& xyz) { mCoord[0] += xyz; mCoord[1] += xyz; return *this; } - // @brief Expand this bounding box to enclose point (i, j, k). + /// @brief Expand this bounding box to enclose point @c xyz. __hostdev__ BaseBBox& expand(const Vec3T& xyz) { mCoord[0].minComponent(xyz); @@ -1624,11 +2254,19 @@ struct BaseBBox return *this; } + /// @brief Expand this bounding box to enclose the given bounding box. + __hostdev__ BaseBBox& expand(const BaseBBox& bbox) + { + mCoord[0].minComponent(bbox[0]); + mCoord[1].maxComponent(bbox[1]); + return *this; + } + /// @brief Intersect this bounding box with the given bounding box. __hostdev__ BaseBBox& intersect(const BaseBBox& bbox) { - mCoord[0].maxComponent(bbox.min()); - mCoord[1].minComponent(bbox.max()); + mCoord[0].maxComponent(bbox[0]); + mCoord[1].minComponent(bbox[1]); return *this; } @@ -1668,6 +2306,7 @@ struct BBox : public BaseBBox static_assert(is_floating_point::value, "Expected a floating point coordinate type"); using BaseT = BaseBBox; using BaseT::mCoord; + /// @brief Default construction sets BBox to an empty bbox __hostdev__ BBox() : BaseT(Vec3T( Maximum::value()), Vec3T(-Maximum::value())) @@ -1682,23 +2321,29 @@ struct BBox : public BaseBBox Vec3T(ValueType(max[0] + 1), ValueType(max[1] + 1), ValueType(max[2] + 1))) { } - __hostdev__ static BBox createCube(const Coord& min, typename Coord::ValueType dim) + __hostdev__ static BBox createCube(const Coord& min, typename Coord::ValueType dim) { return BBox(min, min.offsetBy(dim)); } - __hostdev__ BBox(const BaseBBox& bbox) : BBox(bbox[0], bbox[1]) {} + __hostdev__ BBox(const BaseBBox& bbox) + : BBox(bbox[0], bbox[1]) + { + } __hostdev__ bool empty() const { return mCoord[0][0] >= mCoord[1][0] || mCoord[0][1] >= mCoord[1][1] || mCoord[0][2] >= mCoord[1][2]; } - __hostdev__ Vec3T dim() const { return this->empty() ? Vec3T(0) : this->max() - this->min(); } + __hostdev__ operator bool() const { return mCoord[0][0] < mCoord[1][0] && + mCoord[0][1] < mCoord[1][1] && + mCoord[0][2] < mCoord[1][2]; } + __hostdev__ Vec3T dim() const { return *this ? this->max() - this->min() : Vec3T(0); } __hostdev__ bool isInside(const Vec3T& p) const { return p[0] > mCoord[0][0] && p[1] > mCoord[0][1] && p[2] > mCoord[0][2] && p[0] < mCoord[1][0] && p[1] < mCoord[1][1] && p[2] < mCoord[1][2]; } -};// BBox +}; // BBox /// @brief Partial template specialization for integer coordinate types /// @@ -1716,23 +2361,29 @@ struct BBox : public BaseBBox { const BBox& mBBox; CoordT mPos; + public: __hostdev__ Iterator(const BBox& b) : mBBox(b) , mPos(b.min()) { } + __hostdev__ Iterator(const BBox& b, const Coord& p) + : mBBox(b) + , mPos(p) + { + } __hostdev__ Iterator& operator++() { - if (mPos[2] < mBBox[1][2]) {// this is the most common case - ++mPos[2]; + if (mPos[2] < mBBox[1][2]) { // this is the most common case + ++mPos[2];// increment z } else if (mPos[1] < mBBox[1][1]) { - mPos[2] = mBBox[0][2]; - ++mPos[1]; + mPos[2] = mBBox[0][2];// reset z + ++mPos[1];// increment y } else if (mPos[0] <= mBBox[1][0]) { - mPos[2] = mBBox[0][2]; - mPos[1] = mBBox[0][1]; - ++mPos[0]; + mPos[2] = mBBox[0][2];// reset z + mPos[1] = mBBox[0][1];// reset y + ++mPos[0];// increment x } return *this; } @@ -1742,11 +2393,32 @@ struct BBox : public BaseBBox ++(*this); return tmp; } + __hostdev__ bool operator==(const Iterator& rhs) const + { + NANOVDB_ASSERT(mBBox == rhs.mBBox); + return mPos == rhs.mPos; + } + __hostdev__ bool operator!=(const Iterator& rhs) const + { + NANOVDB_ASSERT(mBBox == rhs.mBBox); + return mPos != rhs.mPos; + } + __hostdev__ bool operator<(const Iterator& rhs) const + { + NANOVDB_ASSERT(mBBox == rhs.mBBox); + return mPos < rhs.mPos; + } + __hostdev__ bool operator<=(const Iterator& rhs) const + { + NANOVDB_ASSERT(mBBox == rhs.mBBox); + return mPos <= rhs.mPos; + } /// @brief Return @c true if the iterator still points to a valid coordinate. - __hostdev__ operator bool() const { return mPos[0] <= mBBox[1][0]; } + __hostdev__ operator bool() const { return mPos <= mBBox[1]; } __hostdev__ const CoordT& operator*() const { return mPos; } }; // Iterator __hostdev__ Iterator begin() const { return Iterator{*this}; } + __hostdev__ Iterator end() const { return Iterator{*this, CoordT(mCoord[1][0]+1, mCoord[0][1], mCoord[0][2])}; } __hostdev__ BBox() : BaseT(CoordT::max(), CoordT::min()) { @@ -1766,23 +2438,36 @@ struct BBox : public BaseBBox other.mCoord[0][n] = mCoord[1][n] + 1; } - __hostdev__ static BBox createCube(const CoordT& min, typename CoordT::ValueType dim) + __hostdev__ static BBox createCube(const CoordT& min, typename CoordT::ValueType dim) { return BBox(min, min.offsetBy(dim - 1)); } + __hostdev__ static BBox createCube(typename CoordT::ValueType min, typename CoordT::ValueType max) + { + return BBox(CoordT(min), CoordT(max)); + } + __hostdev__ bool is_divisible() const { return mCoord[0][0] < mCoord[1][0] && mCoord[0][1] < mCoord[1][1] && mCoord[0][2] < mCoord[1][2]; } - /// @brief Return true if this bounding box is empty, i.e. uninitialized - __hostdev__ bool empty() const { return mCoord[0][0] > mCoord[1][0] || - mCoord[0][1] > mCoord[1][1] || - mCoord[0][2] > mCoord[1][2]; } - __hostdev__ CoordT dim() const { return this->empty() ? Coord(0) : this->max() - this->min() + Coord(1); } - __hostdev__ uint64_t volume() const { auto d = this->dim(); return uint64_t(d[0])*uint64_t(d[1])*uint64_t(d[2]); } - __hostdev__ bool isInside(const CoordT& p) const { return !(CoordT::lessThan(p, this->min()) || CoordT::lessThan(this->max(), p)); } + /// @brief Return true if this bounding box is empty, e.g. uninitialized + __hostdev__ bool empty() const { return mCoord[0][0] > mCoord[1][0] || + mCoord[0][1] > mCoord[1][1] || + mCoord[0][2] > mCoord[1][2]; } + /// @brief Convert this BBox to boolean true if it is not empty + __hostdev__ operator bool() const { return mCoord[0][0] <= mCoord[1][0] && + mCoord[0][1] <= mCoord[1][1] && + mCoord[0][2] <= mCoord[1][2]; } + __hostdev__ CoordT dim() const { return *this ? this->max() - this->min() + Coord(1) : Coord(0); } + __hostdev__ uint64_t volume() const + { + auto d = this->dim(); + return uint64_t(d[0]) * uint64_t(d[1]) * uint64_t(d[2]); + } + __hostdev__ bool isInside(const CoordT& p) const { return !(CoordT::lessThan(p, this->min()) || CoordT::lessThan(this->max(), p)); } /// @brief Return @c true if the given bounding box is inside this bounding box. - __hostdev__ bool isInside(const BBox& b) const + __hostdev__ bool isInside(const BBox& b) const { return !(CoordT::lessThan(b.min(), this->min()) || CoordT::lessThan(this->max(), b.max())); } @@ -1794,7 +2479,7 @@ struct BBox : public BaseBBox } /// @warning This converts a CoordBBox into a floating-point bounding box which implies that max += 1 ! - template + template __hostdev__ BBox> asReal() const { static_assert(is_floating_point::value, "CoordBBox::asReal: Expected a floating point coordinate"); @@ -1806,10 +2491,49 @@ struct BBox : public BaseBBox { return BBox(mCoord[0].offsetBy(-padding), mCoord[1].offsetBy(padding)); } -};// BBox + + /// @brief @brief transform this coordinate bounding box by the specified map + /// @param map mapping of index to world coordinates + /// @return world bounding box + template + __hostdev__ BBox transform(const Map& map) const + { + const Vec3d tmp = map.applyMap(Vec3d(mCoord[0][0], mCoord[0][1], mCoord[0][2])); + BBox bbox(tmp, tmp); + bbox.expand(map.applyMap(Vec3d(mCoord[0][0], mCoord[0][1], mCoord[1][2]))); + bbox.expand(map.applyMap(Vec3d(mCoord[0][0], mCoord[1][1], mCoord[0][2]))); + bbox.expand(map.applyMap(Vec3d(mCoord[1][0], mCoord[0][1], mCoord[0][2]))); + bbox.expand(map.applyMap(Vec3d(mCoord[1][0], mCoord[1][1], mCoord[0][2]))); + bbox.expand(map.applyMap(Vec3d(mCoord[1][0], mCoord[0][1], mCoord[1][2]))); + bbox.expand(map.applyMap(Vec3d(mCoord[0][0], mCoord[1][1], mCoord[1][2]))); + bbox.expand(map.applyMap(Vec3d(mCoord[1][0], mCoord[1][1], mCoord[1][2]))); + return bbox; + } + +#if defined(__CUDACC__) // the following functions only run on the GPU! + __device__ inline BBox& expandAtomic(const CoordT& ijk) + { + mCoord[0].minComponentAtomic(ijk); + mCoord[1].maxComponentAtomic(ijk); + return *this; + } + __device__ inline BBox& expandAtomic(const BBox& bbox) + { + mCoord[0].minComponentAtomic(bbox[0]); + mCoord[1].maxComponentAtomic(bbox[1]); + return *this; + } + __device__ inline BBox& intersectAtomic(const BBox& bbox) + { + mCoord[0].maxComponentAtomic(bbox[0]); + mCoord[1].minComponentAtomic(bbox[1]); + return *this; + } +#endif +}; // BBox using CoordBBox = BBox; -using BBoxR = BBox; +using BBoxR = BBox; // -------------------> Find lowest and highest bit in a word <---------------------------- @@ -1821,7 +2545,7 @@ __hostdev__ static inline uint32_t FindLowestOn(uint32_t v) { NANOVDB_ASSERT(v); #if (defined(__CUDA_ARCH__) || defined(__HIP__)) && defined(NANOVDB_USE_INTRINSICS) - return __ffs(v); + return __ffs(v) - 1; // one based indexing #elif defined(_MSC_VER) && defined(NANOVDB_USE_INTRINSICS) unsigned long index; _BitScanForward(&index, v); @@ -1829,7 +2553,7 @@ __hostdev__ static inline uint32_t FindLowestOn(uint32_t v) #elif (defined(__GNUC__) || defined(__clang__)) && defined(NANOVDB_USE_INTRINSICS) return static_cast(__builtin_ctzl(v)); #else -//#warning Using software implementation for FindLowestOn(uint32_t) + //NANO_WARNING("Using software implementation for FindLowestOn(uint32_t v)") static const unsigned char DeBruijn[32] = { 0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9}; // disable unary minus on unsigned warning @@ -1852,16 +2576,19 @@ NANOVDB_HOSTDEV_DISABLE_WARNING __hostdev__ static inline uint32_t FindHighestOn(uint32_t v) { NANOVDB_ASSERT(v); -#if defined(_MSC_VER) && defined(NANOVDB_USE_INTRINSICS) +#if (defined(__CUDA_ARCH__) || defined(__HIP__)) && defined(NANOVDB_USE_INTRINSICS) + return sizeof(uint32_t) * 8 - 1 - __clz(v); // Return the number of consecutive high-order zero bits in a 32-bit integer. +#elif defined(_MSC_VER) && defined(NANOVDB_USE_INTRINSICS) unsigned long index; _BitScanReverse(&index, v); return static_cast(index); #elif (defined(__GNUC__) || defined(__clang__)) && defined(NANOVDB_USE_INTRINSICS) return sizeof(unsigned long) * 8 - 1 - __builtin_clzl(v); #else -//#warning Using software implementation for FindHighestOn(uint32_t) + //NANO_WARNING("Using software implementation for FindHighestOn(uint32_t)") static const unsigned char DeBruijn[32] = { - 0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30, 8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31}; + 0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30, + 8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31}; v |= v >> 1; // first round down to one less than a power of 2 v |= v >> 2; v |= v >> 4; @@ -1879,7 +2606,7 @@ __hostdev__ static inline uint32_t FindLowestOn(uint64_t v) { NANOVDB_ASSERT(v); #if (defined(__CUDA_ARCH__) || defined(__HIP__)) && defined(NANOVDB_USE_INTRINSICS) - return __ffsll(static_cast(v)); + return __ffsll(static_cast(v)) - 1; // one based indexing #elif defined(_MSC_VER) && defined(NANOVDB_USE_INTRINSICS) unsigned long index; _BitScanForward64(&index, v); @@ -1887,7 +2614,7 @@ __hostdev__ static inline uint32_t FindLowestOn(uint64_t v) #elif (defined(__GNUC__) || defined(__clang__)) && defined(NANOVDB_USE_INTRINSICS) return static_cast(__builtin_ctzll(v)); #else -//#warning Using software implementation for FindLowestOn(uint64_t) + //NANO_WARNING("Using software implementation for FindLowestOn(uint64_t)") static const unsigned char DeBruijn[64] = { 0, 1, 2, 53, 3, 7, 54, 27, 4, 38, 41, 8, 34, 55, 48, 28, 62, 5, 39, 46, 44, 42, 22, 9, 24, 35, 59, 56, 49, 18, 29, 11, @@ -1914,7 +2641,9 @@ NANOVDB_HOSTDEV_DISABLE_WARNING __hostdev__ static inline uint32_t FindHighestOn(uint64_t v) { NANOVDB_ASSERT(v); -#if defined(_MSC_VER) && defined(NANOVDB_USE_INTRINSICS) +#if (defined(__CUDA_ARCH__) || defined(__HIP__)) && defined(NANOVDB_USE_INTRINSICS) + return sizeof(unsigned long) * 8 - 1 - __clzll(static_cast(v)); +#elif defined(_MSC_VER) && defined(NANOVDB_USE_INTRINSICS) unsigned long index; _BitScanReverse64(&index, v); return static_cast(index); @@ -1933,23 +2662,160 @@ NANOVDB_HOSTDEV_DISABLE_WARNING __hostdev__ inline uint32_t CountOn(uint64_t v) { #if (defined(__CUDA_ARCH__) || defined(__HIP__)) && defined(NANOVDB_USE_INTRINSICS) -//#warning Using popcll for CountOn + //#warning Using popcll for CountOn return __popcll(v); // __popcnt64 intrinsic support was added in VS 2019 16.8 #elif defined(_MSC_VER) && defined(_M_X64) && (_MSC_VER >= 1928) && defined(NANOVDB_USE_INTRINSICS) -//#warning Using popcnt64 for CountOn - return __popcnt64(v); + //#warning Using popcnt64 for CountOn + return uint32_t(__popcnt64(v)); #elif (defined(__GNUC__) || defined(__clang__)) && defined(NANOVDB_USE_INTRINSICS) -//#warning Using builtin_popcountll for CountOn + //#warning Using builtin_popcountll for CountOn return __builtin_popcountll(v); -#else// use software implementation -//#warning Using software implementation for CountOn +#else // use software implementation + //NANO_WARNING("Using software implementation for CountOn") v = v - ((v >> 1) & uint64_t(0x5555555555555555)); v = (v & uint64_t(0x3333333333333333)) + ((v >> 2) & uint64_t(0x3333333333333333)); return (((v + (v >> 4)) & uint64_t(0xF0F0F0F0F0F0F0F)) * uint64_t(0x101010101010101)) >> 56; #endif } +// ----------------------------> BitFlags <-------------------------------------- + +template +struct BitArray; +template<> +struct BitArray<8> +{ + uint8_t mFlags{0}; +}; +template<> +struct BitArray<16> +{ + uint16_t mFlags{0}; +}; +template<> +struct BitArray<32> +{ + uint32_t mFlags{0}; +}; +template<> +struct BitArray<64> +{ + uint64_t mFlags{0}; +}; + +template +class BitFlags : public BitArray +{ +protected: + using BitArray::mFlags; + +public: + using Type = decltype(mFlags); + BitFlags() {} + BitFlags(std::initializer_list list) + { + for (auto bit : list) + mFlags |= static_cast(1 << bit); + } + template + BitFlags(std::initializer_list list) + { + for (auto mask : list) + mFlags |= static_cast(mask); + } + __hostdev__ Type data() const { return mFlags; } + __hostdev__ Type& data() { return mFlags; } + __hostdev__ void initBit(std::initializer_list list) + { + mFlags = 0u; + for (auto bit : list) + mFlags |= static_cast(1 << bit); + } + template + __hostdev__ void initMask(std::initializer_list list) + { + mFlags = 0u; + for (auto mask : list) + mFlags |= static_cast(mask); + } + //__hostdev__ Type& data() { return mFlags; } + //__hostdev__ Type data() const { return mFlags; } + __hostdev__ Type getFlags() const { return mFlags & (static_cast(GridFlags::End) - 1u); } // mask out everything except relevant bits + + __hostdev__ void setOn() { mFlags = ~Type(0u); } + __hostdev__ void setOff() { mFlags = Type(0u); } + + __hostdev__ void setBitOn(uint8_t bit) { mFlags |= static_cast(1 << bit); } + __hostdev__ void setBitOff(uint8_t bit) { mFlags &= ~static_cast(1 << bit); } + + __hostdev__ void setBitOn(std::initializer_list list) + { + for (auto bit : list) + mFlags |= static_cast(1 << bit); + } + __hostdev__ void setBitOff(std::initializer_list list) + { + for (auto bit : list) + mFlags &= ~static_cast(1 << bit); + } + + template + __hostdev__ void setMaskOn(MaskT mask) { mFlags |= static_cast(mask); } + template + __hostdev__ void setMaskOff(MaskT mask) { mFlags &= ~static_cast(mask); } + + template + __hostdev__ void setMaskOn(std::initializer_list list) + { + for (auto mask : list) + mFlags |= static_cast(mask); + } + template + __hostdev__ void setMaskOff(std::initializer_list list) + { + for (auto mask : list) + mFlags &= ~static_cast(mask); + } + + __hostdev__ void setBit(uint8_t bit, bool on) { on ? this->setBitOn(bit) : this->setBitOff(bit); } + template + __hostdev__ void setMask(MaskT mask, bool on) { on ? this->setMaskOn(mask) : this->setMaskOff(mask); } + + __hostdev__ bool isOn() const { return mFlags == ~Type(0u); } + __hostdev__ bool isOff() const { return mFlags == Type(0u); } + __hostdev__ bool isBitOn(uint8_t bit) const { return 0 != (mFlags & static_cast(1 << bit)); } + __hostdev__ bool isBitOff(uint8_t bit) const { return 0 == (mFlags & static_cast(1 << bit)); } + template + __hostdev__ bool isMaskOn(MaskT mask) const { return 0 != (mFlags & static_cast(mask)); } + template + __hostdev__ bool isMaskOff(MaskT mask) const { return 0 == (mFlags & static_cast(mask)); } + /// @brief return true if any of the masks in the list are on + template + __hostdev__ bool isMaskOn(std::initializer_list list) const + { + for (auto mask : list) + if (0 != (mFlags & static_cast(mask))) + return true; + return false; + } + /// @brief return true if any of the masks in the list are off + template + __hostdev__ bool isMaskOff(std::initializer_list list) const + { + for (auto mask : list) + if (0 == (mFlags & static_cast(mask))) + return true; + return false; + } + /// @brief required for backwards compatibility + __hostdev__ BitFlags& operator=(Type n) + { + mFlags = n; + return *this; + } +}; // BitFlags + // ----------------------------> Mask <-------------------------------------- /// @brief Bit-mask to encode active states and facilitate sequential iterators @@ -1957,11 +2823,10 @@ __hostdev__ inline uint32_t CountOn(uint64_t v) template class Mask { +public: static constexpr uint32_t SIZE = 1U << (3 * LOG2DIM); // Number of bits in mask static constexpr uint32_t WORD_COUNT = SIZE >> 6; // Number of 64 bit words - uint64_t mWords[WORD_COUNT]; -public: /// @brief Return the memory footprint in bytes of this Mask __hostdev__ static size_t memUsage() { return sizeof(Mask); } @@ -1974,8 +2839,8 @@ class Mask /// @brief Return the total number of set bits in this Mask __hostdev__ uint32_t countOn() const { - uint32_t sum = 0, n = WORD_COUNT; - for (const uint64_t* w = mWords; n--; ++w) + uint32_t sum = 0; + for (const uint64_t *w = mWords, *q = w + WORD_COUNT; w != q; ++w) sum += CountOn(*w); return sum; } @@ -1983,21 +2848,30 @@ class Mask /// @brief Return the number of lower set bits in mask up to but excluding the i'th bit inline __hostdev__ uint32_t countOn(uint32_t i) const { - uint32_t n = i >> 6, sum = CountOn( mWords[n] & ((uint64_t(1) << (i & 63u))-1u) ); - for (const uint64_t* w = mWords; n--; ++w) sum += CountOn(*w); + uint32_t n = i >> 6, sum = CountOn(mWords[n] & ((uint64_t(1) << (i & 63u)) - 1u)); + for (const uint64_t* w = mWords; n--; ++w) + sum += CountOn(*w); return sum; } - template + template class Iterator { public: - __hostdev__ Iterator() : mPos(Mask::SIZE), mParent(nullptr){} - __hostdev__ Iterator(uint32_t pos, const Mask* parent) : mPos(pos), mParent(parent){} - Iterator& operator=(const Iterator&) = default; + __hostdev__ Iterator() + : mPos(Mask::SIZE) + , mParent(nullptr) + { + } + __hostdev__ Iterator(uint32_t pos, const Mask* parent) + : mPos(pos) + , mParent(parent) + { + } + Iterator& operator=(const Iterator&) = default; __hostdev__ uint32_t operator*() const { return mPos; } __hostdev__ uint32_t pos() const { return mPos; } - __hostdev__ operator bool() const { return mPos != Mask::SIZE; } + __hostdev__ operator bool() const { return mPos != Mask::SIZE; } __hostdev__ Iterator& operator++() { mPos = mParent->findNext(mPos + 1); @@ -2015,6 +2889,33 @@ class Mask const Mask* mParent; }; // Member class Iterator + class DenseIterator + { + public: + __hostdev__ DenseIterator(uint32_t pos = Mask::SIZE) + : mPos(pos) + { + } + DenseIterator& operator=(const DenseIterator&) = default; + __hostdev__ uint32_t operator*() const { return mPos; } + __hostdev__ uint32_t pos() const { return mPos; } + __hostdev__ operator bool() const { return mPos != Mask::SIZE; } + __hostdev__ DenseIterator& operator++() + { + ++mPos; + return *this; + } + __hostdev__ DenseIterator operator++(int) + { + auto tmp = *this; + ++mPos; + return tmp; + } + + private: + uint32_t mPos; + }; // Member class DenseIterator + using OnIterator = Iterator; using OffIterator = Iterator; @@ -2022,6 +2923,8 @@ class Mask __hostdev__ OffIterator beginOff() const { return OffIterator(this->findFirst(), this); } + __hostdev__ DenseIterator beginAll() const { return DenseIterator(0); } + /// @brief Initialize all bits to zero. __hostdev__ Mask() { @@ -2042,41 +2945,34 @@ class Mask mWords[i] = other.mWords[i]; } - /// @brief Return a const reference to the nth word of the bit mask, for a word of arbitrary size. - template - __hostdev__ const WordT& getWord(int n) const - { - NANOVDB_ASSERT(n * 8 * sizeof(WordT) < SIZE); - return reinterpret_cast(mWords)[n]; - } - - /// @brief Return a reference to the nth word of the bit mask, for a word of arbitrary size. - template - __hostdev__ WordT& getWord(int n) - { - NANOVDB_ASSERT(n * 8 * sizeof(WordT) < SIZE); - return reinterpret_cast(mWords)[n]; - } + /// @brief Return a pointer to the list of words of the bit mask + __hostdev__ uint64_t* words() { return mWords; } + __hostdev__ const uint64_t* words() const { return mWords; } /// @brief Assignment operator that works with openvdb::util::NodeMask - template - __hostdev__ Mask& operator=(const MaskT& other) + template + __hostdev__ typename enable_if::value, Mask&>::type operator=(const MaskT& other) { static_assert(sizeof(Mask) == sizeof(MaskT), "Mismatching sizeof"); static_assert(WORD_COUNT == MaskT::WORD_COUNT, "Mismatching word count"); static_assert(LOG2DIM == MaskT::LOG2DIM, "Mismatching LOG2DIM"); - auto *src = reinterpret_cast(&other); - uint64_t *dst = mWords; - for (uint32_t i = 0; i < WORD_COUNT; ++i) { - *dst++ = *src++; - } + auto* src = reinterpret_cast(&other); + for (uint64_t *dst = mWords, *end = dst + WORD_COUNT; dst != end; ++dst) + *dst = *src++; + return *this; + } + + __hostdev__ Mask& operator=(const Mask& other) + { + memcpy64(mWords, other.mWords, WORD_COUNT); return *this; } __hostdev__ bool operator==(const Mask& other) const { for (uint32_t i = 0; i < WORD_COUNT; ++i) { - if (mWords[i] != other.mWords[i]) return false; + if (mWords[i] != other.mWords[i]) + return false; } return true; } @@ -2109,20 +3005,33 @@ class Mask /// @brief Set the specified bit on. __hostdev__ void setOn(uint32_t n) { mWords[n >> 6] |= uint64_t(1) << (n & 63); } - /// @brief Set the specified bit off. __hostdev__ void setOff(uint32_t n) { mWords[n >> 6] &= ~(uint64_t(1) << (n & 63)); } +#if defined(__CUDACC__) // the following functions only run on the GPU! + __device__ inline void setOnAtomic(uint32_t n) + { + atomicOr(reinterpret_cast(this) + (n >> 6), 1ull << (n & 63)); + } + __device__ inline void setOffAtomic(uint32_t n) + { + atomicAnd(reinterpret_cast(this) + (n >> 6), ~(1ull << (n & 63))); + } + __device__ inline void setAtomic(uint32_t n, bool on) + { + on ? this->setOnAtomic(n) : this->setOffAtomic(n); + } +#endif /// @brief Set the specified bit on or off. - __hostdev__ void set(uint32_t n, bool On) + __hostdev__ void set(uint32_t n, bool on) { -#if 1 // switch between branchless - auto &word = mWords[n >> 6]; +#if 1 // switch between branchless + auto& word = mWords[n >> 6]; n &= 63; word &= ~(uint64_t(1) << n); - word |= uint64_t(On) << n; + word |= uint64_t(on) << n; #else - On ? this->setOn(n) : this->setOff(n); + on ? this->setOn(n) : this->setOff(n); #endif } @@ -2157,73 +3066,96 @@ class Mask __hostdev__ void toggle(uint32_t n) { mWords[n >> 6] ^= uint64_t(1) << (n & 63); } /// @brief Bitwise intersection - __hostdev__ Mask& operator&=(const Mask& other) + __hostdev__ Mask& operator&=(const Mask& other) { - uint64_t *w1 = mWords; - const uint64_t *w2 = other.mWords; - for (uint32_t n = WORD_COUNT; n--; ++w1, ++w2) *w1 &= *w2; + uint64_t* w1 = mWords; + const uint64_t* w2 = other.mWords; + for (uint32_t n = WORD_COUNT; n--; ++w1, ++w2) + *w1 &= *w2; return *this; } /// @brief Bitwise union __hostdev__ Mask& operator|=(const Mask& other) { - uint64_t *w1 = mWords; - const uint64_t *w2 = other.mWords; - for (uint32_t n = WORD_COUNT; n--; ++w1, ++w2) *w1 |= *w2; + uint64_t* w1 = mWords; + const uint64_t* w2 = other.mWords; + for (uint32_t n = WORD_COUNT; n--; ++w1, ++w2) + *w1 |= *w2; return *this; } /// @brief Bitwise difference __hostdev__ Mask& operator-=(const Mask& other) { - uint64_t *w1 = mWords; - const uint64_t *w2 = other.mWords; - for (uint32_t n = WORD_COUNT; n--; ++w1, ++w2) *w1 &= ~*w2; + uint64_t* w1 = mWords; + const uint64_t* w2 = other.mWords; + for (uint32_t n = WORD_COUNT; n--; ++w1, ++w2) + *w1 &= ~*w2; return *this; } /// @brief Bitwise XOR __hostdev__ Mask& operator^=(const Mask& other) { - uint64_t *w1 = mWords; - const uint64_t *w2 = other.mWords; - for (uint32_t n = WORD_COUNT; n--; ++w1, ++w2) *w1 ^= *w2; + uint64_t* w1 = mWords; + const uint64_t* w2 = other.mWords; + for (uint32_t n = WORD_COUNT; n--; ++w1, ++w2) + *w1 ^= *w2; return *this; } -private: - NANOVDB_HOSTDEV_DISABLE_WARNING - template + template __hostdev__ uint32_t findFirst() const { - uint32_t n = 0; + uint32_t n = 0u; const uint64_t* w = mWords; - for (; n + template __hostdev__ uint32_t findNext(uint32_t start) const { uint32_t n = start >> 6; // initiate if (n >= WORD_COUNT) return SIZE; // check for out of bounds - uint32_t m = start & 63; - uint64_t b = On ? mWords[n] : ~mWords[n]; - if (b & (uint64_t(1) << m)) - return start; // simple case: start is on - b &= ~uint64_t(0) << m; // mask out lower bits + uint32_t m = start & 63u; + uint64_t b = ON ? mWords[n] : ~mWords[n]; + if (b & (uint64_t(1u) << m)) + return start; // simple case: start is on/off + b &= ~uint64_t(0u) << m; // mask out lower bits while (!b && ++n < WORD_COUNT) - b = On ? mWords[n] : ~mWords[n]; // find next non-zero word - return (!b ? SIZE : (n << 6) + FindLowestOn(b)); // catch last word=0 + b = ON ? mWords[n] : ~mWords[n]; // find next non-zero word + return b ? (n << 6) + FindLowestOn(b) : SIZE; // catch last word=0 + } + + NANOVDB_HOSTDEV_DISABLE_WARNING + template + __hostdev__ uint32_t findPrev(uint32_t start) const + { + uint32_t n = start >> 6; // initiate + if (n >= WORD_COUNT) + return SIZE; // check for out of bounds + uint32_t m = start & 63u; + uint64_t b = ON ? mWords[n] : ~mWords[n]; + if (b & (uint64_t(1u) << m)) + return start; // simple case: start is on/off + b &= (uint64_t(1u) << m) - 1u; // mask out higher bits + while (!b && n) + b = ON ? mWords[--n] : ~mWords[--n]; // find previous non-zero word + return b ? (n << 6) + FindHighestOn(b) : SIZE; // catch first word=0 } + +private: + uint64_t mWords[WORD_COUNT]; }; // Mask class // ----------------------------> Map <-------------------------------------- /// @brief Defines an affine transform and its inverse represented as a 3x3 matrix and a vec3 translation struct Map -{ +{ // 264B (not 32B aligned!) float mMatF[9]; // 9*4B <- 3x3 matrix float mInvMatF[9]; // 9*4B <- 3x3 matrix float mVecF[3]; // 3*4B <- translation @@ -2233,108 +3165,235 @@ struct Map double mVecD[3]; // 3*8B <- translation double mTaperD; // 8B, placeholder for taper value - /// @brief Initialize the member data - template - __hostdev__ void set(const Mat3T& mat, const Mat3T& invMat, const Vec3T& translate, double taper); + /// @brief Default constructor for the identity map + __hostdev__ Map() + : mMatF{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f} + , mInvMatF{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f} + , mVecF{0.0f, 0.0f, 0.0f} + , mTaperF{1.0f} + , mMatD{1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0} + , mInvMatD{1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0} + , mVecD{0.0, 0.0, 0.0} + , mTaperD{1.0} + { + } + __hostdev__ Map(double s, const Vec3d& t = Vec3d(0.0, 0.0, 0.0)) + : mMatF{float(s), 0.0f, 0.0f, 0.0f, float(s), 0.0f, 0.0f, 0.0f, float(s)} + , mInvMatF{1.0f / float(s), 0.0f, 0.0f, 0.0f, 1.0f / float(s), 0.0f, 0.0f, 0.0f, 1.0f / float(s)} + , mVecF{float(t[0]), float(t[1]), float(t[2])} + , mTaperF{1.0f} + , mMatD{s, 0.0, 0.0, 0.0, s, 0.0, 0.0, 0.0, s} + , mInvMatD{1.0 / s, 0.0, 0.0, 0.0, 1.0 / s, 0.0, 0.0, 0.0, 1.0 / s} + , mVecD{t[0], t[1], t[2]} + , mTaperD{1.0} + { + } + + /// @brief Initialize the member data from 3x3 or 4x4 matrices + /// @note This is not _hostdev__ since then MatT=openvdb::Mat4d will produce warnings + template + void set(const MatT& mat, const MatT& invMat, const Vec3T& translate, double taper = 1.0); - /// @brief Initialize the member data + /// @brief Initialize the member data from 4x4 matrices /// @note The last (4th) row of invMat is actually ignored. + /// This is not _hostdev__ since then Mat4T=openvdb::Mat4d will produce warnings template - __hostdev__ void set(const Mat4T& mat, const Mat4T& invMat, double taper) {this->set(mat, invMat, mat[3], taper);} + void set(const Mat4T& mat, const Mat4T& invMat, double taper = 1.0) { this->set(mat, invMat, mat[3], taper); } template - __hostdev__ void set(double scale, const Vec3T &translation, double taper); + void set(double scale, const Vec3T& translation, double taper = 1.0); + /// @brief Apply the forward affine transformation to a vector using 64bit floating point arithmetics. + /// @note Typically this operation is used for the scale, rotation and translation of index -> world mapping + /// @tparam Vec3T Template type of the 3D vector to be mapped + /// @param ijk 3D vector to be mapped - typically floating point index coordinates + /// @return Forward mapping for affine transformation, i.e. (mat x ijk) + translation template - __hostdev__ Vec3T applyMap(const Vec3T& xyz) const { return matMult(mMatD, mVecD, xyz); } - template - __hostdev__ Vec3T applyMapF(const Vec3T& xyz) const { return matMult(mMatF, mVecF, xyz); } + __hostdev__ Vec3T applyMap(const Vec3T& ijk) const { return matMult(mMatD, mVecD, ijk); } + /// @brief Apply the forward affine transformation to a vector using 32bit floating point arithmetics. + /// @note Typically this operation is used for the scale, rotation and translation of index -> world mapping + /// @tparam Vec3T Template type of the 3D vector to be mapped + /// @param ijk 3D vector to be mapped - typically floating point index coordinates + /// @return Forward mapping for affine transformation, i.e. (mat x ijk) + translation + template + __hostdev__ Vec3T applyMapF(const Vec3T& ijk) const { return matMult(mMatF, mVecF, ijk); } + + /// @brief Apply the linear forward 3x3 transformation to an input 3d vector using 64bit floating point arithmetics, + /// e.g. scale and rotation WITHOUT translation. + /// @note Typically this operation is used for scale and rotation from index -> world mapping + /// @tparam Vec3T Template type of the 3D vector to be mapped + /// @param ijk 3D vector to be mapped - typically floating point index coordinates + /// @return linear forward 3x3 mapping of the input vector template - __hostdev__ Vec3T applyJacobian(const Vec3T& xyz) const { return matMult(mMatD, xyz); } + __hostdev__ Vec3T applyJacobian(const Vec3T& ijk) const { return matMult(mMatD, ijk); } + + /// @brief Apply the linear forward 3x3 transformation to an input 3d vector using 32bit floating point arithmetics, + /// e.g. scale and rotation WITHOUT translation. + /// @note Typically this operation is used for scale and rotation from index -> world mapping + /// @tparam Vec3T Template type of the 3D vector to be mapped + /// @param ijk 3D vector to be mapped - typically floating point index coordinates + /// @return linear forward 3x3 mapping of the input vector template - __hostdev__ Vec3T applyJacobianF(const Vec3T& xyz) const { return matMult(mMatF, xyz); } + __hostdev__ Vec3T applyJacobianF(const Vec3T& ijk) const { return matMult(mMatF, ijk); } + /// @brief Apply the inverse affine mapping to a vector using 64bit floating point arithmetics. + /// @note Typically this operation is used for the world -> index mapping + /// @tparam Vec3T Template type of the 3D vector to be mapped + /// @param xyz 3D vector to be mapped - typically floating point world coordinates + /// @return Inverse affine mapping of the input @c xyz i.e. (xyz - translation) x mat^-1 template __hostdev__ Vec3T applyInverseMap(const Vec3T& xyz) const { return matMult(mInvMatD, Vec3T(xyz[0] - mVecD[0], xyz[1] - mVecD[1], xyz[2] - mVecD[2])); } + + /// @brief Apply the inverse affine mapping to a vector using 32bit floating point arithmetics. + /// @note Typically this operation is used for the world -> index mapping + /// @tparam Vec3T Template type of the 3D vector to be mapped + /// @param xyz 3D vector to be mapped - typically floating point world coordinates + /// @return Inverse affine mapping of the input @c xyz i.e. (xyz - translation) x mat^-1 template __hostdev__ Vec3T applyInverseMapF(const Vec3T& xyz) const { return matMult(mInvMatF, Vec3T(xyz[0] - mVecF[0], xyz[1] - mVecF[1], xyz[2] - mVecF[2])); } + /// @brief Apply the linear inverse 3x3 transformation to an input 3d vector using 64bit floating point arithmetics, + /// e.g. inverse scale and inverse rotation WITHOUT translation. + /// @note Typically this operation is used for scale and rotation from world -> index mapping + /// @tparam Vec3T Template type of the 3D vector to be mapped + /// @param ijk 3D vector to be mapped - typically floating point index coordinates + /// @return linear inverse 3x3 mapping of the input vector i.e. xyz x mat^-1 template __hostdev__ Vec3T applyInverseJacobian(const Vec3T& xyz) const { return matMult(mInvMatD, xyz); } + + /// @brief Apply the linear inverse 3x3 transformation to an input 3d vector using 32bit floating point arithmetics, + /// e.g. inverse scale and inverse rotation WITHOUT translation. + /// @note Typically this operation is used for scale and rotation from world -> index mapping + /// @tparam Vec3T Template type of the 3D vector to be mapped + /// @param ijk 3D vector to be mapped - typically floating point index coordinates + /// @return linear inverse 3x3 mapping of the input vector i.e. xyz x mat^-1 template __hostdev__ Vec3T applyInverseJacobianF(const Vec3T& xyz) const { return matMult(mInvMatF, xyz); } + /// @brief Apply the transposed inverse 3x3 transformation to an input 3d vector using 64bit floating point arithmetics, + /// e.g. inverse scale and inverse rotation WITHOUT translation. + /// @note Typically this operation is used for scale and rotation from world -> index mapping + /// @tparam Vec3T Template type of the 3D vector to be mapped + /// @param ijk 3D vector to be mapped - typically floating point index coordinates + /// @return linear inverse 3x3 mapping of the input vector i.e. xyz x mat^-1 template __hostdev__ Vec3T applyIJT(const Vec3T& xyz) const { return matMultT(mInvMatD, xyz); } template __hostdev__ Vec3T applyIJTF(const Vec3T& xyz) const { return matMultT(mInvMatF, xyz); } + + /// @brief Return a voxels size in each coordinate direction, measured at the origin + __hostdev__ Vec3d getVoxelSize() const { return this->applyMap(Vec3d(1)) - this->applyMap(Vec3d(0)); } }; // Map -template -__hostdev__ inline void Map::set(const Mat3T& mat, const Mat3T& invMat, const Vec3T& translate, double taper) +template +inline void Map::set(const MatT& mat, const MatT& invMat, const Vec3T& translate, double taper) { - float *mf = mMatF, *vf = mVecF, *mif = mInvMatF; + float * mf = mMatF, *vf = mVecF, *mif = mInvMatF; double *md = mMatD, *vd = mVecD, *mid = mInvMatD; mTaperF = static_cast(taper); mTaperD = taper; for (int i = 0; i < 3; ++i) { *vd++ = translate[i]; //translation - *vf++ = static_cast(translate[i]); + *vf++ = static_cast(translate[i]); //translation for (int j = 0; j < 3; ++j) { *md++ = mat[j][i]; //transposed *mid++ = invMat[j][i]; - *mf++ = static_cast(mat[j][i]); + *mf++ = static_cast(mat[j][i]); //transposed *mif++ = static_cast(invMat[j][i]); } } } template -__hostdev__ inline void Map::set(double dx, const Vec3T &trans, double taper) -{ - const double mat[3][3] = { - {dx, 0.0, 0.0}, // row 0 - {0.0, dx, 0.0}, // row 1 - {0.0, 0.0, dx}, // row 2 - }, idx = 1.0/dx, invMat[3][3] = { - {idx, 0.0, 0.0}, // row 0 - {0.0, idx, 0.0}, // row 1 - {0.0, 0.0, idx}, // row 2 - }; +inline void Map::set(double dx, const Vec3T& trans, double taper) +{ + NANOVDB_ASSERT(dx > 0.0); + const double mat[3][3] = { {dx, 0.0, 0.0}, // row 0 + {0.0, dx, 0.0}, // row 1 + {0.0, 0.0, dx} }; // row 2 + const double idx = 1.0 / dx; + const double invMat[3][3] = { {idx, 0.0, 0.0}, // row 0 + {0.0, idx, 0.0}, // row 1 + {0.0, 0.0, idx} }; // row 2 this->set(mat, invMat, trans, taper); } // ----------------------------> GridBlindMetaData <-------------------------------------- struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) GridBlindMetaData -{ - static const int MaxNameSize = 256;// due to NULL termination the maximum length is one less! - int64_t mByteOffset; // byte offset to the blind data, relative to the GridData. - uint64_t mElementCount; // number of elements, e.g. point count - uint32_t mFlags; // flags +{ // 288 bytes + static const int MaxNameSize = 256; // due to NULL termination the maximum length is one less! + int64_t mDataOffset; // byte offset to the blind data, relative to this GridBlindMetaData. + uint64_t mValueCount; // number of blind values, e.g. point count + uint32_t mValueSize;// byte size of each value, e.g. 4 if mDataType=Float and 1 if mDataType=Unknown since that amounts to char GridBlindDataSemantic mSemantic; // semantic meaning of the data. GridBlindDataClass mDataClass; // 4 bytes GridType mDataType; // 4 bytes - char mName[MaxNameSize];// note this include the NULL termination - - /// @brief return memory usage in bytes for the class (note this computes for all blindMetaData structures.) - __hostdev__ static uint64_t memUsage(uint64_t blindDataCount = 0) - { - return blindDataCount * sizeof(GridBlindMetaData); + char mName[MaxNameSize]; // note this includes the NULL termination + // no padding required for 32 byte alignment + + // disallow copy-construction since methods like blindData and getBlindData uses the this pointer! + GridBlindMetaData(const GridBlindMetaData&) = delete; + + // disallow copy-assignment since methods like blindData and getBlindData uses the this pointer! + const GridBlindMetaData& operator=(const GridBlindMetaData&) = delete; + + __hostdev__ void setBlindData(void* blindData) { mDataOffset = PtrDiff(blindData, this); } + + // unsafe + __hostdev__ const void* blindData() const {return PtrAdd(this, mDataOffset);} + + /// @brief Get a const pointer to the blind data represented by this meta data + /// @tparam BlindDataT Expected value type of the blind data. + /// @return Returns NULL if mGridType!=mapToGridType(), else a const point of type BlindDataT. + /// @note Use mDataType=Unknown if BlindDataT is a custom data type unknown to NanoVDB. + template + __hostdev__ const BlindDataT* getBlindData() const + { + //if (mDataType != mapToGridType()) printf("getBlindData mismatch\n"); + return mDataType == mapToGridType() ? PtrAdd(this, mDataOffset) : nullptr; + } + + /// @brief return true if this meta data has a valid combination of semantic, class and value tags + __hostdev__ bool isValid() const + { + auto check = [&]()->bool{ + switch (mDataType){ + case GridType::Unknown: return mValueSize==1u;// i.e. we encode data as mValueCount chars + case GridType::Float: return mValueSize==4u; + case GridType::Double: return mValueSize==8u; + case GridType::Int16: return mValueSize==2u; + case GridType::Int32: return mValueSize==4u; + case GridType::Int64: return mValueSize==8u; + case GridType::Vec3f: return mValueSize==12u; + case GridType::Vec3d: return mValueSize==24u; + case GridType::Half: return mValueSize==2u; + case GridType::RGBA8: return mValueSize==4u; + case GridType::Fp8: return mValueSize==1u; + case GridType::Fp16: return mValueSize==2u; + case GridType::Vec4f: return mValueSize==16u; + case GridType::Vec4d: return mValueSize==32u; + case GridType::Vec3u8: return mValueSize==3u; + case GridType::Vec3u16: return mValueSize==6u; + default: return true;}// all other combinations are valid + }; + return nanovdb::isValid(mDataClass, mSemantic, mDataType) && check(); + } + + /// @brief return size in bytes of the blind data represented by this blind meta data + /// @note This size includes possible padding for 32 byte alignment. The actual amount + /// of bind data is mValueCount * mValueSize + __hostdev__ uint64_t blindDataSize() const + { + return AlignUp(mValueCount * mValueSize); } - - __hostdev__ void setBlindData(void *ptr) { mByteOffset = PtrDiff(ptr, this); } - - template - __hostdev__ const T* getBlindData() const { return PtrAdd(this, mByteOffset); } - }; // GridBlindMetaData // ----------------------------> NodeTrait <-------------------------------------- @@ -2348,14 +3407,14 @@ struct NodeTrait; template struct NodeTrait { - static_assert(GridOrTreeOrRootT::RootType::LEVEL == 3, "Tree depth is not supported"); + static_assert(GridOrTreeOrRootT::RootNodeType::LEVEL == 3, "Tree depth is not supported"); using Type = typename GridOrTreeOrRootT::LeafNodeType; using type = typename GridOrTreeOrRootT::LeafNodeType; }; template struct NodeTrait { - static_assert(GridOrTreeOrRootT::RootType::LEVEL == 3, "Tree depth is not supported"); + static_assert(GridOrTreeOrRootT::RootNodeType::LEVEL == 3, "Tree depth is not supported"); using Type = const typename GridOrTreeOrRootT::LeafNodeType; using type = const typename GridOrTreeOrRootT::LeafNodeType; }; @@ -2363,47 +3422,66 @@ struct NodeTrait template struct NodeTrait { - static_assert(GridOrTreeOrRootT::RootType::LEVEL == 3, "Tree depth is not supported"); - using Type = typename GridOrTreeOrRootT::RootType::ChildNodeType::ChildNodeType; - using type = typename GridOrTreeOrRootT::RootType::ChildNodeType::ChildNodeType; + static_assert(GridOrTreeOrRootT::RootNodeType::LEVEL == 3, "Tree depth is not supported"); + using Type = typename GridOrTreeOrRootT::RootNodeType::ChildNodeType::ChildNodeType; + using type = typename GridOrTreeOrRootT::RootNodeType::ChildNodeType::ChildNodeType; }; template struct NodeTrait { - static_assert(GridOrTreeOrRootT::RootType::LEVEL == 3, "Tree depth is not supported"); - using Type = const typename GridOrTreeOrRootT::RootType::ChildNodeType::ChildNodeType; - using type = const typename GridOrTreeOrRootT::RootType::ChildNodeType::ChildNodeType; + static_assert(GridOrTreeOrRootT::RootNodeType::LEVEL == 3, "Tree depth is not supported"); + using Type = const typename GridOrTreeOrRootT::RootNodeType::ChildNodeType::ChildNodeType; + using type = const typename GridOrTreeOrRootT::RootNodeType::ChildNodeType::ChildNodeType; }; template struct NodeTrait { - static_assert(GridOrTreeOrRootT::RootType::LEVEL == 3, "Tree depth is not supported"); - using Type = typename GridOrTreeOrRootT::RootType::ChildNodeType; - using type = typename GridOrTreeOrRootT::RootType::ChildNodeType; + static_assert(GridOrTreeOrRootT::RootNodeType::LEVEL == 3, "Tree depth is not supported"); + using Type = typename GridOrTreeOrRootT::RootNodeType::ChildNodeType; + using type = typename GridOrTreeOrRootT::RootNodeType::ChildNodeType; }; template struct NodeTrait { - static_assert(GridOrTreeOrRootT::RootType::LEVEL == 3, "Tree depth is not supported"); - using Type = const typename GridOrTreeOrRootT::RootType::ChildNodeType; - using type = const typename GridOrTreeOrRootT::RootType::ChildNodeType; + static_assert(GridOrTreeOrRootT::RootNodeType::LEVEL == 3, "Tree depth is not supported"); + using Type = const typename GridOrTreeOrRootT::RootNodeType::ChildNodeType; + using type = const typename GridOrTreeOrRootT::RootNodeType::ChildNodeType; }; template struct NodeTrait { - static_assert(GridOrTreeOrRootT::RootType::LEVEL == 3, "Tree depth is not supported"); - using Type = typename GridOrTreeOrRootT::RootType; - using type = typename GridOrTreeOrRootT::RootType; + static_assert(GridOrTreeOrRootT::RootNodeType::LEVEL == 3, "Tree depth is not supported"); + using Type = typename GridOrTreeOrRootT::RootNodeType; + using type = typename GridOrTreeOrRootT::RootNodeType; }; template struct NodeTrait { - static_assert(GridOrTreeOrRootT::RootType::LEVEL == 3, "Tree depth is not supported"); - using Type = const typename GridOrTreeOrRootT::RootType; - using type = const typename GridOrTreeOrRootT::RootType; + static_assert(GridOrTreeOrRootT::RootNodeType::LEVEL == 3, "Tree depth is not supported"); + using Type = const typename GridOrTreeOrRootT::RootNodeType; + using type = const typename GridOrTreeOrRootT::RootNodeType; }; +// ----------------------------> Froward decelerations of random access methods <-------------------------------------- + +template +struct GetValue; +template +struct SetValue; +template +struct SetVoxel; +template +struct GetState; +template +struct GetDim; +template +struct GetLeaf; +template +struct ProbeValue; +template +struct GetNodeInfo; + // ----------------------------> Grid <-------------------------------------- /* @@ -2421,7 +3499,7 @@ struct NodeTrait N0 LeafNodes each with a bit mask, N0 ValueTypes and min/max Example layout: ("---" implies it has a custom offset, "..." implies zero or more) - [GridData][TreeData]---[RootData][ROOT TILES...]---[NodeData<5>]---[ModeData<4>]---[LeafData<3>]---[BLINDMETA...]---[BLIND0]---[BLIND1]---etc. + [GridData][TreeData]---[RootData][ROOT TILES...]---[InternalData<5>]---[InternalData<4>]---[LeafData<3>]---[BLINDMETA...]---[BLIND0]---[BLIND1]---etc. */ /// @brief Struct with all the member data of the Grid (useful during serialization of an openvdb grid) @@ -2431,77 +3509,80 @@ struct NodeTrait /// /// @note No client code should (or can) interface with this struct so it can safely be ignored! struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) GridData -{// sizeof(GridData) = 672B - static const int MaxNameSize = 256;// due to NULL termination the maximum length is one less +{ // sizeof(GridData) = 672B + static const int MaxNameSize = 256; // due to NULL termination the maximum length is one less uint64_t mMagic; // 8B (0) magic to validate it is valid grid data. uint64_t mChecksum; // 8B (8). Checksum of grid buffer. - Version mVersion;// 4B (16) major, minor, and patch version numbers - uint32_t mFlags; // 4B (20). flags for grid. + Version mVersion; // 4B (16) major, minor, and patch version numbers + BitFlags<32> mFlags; // 4B (20). flags for grid. uint32_t mGridIndex; // 4B (24). Index of this grid in the buffer uint32_t mGridCount; // 4B (28). Total number of grids in the buffer uint64_t mGridSize; // 8B (32). byte count of this entire grid occupied in the buffer. char mGridName[MaxNameSize]; // 256B (40) Map mMap; // 264B (296). affine transformation between index and world space in both single and double precision - BBox mWorldBBox; // 48B (560). floating-point AABB of active values in WORLD SPACE (2 x 3 doubles) - Vec3R mVoxelSize; // 24B (608). size of a voxel in world units + BBox mWorldBBox; // 48B (560). floating-point AABB of active values in WORLD SPACE (2 x 3 doubles) + Vec3d mVoxelSize; // 24B (608). size of a voxel in world units GridClass mGridClass; // 4B (632). GridType mGridType; // 4B (636). - int64_t mBlindMetadataOffset; // 8B (640). offset of GridBlindMetaData structures that follow this grid. + int64_t mBlindMetadataOffset; // 8B (640). offset to beginning of GridBlindMetaData structures that follow this grid. uint32_t mBlindMetadataCount; // 4B (648). count of GridBlindMetaData structures that follow this grid. - uint32_t mData0;// 4B (652) - uint64_t mData1, mData2;// 2x8B (656) padding to 32 B alignment. mData1 is use for the total number of values indexed by an IndexGrid - - // Set and unset various bit flags - __hostdev__ void setFlagsOff() { mFlags = uint32_t(0); } - __hostdev__ void setMinMaxOn(bool on = true) - { - if (on) { - mFlags |= static_cast(GridFlags::HasMinMax); - } else { - mFlags &= ~static_cast(GridFlags::HasMinMax); - } - } - __hostdev__ void setBBoxOn(bool on = true) - { - if (on) { - mFlags |= static_cast(GridFlags::HasBBox); - } else { - mFlags &= ~static_cast(GridFlags::HasBBox); - } - } - __hostdev__ void setLongGridNameOn(bool on = true) - { - if (on) { - mFlags |= static_cast(GridFlags::HasLongGridName); - } else { - mFlags &= ~static_cast(GridFlags::HasLongGridName); - } - } - __hostdev__ void setAverageOn(bool on = true) + uint32_t mData0; // 4B (652) + uint64_t mData1, mData2; // 2x8B (656) padding to 32 B alignment. mData1 is use for the total number of values indexed by an IndexGrid + /// @brief Use this method to initiate most member dat + __hostdev__ GridData& operator=(const GridData& other) { - if (on) { - mFlags |= static_cast(GridFlags::HasAverage); - } else { - mFlags &= ~static_cast(GridFlags::HasAverage); - } + static_assert(8 * 84 == sizeof(GridData), "GridData has unexpected size"); + memcpy64(this, &other, 84); + return *this; } - __hostdev__ void setStdDeviationOn(bool on = true) + __hostdev__ void init(std::initializer_list list = {GridFlags::IsBreadthFirst}, + uint64_t gridSize = 0u, + const Map& map = Map(), + GridType gridType = GridType::Unknown, + GridClass gridClass = GridClass::Unknown) { - if (on) { - mFlags |= static_cast(GridFlags::HasStdDeviation); - } else { - mFlags &= ~static_cast(GridFlags::HasStdDeviation); - } +#ifdef NANOVDB_USE_NEW_MAGIC_NUMBERS + mMagic = NANOVDB_MAGIC_GRID; +#else + mMagic = NANOVDB_MAGIC_NUMBER; +#endif + mChecksum = ~uint64_t(0);// all 64 bits ON means checksum is disabled + mVersion = Version(); + mFlags.initMask(list); + mGridIndex = 0u; + mGridCount = 1u; + mGridSize = gridSize; + mGridName[0] = '\0'; + mMap = map; + mWorldBBox = BBox();// invalid bbox + mVoxelSize = map.getVoxelSize(); + mGridClass = gridClass; + mGridType = gridType; + mBlindMetadataOffset = mGridSize; // i.e. no blind data + mBlindMetadataCount = 0u; // i.e. no blind data + mData0 = 0u; + mData1 = 0u; // only used for index and point grids + mData2 = 0u; + } + /// @brief return true if the magic number and the version are both valid + __hostdev__ bool isValid() const { + return mMagic == NANOVDB_MAGIC_GRID || (mMagic == NANOVDB_MAGIC_NUMBER && mVersion.isCompatible()); } - __hostdev__ void setBreadthFirstOn(bool on = true) - { - if (on) { - mFlags |= static_cast(GridFlags::IsBreadthFirst); - } else { - mFlags &= ~static_cast(GridFlags::IsBreadthFirst); - } + // Set and unset various bit flags + __hostdev__ void setMinMaxOn(bool on = true) { mFlags.setMask(GridFlags::HasMinMax, on); } + __hostdev__ void setBBoxOn(bool on = true) { mFlags.setMask(GridFlags::HasBBox, on); } + __hostdev__ void setLongGridNameOn(bool on = true) { mFlags.setMask(GridFlags::HasLongGridName, on); } + __hostdev__ void setAverageOn(bool on = true) { mFlags.setMask(GridFlags::HasAverage, on); } + __hostdev__ void setStdDeviationOn(bool on = true) { mFlags.setMask(GridFlags::HasStdDeviation, on); } + __hostdev__ bool setGridName(const char* src) + { + char *dst = mGridName, *end = dst + MaxNameSize; + while (*src != '\0' && dst < end - 1) + *dst++ = *src++; + while (dst < end) + *dst++ = '\0'; + return *src == '\0'; // returns true if input grid name is NOT longer than MaxNameSize characters } - // Affine transformations based on double precision template __hostdev__ Vec3T applyMap(const Vec3T& xyz) const { return mMap.applyMap(xyz); } // Pos: index -> world @@ -2525,28 +3606,88 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) GridData template __hostdev__ Vec3T applyIJTF(const Vec3T& xyz) const { return mMap.applyIJTF(xyz); } - // @brief Return a non-const void pointer to the tree - __hostdev__ void* treePtr() { return this + 1; } + // @brief Return a non-const uint8_t pointer to the tree + __hostdev__ uint8_t* treePtr() { return reinterpret_cast(this + 1); }// TreeData is always right after GridData + //__hostdev__ TreeData* treePtr() { return reinterpret_cast(this + 1); }// TreeData is always right after GridData - // @brief Return a const void pointer to the tree - __hostdev__ const void* treePtr() const { return this + 1; } + // @brief Return a const uint8_t pointer to the tree + __hostdev__ const uint8_t* treePtr() const { return reinterpret_cast(this + 1); }// TreeData is always right after GridData + //__hostdev__ const TreeData* treePtr() const { return reinterpret_cast(this + 1); }// TreeData is always right after GridData - /// @brief Returns a const reference to the blindMetaData at the specified linear offset. - /// - /// @warning The linear offset is assumed to be in the valid range + /// @brief Return a non-const uint8_t pointer to the first node at @c LEVEL + /// @tparam LEVEL of the node. LEVEL 0 means leaf node and LEVEL 3 means root node + /// @warning If not nodes exist at @c LEVEL NULL is returned + template + __hostdev__ const uint8_t* nodePtr() const + { + static_assert(LEVEL >= 0 && LEVEL <= 3, "invalid LEVEL template parameter"); + auto *treeData = this->treePtr(); + auto nodeOffset = *reinterpret_cast(treeData + 8*LEVEL);// skip LEVEL uint64_t + return nodeOffset ? PtrAdd(treeData, nodeOffset) : nullptr; + } + + /// @brief Return a non-const uint8_t pointer to the first node at @c LEVEL + /// @tparam LEVEL of the node. LEVEL 0 means leaf node and LEVEL 3 means root node + /// @warning If not nodes exist at @c LEVEL NULL is returned + template + __hostdev__ uint8_t* nodePtr(){return const_cast(const_cast(this)->template nodePtr());} + + /// @brief Returns a const reference to the blindMetaData at the specified linear offset. + /// + /// @warning The linear offset is assumed to be in the valid range __hostdev__ const GridBlindMetaData* blindMetaData(uint32_t n) const { NANOVDB_ASSERT(n < mBlindMetadataCount); return PtrAdd(this, mBlindMetadataOffset) + n; } + __hostdev__ const char* gridName() const + { + if (mFlags.isMaskOn(GridFlags::HasLongGridName)) {// search for first blind meta data that contains a name + NANOVDB_ASSERT(mBlindMetadataCount > 0); + for (uint32_t i = 0; i < mBlindMetadataCount; ++i) { + const auto* metaData = this->blindMetaData(i);// EXTREMELY important to be a pointer + if (metaData->mDataClass == GridBlindDataClass::GridName) { + NANOVDB_ASSERT(metaData->mDataType == GridType::Unknown); + return metaData->template getBlindData(); + } + } + NANOVDB_ASSERT(false); // should never hit this! + } + return mGridName; + } + + /// @brief Return memory usage in bytes for this class only. + __hostdev__ static uint64_t memUsage() { return sizeof(GridData); } + + /// @brief return AABB of active values in world space + __hostdev__ const BBox& worldBBox() const { return mWorldBBox; } + + /// @brief return AABB of active values in index space + __hostdev__ const CoordBBox& indexBBox() const {return *(const CoordBBox*)(this->nodePtr<3>());} + + /// @brief return the root table has size + __hostdev__ uint32_t rootTableSize() const { + if (const uint8_t *root = this->nodePtr<3>()) { + return *(const uint32_t*)(root + sizeof(CoordBBox)); + } + return 0u; + } + + /// @brief test if the grid is empty, e.i the root table has size 0 + /// @return true if this grid contains not data whatsoever + __hostdev__ bool isEmpty() const {return this->rootTableSize() == 0u;} + + /// @brief return true if RootData follows TreeData in memory without any extra padding + /// @details TreeData is always following right after GridData, but the same might not be true for RootData + __hostdev__ bool isRootConnected() const { return *(const uint64_t*)((const char*)(this + 1) + 24) == 64u;} }; // GridData // Forward declaration of accelerated random access class -template +template class ReadAccessor; -template +template using DefaultReadAccessor = ReadAccessor; /// @brief Highest level of the data structure. Contains a tree and a world->index @@ -2554,14 +3695,18 @@ using DefaultReadAccessor = ReadAccessor; /// /// @note This the API of this class to interface with client code template -class Grid : private GridData +class Grid : public GridData { public: - using TreeType = TreeT; - using RootType = typename TreeT::RootType; - using DataType = GridData; + using TreeType = TreeT; + using RootType = typename TreeT::RootType; + using RootNodeType = RootType; + using UpperNodeType = typename RootNodeType::ChildNodeType; + using LowerNodeType = typename UpperNodeType::ChildNodeType; + using LeafNodeType = typename RootType::LeafNodeType; + using DataType = GridData; using ValueType = typename TreeT::ValueType; - using BuildType = typename TreeT::BuildType;// in rare cases BuildType != ValueType, e.g. then BuildType = ValueMask and ValueType = bool + using BuildType = typename TreeT::BuildType; // in rare cases BuildType != ValueType, e.g. then BuildType = ValueMask and ValueType = bool using CoordType = typename TreeT::CoordType; using AccessorType = DefaultReadAccessor; @@ -2579,7 +3724,7 @@ class Grid : private GridData __hostdev__ const DataType* data() const { return reinterpret_cast(this); } /// @brief Return memory usage in bytes for this class only. - __hostdev__ static uint64_t memUsage() { return sizeof(GridData); } + //__hostdev__ static uint64_t memUsage() { return sizeof(GridData); } /// @brief Return the memory footprint of the entire grid, i.e. including all nodes and blind data __hostdev__ uint64_t gridSize() const { return DataType::mGridSize; } @@ -2592,9 +3737,17 @@ class Grid : private GridData /// @brief @brief Return the total number of values indexed by this IndexGrid /// - /// @note This method is only defined for IndexGrid = NanoGrid - template - __hostdev__ typename enable_if::value, const uint64_t&>::type valueCount() const {return DataType::mData1;} + /// @note This method is only defined for IndexGrid = NanoGrid + template + __hostdev__ typename enable_if::is_index, const uint64_t&>::type + valueCount() const { return DataType::mData1; } + + /// @brief @brief Return the total number of points indexed by this PointGrid + /// + /// @note This method is only defined for PointGrid = NanoGrid + template + __hostdev__ typename enable_if::value, const uint64_t&>::type + pointCount() const { return DataType::mData1; } /// @brief Return a const reference to the tree __hostdev__ const TreeT& tree() const { return *reinterpret_cast(this->treePtr()); } @@ -2606,7 +3759,7 @@ class Grid : private GridData __hostdev__ AccessorType getAccessor() const { return AccessorType(this->tree().root()); } /// @brief Return a const reference to the size of a voxel in world units - __hostdev__ const Vec3R& voxelSize() const { return DataType::mVoxelSize; } + __hostdev__ const Vec3d& voxelSize() const { return DataType::mVoxelSize; } /// @brief Return a const reference to the Map for this grid __hostdev__ const Map& map() const { return DataType::mMap; } @@ -2658,19 +3811,19 @@ class Grid : private GridData __hostdev__ Vec3T indexToWorldGradF(const Vec3T& grad) const { return DataType::applyIJTF(grad); } /// @brief Computes a AABB of active values in world space - __hostdev__ const BBox& worldBBox() const { return DataType::mWorldBBox; } + //__hostdev__ const BBox& worldBBox() const { return DataType::mWorldBBox; } /// @brief Computes a AABB of active values in index space /// /// @note This method is returning a floating point bounding box and not a CoordBBox. This makes /// it more useful for clipping rays. - __hostdev__ const BBox& indexBBox() const { return this->tree().bbox(); } + //__hostdev__ const BBox& indexBBox() const { return this->tree().bbox(); } /// @brief Return the total number of active voxels in this tree. __hostdev__ uint64_t activeVoxelCount() const { return this->tree().activeVoxelCount(); } /// @brief Methods related to the classification of this grid - __hostdev__ bool isValid() const { return DataType::mMagic == NANOVDB_MAGIC_NUMBER; } + __hostdev__ bool isValid() const { return DataType::isValid(); } __hostdev__ const GridType& gridType() const { return DataType::mGridType; } __hostdev__ const GridClass& gridClass() const { return DataType::mGridClass; } __hostdev__ bool isLevelSet() const { return DataType::mGridClass == GridClass::LevelSet; } @@ -2681,34 +3834,28 @@ class Grid : private GridData __hostdev__ bool isPointData() const { return DataType::mGridClass == GridClass::PointData; } __hostdev__ bool isMask() const { return DataType::mGridClass == GridClass::Topology; } __hostdev__ bool isUnknown() const { return DataType::mGridClass == GridClass::Unknown; } - __hostdev__ bool hasMinMax() const { return DataType::mFlags & static_cast(GridFlags::HasMinMax); } - __hostdev__ bool hasBBox() const { return DataType::mFlags & static_cast(GridFlags::HasBBox); } - __hostdev__ bool hasLongGridName() const { return DataType::mFlags & static_cast(GridFlags::HasLongGridName); } - __hostdev__ bool hasAverage() const { return DataType::mFlags & static_cast(GridFlags::HasAverage); } - __hostdev__ bool hasStdDeviation() const { return DataType::mFlags & static_cast(GridFlags::HasStdDeviation); } - __hostdev__ bool isBreadthFirst() const { return DataType::mFlags & static_cast(GridFlags::IsBreadthFirst); } + __hostdev__ bool hasMinMax() const { return DataType::mFlags.isMaskOn(GridFlags::HasMinMax); } + __hostdev__ bool hasBBox() const { return DataType::mFlags.isMaskOn(GridFlags::HasBBox); } + __hostdev__ bool hasLongGridName() const { return DataType::mFlags.isMaskOn(GridFlags::HasLongGridName); } + __hostdev__ bool hasAverage() const { return DataType::mFlags.isMaskOn(GridFlags::HasAverage); } + __hostdev__ bool hasStdDeviation() const { return DataType::mFlags.isMaskOn(GridFlags::HasStdDeviation); } + __hostdev__ bool isBreadthFirst() const { return DataType::mFlags.isMaskOn(GridFlags::IsBreadthFirst); } /// @brief return true if the specified node type is layed out breadth-first in memory and has a fixed size. /// This allows for sequential access to the nodes. - template + template __hostdev__ bool isSequential() const { return NodeT::FIXED_SIZE && this->isBreadthFirst(); } /// @brief return true if the specified node level is layed out breadth-first in memory and has a fixed size. /// This allows for sequential access to the nodes. - template - __hostdev__ bool isSequential() const { return NodeTrait::type::FIXED_SIZE && this->isBreadthFirst(); } + template + __hostdev__ bool isSequential() const { return NodeTrait::type::FIXED_SIZE && this->isBreadthFirst(); } + + /// @brief return true if nodes at all levels can safely be accessed with simple linear offsets + __hostdev__ bool isSequential() const { return UpperNodeType::FIXED_SIZE && LowerNodeType::FIXED_SIZE && LeafNodeType::FIXED_SIZE && this->isBreadthFirst(); } /// @brief Return a c-string with the name of this grid - __hostdev__ const char* gridName() const - { - if (this->hasLongGridName()) { - NANOVDB_ASSERT(DataType::mBlindMetadataCount>0); - const auto &metaData = this->blindMetaData(DataType::mBlindMetadataCount-1);// always the last - NANOVDB_ASSERT(metaData.mDataClass == GridBlindDataClass::GridName); - return metaData.template getBlindData(); - } - return DataType::mGridName; - } + __hostdev__ const char* gridName() const { return DataType::gridName(); } /// @brief Return a c-string with the name of this grid, truncated to 255 characters __hostdev__ const char* shortGridName() const { return DataType::mGridName; } @@ -2717,24 +3864,40 @@ class Grid : private GridData __hostdev__ uint64_t checksum() const { return DataType::mChecksum; } /// @brief Return true if this grid is empty, i.e. contains no values or nodes. - __hostdev__ bool isEmpty() const { return this->tree().isEmpty(); } + //__hostdev__ bool isEmpty() const { return this->tree().isEmpty(); } /// @brief Return the count of blind-data encoded in this grid __hostdev__ uint32_t blindDataCount() const { return DataType::mBlindMetadataCount; } - /// @brief Return the index of the blind data with specified semantic if found, otherwise -1. + /// @brief Return the index of the first blind data with specified name if found, otherwise -1. + __hostdev__ int findBlindData(const char* name) const; + + /// @brief Return the index of the first blind data with specified semantic if found, otherwise -1. __hostdev__ int findBlindDataForSemantic(GridBlindDataSemantic semantic) const; /// @brief Returns a const pointer to the blindData at the specified linear offset. /// - /// @warning Point might be NULL and the linear offset is assumed to be in the valid range + /// @warning Pointer might be NULL and the linear offset is assumed to be in the valid range + // this method is deprecated !!!! __hostdev__ const void* blindData(uint32_t n) const { - if (DataType::mBlindMetadataCount == 0u) { - return nullptr; - } + printf("\nnanovdb::Grid::blindData is unsafe and hence deprecated! Please use nanovdb::Grid::getBlindData instead.\n\n"); NANOVDB_ASSERT(n < DataType::mBlindMetadataCount); - return this->blindMetaData(n).template getBlindData(); + return this->blindMetaData(n).blindData(); + } + + template + __hostdev__ const BlindDataT* getBlindData(uint32_t n) const + { + if (n >= DataType::mBlindMetadataCount) return nullptr;// index is out of bounds + return this->blindMetaData(n).template getBlindData();// NULL if mismatching BlindDataT + } + + template + __hostdev__ BlindDataT* getBlindData(uint32_t n) + { + if (n >= DataType::mBlindMetadataCount) return nullptr;// index is out of bounds + return const_cast(this->blindMetaData(n).template getBlindData());// NULL if mismatching BlindDataT } __hostdev__ const GridBlindMetaData& blindMetaData(uint32_t n) const { return *DataType::blindMetaData(n); } @@ -2746,36 +3909,62 @@ class Grid : private GridData template __hostdev__ int Grid::findBlindDataForSemantic(GridBlindDataSemantic semantic) const { - for (uint32_t i = 0, n = this->blindDataCount(); i < n; ++i) + for (uint32_t i = 0, n = this->blindDataCount(); i < n; ++i) { if (this->blindMetaData(i).mSemantic == semantic) return int(i); + } + return -1; +} + +template +__hostdev__ int Grid::findBlindData(const char* name) const +{ + auto test = [&](int n) { + const char* str = this->blindMetaData(n).mName; + for (int i = 0; i < GridBlindMetaData::MaxNameSize; ++i) { + if (name[i] != str[i]) + return false; + if (name[i] == '\0' && str[i] == '\0') + return true; + } + return true; // all len characters matched + }; + for (int i = 0, n = this->blindDataCount(); i < n; ++i) + if (test(i)) + return i; return -1; } // ----------------------------> Tree <-------------------------------------- -template struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) TreeData -{// sizeof(TreeData<3>) == 64B - static_assert(ROOT_LEVEL == 3, "Root level is assumed to be three"); - uint64_t mNodeOffset[4];//32B, byte offset from this tree to first leaf, lower, upper and root node - uint32_t mNodeCount[3];// 12B, total number of nodes of type: leaf, lower internal, upper internal - uint32_t mTileCount[3];// 12B, total number of active tile values at the lower internal, upper internal and root node levels - uint64_t mVoxelCount;// 8B, total number of active voxels in the root and all its child nodes. +{ // sizeof(TreeData) == 64B + int64_t mNodeOffset[4];// 32B, byte offset from this tree to first leaf, lower, upper and root node. A zero offset means no node exists + uint32_t mNodeCount[3]; // 12B, total number of nodes of type: leaf, lower internal, upper internal + uint32_t mTileCount[3]; // 12B, total number of active tile values at the lower internal, upper internal and root node levels + uint64_t mVoxelCount; // 8B, total number of active voxels in the root and all its child nodes. // No padding since it's always 32B aligned - template - __hostdev__ void setRoot(const RootT* root) { mNodeOffset[3] = PtrDiff(root, this); } - template - __hostdev__ RootT* getRoot() { return PtrAdd(this, mNodeOffset[3]); } - template - __hostdev__ const RootT* getRoot() const { return PtrAdd(this, mNodeOffset[3]); } - - template - __hostdev__ void setFirstNode(const NodeT* node) + __hostdev__ TreeData& operator=(const TreeData& other) { - mNodeOffset[NodeT::LEVEL] = node ? PtrDiff(node, this) : 0; + static_assert(8 * 8 == sizeof(TreeData), "TreeData has unexpected size"); + memcpy64(this, &other, 8); + return *this; } -}; + __hostdev__ void setRoot(const void* root) {mNodeOffset[3] = root ? PtrDiff(root, this) : 0;} + __hostdev__ uint8_t* getRoot() { return mNodeOffset[3] ? PtrAdd(this, mNodeOffset[3]) : nullptr; } + __hostdev__ const uint8_t* getRoot() const { return mNodeOffset[3] ? PtrAdd(this, mNodeOffset[3]) : nullptr; } + + template + __hostdev__ void setFirstNode(const NodeT* node) {mNodeOffset[NodeT::LEVEL] = node ? PtrDiff(node, this) : 0;} + + __hostdev__ bool isEmpty() const {return mNodeOffset[3] ? *PtrAdd(this, mNodeOffset[3] + sizeof(BBox)) == 0 : true;} + + /// @brief Return the index bounding box of all the active values in this tree, i.e. in all nodes of the tree + __hostdev__ CoordBBox bbox() const {return mNodeOffset[3] ? *PtrAdd(this, mNodeOffset[3]) : CoordBBox();} + + /// @brief return true if RootData is layout out immediately after TreeData in memory + __hostdev__ bool isRootNext() const {return mNodeOffset[3] ? mNodeOffset[3] == sizeof(TreeData) : false; } +};// TreeData // ----------------------------> GridTree <-------------------------------------- @@ -2797,7 +3986,7 @@ struct GridTree /// @brief VDB Tree, which is a thin wrapper around a RootNode. template -class Tree : private TreeData +class Tree : public TreeData { static_assert(RootT::LEVEL == 3, "Tree depth is not supported"); static_assert(RootT::ChildNodeType::LOG2DIM == 5, "Tree configuration is not supported"); @@ -2805,11 +3994,14 @@ class Tree : private TreeData static_assert(RootT::LeafNodeType::LOG2DIM == 3, "Tree configuration is not supported"); public: - using DataType = TreeData; + using DataType = TreeData; using RootType = RootT; - using LeafNodeType = typename RootT::LeafNodeType; + using RootNodeType = RootT; + using UpperNodeType = typename RootNodeType::ChildNodeType; + using LowerNodeType = typename UpperNodeType::ChildNodeType; + using LeafNodeType = typename RootType::LeafNodeType; using ValueType = typename RootT::ValueType; - using BuildType = typename RootT::BuildType;// in rare cases BuildType != ValueType, e.g. then BuildType = ValueMask and ValueType = bool + using BuildType = typename RootT::BuildType; // in rare cases BuildType != ValueType, e.g. then BuildType = ValueMask and ValueType = bool using CoordType = typename RootT::CoordType; using AccessorType = DefaultReadAccessor; @@ -2831,20 +4023,31 @@ class Tree : private TreeData /// @brief return memory usage in bytes for the class __hostdev__ static uint64_t memUsage() { return sizeof(DataType); } - __hostdev__ RootT& root() { return *DataType::template getRoot(); } + __hostdev__ RootT& root() + { + RootT* ptr = reinterpret_cast(DataType::getRoot()); + NANOVDB_ASSERT(ptr); + return *ptr; + } - __hostdev__ const RootT& root() const { return *DataType::template getRoot(); } + __hostdev__ const RootT& root() const + { + const RootT* ptr = reinterpret_cast(DataType::getRoot()); + NANOVDB_ASSERT(ptr); + return *ptr; + } __hostdev__ AccessorType getAccessor() const { return AccessorType(this->root()); } /// @brief Return the value of the given voxel (regardless of state or location in the tree.) __hostdev__ ValueType getValue(const CoordType& ijk) const { return this->root().getValue(ijk); } + __hostdev__ ValueType getValue(int i, int j, int k) const { return this->root().getValue(CoordType(i, j, k)); } /// @brief Return the active state of the given voxel (regardless of state or location in the tree.) __hostdev__ bool isActive(const CoordType& ijk) const { return this->root().isActive(ijk); } /// @brief Return true if this tree is empty, i.e. contains no values or nodes - __hostdev__ bool isEmpty() const { return this->root().isEmpty(); } + //__hostdev__ bool isEmpty() const { return this->root().isEmpty(); } /// @brief Combines the previous two methods in a single call __hostdev__ bool probeValue(const CoordType& ijk, ValueType& v) const { return this->root().probeValue(ijk, v); } @@ -2856,7 +4059,7 @@ class Tree : private TreeData __hostdev__ void extrema(ValueType& min, ValueType& max) const; /// @brief Return a const reference to the index bounding box of all the active values in this tree, i.e. in all nodes of the tree - __hostdev__ const BBox& bbox() const { return this->root().bbox(); } + //__hostdev__ const BBox& bbox() const { return this->root().bbox(); } /// @brief Return the total number of active voxels in this tree. __hostdev__ uint64_t activeVoxelCount() const { return DataType::mVoxelCount; } @@ -2868,7 +4071,7 @@ class Tree : private TreeData /// referred to as active voxels (see activeVoxelCount defined above). __hostdev__ const uint32_t& activeTileCount(uint32_t level) const { - NANOVDB_ASSERT(level > 0 && level <= 3);// 1, 2, or 3 + NANOVDB_ASSERT(level > 0 && level <= 3); // 1, 2, or 3 return DataType::mTileCount[level - 1]; } @@ -2885,53 +4088,70 @@ class Tree : private TreeData return DataType::mNodeCount[level]; } + __hostdev__ uint32_t totalNodeCount() const + { + return DataType::mNodeCount[0] + DataType::mNodeCount[1] + DataType::mNodeCount[2]; + } + /// @brief return a pointer to the first node of the specified type /// /// @warning Note it may return NULL if no nodes exist - template + template __hostdev__ NodeT* getFirstNode() { - const uint64_t offset = DataType::mNodeOffset[NodeT::LEVEL]; - return offset>0 ? PtrAdd(this, offset) : nullptr; + const int64_t offset = DataType::mNodeOffset[NodeT::LEVEL]; + return offset ? PtrAdd(this, offset) : nullptr; } /// @brief return a const pointer to the first node of the specified type /// /// @warning Note it may return NULL if no nodes exist - template + template __hostdev__ const NodeT* getFirstNode() const { - const uint64_t offset = DataType::mNodeOffset[NodeT::LEVEL]; - return offset>0 ? PtrAdd(this, offset) : nullptr; + const int64_t offset = DataType::mNodeOffset[NodeT::LEVEL]; + return offset ? PtrAdd(this, offset) : nullptr; } /// @brief return a pointer to the first node at the specified level /// /// @warning Note it may return NULL if no nodes exist - template + template __hostdev__ typename NodeTrait::type* getFirstNode() { - return this->template getFirstNode::type>(); + return this->template getFirstNode::type>(); } /// @brief return a const pointer to the first node of the specified level /// /// @warning Note it may return NULL if no nodes exist - template + template __hostdev__ const typename NodeTrait::type* getFirstNode() const { - return this->template getFirstNode::type>(); + return this->template getFirstNode::type>(); } /// @brief Template specializations of getFirstNode - __hostdev__ LeafNodeType* getFirstLeaf() {return this->getFirstNode();} - __hostdev__ const LeafNodeType* getFirstLeaf() const {return this->getFirstNode();} - __hostdev__ typename NodeTrait::type* getFirstLower() {return this->getFirstNode<1>();} - __hostdev__ const typename NodeTrait::type* getFirstLower() const {return this->getFirstNode<1>();} - __hostdev__ typename NodeTrait::type* getFirstUpper() {return this->getFirstNode<2>();} - __hostdev__ const typename NodeTrait::type* getFirstUpper() const {return this->getFirstNode<2>();} + __hostdev__ LeafNodeType* getFirstLeaf() { return this->getFirstNode(); } + __hostdev__ const LeafNodeType* getFirstLeaf() const { return this->getFirstNode(); } + __hostdev__ typename NodeTrait::type* getFirstLower() { return this->getFirstNode<1>(); } + __hostdev__ const typename NodeTrait::type* getFirstLower() const { return this->getFirstNode<1>(); } + __hostdev__ typename NodeTrait::type* getFirstUpper() { return this->getFirstNode<2>(); } + __hostdev__ const typename NodeTrait::type* getFirstUpper() const { return this->getFirstNode<2>(); } + + template + __hostdev__ auto get(const CoordType& ijk, ArgsT&&... args) const + { + return this->root().template get(ijk, args...); + } + + template + __hostdev__ auto set(const CoordType& ijk, ArgsT&&... args) + { + return this->root().template set(ijk, args...); + } private: static_assert(sizeof(DataType) % NANOVDB_DATA_ALIGNMENT == 0, "sizeof(TreeData) is misaligned"); @@ -2945,7 +4165,7 @@ __hostdev__ void Tree::extrema(ValueType& min, ValueType& max) const max = this->root().maximum(); } -// --------------------------> RootNode <------------------------------------ +// --------------------------> RootData <------------------------------------ /// @brief Struct with all the member data of the RootNode (useful during serialization of an openvdb RootNode) /// @@ -2954,15 +4174,15 @@ template struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) RootData { using ValueT = typename ChildT::ValueType; - using BuildT = typename ChildT::BuildType;// in rare cases BuildType != ValueType, e.g. then BuildType = ValueMask and ValueType = bool + using BuildT = typename ChildT::BuildType; // in rare cases BuildType != ValueType, e.g. then BuildType = ValueMask and ValueType = bool using CoordT = typename ChildT::CoordType; using StatsT = typename ChildT::FloatType; static constexpr bool FIXED_SIZE = false; /// @brief Return a key based on the coordinates of a voxel -#ifdef USE_SINGLE_ROOT_KEY +#ifdef NANOVDB_USE_SINGLE_ROOT_KEY using KeyT = uint64_t; - template + template __hostdev__ static KeyT CoordToKey(const CoordType& ijk) { static_assert(sizeof(CoordT) == sizeof(CoordType), "Mismatching sizeof"); @@ -2973,10 +4193,10 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) RootData } __hostdev__ static CoordT KeyToCoord(const KeyT& key) { - static constexpr uint64_t MASK = (1u << 21) - 1; - return CoordT(((key >> 42) & MASK) << ChildT::TOTAL, - ((key >> 21) & MASK) << ChildT::TOTAL, - (key & MASK) << ChildT::TOTAL); + static constexpr uint64_t MASK = (1u << 21) - 1; // used to mask out 21 lower bits + return CoordT(((key >> 42) & MASK) << ChildT::TOTAL, // x are the upper 21 bits + ((key >> 21) & MASK) << ChildT::TOTAL, // y are the middle 21 bits + (key & MASK) << ChildT::TOTAL); // z are the lower 21 bits } #else using KeyT = CoordT; @@ -2995,31 +4215,33 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) RootData /// @brief Return padding of this class in bytes, due to aliasing and 32B alignment /// /// @note The extra bytes are not necessarily at the end, but can come from aliasing of individual data members. - __hostdev__ static constexpr uint32_t padding() { - return sizeof(RootData) - (24 + 4 + 3*sizeof(ValueT) + 2*sizeof(StatsT)); + __hostdev__ static constexpr uint32_t padding() + { + return sizeof(RootData) - (24 + 4 + 3 * sizeof(ValueT) + 2 * sizeof(StatsT)); } struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) Tile { - template - __hostdev__ void setChild(const CoordType& k, const ChildT *ptr, const RootData *data) + template + __hostdev__ void setChild(const CoordType& k, const void* ptr, const RootData* data) { key = CoordToKey(k); + state = false; child = PtrDiff(ptr, data); } - template - __hostdev__ void setValue(const CoordType& k, bool s, const ValueType &v) + template + __hostdev__ void setValue(const CoordType& k, bool s, const ValueType& v) { key = CoordToKey(k); state = s; value = v; child = 0; } - __hostdev__ bool isChild() const { return child!=0; } - __hostdev__ bool isValue() const { return child==0; } - __hostdev__ bool isActive() const { return child==0 && state; } + __hostdev__ bool isChild() const { return child != 0; } + __hostdev__ bool isValue() const { return child == 0; } + __hostdev__ bool isActive() const { return child == 0 && state; } __hostdev__ CoordT origin() const { return KeyToCoord(key); } - KeyT key; // USE_SINGLE_ROOT_KEY ? 8B : 12B + KeyT key; // NANOVDB_USE_SINGLE_ROOT_KEY ? 8B : 12B int64_t child; // 8B. signed byte offset from this node to the child node. 0 means it is a constant tile, so use value. uint32_t state; // 4B. state of tile value ValueT value; // value of tile (i.e. no child node) @@ -3039,6 +4261,36 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) RootData return reinterpret_cast(this + 1) + n; } + __hostdev__ Tile* probeTile(const CoordT& ijk) + { +#if 1 // switch between linear and binary seach + const auto key = CoordToKey(ijk); + for (Tile *p = reinterpret_cast(this + 1), *q = p + mTableSize; p < q; ++p) + if (p->key == key) + return p; + return nullptr; +#else // do not enable binary search if tiles are not guaranteed to be sorted!!!!!! + int32_t low = 0, high = mTableSize; // low is inclusive and high is exclusive + while (low != high) { + int mid = low + ((high - low) >> 1); + const Tile* tile = &tiles[mid]; + if (tile->key == key) { + return tile; + } else if (tile->key < key) { + low = mid + 1; + } else { + high = mid; + } + } + return nullptr; +#endif + } + + __hostdev__ inline const Tile* probeTile(const CoordT& ijk) const + { + return const_cast(this)->probeTile(ijk); + } + /// @brief Returns a const reference to the child node in the specified tile. /// /// @warning A child node is assumed to exist in the specified tile @@ -3053,9 +4305,9 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) RootData return PtrAdd(this, tile->child); } - __hostdev__ const ValueT& getMin() const { return mMinimum; } - __hostdev__ const ValueT& getMax() const { return mMaximum; } - __hostdev__ const StatsT& average() const { return mAverage; } + __hostdev__ const ValueT& getMin() const { return mMinimum; } + __hostdev__ const ValueT& getMax() const { return mMaximum; } + __hostdev__ const StatsT& average() const { return mAverage; } __hostdev__ const StatsT& stdDeviation() const { return mStdDevi; } __hostdev__ void setMin(const ValueT& v) { mMinimum = v; } @@ -3070,119 +4322,261 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) RootData ~RootData() = delete; }; // RootData +// --------------------------> RootNode <------------------------------------ + /// @brief Top-most node of the VDB tree structure. template -class RootNode : private RootData +class RootNode : public RootData { public: using DataType = RootData; - using LeafNodeType = typename ChildT::LeafNodeType; using ChildNodeType = ChildT; - using RootType = RootNode;// this allows RootNode to behave like a Tree - + using RootType = RootNode; // this allows RootNode to behave like a Tree + using RootNodeType = RootType; + using UpperNodeType = ChildT; + using LowerNodeType = typename UpperNodeType::ChildNodeType; + using LeafNodeType = typename ChildT::LeafNodeType; using ValueType = typename DataType::ValueT; using FloatType = typename DataType::StatsT; - using BuildType = typename DataType::BuildT;// in rare cases BuildType != ValueType, e.g. then BuildType = ValueMask and ValueType = bool + using BuildType = typename DataType::BuildT; // in rare cases BuildType != ValueType, e.g. then BuildType = ValueMask and ValueType = bool using CoordType = typename ChildT::CoordType; - using BBoxType = BBox; + using BBoxType = BBox; using AccessorType = DefaultReadAccessor; using Tile = typename DataType::Tile; static constexpr bool FIXED_SIZE = DataType::FIXED_SIZE; static constexpr uint32_t LEVEL = 1 + ChildT::LEVEL; // level 0 = leaf - class ChildIterator + template + class BaseIter + { + protected: + using DataT = typename match_const::type; + using TileT = typename match_const::type; + DataT* mData; + uint32_t mPos, mSize; + __hostdev__ BaseIter(DataT* data = nullptr, uint32_t n = 0) + : mData(data) + , mPos(0) + , mSize(n) + { + } + + public: + __hostdev__ operator bool() const { return mPos < mSize; } + __hostdev__ uint32_t pos() const { return mPos; } + __hostdev__ void next() { ++mPos; } + __hostdev__ TileT* tile() const { return mData->tile(mPos); } + __hostdev__ CoordType getOrigin() const + { + NANOVDB_ASSERT(*this); + return this->tile()->origin(); + } + __hostdev__ CoordType getCoord() const + { + NANOVDB_ASSERT(*this); + return this->tile()->origin(); + } + }; // Member class BaseIter + + template + class ChildIter : public BaseIter { - const DataType *mParent; - uint32_t mPos, mSize; + static_assert(is_same::type, RootNode>::value, "Invalid RootT"); + using BaseT = BaseIter; + using NodeT = typename match_const::type; + public: - __hostdev__ ChildIterator() : mParent(nullptr), mPos(0), mSize(0) {} - __hostdev__ ChildIterator(const RootNode *parent) : mParent(parent->data()), mPos(0), mSize(parent->tileCount()) { - NANOVDB_ASSERT(mParent); - while (mPostile(mPos)->isChild()) ++mPos; - } - ChildIterator& operator=(const ChildIterator&) = default; - __hostdev__ const ChildT& operator*() const {NANOVDB_ASSERT(*this); return *mParent->getChild(mParent->tile(mPos));} - __hostdev__ const ChildT* operator->() const {NANOVDB_ASSERT(*this); return mParent->getChild(mParent->tile(mPos));} - __hostdev__ CoordType getOrigin() const { NANOVDB_ASSERT(*this); mParent->tile(mPos)->origin();} - __hostdev__ operator bool() const {return mPos < mSize;} - __hostdev__ uint32_t pos() const {return mPos;} - __hostdev__ ChildIterator& operator++() { - NANOVDB_ASSERT(mParent); - ++mPos; - while (mPos < mSize && mParent->tile(mPos)->isValue()) ++mPos; + __hostdev__ ChildIter() + : BaseT() + { + } + __hostdev__ ChildIter(RootT* parent) + : BaseT(parent->data(), parent->tileCount()) + { + NANOVDB_ASSERT(BaseT::mData); + while (*this && !this->tile()->isChild()) + this->next(); + } + __hostdev__ NodeT& operator*() const + { + NANOVDB_ASSERT(*this); + return *BaseT::mData->getChild(this->tile()); + } + __hostdev__ NodeT* operator->() const + { + NANOVDB_ASSERT(*this); + return BaseT::mData->getChild(this->tile()); + } + __hostdev__ ChildIter& operator++() + { + NANOVDB_ASSERT(BaseT::mData); + this->next(); + while (*this && this->tile()->isValue()) + this->next(); return *this; } - __hostdev__ ChildIterator operator++(int) { + __hostdev__ ChildIter operator++(int) + { auto tmp = *this; ++(*this); return tmp; } - }; // Member class ChildIterator + }; // Member class ChildIter - ChildIterator beginChild() const {return ChildIterator(this);} + using ChildIterator = ChildIter; + using ConstChildIterator = ChildIter; - class ValueIterator + __hostdev__ ChildIterator beginChild() { return ChildIterator(this); } + __hostdev__ ConstChildIterator cbeginChild() const { return ConstChildIterator(this); } + + template + class ValueIter : public BaseIter { - const DataType *mParent; - uint32_t mPos, mSize; + using BaseT = BaseIter; + public: - __hostdev__ ValueIterator() : mParent(nullptr), mPos(0), mSize(0) {} - __hostdev__ ValueIterator(const RootNode *parent) : mParent(parent->data()), mPos(0), mSize(parent->tileCount()){ - NANOVDB_ASSERT(mParent); - while (mPos < mSize && mParent->tile(mPos)->isChild()) ++mPos; - } - ValueIterator& operator=(const ValueIterator&) = default; - __hostdev__ ValueType operator*() const {NANOVDB_ASSERT(*this); return mParent->tile(mPos)->value;} - __hostdev__ bool isActive() const {NANOVDB_ASSERT(*this); return mParent->tile(mPos)->state;} - __hostdev__ operator bool() const {return mPos < mSize;} - __hostdev__ uint32_t pos() const {return mPos;} - __hostdev__ CoordType getOrigin() const { NANOVDB_ASSERT(*this); mParent->tile(mPos)->origin();} - __hostdev__ ValueIterator& operator++() { - NANOVDB_ASSERT(mParent); - ++mPos; - while (mPos < mSize && mParent->tile(mPos)->isChild()) ++mPos; + __hostdev__ ValueIter() + : BaseT() + { + } + __hostdev__ ValueIter(RootT* parent) + : BaseT(parent->data(), parent->tileCount()) + { + NANOVDB_ASSERT(BaseT::mData); + while (*this && this->tile()->isChild()) + this->next(); + } + __hostdev__ ValueType operator*() const + { + NANOVDB_ASSERT(*this); + return this->tile()->value; + } + __hostdev__ bool isActive() const + { + NANOVDB_ASSERT(*this); + return this->tile()->state; + } + __hostdev__ ValueIter& operator++() + { + NANOVDB_ASSERT(BaseT::mData); + this->next(); + while (*this && this->tile()->isChild()) + this->next(); return *this; } - __hostdev__ ValueIterator operator++(int) { + __hostdev__ ValueIter operator++(int) + { auto tmp = *this; ++(*this); return tmp; } - }; // Member class ValueIterator + }; // Member class ValueIter + + using ValueIterator = ValueIter; + using ConstValueIterator = ValueIter; - ValueIterator beginValue() const {return ValueIterator(this);} + __hostdev__ ValueIterator beginValue() { return ValueIterator(this); } + __hostdev__ ConstValueIterator cbeginValueAll() const { return ConstValueIterator(this); } - class ValueOnIterator + template + class ValueOnIter : public BaseIter { - const DataType *mParent; - uint32_t mPos, mSize; + using BaseT = BaseIter; + public: - __hostdev__ ValueOnIterator() : mParent(nullptr), mPos(0), mSize(0) {} - __hostdev__ ValueOnIterator(const RootNode *parent) : mParent(parent->data()), mPos(0), mSize(parent->tileCount()){ - NANOVDB_ASSERT(mParent); - while (mPos < mSize && !mParent->tile(mPos)->isActive()) ++mPos; - } - ValueOnIterator& operator=(const ValueOnIterator&) = default; - __hostdev__ ValueType operator*() const {NANOVDB_ASSERT(*this); return mParent->tile(mPos)->value;} - __hostdev__ operator bool() const {return mPos < mSize;} - __hostdev__ uint32_t pos() const {return mPos;} - __hostdev__ CoordType getOrigin() const { NANOVDB_ASSERT(*this); mParent->tile(mPos)->origin();} - __hostdev__ ValueOnIterator& operator++() { - NANOVDB_ASSERT(mParent); - ++mPos; - while (mPos < mSize && !mParent->tile(mPos)->isActive()) ++mPos; + __hostdev__ ValueOnIter() + : BaseT() + { + } + __hostdev__ ValueOnIter(RootT* parent) + : BaseT(parent->data(), parent->tileCount()) + { + NANOVDB_ASSERT(BaseT::mData); + while (*this && !this->tile()->isActive()) + ++BaseT::mPos; + } + __hostdev__ ValueType operator*() const + { + NANOVDB_ASSERT(*this); + return this->tile()->value; + } + __hostdev__ ValueOnIter& operator++() + { + NANOVDB_ASSERT(BaseT::mData); + this->next(); + while (*this && !this->tile()->isActive()) + this->next(); return *this; } - __hostdev__ ValueOnIterator operator++(int) { + __hostdev__ ValueOnIter operator++(int) + { auto tmp = *this; ++(*this); return tmp; } - }; // Member class ValueOnIterator + }; // Member class ValueOnIter + + using ValueOnIterator = ValueOnIter; + using ConstValueOnIterator = ValueOnIter; - ValueOnIterator beginValueOn() const {return ValueOnIterator(this);} + __hostdev__ ValueOnIterator beginValueOn() { return ValueOnIterator(this); } + __hostdev__ ConstValueOnIterator cbeginValueOn() const { return ConstValueOnIterator(this); } + + template + class DenseIter : public BaseIter + { + using BaseT = BaseIter; + using NodeT = typename match_const::type; + + public: + __hostdev__ DenseIter() + : BaseT() + { + } + __hostdev__ DenseIter(RootT* parent) + : BaseT(parent->data(), parent->tileCount()) + { + NANOVDB_ASSERT(BaseT::mData); + } + __hostdev__ NodeT* probeChild(ValueType& value) const + { + NANOVDB_ASSERT(*this); + NodeT* child = nullptr; + auto* t = this->tile(); + if (t->isChild()) { + child = BaseT::mData->getChild(t); + } else { + value = t->value; + } + return child; + } + __hostdev__ bool isValueOn() const + { + NANOVDB_ASSERT(*this); + return this->tile()->state; + } + __hostdev__ DenseIter& operator++() + { + NANOVDB_ASSERT(BaseT::mData); + this->next(); + return *this; + } + __hostdev__ DenseIter operator++(int) + { + auto tmp = *this; + ++(*this); + return tmp; + } + }; // Member class DenseIter + + using DenseIterator = DenseIter; + using ConstDenseIterator = DenseIter; + + __hostdev__ DenseIterator beginDense() { return DenseIterator(this); } + __hostdev__ ConstDenseIterator cbeginDense() const { return ConstDenseIterator(this); } + __hostdev__ ConstDenseIterator cbeginChildAll() const { return ConstDenseIterator(this); } /// @brief This class cannot be constructed or deleted RootNode() = delete; @@ -3207,18 +4601,19 @@ class RootNode : private RootData /// @brief Return the number of tiles encoded in this root node __hostdev__ const uint32_t& tileCount() const { return DataType::mTableSize; } + __hostdev__ const uint32_t& getTableSize() const { return DataType::mTableSize; } /// @brief Return a const reference to the minimum active value encoded in this root node and any of its child nodes - __hostdev__ const ValueType& minimum() const { return this->getMin(); } + __hostdev__ const ValueType& minimum() const { return DataType::mMinimum; } /// @brief Return a const reference to the maximum active value encoded in this root node and any of its child nodes - __hostdev__ const ValueType& maximum() const { return this->getMax(); } + __hostdev__ const ValueType& maximum() const { return DataType::mMaximum; } /// @brief Return a const reference to the average of all the active values encoded in this root node and any of its child nodes __hostdev__ const FloatType& average() const { return DataType::mAverage; } /// @brief Return the variance of all the active values encoded in this root node and any of its child nodes - __hostdev__ FloatType variance() const { return DataType::mStdDevi * DataType::mStdDevi; } + __hostdev__ FloatType variance() const { return Pow2(DataType::mStdDevi); } /// @brief Return a const reference to the standard deviation of all the active values encoded in this root node and any of its child nodes __hostdev__ const FloatType& stdDeviation() const { return DataType::mStdDevi; } @@ -3229,31 +4624,42 @@ class RootNode : private RootData /// @brief Return the actual memory footprint of this root node __hostdev__ uint64_t memUsage() const { return sizeof(RootNode) + DataType::mTableSize * sizeof(Tile); } + /// @brief Return true if this RootNode is empty, i.e. contains no values or nodes + __hostdev__ bool isEmpty() const { return DataType::mTableSize == uint32_t(0); } + +#ifdef NANOVDB_NEW_ACCESSOR_METHODS + /// @brief Return the value of the given voxel + __hostdev__ ValueType getValue(const CoordType& ijk) const { return this->template get>(ijk); } + __hostdev__ ValueType getValue(int i, int j, int k) const { return this->template get>(CoordType(i, j, k)); } + __hostdev__ bool isActive(const CoordType& ijk) const { return this->template get>(ijk); } + /// @brief return the state and updates the value of the specified voxel + __hostdev__ bool probeValue(const CoordType& ijk, ValueType& v) const { return this->template get>(ijk, v); } + __hostdev__ const LeafNodeType* probeLeaf(const CoordType& ijk) const { return this->template get>(ijk); } +#else // NANOVDB_NEW_ACCESSOR_METHODS + /// @brief Return the value of the given voxel __hostdev__ ValueType getValue(const CoordType& ijk) const { - if (const Tile* tile = this->probeTile(ijk)) { + if (const Tile* tile = DataType::probeTile(ijk)) { return tile->isChild() ? this->getChild(tile)->getValue(ijk) : tile->value; } return DataType::mBackground; } + __hostdev__ ValueType getValue(int i, int j, int k) const { return this->getValue(CoordType(i, j, k)); } __hostdev__ bool isActive(const CoordType& ijk) const { - if (const Tile* tile = this->probeTile(ijk)) { + if (const Tile* tile = DataType::probeTile(ijk)) { return tile->isChild() ? this->getChild(tile)->isActive(ijk) : tile->state; } return false; } - /// @brief Return true if this RootNode is empty, i.e. contains no values or nodes - __hostdev__ bool isEmpty() const { return DataType::mTableSize == uint32_t(0); } - __hostdev__ bool probeValue(const CoordType& ijk, ValueType& v) const { - if (const Tile* tile = this->probeTile(ijk)) { + if (const Tile* tile = DataType::probeTile(ijk)) { if (tile->isChild()) { - const auto *child = this->getChild(tile); + const auto* child = this->getChild(tile); return child->probeValue(ijk, v); } v = tile->value; @@ -3265,48 +4671,50 @@ class RootNode : private RootData __hostdev__ const LeafNodeType* probeLeaf(const CoordType& ijk) const { - const Tile* tile = this->probeTile(ijk); + const Tile* tile = DataType::probeTile(ijk); if (tile && tile->isChild()) { - const auto *child = this->getChild(tile); + const auto* child = this->getChild(tile); return child->probeLeaf(ijk); } return nullptr; } +#endif // NANOVDB_NEW_ACCESSOR_METHODS + __hostdev__ const ChildNodeType* probeChild(const CoordType& ijk) const { - const Tile* tile = this->probeTile(ijk); - if (tile && tile->isChild()) { - return this->getChild(tile); - } - return nullptr; + const Tile* tile = DataType::probeTile(ijk); + return tile && tile->isChild() ? this->getChild(tile) : nullptr; + } + + __hostdev__ ChildNodeType* probeChild(const CoordType& ijk) + { + const Tile* tile = DataType::probeTile(ijk); + return tile && tile->isChild() ? this->getChild(tile) : nullptr; } - /// @brief Find and return a Tile of this root node - __hostdev__ const Tile* probeTile(const CoordType& ijk) const + template + __hostdev__ auto get(const CoordType& ijk, ArgsT&&... args) const { - const Tile* tiles = reinterpret_cast(this + 1); - const auto key = DataType::CoordToKey(ijk); -#if 1 // switch between linear and binary seach - for (uint32_t i = 0; i < DataType::mTableSize; ++i) { - if (tiles[i].key == key) return &tiles[i]; + if (const Tile* tile = this->probeTile(ijk)) { + if (tile->isChild()) + return this->getChild(tile)->template get(ijk, args...); + return OpT::get(*tile, args...); } -#else// do not enable binary search if tiles are not guaranteed to be sorted!!!!!! - // binary-search of pre-sorted elements - int32_t low = 0, high = DataType::mTableSize; // low is inclusive and high is exclusive - while (low != high) { - int mid = low + ((high - low) >> 1); - const Tile* tile = &tiles[mid]; - if (tile->key == key) { - return tile; - } else if (tile->key < key) { - low = mid + 1; - } else { - high = mid; - } + return OpT::get(*this, args...); + } + + template + // __hostdev__ auto // occasionally fails with NVCC + __hostdev__ decltype(OpT::set(std::declval(), std::declval()...)) + set(const CoordType& ijk, ArgsT&&... args) + { + if (Tile* tile = DataType::probeTile(ijk)) { + if (tile->isChild()) + return this->getChild(tile)->template set(ijk, args...); + return OpT::set(*tile, args...); } -#endif - return nullptr; + return OpT::set(*this, args...); } private: @@ -3318,7 +4726,7 @@ class RootNode : private RootData template friend class Tree; - +#ifndef NANOVDB_NEW_ACCESSOR_METHODS /// @brief Private method to return node information and update a ReadAccessor template __hostdev__ typename AccT::NodeInfo getNodeInfoAndCache(const CoordType& ijk, const AccT& acc) const @@ -3326,15 +4734,13 @@ class RootNode : private RootData using NodeInfoT = typename AccT::NodeInfo; if (const Tile* tile = this->probeTile(ijk)) { if (tile->isChild()) { - const auto *child = this->getChild(tile); + const auto* child = this->getChild(tile); acc.insert(ijk, child); return child->getNodeInfoAndCache(ijk, acc); } - return NodeInfoT{LEVEL, ChildT::dim(), tile->value, tile->value, tile->value, - 0, tile->origin(), tile->origin() + CoordType(ChildT::DIM)}; + return NodeInfoT{LEVEL, ChildT::dim(), tile->value, tile->value, tile->value, 0, tile->origin(), tile->origin() + CoordType(ChildT::DIM)}; } - return NodeInfoT{LEVEL, ChildT::dim(), this->minimum(), this->maximum(), - this->average(), this->stdDeviation(), this->bbox()[0], this->bbox()[1]}; + return NodeInfoT{LEVEL, ChildT::dim(), this->minimum(), this->maximum(), this->average(), this->stdDeviation(), this->bbox()[0], this->bbox()[1]}; } /// @brief Private method to return a voxel value and update a ReadAccessor @@ -3343,7 +4749,7 @@ class RootNode : private RootData { if (const Tile* tile = this->probeTile(ijk)) { if (tile->isChild()) { - const auto *child = this->getChild(tile); + const auto* child = this->getChild(tile); acc.insert(ijk, child); return child->getValueAndCache(ijk, acc); } @@ -3357,7 +4763,7 @@ class RootNode : private RootData { const Tile* tile = this->probeTile(ijk); if (tile && tile->isChild()) { - const auto *child = this->getChild(tile); + const auto* child = this->getChild(tile); acc.insert(ijk, child); return child->isActiveAndCache(ijk, acc); } @@ -3369,7 +4775,7 @@ class RootNode : private RootData { if (const Tile* tile = this->probeTile(ijk)) { if (tile->isChild()) { - const auto *child = this->getChild(tile); + const auto* child = this->getChild(tile); acc.insert(ijk, child); return child->probeValueAndCache(ijk, v, acc); } @@ -3385,19 +4791,20 @@ class RootNode : private RootData { const Tile* tile = this->probeTile(ijk); if (tile && tile->isChild()) { - const auto *child = this->getChild(tile); + const auto* child = this->getChild(tile); acc.insert(ijk, child); return child->probeLeafAndCache(ijk, acc); } return nullptr; } +#endif // NANOVDB_NEW_ACCESSOR_METHODS template __hostdev__ uint32_t getDimAndCache(const CoordType& ijk, const RayT& ray, const AccT& acc) const { if (const Tile* tile = this->probeTile(ijk)) { if (tile->isChild()) { - const auto *child = this->getChild(tile); + const auto* child = this->getChild(tile); acc.insert(ijk, child); return child->getDimAndCache(ijk, ray, acc); } @@ -3406,6 +4813,38 @@ class RootNode : private RootData return ChildNodeType::dim(); // background } + template + //__hostdev__ decltype(OpT::get(std::declval(), std::declval()...)) + __hostdev__ auto + getAndCache(const CoordType& ijk, const AccT& acc, ArgsT&&... args) const + { + if (const Tile* tile = this->probeTile(ijk)) { + if (tile->isChild()) { + const ChildT* child = this->getChild(tile); + acc.insert(ijk, child); + return child->template getAndCache(ijk, acc, args...); + } + return OpT::get(*tile, args...); + } + return OpT::get(*this, args...); + } + + template + // __hostdev__ auto // occasionally fails with NVCC + __hostdev__ decltype(OpT::set(std::declval(), std::declval()...)) + setAndCache(const CoordType& ijk, const AccT& acc, ArgsT&&... args) + { + if (Tile* tile = DataType::probeTile(ijk)) { + if (tile->isChild()) { + ChildT* child = this->getChild(tile); + acc.insert(ijk, child); + return child->template setAndCache(ijk, acc, args...); + } + return OpT::set(*tile, args...); + } + return OpT::set(*this, args...); + } + }; // RootNode class // After the RootNode the memory layout is assumed to be the sorted Tiles @@ -3419,16 +4858,16 @@ template struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) InternalData { using ValueT = typename ChildT::ValueType; - using BuildT = typename ChildT::BuildType;// in rare cases BuildType != ValueType, e.g. then BuildType = ValueMask and ValueType = bool + using BuildT = typename ChildT::BuildType; // in rare cases BuildType != ValueType, e.g. then BuildType = ValueMask and ValueType = bool using StatsT = typename ChildT::FloatType; using CoordT = typename ChildT::CoordType; - using MaskT = typename ChildT::template MaskType; + using MaskT = typename ChildT::template MaskType; static constexpr bool FIXED_SIZE = true; union Tile { ValueT value; - int64_t child;//signed 64 bit byte offset relative to the InternalData!! + int64_t child; //signed 64 bit byte offset relative to this InternalData, i.e. child-pointer = Tile::child + this /// @brief This class cannot be constructed or deleted Tile() = delete; Tile(const Tile&) = delete; @@ -3450,22 +4889,22 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) InternalData /// @brief Return padding of this class in bytes, due to aliasing and 32B alignment /// /// @note The extra bytes are not necessarily at the end, but can come from aliasing of individual data members. - __hostdev__ static constexpr uint32_t padding() { - return sizeof(InternalData) - (24u + 8u + 2*(sizeof(MaskT) + sizeof(ValueT) + sizeof(StatsT)) - + (1u << (3 * LOG2DIM))*(sizeof(ValueT) > 8u ? sizeof(ValueT) : 8u)); + __hostdev__ static constexpr uint32_t padding() + { + return sizeof(InternalData) - (24u + 8u + 2 * (sizeof(MaskT) + sizeof(ValueT) + sizeof(StatsT)) + (1u << (3 * LOG2DIM)) * (sizeof(ValueT) > 8u ? sizeof(ValueT) : 8u)); } alignas(32) Tile mTable[1u << (3 * LOG2DIM)]; // sizeof(ValueT) x (16*16*16 or 32*32*32) __hostdev__ static uint64_t memUsage() { return sizeof(InternalData); } - __hostdev__ void setChild(uint32_t n, const void *ptr) + __hostdev__ void setChild(uint32_t n, const void* ptr) { NANOVDB_ASSERT(mChildMask.isOn(n)); mTable[n].child = PtrDiff(ptr, this); } - template - __hostdev__ void setValue(uint32_t n, const ValueT &v) + template + __hostdev__ void setValue(uint32_t n, const ValueT& v) { NANOVDB_ASSERT(!mChildMask.isOn(n)); mTable[n].value = v; @@ -3485,30 +4924,37 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) InternalData __hostdev__ ValueT getValue(uint32_t n) const { - NANOVDB_ASSERT(!mChildMask.isOn(n)); + NANOVDB_ASSERT(mChildMask.isOff(n)); return mTable[n].value; } __hostdev__ bool isActive(uint32_t n) const { - NANOVDB_ASSERT(!mChildMask.isOn(n)); + NANOVDB_ASSERT(mChildMask.isOff(n)); return mValueMask.isOn(n); } - __hostdev__ bool isChild(uint32_t n) const {return mChildMask.isOn(n);} + __hostdev__ bool isChild(uint32_t n) const { return mChildMask.isOn(n); } - template + template __hostdev__ void setOrigin(const T& ijk) { mBBox[0] = ijk; } - __hostdev__ const ValueT& getMin() const { return mMinimum; } - __hostdev__ const ValueT& getMax() const { return mMaximum; } - __hostdev__ const StatsT& average() const { return mAverage; } + __hostdev__ const ValueT& getMin() const { return mMinimum; } + __hostdev__ const ValueT& getMax() const { return mMaximum; } + __hostdev__ const StatsT& average() const { return mAverage; } __hostdev__ const StatsT& stdDeviation() const { return mStdDevi; } +#if defined(__GNUC__) && !defined(__APPLE__) && !defined(__llvm__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wstringop-overflow" +#endif __hostdev__ void setMin(const ValueT& v) { mMinimum = v; } __hostdev__ void setMax(const ValueT& v) { mMaximum = v; } __hostdev__ void setAvg(const StatsT& v) { mAverage = v; } __hostdev__ void setDev(const StatsT& v) { mStdDevi = v; } +#if defined(__GNUC__) && !defined(__APPLE__) && !defined(__llvm__) +#pragma GCC diagnostic pop +#endif /// @brief This class cannot be constructed or deleted InternalData() = delete; @@ -3519,7 +4965,7 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) InternalData /// @brief Internal nodes of a VDB treedim(), template -class InternalNode : private InternalData +class InternalNode : public InternalData { public: using DataType = InternalData; @@ -3544,51 +4990,167 @@ class InternalNode : private InternalData static constexpr uint64_t NUM_VALUES = uint64_t(1) << (3 * TOTAL); // total voxel count represented by this node /// @brief Visits child nodes of this node only - class ChildIterator : public MaskIterT + template + class ChildIter : public MaskIterT { + static_assert(is_same::type, InternalNode>::value, "Invalid ParentT"); using BaseT = MaskIterT; - const DataType *mParent; + using NodeT = typename match_const::type; + ParentT* mParent; + public: - __hostdev__ ChildIterator() : BaseT(), mParent(nullptr) {} - __hostdev__ ChildIterator(const InternalNode* parent) : BaseT(parent->data()->mChildMask.beginOn()), mParent(parent->data()) {} - ChildIterator& operator=(const ChildIterator&) = default; - __hostdev__ const ChildT& operator*() const {NANOVDB_ASSERT(*this); return *mParent->getChild(BaseT::pos());} - __hostdev__ const ChildT* operator->() const {NANOVDB_ASSERT(*this); return mParent->getChild(BaseT::pos());} - __hostdev__ CoordType getOrigin() const { NANOVDB_ASSERT(*this); return (*this)->origin();} - }; // Member class ChildIterator + __hostdev__ ChildIter() + : BaseT() + , mParent(nullptr) + { + } + __hostdev__ ChildIter(ParentT* parent) + : BaseT(parent->mChildMask.beginOn()) + , mParent(parent) + { + } + ChildIter& operator=(const ChildIter&) = default; + __hostdev__ NodeT& operator*() const + { + NANOVDB_ASSERT(*this); + return *mParent->getChild(BaseT::pos()); + } + __hostdev__ NodeT* operator->() const + { + NANOVDB_ASSERT(*this); + return mParent->getChild(BaseT::pos()); + } + __hostdev__ CoordType getOrigin() const + { + NANOVDB_ASSERT(*this); + return (*this)->origin(); + } + __hostdev__ CoordType getCoord() const {return this->getOrigin();} + }; // Member class ChildIter - ChildIterator beginChild() const {return ChildIterator(this);} + using ChildIterator = ChildIter; + using ConstChildIterator = ChildIter; + + __hostdev__ ChildIterator beginChild() { return ChildIterator(this); } + __hostdev__ ConstChildIterator cbeginChild() const { return ConstChildIterator(this); } /// @brief Visits all tile values in this node, i.e. both inactive and active tiles class ValueIterator : public MaskIterT { using BaseT = MaskIterT; - const InternalNode *mParent; + const InternalNode* mParent; + public: - __hostdev__ ValueIterator() : BaseT(), mParent(nullptr) {} - __hostdev__ ValueIterator(const InternalNode* parent) : BaseT(parent->data()->mChildMask.beginOff()), mParent(parent) {} - ValueIterator& operator=(const ValueIterator&) = default; - __hostdev__ ValueType operator*() const {NANOVDB_ASSERT(*this); return mParent->data()->getValue(BaseT::pos());} - __hostdev__ CoordType getOrigin() const { NANOVDB_ASSERT(*this); return mParent->localToGlobalCoord(BaseT::pos());} - __hostdev__ bool isActive() const { NANOVDB_ASSERT(*this); return mParent->data()->isActive(BaseT::mPos);} + __hostdev__ ValueIterator() + : BaseT() + , mParent(nullptr) + { + } + __hostdev__ ValueIterator(const InternalNode* parent) + : BaseT(parent->data()->mChildMask.beginOff()) + , mParent(parent) + { + } + ValueIterator& operator=(const ValueIterator&) = default; + __hostdev__ ValueType operator*() const + { + NANOVDB_ASSERT(*this); + return mParent->data()->getValue(BaseT::pos()); + } + __hostdev__ CoordType getOrigin() const + { + NANOVDB_ASSERT(*this); + return mParent->offsetToGlobalCoord(BaseT::pos()); + } + __hostdev__ CoordType getCoord() const {return this->getOrigin();} + __hostdev__ bool isActive() const + { + NANOVDB_ASSERT(*this); + return mParent->data()->isActive(BaseT::mPos); + } }; // Member class ValueIterator - ValueIterator beginValue() const {return ValueIterator(this);} + __hostdev__ ValueIterator beginValue() const { return ValueIterator(this); } + __hostdev__ ValueIterator cbeginValueAll() const { return ValueIterator(this); } /// @brief Visits active tile values of this node only class ValueOnIterator : public MaskIterT { using BaseT = MaskIterT; - const InternalNode *mParent; + const InternalNode* mParent; + public: - __hostdev__ ValueOnIterator() : BaseT(), mParent(nullptr) {} - __hostdev__ ValueOnIterator(const InternalNode* parent) : BaseT(parent->data()->mValueMask.beginOn()), mParent(parent) {} - ValueOnIterator& operator=(const ValueOnIterator&) = default; - __hostdev__ ValueType operator*() const {NANOVDB_ASSERT(*this); return mParent->data()->getValue(BaseT::pos());} - __hostdev__ CoordType getOrigin() const { NANOVDB_ASSERT(*this); return mParent->localToGlobalCoord(BaseT::pos());} + __hostdev__ ValueOnIterator() + : BaseT() + , mParent(nullptr) + { + } + __hostdev__ ValueOnIterator(const InternalNode* parent) + : BaseT(parent->data()->mValueMask.beginOn()) + , mParent(parent) + { + } + ValueOnIterator& operator=(const ValueOnIterator&) = default; + __hostdev__ ValueType operator*() const + { + NANOVDB_ASSERT(*this); + return mParent->data()->getValue(BaseT::pos()); + } + __hostdev__ CoordType getOrigin() const + { + NANOVDB_ASSERT(*this); + return mParent->offsetToGlobalCoord(BaseT::pos()); + } + __hostdev__ CoordType getCoord() const {return this->getOrigin();} }; // Member class ValueOnIterator - ValueOnIterator beginValueOn() const {return ValueOnIterator(this);} + __hostdev__ ValueOnIterator beginValueOn() const { return ValueOnIterator(this); } + __hostdev__ ValueOnIterator cbeginValueOn() const { return ValueOnIterator(this); } + + /// @brief Visits all tile values and child nodes of this node + class DenseIterator : public Mask::DenseIterator + { + using BaseT = typename Mask::DenseIterator; + const DataType* mParent; + + public: + __hostdev__ DenseIterator() + : BaseT() + , mParent(nullptr) + { + } + __hostdev__ DenseIterator(const InternalNode* parent) + : BaseT(0) + , mParent(parent->data()) + { + } + DenseIterator& operator=(const DenseIterator&) = default; + __hostdev__ const ChildT* probeChild(ValueType& value) const + { + NANOVDB_ASSERT(mParent && bool(*this)); + const ChildT* child = nullptr; + if (mParent->mChildMask.isOn(BaseT::pos())) { + child = mParent->getChild(BaseT::pos()); + } else { + value = mParent->getValue(BaseT::pos()); + } + return child; + } + __hostdev__ bool isValueOn() const + { + NANOVDB_ASSERT(mParent && bool(*this)); + return mParent->isActive(BaseT::pos()); + } + __hostdev__ CoordType getOrigin() const + { + NANOVDB_ASSERT(mParent && bool(*this)); + return mParent->offsetToGlobalCoord(BaseT::pos()); + } + __hostdev__ CoordType getCoord() const {return this->getOrigin();} + }; // Member class DenseIterator + + __hostdev__ DenseIterator beginDense() const { return DenseIterator(this); } + __hostdev__ DenseIterator cbeginChildAll() const { return DenseIterator(this); } // matches openvdb /// @brief This class cannot be constructed or deleted InternalNode() = delete; @@ -3608,9 +5170,11 @@ class InternalNode : private InternalData /// @brief Return a const reference to the bit mask of active voxels in this internal node __hostdev__ const MaskType& valueMask() const { return DataType::mValueMask; } + __hostdev__ const MaskType& getValueMask() const { return DataType::mValueMask; } /// @brief Return a const reference to the bit mask of child nodes in this internal node __hostdev__ const MaskType& childMask() const { return DataType::mChildMask; } + __hostdev__ const MaskType& getChildMask() const { return DataType::mChildMask; } /// @brief Return the origin in index space of this leaf node __hostdev__ CoordType origin() const { return DataType::mBBox.min() & ~MASK; } @@ -3625,7 +5189,7 @@ class InternalNode : private InternalData __hostdev__ const FloatType& average() const { return DataType::mAverage; } /// @brief Return the variance of all the active values encoded in this internal node and any of its child nodes - __hostdev__ FloatType variance() const { return DataType::mStdDevi*DataType::mStdDevi; } + __hostdev__ FloatType variance() const { return DataType::mStdDevi * DataType::mStdDevi; } /// @brief Return a const reference to the standard deviation of all the active values encoded in this internal node and any of its child nodes __hostdev__ const FloatType& stdDeviation() const { return DataType::mStdDevi; } @@ -3633,20 +5197,38 @@ class InternalNode : private InternalData /// @brief Return a const reference to the bounding box in index space of active values in this internal node and any of its child nodes __hostdev__ const BBox& bbox() const { return DataType::mBBox; } + /// @brief If the first entry in this node's table is a tile, return the tile's value. + /// Otherwise, return the result of calling getFirstValue() on the child. + __hostdev__ ValueType getFirstValue() const + { + return DataType::mChildMask.isOn(0) ? this->getChild(0)->getFirstValue() : DataType::getValue(0); + } + + /// @brief If the last entry in this node's table is a tile, return the tile's value. + /// Otherwise, return the result of calling getLastValue() on the child. + __hostdev__ ValueType getLastValue() const + { + return DataType::mChildMask.isOn(SIZE - 1) ? this->getChild(SIZE - 1)->getLastValue() : DataType::getValue(SIZE - 1); + } + +#ifdef NANOVDB_NEW_ACCESSOR_METHODS /// @brief Return the value of the given voxel + __hostdev__ ValueType getValue(const CoordType& ijk) const { return this->template get>(ijk); } + __hostdev__ bool isActive(const CoordType& ijk) const { return this->template get>(ijk); } + /// @brief return the state and updates the value of the specified voxel + __hostdev__ bool probeValue(const CoordType& ijk, ValueType& v) const { return this->template get>(ijk, v); } + __hostdev__ const LeafNodeType* probeLeaf(const CoordType& ijk) const { return this->template get>(ijk); } +#else // NANOVDB_NEW_ACCESSOR_METHODS __hostdev__ ValueType getValue(const CoordType& ijk) const { const uint32_t n = CoordToOffset(ijk); return DataType::mChildMask.isOn(n) ? this->getChild(n)->getValue(ijk) : DataType::getValue(n); } - __hostdev__ bool isActive(const CoordType& ijk) const { const uint32_t n = CoordToOffset(ijk); return DataType::mChildMask.isOn(n) ? this->getChild(n)->isActive(ijk) : DataType::isActive(n); } - - /// @brief return the state and updates the value of the specified voxel __hostdev__ bool probeValue(const CoordType& ijk, ValueType& v) const { const uint32_t n = CoordToOffset(ijk); @@ -3655,7 +5237,6 @@ class InternalNode : private InternalData v = DataType::getValue(n); return DataType::isActive(n); } - __hostdev__ const LeafNodeType* probeLeaf(const CoordType& ijk) const { const uint32_t n = CoordToOffset(ijk); @@ -3664,6 +5245,13 @@ class InternalNode : private InternalData return nullptr; } +#endif // NANOVDB_NEW_ACCESSOR_METHODS + + __hostdev__ ChildNodeType* probeChild(const CoordType& ijk) + { + const uint32_t n = CoordToOffset(ijk); + return DataType::mChildMask.isOn(n) ? this->getChild(n) : nullptr; + } __hostdev__ const ChildNodeType* probeChild(const CoordType& ijk) const { const uint32_t n = CoordToOffset(ijk); @@ -3673,15 +5261,9 @@ class InternalNode : private InternalData /// @brief Return the linear offset corresponding to the given coordinate __hostdev__ static uint32_t CoordToOffset(const CoordType& ijk) { -#if 0 - return (((ijk[0] & MASK) >> ChildT::TOTAL) << (2 * LOG2DIM)) + - (((ijk[1] & MASK) >> ChildT::TOTAL) << (LOG2DIM)) + - ((ijk[2] & MASK) >> ChildT::TOTAL); -#else - return (((ijk[0] & MASK) >> ChildT::TOTAL) << (2 * LOG2DIM)) | + return (((ijk[0] & MASK) >> ChildT::TOTAL) << (2 * LOG2DIM)) | // note, we're using bitwise OR instead of + (((ijk[1] & MASK) >> ChildT::TOTAL) << (LOG2DIM)) | - ((ijk[2] & MASK) >> ChildT::TOTAL); -#endif + ((ijk[2] & MASK) >> ChildT::TOTAL); } /// @return the local coordinate of the n'th tile or child node @@ -3707,14 +5289,30 @@ class InternalNode : private InternalData } /// @brief Return true if this node or any of its child nodes contain active values - __hostdev__ bool isActive() const + __hostdev__ bool isActive() const { return DataType::mFlags & uint32_t(2); } + + template + __hostdev__ auto get(const CoordType& ijk, ArgsT&&... args) const { - return DataType::mFlags & uint32_t(2); + const uint32_t n = CoordToOffset(ijk); + if (this->isChild(n)) + return this->getChild(n)->template get(ijk, args...); + return OpT::get(*this, n, args...); + } + + template + //__hostdev__ auto // occasionally fails with NVCC + __hostdev__ decltype(OpT::set(std::declval(), std::declval(), std::declval()...)) + set(const CoordType& ijk, ArgsT&&... args) + { + const uint32_t n = CoordToOffset(ijk); + if (this->isChild(n)) + return this->getChild(n)->template set(ijk, args...); + return OpT::set(*this, n, args...); } private: static_assert(sizeof(DataType) % NANOVDB_DATA_ALIGNMENT == 0, "sizeof(InternalData) is misaligned"); - //static_assert(offsetof(DataType, mTable) % 32 == 0, "InternalData::mTable is misaligned"); template friend class ReadAccessor; @@ -3724,48 +5322,33 @@ class InternalNode : private InternalData template friend class InternalNode; +#ifndef NANOVDB_NEW_ACCESSOR_METHODS /// @brief Private read access method used by the ReadAccessor template __hostdev__ ValueType getValueAndCache(const CoordType& ijk, const AccT& acc) const { const uint32_t n = CoordToOffset(ijk); - if (!DataType::mChildMask.isOn(n)) + if (DataType::mChildMask.isOff(n)) return DataType::getValue(n); const ChildT* child = this->getChild(n); acc.insert(ijk, child); return child->getValueAndCache(ijk, acc); } - - template - __hostdev__ typename AccT::NodeInfo getNodeInfoAndCache(const CoordType& ijk, const AccT& acc) const - { - using NodeInfoT = typename AccT::NodeInfo; - const uint32_t n = CoordToOffset(ijk); - if (!DataType::mChildMask.isOn(n)) { - return NodeInfoT{LEVEL, this->dim(), this->minimum(), this->maximum(), this->average(), - this->stdDeviation(), this->bbox()[0], this->bbox()[1]}; - } - const ChildT* child = this->getChild(n); - acc.insert(ijk, child); - return child->getNodeInfoAndCache(ijk, acc); - } - template __hostdev__ bool isActiveAndCache(const CoordType& ijk, const AccT& acc) const { const uint32_t n = CoordToOffset(ijk); - if (!DataType::mChildMask.isOn(n)) + if (DataType::mChildMask.isOff(n)) return DataType::isActive(n); const ChildT* child = this->getChild(n); acc.insert(ijk, child); return child->isActiveAndCache(ijk, acc); } - template __hostdev__ bool probeValueAndCache(const CoordType& ijk, ValueType& v, const AccT& acc) const { const uint32_t n = CoordToOffset(ijk); - if (!DataType::mChildMask.isOn(n)) { + if (DataType::mChildMask.isOff(n)) { v = DataType::getValue(n); return DataType::isActive(n); } @@ -3773,22 +5356,35 @@ class InternalNode : private InternalData acc.insert(ijk, child); return child->probeValueAndCache(ijk, v, acc); } - template __hostdev__ const LeafNodeType* probeLeafAndCache(const CoordType& ijk, const AccT& acc) const { const uint32_t n = CoordToOffset(ijk); - if (!DataType::mChildMask.isOn(n)) + if (DataType::mChildMask.isOff(n)) return nullptr; const ChildT* child = this->getChild(n); acc.insert(ijk, child); return child->probeLeafAndCache(ijk, acc); } + template + __hostdev__ typename AccT::NodeInfo getNodeInfoAndCache(const CoordType& ijk, const AccT& acc) const + { + using NodeInfoT = typename AccT::NodeInfo; + const uint32_t n = CoordToOffset(ijk); + if (DataType::mChildMask.isOff(n)) { + return NodeInfoT{LEVEL, this->dim(), this->minimum(), this->maximum(), this->average(), this->stdDeviation(), this->bbox()[0], this->bbox()[1]}; + } + const ChildT* child = this->getChild(n); + acc.insert(ijk, child); + return child->getNodeInfoAndCache(ijk, acc); + } +#endif // NANOVDB_NEW_ACCESSOR_METHODS template __hostdev__ uint32_t getDimAndCache(const CoordType& ijk, const RayT& ray, const AccT& acc) const { - if (DataType::mFlags & uint32_t(1u)) return this->dim(); // skip this node if the 1st bit is set + if (DataType::mFlags & uint32_t(1u)) + return this->dim(); // skip this node if the 1st bit is set //if (!ray.intersects( this->bbox() )) return 1< return ChildNodeType::dim(); // tile value } + template + __hostdev__ auto + //__hostdev__ decltype(OpT::get(std::declval(), std::declval(), std::declval()...)) + getAndCache(const CoordType& ijk, const AccT& acc, ArgsT&&... args) const + { + const uint32_t n = CoordToOffset(ijk); + if (DataType::mChildMask.isOff(n)) + return OpT::get(*this, n, args...); + const ChildT* child = this->getChild(n); + acc.insert(ijk, child); + return child->template getAndCache(ijk, acc, args...); + } + + template + //__hostdev__ auto // occasionally fails with NVCC + __hostdev__ decltype(OpT::set(std::declval(), std::declval(), std::declval()...)) + setAndCache(const CoordType& ijk, const AccT& acc, ArgsT&&... args) + { + const uint32_t n = CoordToOffset(ijk); + if (DataType::mChildMask.isOff(n)) + return OpT::set(*this, n, args...); + ChildT* child = this->getChild(n); + acc.insert(ijk, child); + return child->template setAndCache(ijk, acc, args...); + } + }; // InternalNode class -// --------------------------> LeafNode <------------------------------------ +// --------------------------> LeafData <------------------------------------ /// @brief Stuct with all the member data of the LeafNode (useful during serialization of an openvdb LeafNode) /// @@ -3815,12 +5437,12 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData using ValueType = ValueT; using BuildType = ValueT; using FloatType = typename FloatTraits::FloatType; - using ArrayType = ValueT;// type used for the internal mValue array + using ArrayType = ValueT; // type used for the internal mValue array static constexpr bool FIXED_SIZE = true; CoordT mBBoxMin; // 12B. uint8_t mBBoxDif[3]; // 3B. - uint8_t mFlags; // 1B. bit0: skip render?, bit1: has bbox?, bit3: unused, bit4: is sparse ValueIndex, bits5,6,7: bit-width for FpN + uint8_t mFlags; // 1B. bit0: skip render?, bit1: has bbox?, bit3: unused, bit4: has stats, bits5,6,7: bit-width for FpN MaskT mValueMask; // LOG2DIM(3): 64B. ValueType mMinimum; // typically 4B @@ -3832,21 +5454,22 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData /// @brief Return padding of this class in bytes, due to aliasing and 32B alignment /// /// @note The extra bytes are not necessarily at the end, but can come from aliasing of individual data members. - __hostdev__ static constexpr uint32_t padding() { - return sizeof(LeafData) - (12 + 3 + 1 + sizeof(MaskT) - + 2*(sizeof(ValueT) + sizeof(FloatType)) - + (1u << (3 * LOG2DIM))*sizeof(ValueT)); + __hostdev__ static constexpr uint32_t padding() + { + return sizeof(LeafData) - (12 + 3 + 1 + sizeof(MaskT) + 2 * (sizeof(ValueT) + sizeof(FloatType)) + (1u << (3 * LOG2DIM)) * sizeof(ValueT)); } __hostdev__ static uint64_t memUsage() { return sizeof(LeafData); } - //__hostdev__ const ValueType* values() const { return mValues; } + __hostdev__ static bool hasStats() { return true; } + __hostdev__ ValueType getValue(uint32_t i) const { return mValues[i]; } - __hostdev__ void setValueOnly(uint32_t offset, const ValueType& value) { mValues[offset] = value; } - __hostdev__ void setValue(uint32_t offset, const ValueType& value) + __hostdev__ void setValueOnly(uint32_t offset, const ValueType& value) { mValues[offset] = value; } + __hostdev__ void setValue(uint32_t offset, const ValueType& value) { mValueMask.setOn(offset); mValues[offset] = value; } + __hostdev__ void setOn(uint32_t offset) { mValueMask.setOn(offset); } __hostdev__ ValueType getMin() const { return mMinimum; } __hostdev__ ValueType getMax() const { return mMaximum; } @@ -3858,9 +5481,15 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData __hostdev__ void setAvg(const FloatType& v) { mAverage = v; } __hostdev__ void setDev(const FloatType& v) { mStdDevi = v; } - template + template __hostdev__ void setOrigin(const T& ijk) { mBBoxMin = ijk; } + __hostdev__ void fill(const ValueType& v) + { + for (auto *p = mValues, *q = p + 512; p != q; ++p) + *p = v; + } + /// @brief This class cannot be constructed or deleted LeafData() = delete; LeafData(const LeafData&) = delete; @@ -3868,6 +5497,8 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData ~LeafData() = delete; }; // LeafData +// --------------------------> LeafFnBase <------------------------------------ + /// @brief Base-class for quantized float leaf nodes template class MaskT, uint32_t LOG2DIM> struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafFnBase @@ -3879,55 +5510,62 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafFnBase CoordT mBBoxMin; // 12B. uint8_t mBBoxDif[3]; // 3B. - uint8_t mFlags; // 1B. bit0: skip render?, bit1: has bbox?, bit3: unused, bit4: is sparse ValueIndex, bits5,6,7: bit-width for FpN + uint8_t mFlags; // 1B. bit0: skip render?, bit1: has bbox?, bit3: unused, bit4: has stats, bits5,6,7: bit-width for FpN MaskT mValueMask; // LOG2DIM(3): 64B. - float mMinimum; // 4B - minimum of ALL values in this node - float mQuantum; // = (max - min)/15 4B - uint16_t mMin, mMax, mAvg, mDev;// quantized representations of statistics of active values + float mMinimum; // 4B - minimum of ALL values in this node + float mQuantum; // = (max - min)/15 4B + uint16_t mMin, mMax, mAvg, mDev; // quantized representations of statistics of active values // no padding since it's always 32B aligned __hostdev__ static uint64_t memUsage() { return sizeof(LeafFnBase); } + __hostdev__ static bool hasStats() { return true; } + /// @brief Return padding of this class in bytes, due to aliasing and 32B alignment /// /// @note The extra bytes are not necessarily at the end, but can come from aliasing of individual data members. - __hostdev__ static constexpr uint32_t padding() { - return sizeof(LeafFnBase) - (12 + 3 + 1 + sizeof(MaskT) + 2*4 + 4*2); + __hostdev__ static constexpr uint32_t padding() + { + return sizeof(LeafFnBase) - (12 + 3 + 1 + sizeof(MaskT) + 2 * 4 + 4 * 2); } __hostdev__ void init(float min, float max, uint8_t bitWidth) { mMinimum = min; - mQuantum = (max - min)/float((1 << bitWidth)-1); + mQuantum = (max - min) / float((1 << bitWidth) - 1); } + __hostdev__ void setOn(uint32_t offset) { mValueMask.setOn(offset); } + /// @brief return the quantized minimum of the active values in this node - __hostdev__ float getMin() const { return mMin*mQuantum + mMinimum; } + __hostdev__ float getMin() const { return mMin * mQuantum + mMinimum; } /// @brief return the quantized maximum of the active values in this node - __hostdev__ float getMax() const { return mMax*mQuantum + mMinimum; } + __hostdev__ float getMax() const { return mMax * mQuantum + mMinimum; } /// @brief return the quantized average of the active values in this node - __hostdev__ float getAvg() const { return mAvg*mQuantum + mMinimum; } + __hostdev__ float getAvg() const { return mAvg * mQuantum + mMinimum; } /// @brief return the quantized standard deviation of the active values in this node /// @note 0 <= StdDev <= max-min or 0 <= StdDev/(max-min) <= 1 - __hostdev__ float getDev() const { return mDev*mQuantum; } + __hostdev__ float getDev() const { return mDev * mQuantum; } /// @note min <= X <= max or 0 <= (X-min)/(min-max) <= 1 - __hostdev__ void setMin(float min) { mMin = uint16_t((min - mMinimum)/mQuantum + 0.5f); } + __hostdev__ void setMin(float min) { mMin = uint16_t((min - mMinimum) / mQuantum + 0.5f); } /// @note min <= X <= max or 0 <= (X-min)/(min-max) <= 1 - __hostdev__ void setMax(float max) { mMax = uint16_t((max - mMinimum)/mQuantum + 0.5f); } + __hostdev__ void setMax(float max) { mMax = uint16_t((max - mMinimum) / mQuantum + 0.5f); } /// @note min <= avg <= max or 0 <= (avg-min)/(min-max) <= 1 - __hostdev__ void setAvg(float avg) { mAvg = uint16_t((avg - mMinimum)/mQuantum + 0.5f); } + __hostdev__ void setAvg(float avg) { mAvg = uint16_t((avg - mMinimum) / mQuantum + 0.5f); } /// @note 0 <= StdDev <= max-min or 0 <= StdDev/(max-min) <= 1 - __hostdev__ void setDev(float dev) { mDev = uint16_t(dev/mQuantum + 0.5f); } + __hostdev__ void setDev(float dev) { mDev = uint16_t(dev / mQuantum + 0.5f); } - template + template __hostdev__ void setOrigin(const T& ijk) { mBBoxMin = ijk; } -};// LeafFnBase +}; // LeafFnBase + +// --------------------------> LeafData <------------------------------------ /// @brief Stuct with all the member data of the LeafNode (useful during serialization of an openvdb LeafNode) /// @@ -3938,24 +5576,25 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData; using BuildType = Fp4; - using ArrayType = uint8_t;// type used for the internal mValue array + using ArrayType = uint8_t; // type used for the internal mValue array static constexpr bool FIXED_SIZE = true; - alignas(32) uint8_t mCode[1u << (3 * LOG2DIM - 1)];// LeafFnBase is 32B aligned and so is mCode + alignas(32) uint8_t mCode[1u << (3 * LOG2DIM - 1)]; // LeafFnBase is 32B aligned and so is mCode __hostdev__ static constexpr uint64_t memUsage() { return sizeof(LeafData); } - __hostdev__ static constexpr uint32_t padding() { - static_assert(BaseT::padding()==0, "expected no padding in LeafFnBase"); + __hostdev__ static constexpr uint32_t padding() + { + static_assert(BaseT::padding() == 0, "expected no padding in LeafFnBase"); return sizeof(LeafData) - sizeof(BaseT) - (1u << (3 * LOG2DIM - 1)); } __hostdev__ static constexpr uint8_t bitWidth() { return 4u; } - __hostdev__ float getValue(uint32_t i) const + __hostdev__ float getValue(uint32_t i) const { #if 0 const uint8_t c = mCode[i>>1]; return ( (i&1) ? c >> 4 : c & uint8_t(15) )*BaseT::mQuantum + BaseT::mMinimum; #else - return ((mCode[i>>1] >> ((i&1)<<2)) & uint8_t(15))*BaseT::mQuantum + BaseT::mMinimum; + return ((mCode[i >> 1] >> ((i & 1) << 2)) & uint8_t(15)) * BaseT::mQuantum + BaseT::mMinimum; #endif } @@ -3966,25 +5605,28 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData +// --------------------------> LeafBase <------------------------------------ + template class MaskT, uint32_t LOG2DIM> struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData : public LeafFnBase { using BaseT = LeafFnBase; using BuildType = Fp8; - using ArrayType = uint8_t;// type used for the internal mValue array + using ArrayType = uint8_t; // type used for the internal mValue array static constexpr bool FIXED_SIZE = true; alignas(32) uint8_t mCode[1u << 3 * LOG2DIM]; - __hostdev__ static constexpr int64_t memUsage() { return sizeof(LeafData); } - __hostdev__ static constexpr uint32_t padding() { - static_assert(BaseT::padding()==0, "expected no padding in LeafFnBase"); + __hostdev__ static constexpr int64_t memUsage() { return sizeof(LeafData); } + __hostdev__ static constexpr uint32_t padding() + { + static_assert(BaseT::padding() == 0, "expected no padding in LeafFnBase"); return sizeof(LeafData) - sizeof(BaseT) - (1u << 3 * LOG2DIM); } __hostdev__ static constexpr uint8_t bitWidth() { return 8u; } - __hostdev__ float getValue(uint32_t i) const + __hostdev__ float getValue(uint32_t i) const { - return mCode[i]*BaseT::mQuantum + BaseT::mMinimum;// code * (max-min)/255 + min + return mCode[i] * BaseT::mQuantum + BaseT::mMinimum; // code * (max-min)/255 + min } /// @brief This class cannot be constructed or deleted LeafData() = delete; @@ -3993,26 +5635,29 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData +// --------------------------> LeafData <------------------------------------ + template class MaskT, uint32_t LOG2DIM> struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData : public LeafFnBase { using BaseT = LeafFnBase; using BuildType = Fp16; - using ArrayType = uint16_t;// type used for the internal mValue array + using ArrayType = uint16_t; // type used for the internal mValue array static constexpr bool FIXED_SIZE = true; alignas(32) uint16_t mCode[1u << 3 * LOG2DIM]; __hostdev__ static constexpr uint64_t memUsage() { return sizeof(LeafData); } - __hostdev__ static constexpr uint32_t padding() { - static_assert(BaseT::padding()==0, "expected no padding in LeafFnBase"); - return sizeof(LeafData) - sizeof(BaseT) - 2*(1u << 3 * LOG2DIM); + __hostdev__ static constexpr uint32_t padding() + { + static_assert(BaseT::padding() == 0, "expected no padding in LeafFnBase"); + return sizeof(LeafData) - sizeof(BaseT) - 2 * (1u << 3 * LOG2DIM); } __hostdev__ static constexpr uint8_t bitWidth() { return 16u; } - __hostdev__ float getValue(uint32_t i) const + __hostdev__ float getValue(uint32_t i) const { - return mCode[i]*BaseT::mQuantum + BaseT::mMinimum;// code * (max-min)/65535 + min + return mCode[i] * BaseT::mQuantum + BaseT::mMinimum; // code * (max-min)/65535 + min } /// @brief This class cannot be constructed or deleted @@ -4022,59 +5667,61 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData +// --------------------------> LeafData <------------------------------------ + template class MaskT, uint32_t LOG2DIM> struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData : public LeafFnBase -{// this class has no data members, however every instance is immediately followed -// bitWidth*64 bytes. Since its base class is 32B aligned so are the bitWidth*64 bytes +{ // this class has no additional data members, however every instance is immediately followed by + // bitWidth*64 bytes. Since its base class is 32B aligned so are the bitWidth*64 bytes using BaseT = LeafFnBase; using BuildType = FpN; static constexpr bool FIXED_SIZE = false; - __hostdev__ static constexpr uint32_t padding() { - static_assert(BaseT::padding()==0, "expected no padding in LeafFnBase"); + __hostdev__ static constexpr uint32_t padding() + { + static_assert(BaseT::padding() == 0, "expected no padding in LeafFnBase"); return 0; } - __hostdev__ uint8_t bitWidth() const { return 1 << (BaseT::mFlags >> 5); }// 4,8,16,32 = 2^(2,3,4,5) - __hostdev__ size_t memUsage() const { return sizeof(*this) + this->bitWidth()*64; } - __hostdev__ static size_t memUsage(uint32_t bitWidth) { return 96u + bitWidth*64; } - __hostdev__ float getValue(uint32_t i) const + __hostdev__ uint8_t bitWidth() const { return 1 << (BaseT::mFlags >> 5); } // 4,8,16,32 = 2^(2,3,4,5) + __hostdev__ size_t memUsage() const { return sizeof(*this) + this->bitWidth() * 64; } + __hostdev__ static size_t memUsage(uint32_t bitWidth) { return 96u + bitWidth * 64; } + __hostdev__ float getValue(uint32_t i) const { -#ifdef NANOVDB_FPN_BRANCHLESS// faster - const int b = BaseT::mFlags >> 5;// b = 0, 1, 2, 3, 4 corresponding to 1, 2, 4, 8, 16 bits -#if 0// use LUT +#ifdef NANOVDB_FPN_BRANCHLESS // faster + const int b = BaseT::mFlags >> 5; // b = 0, 1, 2, 3, 4 corresponding to 1, 2, 4, 8, 16 bits +#if 0 // use LUT uint16_t code = reinterpret_cast(this + 1)[i >> (4 - b)]; const static uint8_t shift[5] = {15, 7, 3, 1, 0}; const static uint16_t mask[5] = {1, 3, 15, 255, 65535}; code >>= (i & shift[b]) << b; code &= mask[b]; -#else// no LUT +#else // no LUT uint32_t code = reinterpret_cast(this + 1)[i >> (5 - b)]; - //code >>= (i & ((16 >> b) - 1)) << b; code >>= (i & ((32 >> b) - 1)) << b; - code &= (1 << (1 << b)) - 1; + code &= (1 << (1 << b)) - 1; #endif -#else// use branched version (slow) +#else // use branched version (slow) float code; - auto *values = reinterpret_cast(this+1); + auto* values = reinterpret_cast(this + 1); switch (BaseT::mFlags >> 5) { - case 0u:// 1 bit float - code = float((values[i>>3] >> (i&7) ) & uint8_t(1)); - break; - case 1u:// 2 bits float - code = float((values[i>>2] >> ((i&3)<<1)) & uint8_t(3)); - break; - case 2u:// 4 bits float - code = float((values[i>>1] >> ((i&1)<<2)) & uint8_t(15)); - break; - case 3u:// 8 bits float - code = float(values[i]); - break; - default:// 16 bits float - code = float(reinterpret_cast(values)[i]); + case 0u: // 1 bit float + code = float((values[i >> 3] >> (i & 7)) & uint8_t(1)); + break; + case 1u: // 2 bits float + code = float((values[i >> 2] >> ((i & 3) << 1)) & uint8_t(3)); + break; + case 2u: // 4 bits float + code = float((values[i >> 1] >> ((i & 1) << 2)) & uint8_t(15)); + break; + case 3u: // 8 bits float + code = float(values[i]); + break; + default: // 16 bits float + code = float(reinterpret_cast(values)[i]); } #endif - return float(code) * BaseT::mQuantum + BaseT::mMinimum;// code * (max-min)/UNITS + min + return float(code) * BaseT::mQuantum + BaseT::mMinimum; // code * (max-min)/UNITS + min } /// @brief This class cannot be constructed or deleted @@ -4084,6 +5731,8 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData +// --------------------------> LeafData <------------------------------------ + // Partial template specialization of LeafData with bool template class MaskT, uint32_t LOG2DIM> struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData @@ -4092,38 +5741,37 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData) == sizeof(Mask), "Mismatching sizeof"); using ValueType = bool; using BuildType = bool; - using FloatType = bool;// dummy value type - using ArrayType = MaskT;// type used for the internal mValue array + using FloatType = bool; // dummy value type + using ArrayType = MaskT; // type used for the internal mValue array static constexpr bool FIXED_SIZE = true; CoordT mBBoxMin; // 12B. uint8_t mBBoxDif[3]; // 3B. - uint8_t mFlags; // 1B. bit0: skip render?, bit1: has bbox?, bit3: unused, bit4: is sparse ValueIndex, bits5,6,7: bit-width for FpN + uint8_t mFlags; // 1B. bit0: skip render?, bit1: has bbox?, bit3: unused, bit4: has stats, bits5,6,7: bit-width for FpN MaskT mValueMask; // LOG2DIM(3): 64B. MaskT mValues; // LOG2DIM(3): 64B. - uint64_t mPadding[2];// 16B padding to 32B alignment - - __hostdev__ static constexpr uint32_t padding() {return sizeof(LeafData) - 12u - 3u - 1u - 2*sizeof(MaskT) - 16u;} - __hostdev__ static uint64_t memUsage() { return sizeof(LeafData); } + uint64_t mPadding[2]; // 16B padding to 32B alignment - //__hostdev__ const ValueType* values() const { return nullptr; } + __hostdev__ static constexpr uint32_t padding() { return sizeof(LeafData) - 12u - 3u - 1u - 2 * sizeof(MaskT) - 16u; } + __hostdev__ static uint64_t memUsage() { return sizeof(LeafData); } + __hostdev__ static bool hasStats() { return false; } __hostdev__ bool getValue(uint32_t i) const { return mValues.isOn(i); } - __hostdev__ bool getMin() const { return false; }// dummy - __hostdev__ bool getMax() const { return false; }// dummy - __hostdev__ bool getAvg() const { return false; }// dummy - __hostdev__ bool getDev() const { return false; }// dummy + __hostdev__ bool getMin() const { return false; } // dummy + __hostdev__ bool getMax() const { return false; } // dummy + __hostdev__ bool getAvg() const { return false; } // dummy + __hostdev__ bool getDev() const { return false; } // dummy __hostdev__ void setValue(uint32_t offset, bool v) { mValueMask.setOn(offset); mValues.set(offset, v); } + __hostdev__ void setOn(uint32_t offset) { mValueMask.setOn(offset); } + __hostdev__ void setMin(const bool&) {} // no-op + __hostdev__ void setMax(const bool&) {} // no-op + __hostdev__ void setAvg(const bool&) {} // no-op + __hostdev__ void setDev(const bool&) {} // no-op - __hostdev__ void setMin(const bool&) {}// no-op - __hostdev__ void setMax(const bool&) {}// no-op - __hostdev__ void setAvg(const bool&) {}// no-op - __hostdev__ void setDev(const bool&) {}// no-op - - template + template __hostdev__ void setOrigin(const T& ijk) { mBBoxMin = ijk; } /// @brief This class cannot be constructed or deleted @@ -4133,6 +5781,8 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData +// --------------------------> LeafData <------------------------------------ + // Partial template specialization of LeafData with ValueMask template class MaskT, uint32_t LOG2DIM> struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData @@ -4141,39 +5791,36 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData) == sizeof(Mask), "Mismatching sizeof"); using ValueType = bool; using BuildType = ValueMask; - using FloatType = bool;// dummy value type - using ArrayType = void;// type used for the internal mValue array - void means missing + using FloatType = bool; // dummy value type + using ArrayType = void; // type used for the internal mValue array - void means missing static constexpr bool FIXED_SIZE = true; CoordT mBBoxMin; // 12B. uint8_t mBBoxDif[3]; // 3B. - uint8_t mFlags; // 1B. bit0: skip render?, bit1: has bbox?, bit3: unused, bit4: is sparse ValueIndex, bits5,6,7: bit-width for FpN + uint8_t mFlags; // 1B. bit0: skip render?, bit1: has bbox?, bit3: unused, bit4: has stats, bits5,6,7: bit-width for FpN MaskT mValueMask; // LOG2DIM(3): 64B. - uint64_t mPadding[2];// 16B padding to 32B alignment + uint64_t mPadding[2]; // 16B padding to 32B alignment __hostdev__ static uint64_t memUsage() { return sizeof(LeafData); } - - __hostdev__ static constexpr uint32_t padding() { - return sizeof(LeafData) - (12u + 3u + 1u + sizeof(MaskT) + 2*8u); - } - - //__hostdev__ const ValueType* values() const { return nullptr; } - __hostdev__ bool getValue(uint32_t i) const { return mValueMask.isOn(i); } - __hostdev__ bool getMin() const { return false; }// dummy - __hostdev__ bool getMax() const { return false; }// dummy - __hostdev__ bool getAvg() const { return false; }// dummy - __hostdev__ bool getDev() const { return false; }// dummy - __hostdev__ void setValue(uint32_t offset, bool) + __hostdev__ static bool hasStats() { return false; } + __hostdev__ static constexpr uint32_t padding() { - mValueMask.setOn(offset); + return sizeof(LeafData) - (12u + 3u + 1u + sizeof(MaskT) + 2 * 8u); } - __hostdev__ void setMin(const ValueType&) {}// no-op - __hostdev__ void setMax(const ValueType&) {}// no-op - __hostdev__ void setAvg(const FloatType&) {}// no-op - __hostdev__ void setDev(const FloatType&) {}// no-op - - template + __hostdev__ bool getValue(uint32_t i) const { return mValueMask.isOn(i); } + __hostdev__ bool getMin() const { return false; } // dummy + __hostdev__ bool getMax() const { return false; } // dummy + __hostdev__ bool getAvg() const { return false; } // dummy + __hostdev__ bool getDev() const { return false; } // dummy + __hostdev__ void setValue(uint32_t offset, bool) { mValueMask.setOn(offset); } + __hostdev__ void setOn(uint32_t offset) { mValueMask.setOn(offset); } + __hostdev__ void setMin(const ValueType&) {} // no-op + __hostdev__ void setMax(const ValueType&) {} // no-op + __hostdev__ void setAvg(const FloatType&) {} // no-op + __hostdev__ void setDev(const FloatType&) {} // no-op + + template __hostdev__ void setOrigin(const T& ijk) { mBBoxMin = ijk; } /// @brief This class cannot be constructed or deleted @@ -4183,80 +5830,206 @@ struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData +// --------------------------> LeafIndexBase <------------------------------------ + // Partial template specialization of LeafData with ValueIndex template class MaskT, uint32_t LOG2DIM> -struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData +struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafIndexBase { static_assert(sizeof(CoordT) == sizeof(Coord), "Mismatching sizeof"); static_assert(sizeof(MaskT) == sizeof(Mask), "Mismatching sizeof"); using ValueType = uint64_t; - using BuildType = ValueIndex; using FloatType = uint64_t; - using ArrayType = void;// type used for the internal mValue array - void means missing + using ArrayType = void; // type used for the internal mValue array - void means missing static constexpr bool FIXED_SIZE = true; CoordT mBBoxMin; // 12B. uint8_t mBBoxDif[3]; // 3B. - uint8_t mFlags; // 1B. bit0: skip render?, bit1: has bbox?, bit3: unused, bit4: is sparse ValueIndex, bits5,6,7: bit-width for FpN - + uint8_t mFlags; // 1B. bit0: skip render?, bit1: has bbox?, bit3: unused, bit4: has stats, bits5,6,7: bit-width for FpN MaskT mValueMask; // LOG2DIM(3): 64B. - uint64_t mStatsOff;// 8B offset to min/max/avg/sdv - uint64_t mValueOff;// 8B offset to values - // No padding since it's always 32B aligned + uint64_t mOffset, mPrefixSum; // 8B offset to first value in this leaf node and 9-bit prefix sum + __hostdev__ static constexpr uint32_t padding() + { + return sizeof(LeafIndexBase) - (12u + 3u + 1u + sizeof(MaskT) + 2 * 8u); + } + __hostdev__ static uint64_t memUsage() { return sizeof(LeafIndexBase); } + __hostdev__ bool hasStats() const { return mFlags & (uint8_t(1) << 4); } + // return the offset to the first value indexed by this leaf node + __hostdev__ const uint64_t& firstOffset() const { return mOffset; } + __hostdev__ void setMin(const ValueType&) {} // no-op + __hostdev__ void setMax(const ValueType&) {} // no-op + __hostdev__ void setAvg(const FloatType&) {} // no-op + __hostdev__ void setDev(const FloatType&) {} // no-op + __hostdev__ void setOn(uint32_t offset) { mValueMask.setOn(offset); } + template + __hostdev__ void setOrigin(const T& ijk) { mBBoxMin = ijk; } +}; // LeafIndexBase + +// --------------------------> LeafData <------------------------------------ + +// Partial template specialization of LeafData with ValueIndex +template class MaskT, uint32_t LOG2DIM> +struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData + : public LeafIndexBase +{ + using BaseT = LeafIndexBase; + using BuildType = ValueIndex; + // return the total number of values indexed by this leaf node, excluding the optional 4 stats + __hostdev__ static uint32_t valueCount() { return uint32_t(512); } // 8^3 = 2^9 + // return the offset to the last value indexed by this leaf node (disregarding optional stats) + __hostdev__ uint64_t lastOffset() const { return BaseT::mOffset + 511u; } // 2^9 - 1 + // if stats are available, they are always placed after the last voxel value in this leaf node + __hostdev__ uint64_t getMin() const { return this->hasStats() ? BaseT::mOffset + 512u : 0u; } + __hostdev__ uint64_t getMax() const { return this->hasStats() ? BaseT::mOffset + 513u : 0u; } + __hostdev__ uint64_t getAvg() const { return this->hasStats() ? BaseT::mOffset + 514u : 0u; } + __hostdev__ uint64_t getDev() const { return this->hasStats() ? BaseT::mOffset + 515u : 0u; } + __hostdev__ uint64_t getValue(uint32_t i) const { return BaseT::mOffset + i; } // dense leaf node with active and inactive voxels + + /// @brief This class cannot be constructed or deleted + LeafData() = delete; + LeafData(const LeafData&) = delete; + LeafData& operator=(const LeafData&) = delete; + ~LeafData() = delete; +}; // LeafData - __hostdev__ static constexpr uint32_t padding() { - return sizeof(LeafData) - (12u + 3u + 1u + sizeof(MaskT) + 2*8u); +// --------------------------> LeafData <------------------------------------ + +template class MaskT, uint32_t LOG2DIM> +struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData + : public LeafIndexBase +{ + using BaseT = LeafIndexBase; + using BuildType = ValueOnIndex; + __hostdev__ uint32_t valueCount() const + { + return CountOn(BaseT::mValueMask.words()[7]) + (BaseT::mPrefixSum >> 54u & 511u); // last 9 bits of mPrefixSum do not account for the last word in mValueMask + } + __hostdev__ uint64_t lastOffset() const { return BaseT::mOffset + this->valueCount() - 1u; } + __hostdev__ uint64_t getMin() const { return this->hasStats() ? this->lastOffset() + 1u : 0u; } + __hostdev__ uint64_t getMax() const { return this->hasStats() ? this->lastOffset() + 2u : 0u; } + __hostdev__ uint64_t getAvg() const { return this->hasStats() ? this->lastOffset() + 3u : 0u; } + __hostdev__ uint64_t getDev() const { return this->hasStats() ? this->lastOffset() + 4u : 0u; } + __hostdev__ uint64_t getValue(uint32_t i) const + { + //return mValueMask.isOn(i) ? mOffset + mValueMask.countOn(i) : 0u;// for debugging + uint32_t n = i >> 6; + const uint64_t w = BaseT::mValueMask.words()[n], mask = uint64_t(1) << (i & 63u); + if (!(w & mask)) return uint64_t(0); // if i'th value is inactive return offset to background value + uint64_t sum = BaseT::mOffset + CountOn(w & (mask - 1u)); + if (n--) sum += BaseT::mPrefixSum >> (9u * n) & 511u; + return sum; } + /// @brief This class cannot be constructed or deleted + LeafData() = delete; + LeafData(const LeafData&) = delete; + LeafData& operator=(const LeafData&) = delete; + ~LeafData() = delete; +}; // LeafData + +// --------------------------> LeafData <------------------------------------ + +template class MaskT, uint32_t LOG2DIM> +struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData + : public LeafData +{ + using BuildType = ValueIndexMask; + MaskT mMask; + __hostdev__ static uint64_t memUsage() { return sizeof(LeafData); } + __hostdev__ bool isMaskOn(uint32_t offset) const { return mMask.isOn(offset); } + __hostdev__ void setMask(uint32_t offset, bool v) { mMask.set(offset, v); } +}; // LeafData + +template class MaskT, uint32_t LOG2DIM> +struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData + : public LeafData +{ + using BuildType = ValueOnIndexMask; + MaskT mMask; __hostdev__ static uint64_t memUsage() { return sizeof(LeafData); } + __hostdev__ bool isMaskOn(uint32_t offset) const { return mMask.isOn(offset); } + __hostdev__ void setMask(uint32_t offset, bool v) { mMask.set(offset, v); } +}; // LeafData + +// --------------------------> LeafData <------------------------------------ - __hostdev__ uint64_t getMin() const { NANOVDB_ASSERT(mStatsOff); return mStatsOff + 0; } - __hostdev__ uint64_t getMax() const { NANOVDB_ASSERT(mStatsOff); return mStatsOff + 1; } - __hostdev__ uint64_t getAvg() const { NANOVDB_ASSERT(mStatsOff); return mStatsOff + 2; } - __hostdev__ uint64_t getDev() const { NANOVDB_ASSERT(mStatsOff); return mStatsOff + 3; } - __hostdev__ void setValue(uint32_t offset, uint64_t) +template class MaskT, uint32_t LOG2DIM> +struct NANOVDB_ALIGN(NANOVDB_DATA_ALIGNMENT) LeafData +{ + static_assert(sizeof(CoordT) == sizeof(Coord), "Mismatching sizeof"); + static_assert(sizeof(MaskT) == sizeof(Mask), "Mismatching sizeof"); + using ValueType = uint64_t; + using BuildType = Point; + using FloatType = typename FloatTraits::FloatType; + using ArrayType = uint16_t; // type used for the internal mValue array + static constexpr bool FIXED_SIZE = true; + + CoordT mBBoxMin; // 12B. + uint8_t mBBoxDif[3]; // 3B. + uint8_t mFlags; // 1B. bit0: skip render?, bit1: has bbox?, bit3: unused, bit4: has stats, bits5,6,7: bit-width for FpN + MaskT mValueMask; // LOG2DIM(3): 64B. + + uint64_t mOffset; // 8B + uint64_t mPointCount; // 8B + alignas(32) uint16_t mValues[1u << 3 * LOG2DIM]; // 1KB + // no padding + + /// @brief Return padding of this class in bytes, due to aliasing and 32B alignment + /// + /// @note The extra bytes are not necessarily at the end, but can come from aliasing of individual data members. + __hostdev__ static constexpr uint32_t padding() { - mValueMask.setOn(offset); + return sizeof(LeafData) - (12u + 3u + 1u + sizeof(MaskT) + 2 * 8u + (1u << 3 * LOG2DIM) * 2u); } + __hostdev__ static uint64_t memUsage() { return sizeof(LeafData); } - __hostdev__ uint64_t getValue(uint32_t i) const + __hostdev__ uint64_t offset() const { return mOffset; } + __hostdev__ uint64_t pointCount() const { return mPointCount; } + __hostdev__ uint64_t first(uint32_t i) const { return i ? uint64_t(mValues[i - 1u]) + mOffset : mOffset; } + __hostdev__ uint64_t last(uint32_t i) const { return uint64_t(mValues[i]) + mOffset; } + __hostdev__ uint64_t getValue(uint32_t i) const { return uint64_t(mValues[i]); } + __hostdev__ void setValueOnly(uint32_t offset, uint16_t value) { mValues[offset] = value; } + __hostdev__ void setValue(uint32_t offset, uint16_t value) { - if (mFlags & uint8_t(16u)) {// if 4th bit is set only active voxels are indexed - return mValueMask.isOn(i) ? mValueOff + mValueMask.countOn(i) : 0;// 0 is background - } - return mValueOff + i;// dense array of active and inactive voxels + mValueMask.setOn(offset); + mValues[offset] = value; } + __hostdev__ void setOn(uint32_t offset) { mValueMask.setOn(offset); } + + __hostdev__ ValueType getMin() const { return mOffset; } + __hostdev__ ValueType getMax() const { return mPointCount; } + __hostdev__ FloatType getAvg() const { return 0.0f; } + __hostdev__ FloatType getDev() const { return 0.0f; } - template - __hostdev__ void setMin(const T &min, T *p) { NANOVDB_ASSERT(mStatsOff); p[mStatsOff + 0] = min; } - template - __hostdev__ void setMax(const T &max, T *p) { NANOVDB_ASSERT(mStatsOff); p[mStatsOff + 1] = max; } - template - __hostdev__ void setAvg(const T &avg, T *p) { NANOVDB_ASSERT(mStatsOff); p[mStatsOff + 2] = avg; } - template - __hostdev__ void setDev(const T &dev, T *p) { NANOVDB_ASSERT(mStatsOff); p[mStatsOff + 3] = dev; } - template - __hostdev__ void setOrigin(const T &ijk) { mBBoxMin = ijk; } + __hostdev__ void setMin(const ValueType&) {} + __hostdev__ void setMax(const ValueType&) {} + __hostdev__ void setAvg(const FloatType&) {} + __hostdev__ void setDev(const FloatType&) {} + + template + __hostdev__ void setOrigin(const T& ijk) { mBBoxMin = ijk; } /// @brief This class cannot be constructed or deleted LeafData() = delete; LeafData(const LeafData&) = delete; LeafData& operator=(const LeafData&) = delete; ~LeafData() = delete; -}; // LeafData +}; // LeafData + +// --------------------------> LeafNode <------------------------------------ /// @brief Leaf nodes of the VDB tree. (defaults to 8x8x8 = 512 voxels) template class MaskT = Mask, uint32_t Log2Dim = 3> -class LeafNode : private LeafData +class LeafNode : public LeafData { public: struct ChildNodeType { - static constexpr uint32_t TOTAL = 0; - static constexpr uint32_t DIM = 1; + static constexpr uint32_t TOTAL = 0; + static constexpr uint32_t DIM = 1; __hostdev__ static uint32_t dim() { return 1u; } }; // Voxel using LeafNodeType = LeafNode; @@ -4275,56 +6048,120 @@ class LeafNode : private LeafData class ValueOnIterator : public MaskIterT { using BaseT = MaskIterT; - const LeafNode *mParent; + const LeafNode* mParent; + public: - __hostdev__ ValueOnIterator() : BaseT(), mParent(nullptr) {} - __hostdev__ ValueOnIterator(const LeafNode* parent) : BaseT(parent->data()->mValueMask.beginOn()), mParent(parent) {} - ValueOnIterator& operator=(const ValueOnIterator&) = default; - __hostdev__ ValueType operator*() const {NANOVDB_ASSERT(*this); return mParent->getValue(BaseT::pos());} - __hostdev__ CoordT getCoord() const { NANOVDB_ASSERT(*this); return mParent->offsetToGlobalCoord(BaseT::pos());} + __hostdev__ ValueOnIterator() + : BaseT() + , mParent(nullptr) + { + } + __hostdev__ ValueOnIterator(const LeafNode* parent) + : BaseT(parent->data()->mValueMask.beginOn()) + , mParent(parent) + { + } + ValueOnIterator& operator=(const ValueOnIterator&) = default; + __hostdev__ ValueType operator*() const + { + NANOVDB_ASSERT(*this); + return mParent->getValue(BaseT::pos()); + } + __hostdev__ CoordT getCoord() const + { + NANOVDB_ASSERT(*this); + return mParent->offsetToGlobalCoord(BaseT::pos()); + } }; // Member class ValueOnIterator - ValueOnIterator beginValueOn() const {return ValueOnIterator(this);} + __hostdev__ ValueOnIterator beginValueOn() const { return ValueOnIterator(this); } + __hostdev__ ValueOnIterator cbeginValueOn() const { return ValueOnIterator(this); } /// @brief Visits all inactive values in a leaf node class ValueOffIterator : public MaskIterT { using BaseT = MaskIterT; - const LeafNode *mParent; + const LeafNode* mParent; + public: - __hostdev__ ValueOffIterator() : BaseT(), mParent(nullptr) {} - __hostdev__ ValueOffIterator(const LeafNode* parent) : BaseT(parent->data()->mValueMask.beginOff()), mParent(parent) {} - ValueOffIterator& operator=(const ValueOffIterator&) = default; - __hostdev__ ValueType operator*() const {NANOVDB_ASSERT(*this); return mParent->getValue(BaseT::pos());} - __hostdev__ CoordT getCoord() const { NANOVDB_ASSERT(*this); return mParent->offsetToGlobalCoord(BaseT::pos());} + __hostdev__ ValueOffIterator() + : BaseT() + , mParent(nullptr) + { + } + __hostdev__ ValueOffIterator(const LeafNode* parent) + : BaseT(parent->data()->mValueMask.beginOff()) + , mParent(parent) + { + } + ValueOffIterator& operator=(const ValueOffIterator&) = default; + __hostdev__ ValueType operator*() const + { + NANOVDB_ASSERT(*this); + return mParent->getValue(BaseT::pos()); + } + __hostdev__ CoordT getCoord() const + { + NANOVDB_ASSERT(*this); + return mParent->offsetToGlobalCoord(BaseT::pos()); + } }; // Member class ValueOffIterator - ValueOffIterator beginValueOff() const {return ValueOffIterator(this);} + __hostdev__ ValueOffIterator beginValueOff() const { return ValueOffIterator(this); } + __hostdev__ ValueOffIterator cbeginValueOff() const { return ValueOffIterator(this); } /// @brief Visits all values in a leaf node, i.e. both active and inactive values class ValueIterator { - const LeafNode *mParent; - uint32_t mPos; + const LeafNode* mParent; + uint32_t mPos; + public: - __hostdev__ ValueIterator() : mParent(nullptr), mPos(1u << 3 * Log2Dim) {} - __hostdev__ ValueIterator(const LeafNode* parent) : mParent(parent), mPos(0) {NANOVDB_ASSERT(parent);} - ValueIterator& operator=(const ValueIterator&) = default; - __hostdev__ ValueType operator*() const { NANOVDB_ASSERT(*this); return mParent->getValue(mPos);} - __hostdev__ CoordT getCoord() const { NANOVDB_ASSERT(*this); return mParent->offsetToGlobalCoord(mPos);} - __hostdev__ bool isActive() const { NANOVDB_ASSERT(*this); return mParent->isActive(mPos);} - __hostdev__ operator bool() const {return mPos < (1u << 3 * Log2Dim);} - __hostdev__ ValueIterator& operator++() {++mPos; return *this;} - __hostdev__ ValueIterator operator++(int) { + __hostdev__ ValueIterator() + : mParent(nullptr) + , mPos(1u << 3 * Log2Dim) + { + } + __hostdev__ ValueIterator(const LeafNode* parent) + : mParent(parent) + , mPos(0) + { + NANOVDB_ASSERT(parent); + } + ValueIterator& operator=(const ValueIterator&) = default; + __hostdev__ ValueType operator*() const + { + NANOVDB_ASSERT(*this); + return mParent->getValue(mPos); + } + __hostdev__ CoordT getCoord() const + { + NANOVDB_ASSERT(*this); + return mParent->offsetToGlobalCoord(mPos); + } + __hostdev__ bool isActive() const + { + NANOVDB_ASSERT(*this); + return mParent->isActive(mPos); + } + __hostdev__ operator bool() const { return mPos < (1u << 3 * Log2Dim); } + __hostdev__ ValueIterator& operator++() + { + ++mPos; + return *this; + } + __hostdev__ ValueIterator operator++(int) + { auto tmp = *this; ++(*this); return tmp; } }; // Member class ValueIterator - ValueIterator beginValue() const {return ValueIterator(this);} + __hostdev__ ValueIterator beginValue() const { return ValueIterator(this); } + __hostdev__ ValueIterator cbeginValueAll() const { return ValueIterator(this); } - static_assert(is_same::Type>::value, "Mismatching BuildType"); + static_assert(is_same::Type>::value, "Mismatching BuildType"); static constexpr uint32_t LOG2DIM = Log2Dim; static constexpr uint32_t TOTAL = LOG2DIM; // needed by parent nodes static constexpr uint32_t DIM = 1u << TOTAL; // number of voxels along each axis of this node @@ -4339,18 +6176,19 @@ class LeafNode : private LeafData /// @brief Return a const reference to the bit mask of active voxels in this leaf node __hostdev__ const MaskType& valueMask() const { return DataType::mValueMask; } + __hostdev__ const MaskType& getValueMask() const { return DataType::mValueMask; } /// @brief Return a const reference to the minimum active value encoded in this leaf node - __hostdev__ ValueType minimum() const { return this->getMin(); } + __hostdev__ ValueType minimum() const { return DataType::getMin(); } /// @brief Return a const reference to the maximum active value encoded in this leaf node - __hostdev__ ValueType maximum() const { return this->getMax(); } + __hostdev__ ValueType maximum() const { return DataType::getMax(); } /// @brief Return a const reference to the average of all the active values encoded in this leaf node __hostdev__ FloatType average() const { return DataType::getAvg(); } /// @brief Return the variance of all the active values encoded in this leaf node - __hostdev__ FloatType variance() const { return DataType::getDev()*DataType::getDev(); } + __hostdev__ FloatType variance() const { return Pow2(DataType::getDev()); } /// @brief Return a const reference to the standard deviation of all the active values encoded in this leaf node __hostdev__ FloatType stdDeviation() const { return DataType::getDev(); } @@ -4360,6 +6198,9 @@ class LeafNode : private LeafData /// @brief Return the origin in index space of this leaf node __hostdev__ CoordT origin() const { return DataType::mBBoxMin & ~MASK; } + /// @brief Compute the local coordinates from a linear offset + /// @param n Linear offset into this nodes dense table + /// @return Local (vs global) 3D coordinates __hostdev__ static CoordT OffsetToLocalCoord(uint32_t n) { NANOVDB_ASSERT(n < SIZE); @@ -4382,12 +6223,12 @@ class LeafNode : private LeafData __hostdev__ BBox bbox() const { BBox bbox(DataType::mBBoxMin, DataType::mBBoxMin); - if ( this->hasBBox() ) { + if (this->hasBBox()) { bbox.max()[0] += DataType::mBBoxDif[0]; bbox.max()[1] += DataType::mBBoxDif[1]; bbox.max()[2] += DataType::mBBoxDif[2]; - } else {// very rare case - bbox = BBox();// invalid + } else { // very rare case + bbox = BBox(); // invalid } return bbox; } @@ -4395,10 +6236,10 @@ class LeafNode : private LeafData /// @brief Return the total number of voxels (e.g. values) encoded in this leaf node __hostdev__ static uint32_t voxelCount() { return 1u << (3 * LOG2DIM); } - __hostdev__ static uint32_t padding() {return DataType::padding();} + __hostdev__ static uint32_t padding() { return DataType::padding(); } - /// @brief return memory usage in bytes for the class - __hostdev__ uint64_t memUsage() { return DataType::memUsage(); } + /// @brief return memory usage in bytes for the leaf node + __hostdev__ uint64_t memUsage() const { return DataType::memUsage(); } /// @brief This class cannot be constructed or deleted LeafNode() = delete; @@ -4412,6 +6253,11 @@ class LeafNode : private LeafData /// @brief Return the voxel value at the given coordinate. __hostdev__ ValueType getValue(const CoordT& ijk) const { return DataType::getValue(CoordToOffset(ijk)); } + /// @brief Return the first value in this leaf node. + __hostdev__ ValueType getFirstValue() const { return this->getValue(0); } + /// @brief Return the last value in this leaf node. + __hostdev__ ValueType getLastValue() const { return this->getValue(SIZE - 1); } + /// @brief Sets the value at the specified location and activate its state. /// /// @note This is safe since it does not change the topology of the tree (unlike setValue methods on the other nodes) @@ -4435,13 +6281,13 @@ class LeafNode : private LeafData return !DataType::mValueMask.isOff(); } - __hostdev__ bool hasBBox() const {return DataType::mFlags & uint8_t(2);} + __hostdev__ bool hasBBox() const { return DataType::mFlags & uint8_t(2); } /// @brief Return @c true if the voxel value at the given coordinate is active and updates @c v with the value. __hostdev__ bool probeValue(const CoordT& ijk, ValueType& v) const { const uint32_t n = CoordToOffset(ijk); - v = DataType::getValue(n); + v = DataType::getValue(n); return DataType::mValueMask.isOn(n); } @@ -4450,11 +6296,7 @@ class LeafNode : private LeafData /// @brief Return the linear offset corresponding to the given coordinate __hostdev__ static uint32_t CoordToOffset(const CoordT& ijk) { - #if 0 - return ((ijk[0] & MASK) << (2 * LOG2DIM)) + ((ijk[1] & MASK) << LOG2DIM) + (ijk[2] & MASK); - #else return ((ijk[0] & MASK) << (2 * LOG2DIM)) | ((ijk[1] & MASK) << LOG2DIM) | (ijk[2] & MASK); - #endif } /// @brief Updates the local bounding box of active voxels in this node. Return true if bbox was updated. @@ -4462,13 +6304,36 @@ class LeafNode : private LeafData /// @warning It assumes that the origin and value mask have already been set. /// /// @details This method is based on few (intrinsic) bit operations and hence is relatively fast. - /// However, it should only only be called of either the value mask has changed or if the + /// However, it should only only be called if either the value mask has changed or if the /// active bounding box is still undefined. e.g. during construction of this node. __hostdev__ bool updateBBox(); + template + __hostdev__ auto get(const CoordType& ijk, ArgsT&&... args) const + { + return OpT::get(*this, CoordToOffset(ijk), args...); + } + + template + __hostdev__ auto get(const uint32_t n, ArgsT&&... args) const + { + return OpT::get(*this, n, args...); + } + + template + __hostdev__ auto set(const CoordType& ijk, ArgsT&&... args) + { + return OpT::set(*this, CoordToOffset(ijk), args...); + } + + template + __hostdev__ auto set(const uint32_t n, ArgsT&&... args) + { + return OpT::set(*this, n, args...); + } + private: static_assert(sizeof(DataType) % NANOVDB_DATA_ALIGNMENT == 0, "sizeof(LeafData) is misaligned"); - //static_assert(offsetof(DataType, mValues) % 32 == 0, "LeafData::mValues is misaligned"); template friend class ReadAccessor; @@ -4478,16 +6343,17 @@ class LeafNode : private LeafData template friend class InternalNode; +#ifndef NANOVDB_NEW_ACCESSOR_METHODS /// @brief Private method to return a voxel value and update a (dummy) ReadAccessor template __hostdev__ ValueType getValueAndCache(const CoordT& ijk, const AccT&) const { return this->getValue(ijk); } /// @brief Return the node information. template - __hostdev__ typename AccT::NodeInfo getNodeInfoAndCache(const CoordType& /*ijk*/, const AccT& /*acc*/) const { + __hostdev__ typename AccT::NodeInfo getNodeInfoAndCache(const CoordType& /*ijk*/, const AccT& /*acc*/) const + { using NodeInfoT = typename AccT::NodeInfo; - return NodeInfoT{LEVEL, this->dim(), this->minimum(), this->maximum(), - this->average(), this->stdDeviation(), this->bbox()[0], this->bbox()[1]}; + return NodeInfoT{LEVEL, this->dim(), this->minimum(), this->maximum(), this->average(), this->stdDeviation(), this->bbox()[0], this->bbox()[1]}; } template @@ -4498,24 +6364,44 @@ class LeafNode : private LeafData template __hostdev__ const LeafNode* probeLeafAndCache(const CoordT&, const AccT&) const { return this; } +#endif template __hostdev__ uint32_t getDimAndCache(const CoordT&, const RayT& /*ray*/, const AccT&) const { - if (DataType::mFlags & uint8_t(1u)) return this->dim(); // skip this node if the 1st bit is set + if (DataType::mFlags & uint8_t(1u)) + return this->dim(); // skip this node if the 1st bit is set + + //if (!ray.intersects( this->bbox() )) return 1 << LOG2DIM; + return ChildNodeType::dim(); + } + + template + __hostdev__ auto + //__hostdev__ decltype(OpT::get(std::declval(), std::declval(), std::declval()...)) + getAndCache(const CoordType& ijk, const AccT&, ArgsT&&... args) const + { + return OpT::get(*this, CoordToOffset(ijk), args...); + } - //if (!ray.intersects( this->bbox() )) return 1 << LOG2DIM; - return ChildNodeType::dim(); + template + //__hostdev__ auto // occasionally fails with NVCC + __hostdev__ decltype(OpT::set(std::declval(), std::declval(), std::declval()...)) + setAndCache(const CoordType& ijk, const AccT&, ArgsT&&... args) + { + return OpT::set(*this, CoordToOffset(ijk), args...); } }; // LeafNode class +// --------------------------> LeafNode::updateBBox <------------------------------------ + template class MaskT, uint32_t LOG2DIM> __hostdev__ inline bool LeafNode::updateBBox() { static_assert(LOG2DIM == 3, "LeafNode::updateBBox: only supports LOGDIM = 3!"); if (DataType::mValueMask.isOff()) { - DataType::mFlags &= ~uint8_t(2);// set 2nd bit off, which indicates that this nodes has no bbox + DataType::mFlags &= ~uint8_t(2); // set 2nd bit off, which indicates that this nodes has no bbox return false; } auto update = [&](uint32_t min, uint32_t max, int axis) { @@ -4523,15 +6409,13 @@ __hostdev__ inline bool LeafNode::updateBBox() DataType::mBBoxMin[axis] = (DataType::mBBoxMin[axis] & ~MASK) + int(min); DataType::mBBoxDif[axis] = uint8_t(max - min); }; - uint64_t word64 = DataType::mValueMask.template getWord(0); - uint32_t Xmin = word64 ? 0u : 8u; - uint32_t Xmax = Xmin; - for (int i = 1; i < 8; ++i) { // last loop over 8 64 words - if (uint64_t w = DataType::mValueMask.template getWord(i)) { // skip if word has no set bits - word64 |= w; // union 8 x 64 bits words into one 64 bit word - if (Xmin == 8) { + uint64_t *w = DataType::mValueMask.words(), word64 = *w; + uint32_t Xmin = word64 ? 0u : 8u, Xmax = Xmin; + for (int i = 1; i < 8; ++i) { // last loop over 8 64 bit words + if (w[i]) { // skip if word has no set bits + word64 |= w[i]; // union 8 x 64 bits words into one 64 bit word + if (Xmin == 8) Xmin = i; // only set once - } Xmax = i; } } @@ -4540,10 +6424,10 @@ __hostdev__ inline bool LeafNode::updateBBox() update(FindLowestOn(word64) >> 3, FindHighestOn(word64) >> 3, 1); const uint32_t *p = reinterpret_cast(&word64), word32 = p[0] | p[1]; const uint16_t *q = reinterpret_cast(&word32), word16 = q[0] | q[1]; - const uint8_t *b = reinterpret_cast(&word16), byte = b[0] | b[1]; + const uint8_t * b = reinterpret_cast(&word16), byte = b[0] | b[1]; NANOVDB_ASSERT(byte); update(FindLowestOn(static_cast(byte)), FindHighestOn(static_cast(byte)), 2); - DataType::mFlags |= uint8_t(2);// set 2nd bit on, which indicates that this nodes has a bbox + DataType::mFlags |= uint8_t(2); // set 2nd bit on, which indicates that this nodes has a bbox return true; } // LeafNode::updateBBox @@ -4594,33 +6478,48 @@ struct NanoNode using type = NanoRoot; }; -using FloatTree = NanoTree; +using FloatTree = NanoTree; +using Fp4Tree = NanoTree; +using Fp8Tree = NanoTree; +using Fp16Tree = NanoTree; +using FpNTree = NanoTree; using DoubleTree = NanoTree; -using Int32Tree = NanoTree; +using Int32Tree = NanoTree; using UInt32Tree = NanoTree; -using Int64Tree = NanoTree; -using Vec3fTree = NanoTree; -using Vec3dTree = NanoTree; -using Vec4fTree = NanoTree; -using Vec4dTree = NanoTree; -using Vec3ITree = NanoTree; -using MaskTree = NanoTree; -using IndexTree = NanoTree; -using BoolTree = NanoTree; - -using FloatGrid = Grid; +using Int64Tree = NanoTree; +using Vec3fTree = NanoTree; +using Vec3dTree = NanoTree; +using Vec4fTree = NanoTree; +using Vec4dTree = NanoTree; +using Vec3ITree = NanoTree; +using MaskTree = NanoTree; +using BoolTree = NanoTree; +using IndexTree = NanoTree; +using OnIndexTree = NanoTree; +using IndexMaskTree = NanoTree; +using OnIndexMaskTree = NanoTree; + +using FloatGrid = Grid; +using Fp4Grid = Grid; +using Fp8Grid = Grid; +using Fp16Grid = Grid; +using FpNGrid = Grid; using DoubleGrid = Grid; -using Int32Grid = Grid; +using Int32Grid = Grid; using UInt32Grid = Grid; -using Int64Grid = Grid; -using Vec3fGrid = Grid; -using Vec3dGrid = Grid; -using Vec4fGrid = Grid; -using Vec4dGrid = Grid; -using Vec3IGrid = Grid; -using MaskGrid = Grid; -using IndexGrid = Grid; -using BoolGrid = Grid; +using Int64Grid = Grid; +using Vec3fGrid = Grid; +using Vec3dGrid = Grid; +using Vec4fGrid = Grid; +using Vec4dGrid = Grid; +using Vec3IGrid = Grid; +using MaskGrid = Grid; +using BoolGrid = Grid; +using PointGrid = Grid; +using IndexGrid = Grid; +using OnIndexGrid = Grid; +using IndexMaskGrid = Grid; +using OnIndexMaskGrid = Grid; // --------------------------> ReadAccessor <------------------------------------ @@ -4643,24 +6542,26 @@ using BoolGrid = Grid; /// O(1) random access operations by means of inverse tree traversal, /// which amortizes the non-const time complexity of the root node. -template +template class ReadAccessor { - using GridT = NanoGrid;// grid - using TreeT = NanoTree;// tree - using RootT = NanoRoot; // root node - using LeafT = NanoLeaf; // Leaf node + using GridT = NanoGrid; // grid + using TreeT = NanoTree; // tree + using RootT = NanoRoot; // root node + using LeafT = NanoLeaf; // Leaf node using FloatType = typename RootT::FloatType; using CoordValueType = typename RootT::CoordType::ValueType; mutable const RootT* mRoot; // 8 bytes (mutable to allow for access methods to be const) public: + using BuildType = BuildT; using ValueType = typename RootT::ValueType; using CoordType = typename RootT::CoordType; static const int CacheLevels = 0; - - struct NodeInfo { +#ifndef NANOVDB_NEW_ACCESSOR_METHODS + struct NodeInfo + { uint32_t mLevel; // 4B uint32_t mDim; // 4B ValueType mMinimum; // typically 4B @@ -4670,15 +6571,24 @@ class ReadAccessor CoordType mBBoxMin; // 3*4B CoordType mBBoxMax; // 3*4B }; - +#endif /// @brief Constructor from a root node - __hostdev__ ReadAccessor(const RootT& root) : mRoot{&root} {} + __hostdev__ ReadAccessor(const RootT& root) + : mRoot{&root} + { + } - /// @brief Constructor from a grid - __hostdev__ ReadAccessor(const GridT& grid) : ReadAccessor(grid.tree().root()) {} + /// @brief Constructor from a grid + __hostdev__ ReadAccessor(const GridT& grid) + : ReadAccessor(grid.tree().root()) + { + } /// @brief Constructor from a tree - __hostdev__ ReadAccessor(const TreeT& tree) : ReadAccessor(tree.root()) {} + __hostdev__ ReadAccessor(const TreeT& tree) + : ReadAccessor(tree.root()) + { + } /// @brief Reset this access to its initial state, i.e. with an empty cache /// @node Noop since this template specialization has no cache @@ -4690,18 +6600,34 @@ class ReadAccessor ReadAccessor(const ReadAccessor&) = default; ~ReadAccessor() = default; ReadAccessor& operator=(const ReadAccessor&) = default; - +#ifdef NANOVDB_NEW_ACCESSOR_METHODS + __hostdev__ ValueType getValue(const CoordType& ijk) const + { + return this->template get>(ijk); + } + __hostdev__ ValueType getValue(int i, int j, int k) const { return this->template get>(CoordType(i, j, k)); } + __hostdev__ ValueType operator()(const CoordType& ijk) const { return this->template get>(ijk); } + __hostdev__ ValueType operator()(int i, int j, int k) const { return this->template get>(CoordType(i, j, k)); } + __hostdev__ auto getNodeInfo(const CoordType& ijk) const { return this->template get>(ijk); } + __hostdev__ bool isActive(const CoordType& ijk) const { return this->template get>(ijk); } + __hostdev__ bool probeValue(const CoordType& ijk, ValueType& v) const { return this->template get>(ijk, v); } + __hostdev__ const LeafT* probeLeaf(const CoordType& ijk) const { return this->template get>(ijk); } +#else // NANOVDB_NEW_ACCESSOR_METHODS __hostdev__ ValueType getValue(const CoordType& ijk) const { return mRoot->getValueAndCache(ijk, *this); } + __hostdev__ ValueType getValue(int i, int j, int k) const + { + return this->getValue(CoordType(i, j, k)); + } __hostdev__ ValueType operator()(const CoordType& ijk) const { return this->getValue(ijk); } __hostdev__ ValueType operator()(int i, int j, int k) const { - return this->getValue(CoordType(i,j,k)); + return this->getValue(CoordType(i, j, k)); } __hostdev__ NodeInfo getNodeInfo(const CoordType& ijk) const @@ -4723,12 +6649,23 @@ class ReadAccessor { return mRoot->probeLeafAndCache(ijk, *this); } - +#endif // NANOVDB_NEW_ACCESSOR_METHODS template __hostdev__ uint32_t getDim(const CoordType& ijk, const RayT& ray) const { return mRoot->getDimAndCache(ijk, ray, *this); } + template + __hostdev__ auto get(const CoordType& ijk, ArgsT&&... args) const + { + return mRoot->template get(ijk, args...); + } + + template + __hostdev__ auto set(const CoordType& ijk, ArgsT&&... args) const + { + return const_cast(mRoot)->template set(ijk, args...); + } private: /// @brief Allow nodes to insert themselves into the cache. @@ -4745,16 +6682,16 @@ class ReadAccessor }; // ReadAccessor class /// @brief Node caching at a single tree level -template -class ReadAccessor//e.g. 0, 1, 2 +template +class ReadAccessor //e.g. 0, 1, 2 { static_assert(LEVEL0 >= 0 && LEVEL0 <= 2, "LEVEL0 should be 0, 1, or 2"); - using GridT = NanoGrid;// grid - using TreeT = NanoTree; - using RootT = NanoRoot; // root node - using LeafT = NanoLeaf; // Leaf node - using NodeT = typename NodeTrait::type; + using GridT = NanoGrid; // grid + using TreeT = NanoTree; + using RootT = NanoRoot; // root node + using LeafT = NanoLeaf; // Leaf node + using NodeT = typename NodeTrait::type; using CoordT = typename RootT::CoordType; using ValueT = typename RootT::ValueType; @@ -4767,13 +6704,14 @@ class ReadAccessor//e.g. 0, 1, 2 mutable const NodeT* mNode; // 8 bytes public: + using BuildType = BuildT; using ValueType = ValueT; using CoordType = CoordT; static const int CacheLevels = 1; - +#ifndef NANOVDB_NEW_ACCESSOR_METHODS using NodeInfo = typename ReadAccessor::NodeInfo; - +#endif /// @brief Constructor from a root node __hostdev__ ReadAccessor(const RootT& root) : mKey(CoordType::max()) @@ -4783,10 +6721,16 @@ class ReadAccessor//e.g. 0, 1, 2 } /// @brief Constructor from a grid - __hostdev__ ReadAccessor(const GridT& grid) : ReadAccessor(grid.tree().root()) {} + __hostdev__ ReadAccessor(const GridT& grid) + : ReadAccessor(grid.tree().root()) + { + } /// @brief Constructor from a tree - __hostdev__ ReadAccessor(const TreeT& tree) : ReadAccessor(tree.root()) {} + __hostdev__ ReadAccessor(const TreeT& tree) + : ReadAccessor(tree.root()) + { + } /// @brief Reset this access to its initial state, i.e. with an empty cache __hostdev__ void clear() @@ -4809,63 +6753,90 @@ class ReadAccessor//e.g. 0, 1, 2 (ijk[2] & int32_t(~NodeT::MASK)) == mKey[2]; } +#ifdef NANOVDB_NEW_ACCESSOR_METHODS + __hostdev__ ValueType getValue(const CoordType& ijk) const + { + return this->template get>(ijk); + } + __hostdev__ ValueType getValue(int i, int j, int k) const { return this->template get>(CoordType(i, j, k)); } + __hostdev__ ValueType operator()(const CoordType& ijk) const { return this->template get>(ijk); } + __hostdev__ ValueType operator()(int i, int j, int k) const { return this->template get>(CoordType(i, j, k)); } + __hostdev__ auto getNodeInfo(const CoordType& ijk) const { return this->template get>(ijk); } + __hostdev__ bool isActive(const CoordType& ijk) const { return this->template get>(ijk); } + __hostdev__ bool probeValue(const CoordType& ijk, ValueType& v) const { return this->template get>(ijk, v); } + __hostdev__ const LeafT* probeLeaf(const CoordType& ijk) const { return this->template get>(ijk); } +#else // NANOVDB_NEW_ACCESSOR_METHODS __hostdev__ ValueType getValue(const CoordType& ijk) const { - if (this->isCached(ijk)) { + if (this->isCached(ijk)) return mNode->getValueAndCache(ijk, *this); - } return mRoot->getValueAndCache(ijk, *this); } + __hostdev__ ValueType getValue(int i, int j, int k) const + { + return this->getValue(CoordType(i, j, k)); + } __hostdev__ ValueType operator()(const CoordType& ijk) const { return this->getValue(ijk); } __hostdev__ ValueType operator()(int i, int j, int k) const { - return this->getValue(CoordType(i,j,k)); + return this->getValue(CoordType(i, j, k)); } __hostdev__ NodeInfo getNodeInfo(const CoordType& ijk) const { - if (this->isCached(ijk)) { + if (this->isCached(ijk)) return mNode->getNodeInfoAndCache(ijk, *this); - } return mRoot->getNodeInfoAndCache(ijk, *this); } __hostdev__ bool isActive(const CoordType& ijk) const { - if (this->isCached(ijk)) { + if (this->isCached(ijk)) return mNode->isActiveAndCache(ijk, *this); - } return mRoot->isActiveAndCache(ijk, *this); } __hostdev__ bool probeValue(const CoordType& ijk, ValueType& v) const { - if (this->isCached(ijk)) { + if (this->isCached(ijk)) return mNode->probeValueAndCache(ijk, v, *this); - } return mRoot->probeValueAndCache(ijk, v, *this); } __hostdev__ const LeafT* probeLeaf(const CoordType& ijk) const { - if (this->isCached(ijk)) { + if (this->isCached(ijk)) return mNode->probeLeafAndCache(ijk, *this); - } return mRoot->probeLeafAndCache(ijk, *this); } - +#endif // NANOVDB_NEW_ACCESSOR_METHODS template __hostdev__ uint32_t getDim(const CoordType& ijk, const RayT& ray) const { - if (this->isCached(ijk)) { + if (this->isCached(ijk)) return mNode->getDimAndCache(ijk, ray, *this); - } return mRoot->getDimAndCache(ijk, ray, *this); } + template + __hostdev__ auto get(const CoordType& ijk, ArgsT&&... args) const + { + if (this->isCached(ijk)) + return mNode->template getAndCache(ijk, *this, args...); + return mRoot->template getAndCache(ijk, *this, args...); + } + + template + __hostdev__ auto set(const CoordType& ijk, ArgsT&&... args) const + { + if (this->isCached(ijk)) + return const_cast(mNode)->template setAndCache(ijk, *this, args...); + return const_cast(mRoot)->template setAndCache(ijk, *this, args...); + } + private: /// @brief Allow nodes to insert themselves into the cache. template @@ -4888,16 +6859,16 @@ class ReadAccessor//e.g. 0, 1, 2 }; // ReadAccessor -template -class ReadAccessor//e.g. (0,1), (1,2), (0,2) +template +class ReadAccessor //e.g. (0,1), (1,2), (0,2) { static_assert(LEVEL0 >= 0 && LEVEL0 <= 2, "LEVEL0 must be 0, 1, 2"); static_assert(LEVEL1 >= 0 && LEVEL1 <= 2, "LEVEL1 must be 0, 1, 2"); static_assert(LEVEL0 < LEVEL1, "Level 0 must be lower than level 1"); - using GridT = NanoGrid;// grid - using TreeT = NanoTree; - using RootT = NanoRoot; - using LeafT = NanoLeaf; + using GridT = NanoGrid; // grid + using TreeT = NanoTree; + using RootT = NanoRoot; + using LeafT = NanoLeaf; using Node1T = typename NodeTrait::type; using Node2T = typename NodeTrait::type; using CoordT = typename RootT::CoordType; @@ -4906,7 +6877,7 @@ class ReadAccessor//e.g. (0,1), (1,2), (0,2) using CoordValueType = typename RootT::CoordT::ValueType; // All member data are mutable to allow for access methods to be const -#ifdef USE_SINGLE_ACCESSOR_KEY // 44 bytes total +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY // 44 bytes total mutable CoordT mKey; // 3*4 = 12 bytes #else // 68 bytes total mutable CoordT mKeys[2]; // 2*3*4 = 24 bytes @@ -4916,16 +6887,17 @@ class ReadAccessor//e.g. (0,1), (1,2), (0,2) mutable const Node2T* mNode2; public: + using BuildType = BuildT; using ValueType = ValueT; using CoordType = CoordT; static const int CacheLevels = 2; - - using NodeInfo = typename ReadAccessor::NodeInfo; - +#ifndef NANOVDB_NEW_ACCESSOR_METHODS + using NodeInfo = typename ReadAccessor::NodeInfo; +#endif /// @brief Constructor from a root node __hostdev__ ReadAccessor(const RootT& root) -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY : mKey(CoordType::max()) #else : mKeys{CoordType::max(), CoordType::max()} @@ -4936,16 +6908,22 @@ class ReadAccessor//e.g. (0,1), (1,2), (0,2) { } - /// @brief Constructor from a grid - __hostdev__ ReadAccessor(const GridT& grid) : ReadAccessor(grid.tree().root()) {} + /// @brief Constructor from a grid + __hostdev__ ReadAccessor(const GridT& grid) + : ReadAccessor(grid.tree().root()) + { + } /// @brief Constructor from a tree - __hostdev__ ReadAccessor(const TreeT& tree) : ReadAccessor(tree.root()) {} + __hostdev__ ReadAccessor(const TreeT& tree) + : ReadAccessor(tree.root()) + { + } /// @brief Reset this access to its initial state, i.e. with an empty cache __hostdev__ void clear() { -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY mKey = CoordType::max(); #else mKeys[0] = mKeys[1] = CoordType::max(); @@ -4961,7 +6939,7 @@ class ReadAccessor//e.g. (0,1), (1,2), (0,2) ~ReadAccessor() = default; ReadAccessor& operator=(const ReadAccessor&) = default; -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY __hostdev__ bool isCached1(CoordValueType dirty) const { if (!mNode1) @@ -5001,9 +6979,23 @@ class ReadAccessor//e.g. (0,1), (1,2), (0,2) } #endif +#ifdef NANOVDB_NEW_ACCESSOR_METHODS __hostdev__ ValueType getValue(const CoordType& ijk) const { -#ifdef USE_SINGLE_ACCESSOR_KEY + return this->template get>(ijk); + } + __hostdev__ ValueType getValue(int i, int j, int k) const { return this->template get>(CoordType(i, j, k)); } + __hostdev__ ValueType operator()(const CoordType& ijk) const { return this->template get>(ijk); } + __hostdev__ ValueType operator()(int i, int j, int k) const { return this->template get>(CoordType(i, j, k)); } + __hostdev__ auto getNodeInfo(const CoordType& ijk) const { return this->template get>(ijk); } + __hostdev__ bool isActive(const CoordType& ijk) const { return this->template get>(ijk); } + __hostdev__ bool probeValue(const CoordType& ijk, ValueType& v) const { return this->template get>(ijk, v); } + __hostdev__ const LeafT* probeLeaf(const CoordType& ijk) const { return this->template get>(ijk); } +#else // NANOVDB_NEW_ACCESSOR_METHODS + + __hostdev__ ValueType getValue(const CoordType& ijk) const + { +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY const CoordValueType dirty = this->computeDirty(ijk); #else auto&& dirty = ijk; @@ -5021,12 +7013,15 @@ class ReadAccessor//e.g. (0,1), (1,2), (0,2) } __hostdev__ ValueType operator()(int i, int j, int k) const { - return this->getValue(CoordType(i,j,k)); + return this->getValue(CoordType(i, j, k)); + } + __hostdev__ ValueType getValue(int i, int j, int k) const + { + return this->getValue(CoordType(i, j, k)); } - __hostdev__ NodeInfo getNodeInfo(const CoordType& ijk) const { -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY const CoordValueType dirty = this->computeDirty(ijk); #else auto&& dirty = ijk; @@ -5041,7 +7036,7 @@ class ReadAccessor//e.g. (0,1), (1,2), (0,2) __hostdev__ bool isActive(const CoordType& ijk) const { -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY const CoordValueType dirty = this->computeDirty(ijk); #else auto&& dirty = ijk; @@ -5056,7 +7051,7 @@ class ReadAccessor//e.g. (0,1), (1,2), (0,2) __hostdev__ bool probeValue(const CoordType& ijk, ValueType& v) const { -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY const CoordValueType dirty = this->computeDirty(ijk); #else auto&& dirty = ijk; @@ -5071,7 +7066,7 @@ class ReadAccessor//e.g. (0,1), (1,2), (0,2) __hostdev__ const LeafT* probeLeaf(const CoordType& ijk) const { -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY const CoordValueType dirty = this->computeDirty(ijk); #else auto&& dirty = ijk; @@ -5083,11 +7078,12 @@ class ReadAccessor//e.g. (0,1), (1,2), (0,2) } return mRoot->probeLeafAndCache(ijk, *this); } +#endif // NANOVDB_NEW_ACCESSOR_METHODS template __hostdev__ uint32_t getDim(const CoordType& ijk, const RayT& ray) const { -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY const CoordValueType dirty = this->computeDirty(ijk); #else auto&& dirty = ijk; @@ -5100,6 +7096,38 @@ class ReadAccessor//e.g. (0,1), (1,2), (0,2) return mRoot->getDimAndCache(ijk, ray, *this); } + template + __hostdev__ auto get(const CoordType& ijk, ArgsT&&... args) const + { +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY + const CoordValueType dirty = this->computeDirty(ijk); +#else + auto&& dirty = ijk; +#endif + if (this->isCached1(dirty)) { + return mNode1->template getAndCache(ijk, *this, args...); + } else if (this->isCached2(dirty)) { + return mNode2->template getAndCache(ijk, *this, args...); + } + return mRoot->template getAndCache(ijk, *this, args...); + } + + template + __hostdev__ auto set(const CoordType& ijk, ArgsT&&... args) const + { +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY + const CoordValueType dirty = this->computeDirty(ijk); +#else + auto&& dirty = ijk; +#endif + if (this->isCached1(dirty)) { + return const_cast(mNode1)->template setAndCache(ijk, *this, args...); + } else if (this->isCached2(dirty)) { + return const_cast(mNode2)->template setAndCache(ijk, *this, args...); + } + return const_cast(mRoot)->template setAndCache(ijk, *this, args...); + } + private: /// @brief Allow nodes to insert themselves into the cache. template @@ -5112,7 +7140,7 @@ class ReadAccessor//e.g. (0,1), (1,2), (0,2) /// @brief Inserts a leaf node and key pair into this ReadAccessor __hostdev__ void insert(const CoordType& ijk, const Node1T* node) const { -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY mKey = ijk; #else mKeys[0] = ijk & ~Node1T::MASK; @@ -5121,28 +7149,27 @@ class ReadAccessor//e.g. (0,1), (1,2), (0,2) } __hostdev__ void insert(const CoordType& ijk, const Node2T* node) const { -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY mKey = ijk; #else mKeys[1] = ijk & ~Node2T::MASK; #endif mNode2 = node; } - template + template __hostdev__ void insert(const CoordType&, const OtherNodeT*) const {} }; // ReadAccessor - /// @brief Node caching at all (three) tree levels -template +template class ReadAccessor { - using GridT = NanoGrid;// grid - using TreeT = NanoTree; - using RootT = NanoRoot; // root node + using GridT = NanoGrid; // grid + using TreeT = NanoTree; + using RootT = NanoRoot; // root node using NodeT2 = NanoUpper; // upper internal node using NodeT1 = NanoLower; // lower internal node - using LeafT = NanoLeaf< BuildT>; // Leaf node + using LeafT = NanoLeaf; // Leaf node using CoordT = typename RootT::CoordType; using ValueT = typename RootT::ValueType; @@ -5150,25 +7177,26 @@ class ReadAccessor using CoordValueType = typename RootT::CoordT::ValueType; // All member data are mutable to allow for access methods to be const -#ifdef USE_SINGLE_ACCESSOR_KEY // 44 bytes total +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY // 44 bytes total mutable CoordT mKey; // 3*4 = 12 bytes #else // 68 bytes total mutable CoordT mKeys[3]; // 3*3*4 = 36 bytes #endif mutable const RootT* mRoot; - mutable const void* mNode[3]; // 4*8 = 32 bytes + mutable const void* mNode[3]; // 4*8 = 32 bytes public: + using BuildType = BuildT; using ValueType = ValueT; using CoordType = CoordT; static const int CacheLevels = 3; - +#ifndef NANOVDB_NEW_ACCESSOR_METHODS using NodeInfo = typename ReadAccessor::NodeInfo; - +#endif /// @brief Constructor from a root node __hostdev__ ReadAccessor(const RootT& root) -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY : mKey(CoordType::max()) #else : mKeys{CoordType::max(), CoordType::max(), CoordType::max()} @@ -5178,11 +7206,17 @@ class ReadAccessor { } - /// @brief Constructor from a grid - __hostdev__ ReadAccessor(const GridT& grid) : ReadAccessor(grid.tree().root()) {} + /// @brief Constructor from a grid + __hostdev__ ReadAccessor(const GridT& grid) + : ReadAccessor(grid.tree().root()) + { + } /// @brief Constructor from a tree - __hostdev__ ReadAccessor(const TreeT& tree) : ReadAccessor(tree.root()) {} + __hostdev__ ReadAccessor(const TreeT& tree) + : ReadAccessor(tree.root()) + { + } __hostdev__ const RootT& root() const { return *mRoot; } @@ -5202,19 +7236,18 @@ class ReadAccessor return reinterpret_cast(mNode[NodeT::LEVEL]); } - template + template __hostdev__ const typename NodeTrait::type* getNode() const { using T = typename NodeTrait::type; - static_assert(LEVEL>=0 && LEVEL<=2, "ReadAccessor::getNode: Invalid node type"); + static_assert(LEVEL >= 0 && LEVEL <= 2, "ReadAccessor::getNode: Invalid node type"); return reinterpret_cast(mNode[LEVEL]); } - /// @brief Reset this access to its initial state, i.e. with an empty cache __hostdev__ void clear() { -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY mKey = CoordType::max(); #else mKeys[0] = mKeys[1] = mKeys[2] = CoordType::max(); @@ -5222,7 +7255,7 @@ class ReadAccessor mNode[0] = mNode[1] = mNode[2] = nullptr; } -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY template __hostdev__ bool isCached(CoordValueType dirty) const { @@ -5243,13 +7276,29 @@ class ReadAccessor template __hostdev__ bool isCached(const CoordType& ijk) const { - return (ijk[0] & int32_t(~NodeT::MASK)) == mKeys[NodeT::LEVEL][0] && (ijk[1] & int32_t(~NodeT::MASK)) == mKeys[NodeT::LEVEL][1] && (ijk[2] & int32_t(~NodeT::MASK)) == mKeys[NodeT::LEVEL][2]; + return (ijk[0] & int32_t(~NodeT::MASK)) == mKeys[NodeT::LEVEL][0] && + (ijk[1] & int32_t(~NodeT::MASK)) == mKeys[NodeT::LEVEL][1] && + (ijk[2] & int32_t(~NodeT::MASK)) == mKeys[NodeT::LEVEL][2]; } #endif +#ifdef NANOVDB_NEW_ACCESSOR_METHODS + __hostdev__ ValueType getValue(const CoordType& ijk) const + { + return this->template get>(ijk); + } + __hostdev__ ValueType getValue(int i, int j, int k) const { return this->template get>(CoordType(i, j, k)); } + __hostdev__ ValueType operator()(const CoordType& ijk) const { return this->template get>(ijk); } + __hostdev__ ValueType operator()(int i, int j, int k) const { return this->template get>(CoordType(i, j, k)); } + __hostdev__ auto getNodeInfo(const CoordType& ijk) const { return this->template get>(ijk); } + __hostdev__ bool isActive(const CoordType& ijk) const { return this->template get>(ijk); } + __hostdev__ bool probeValue(const CoordType& ijk, ValueType& v) const { return this->template get>(ijk, v); } + __hostdev__ const LeafT* probeLeaf(const CoordType& ijk) const { return this->template get>(ijk); } +#else // NANOVDB_NEW_ACCESSOR_METHODS + __hostdev__ ValueType getValue(const CoordType& ijk) const { -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY const CoordValueType dirty = this->computeDirty(ijk); #else auto&& dirty = ijk; @@ -5269,12 +7318,16 @@ class ReadAccessor } __hostdev__ ValueType operator()(int i, int j, int k) const { - return this->getValue(CoordType(i,j,k)); + return this->getValue(CoordType(i, j, k)); + } + __hostdev__ ValueType getValue(int i, int j, int k) const + { + return this->getValue(CoordType(i, j, k)); } __hostdev__ NodeInfo getNodeInfo(const CoordType& ijk) const { -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY const CoordValueType dirty = this->computeDirty(ijk); #else auto&& dirty = ijk; @@ -5291,7 +7344,7 @@ class ReadAccessor __hostdev__ bool isActive(const CoordType& ijk) const { -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY const CoordValueType dirty = this->computeDirty(ijk); #else auto&& dirty = ijk; @@ -5308,7 +7361,7 @@ class ReadAccessor __hostdev__ bool probeValue(const CoordType& ijk, ValueType& v) const { -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY const CoordValueType dirty = this->computeDirty(ijk); #else auto&& dirty = ijk; @@ -5322,10 +7375,9 @@ class ReadAccessor } return mRoot->probeValueAndCache(ijk, v, *this); } - __hostdev__ const LeafT* probeLeaf(const CoordType& ijk) const { -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY const CoordValueType dirty = this->computeDirty(ijk); #else auto&& dirty = ijk; @@ -5339,11 +7391,48 @@ class ReadAccessor } return mRoot->probeLeafAndCache(ijk, *this); } +#endif // NANOVDB_NEW_ACCESSOR_METHODS + + template + __hostdev__ auto get(const CoordType& ijk, ArgsT&&... args) const + { +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY + const CoordValueType dirty = this->computeDirty(ijk); +#else + auto&& dirty = ijk; +#endif + if (this->isCached(dirty)) { + return ((const LeafT*)mNode[0])->template getAndCache(ijk, *this, args...); + } else if (this->isCached(dirty)) { + return ((const NodeT1*)mNode[1])->template getAndCache(ijk, *this, args...); + } else if (this->isCached(dirty)) { + return ((const NodeT2*)mNode[2])->template getAndCache(ijk, *this, args...); + } + return mRoot->template getAndCache(ijk, *this, args...); + } + + template + __hostdev__ auto set(const CoordType& ijk, ArgsT&&... args) const + { +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY + const CoordValueType dirty = this->computeDirty(ijk); +#else + auto&& dirty = ijk; +#endif + if (this->isCached(dirty)) { + return ((LeafT*)mNode[0])->template setAndCache(ijk, *this, args...); + } else if (this->isCached(dirty)) { + return ((NodeT1*)mNode[1])->template setAndCache(ijk, *this, args...); + } else if (this->isCached(dirty)) { + return ((NodeT2*)mNode[2])->template setAndCache(ijk, *this, args...); + } + return ((RootT*)mRoot)->template setAndCache(ijk, *this, args...); + } template __hostdev__ uint32_t getDim(const CoordType& ijk, const RayT& ray) const { -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY const CoordValueType dirty = this->computeDirty(ijk); #else auto&& dirty = ijk; @@ -5371,7 +7460,7 @@ class ReadAccessor template __hostdev__ void insert(const CoordType& ijk, const NodeT* node) const { -#ifdef USE_SINGLE_ACCESSOR_KEY +#ifdef NANOVDB_USE_SINGLE_ACCESSOR_KEY mKey = ijk; #else mKeys[NodeT::LEVEL] = ijk & ~NodeT::MASK; @@ -5394,20 +7483,20 @@ class ReadAccessor /// createAccessor<1,2>(grid): Caching of lower and upper internal nodes /// createAccessor<0,1,2>(grid): Caching of all nodes at all tree levels -template -ReadAccessor createAccessor(const NanoGrid &grid) +template +ReadAccessor createAccessor(const NanoGrid& grid) { return ReadAccessor(grid); } -template -ReadAccessor createAccessor(const NanoTree &tree) +template +ReadAccessor createAccessor(const NanoTree& tree) { return ReadAccessor(tree); } -template -ReadAccessor createAccessor(const NanoRoot &root) +template +ReadAccessor createAccessor(const NanoRoot& root) { return ReadAccessor(root); } @@ -5419,70 +7508,114 @@ ReadAccessor createAccessor(const NanoRoot; - __hostdev__ const GridT& grid() const { return *reinterpret_cast(this); } +{ // 768 bytes (32 byte aligned) + GridData mGridData; // 672B + TreeData mTreeData; // 64B + CoordBBox mIndexBBox; // 24B. AABB of active values in index space. + uint32_t mRootTableSize, mPadding{0}; // 8B public: - __hostdev__ bool isValid() const { return this->grid().isValid(); } - __hostdev__ uint64_t gridSize() const { return this->grid().gridSize(); } - __hostdev__ uint32_t gridIndex() const { return this->grid().gridIndex(); } - __hostdev__ uint32_t gridCount() const { return this->grid().gridCount(); } - __hostdev__ const char* shortGridName() const { return this->grid().shortGridName(); } - __hostdev__ GridType gridType() const { return this->grid().gridType(); } - __hostdev__ GridClass gridClass() const { return this->grid().gridClass(); } - __hostdev__ bool isLevelSet() const { return this->grid().isLevelSet(); } - __hostdev__ bool isFogVolume() const { return this->grid().isFogVolume(); } - __hostdev__ bool isPointIndex() const { return this->grid().isPointIndex(); } - __hostdev__ bool isPointData() const { return this->grid().isPointData(); } - __hostdev__ bool isMask() const { return this->grid().isMask(); } - __hostdev__ bool isStaggered() const { return this->grid().isStaggered(); } - __hostdev__ bool isUnknown() const { return this->grid().isUnknown(); } - __hostdev__ const Map& map() const { return this->grid().map(); } - __hostdev__ const BBox& worldBBox() const { return this->grid().worldBBox(); } - __hostdev__ const BBox& indexBBox() const { return this->grid().indexBBox(); } - __hostdev__ Vec3R voxelSize() const { return this->grid().voxelSize(); } - __hostdev__ int blindDataCount() const { return this->grid().blindDataCount(); } - __hostdev__ const GridBlindMetaData& blindMetaData(uint32_t n) const { return this->grid().blindMetaData(n); } - __hostdev__ uint64_t activeVoxelCount() const { return this->grid().activeVoxelCount(); } - __hostdev__ const uint32_t& activeTileCount(uint32_t level) const { return this->grid().tree().activeTileCount(level); } - __hostdev__ uint32_t nodeCount(uint32_t level) const { return this->grid().tree().nodeCount(level); } - __hostdev__ uint64_t checksum() const { return this->grid().checksum(); } - __hostdev__ bool isEmpty() const { return this->grid().isEmpty(); } - __hostdev__ Version version() const { return this->grid().version(); } + template + GridMetaData(const NanoGrid& grid) + { + mGridData = *grid.data(); + mTreeData = *grid.tree().data(); + mIndexBBox = grid.indexBBox(); + mRootTableSize = grid.tree().root().getTableSize(); + } + GridMetaData(const GridData* gridData) + { + static_assert(8 * 96 == sizeof(GridMetaData), "GridMetaData has unexpected size"); + if (GridMetaData::safeCast(gridData)) { + memcpy64(this, gridData, 96); + } else {// otherwise copy each member individually + mGridData = *gridData; + mTreeData = *reinterpret_cast(gridData->treePtr()); + mIndexBBox = gridData->indexBBox(); + mRootTableSize = gridData->rootTableSize(); + } + } + /// @brief return true if the RootData follows right after the TreeData. + /// If so, this implies that it's safe to cast the grid from which + /// this instance was constructed to a GridMetaData + __hostdev__ bool safeCast() const { return mTreeData.isRootNext(); } + + /// @brief return true if it is safe to cast the grid to a pointer + /// of type GridMetaData, i.e. construction can be avoided. + __hostdev__ static bool safeCast(const GridData *gridData){ + NANOVDB_ASSERT(gridData && gridData->isValid()); + return gridData->isRootConnected(); + } + /// @brief return true if it is safe to cast the grid to a pointer + /// of type GridMetaData, i.e. construction can be avoided. + template + __hostdev__ static bool safeCast(const NanoGrid& grid){return grid.tree().isRootNext();} + __hostdev__ bool isValid() const { return mGridData.isValid(); } + __hostdev__ const GridType& gridType() const { return mGridData.mGridType; } + __hostdev__ const GridClass& gridClass() const { return mGridData.mGridClass; } + __hostdev__ bool isLevelSet() const { return mGridData.mGridClass == GridClass::LevelSet; } + __hostdev__ bool isFogVolume() const { return mGridData.mGridClass == GridClass::FogVolume; } + __hostdev__ bool isStaggered() const { return mGridData.mGridClass == GridClass::Staggered; } + __hostdev__ bool isPointIndex() const { return mGridData.mGridClass == GridClass::PointIndex; } + __hostdev__ bool isGridIndex() const { return mGridData.mGridClass == GridClass::IndexGrid; } + __hostdev__ bool isPointData() const { return mGridData.mGridClass == GridClass::PointData; } + __hostdev__ bool isMask() const { return mGridData.mGridClass == GridClass::Topology; } + __hostdev__ bool isUnknown() const { return mGridData.mGridClass == GridClass::Unknown; } + __hostdev__ bool hasMinMax() const { return mGridData.mFlags.isMaskOn(GridFlags::HasMinMax); } + __hostdev__ bool hasBBox() const { return mGridData.mFlags.isMaskOn(GridFlags::HasBBox); } + __hostdev__ bool hasLongGridName() const { return mGridData.mFlags.isMaskOn(GridFlags::HasLongGridName); } + __hostdev__ bool hasAverage() const { return mGridData.mFlags.isMaskOn(GridFlags::HasAverage); } + __hostdev__ bool hasStdDeviation() const { return mGridData.mFlags.isMaskOn(GridFlags::HasStdDeviation); } + __hostdev__ bool isBreadthFirst() const { return mGridData.mFlags.isMaskOn(GridFlags::IsBreadthFirst); } + __hostdev__ uint64_t gridSize() const { return mGridData.mGridSize; } + __hostdev__ uint32_t gridIndex() const { return mGridData.mGridIndex; } + __hostdev__ uint32_t gridCount() const { return mGridData.mGridCount; } + __hostdev__ const char* shortGridName() const { return mGridData.mGridName; } + __hostdev__ const Map& map() const { return mGridData.mMap; } + __hostdev__ const BBox& worldBBox() const { return mGridData.mWorldBBox; } + __hostdev__ const BBox& indexBBox() const { return mIndexBBox; } + __hostdev__ Vec3d voxelSize() const { return mGridData.mVoxelSize; } + __hostdev__ int blindDataCount() const { return mGridData.mBlindMetadataCount; } + __hostdev__ uint64_t activeVoxelCount() const { return mTreeData.mVoxelCount; } + __hostdev__ const uint32_t& activeTileCount(uint32_t level) const { return mTreeData.mTileCount[level - 1]; } + __hostdev__ uint32_t nodeCount(uint32_t level) const { return mTreeData.mNodeCount[level]; } + __hostdev__ uint64_t checksum() const { return mGridData.mChecksum; } + __hostdev__ uint32_t rootTableSize() const { return mRootTableSize; } + __hostdev__ bool isEmpty() const { return mRootTableSize == 0; } + __hostdev__ Version version() const { return mGridData.mVersion; } }; // GridMetaData /// @brief Class to access points at a specific voxel location -template -class PointAccessor : public DefaultReadAccessor +/// +/// @note If GridClass::PointIndex AttT should be uint32_t and if GridClass::PointData Vec3f +template +class PointAccessor : public DefaultReadAccessor { - using AccT = DefaultReadAccessor; - const UInt32Grid* mGrid; - const AttT* mData; + using AccT = DefaultReadAccessor; + const NanoGrid& mGrid; + const AttT* mData; public: - using LeafNodeType = typename NanoRoot::LeafNodeType; - - PointAccessor(const UInt32Grid& grid) + PointAccessor(const NanoGrid& grid) : AccT(grid.tree().root()) - , mGrid(&grid) - , mData(reinterpret_cast(grid.blindData(0))) + , mGrid(grid) + , mData(grid.template getBlindData(0)) { - NANOVDB_ASSERT(grid.gridType() == GridType::UInt32); + NANOVDB_ASSERT(grid.gridType() == mapToGridType()); NANOVDB_ASSERT((grid.gridClass() == GridClass::PointIndex && is_same::value) || (grid.gridClass() == GridClass::PointData && is_same::value)); - NANOVDB_ASSERT(grid.blindDataCount() >= 1); } + + /// @brief return true if this access was initialized correctly + __hostdev__ operator bool() const { return mData != nullptr; } + + __hostdev__ const NanoGrid& grid() const { return mGrid; } + /// @brief Return the total number of point in the grid and set the /// iterators to the complete range of points. __hostdev__ uint64_t gridPoints(const AttT*& begin, const AttT*& end) const { - const uint64_t count = mGrid->blindMetaData(0u).mElementCount; + const uint64_t count = mGrid.blindMetaData(0u).mValueCount; begin = mData; end = begin + count; return count; @@ -5501,100 +7634,168 @@ class PointAccessor : public DefaultReadAccessor return leaf->maximum(); } - /// @brief get iterators over offsets to points at a specific voxel location + /// @brief get iterators over attributes to points at a specific voxel location __hostdev__ uint64_t voxelPoints(const Coord& ijk, const AttT*& begin, const AttT*& end) const + { + begin = end = nullptr; + if (auto* leaf = this->probeLeaf(ijk)) { + const uint32_t offset = NanoLeaf::CoordToOffset(ijk); + if (leaf->isActive(offset)) { + begin = mData + leaf->minimum(); + end = begin + leaf->getValue(offset); + if (offset > 0u) + begin += leaf->getValue(offset - 1); + } + } + return end - begin; + } +}; // PointAccessor + +template +class PointAccessor : public DefaultReadAccessor +{ + using AccT = DefaultReadAccessor; + const NanoGrid& mGrid; + const AttT* mData; + +public: + PointAccessor(const NanoGrid& grid) + : AccT(grid.tree().root()) + , mGrid(grid) + , mData(grid.template getBlindData(0)) + { + NANOVDB_ASSERT(mData); + NANOVDB_ASSERT(grid.gridType() == GridType::PointIndex); + NANOVDB_ASSERT((grid.gridClass() == GridClass::PointIndex && is_same::value) || + (grid.gridClass() == GridClass::PointData && is_same::value) || + (grid.gridClass() == GridClass::PointData && is_same::value) || + (grid.gridClass() == GridClass::PointData && is_same::value) || + (grid.gridClass() == GridClass::PointData && is_same::value)); + } + + /// @brief return true if this access was initialized correctly + __hostdev__ operator bool() const { return mData != nullptr; } + + __hostdev__ const NanoGrid& grid() const { return mGrid; } + + /// @brief Return the total number of point in the grid and set the + /// iterators to the complete range of points. + __hostdev__ uint64_t gridPoints(const AttT*& begin, const AttT*& end) const + { + const uint64_t count = mGrid.blindMetaData(0u).mValueCount; + begin = mData; + end = begin + count; + return count; + } + /// @brief Return the number of points in the leaf node containing the coordinate @a ijk. + /// If this return value is larger than zero then the iterators @a begin and @a end + /// will point to all the attributes contained within that leaf node. + __hostdev__ uint64_t leafPoints(const Coord& ijk, const AttT*& begin, const AttT*& end) const { auto* leaf = this->probeLeaf(ijk); if (leaf == nullptr) return 0; - const uint32_t offset = LeafNodeType::CoordToOffset(ijk); - if (leaf->isActive(offset)) { - auto* p = mData + leaf->minimum(); - begin = p + (offset == 0 ? 0 : leaf->getValue(offset - 1)); - end = p + leaf->getValue(offset); - return end - begin; + begin = mData + leaf->offset(); + end = begin + leaf->pointCount(); + return leaf->pointCount(); + } + + /// @brief get iterators over attributes to points at a specific voxel location + __hostdev__ uint64_t voxelPoints(const Coord& ijk, const AttT*& begin, const AttT*& end) const + { + if (auto* leaf = this->probeLeaf(ijk)) { + const uint32_t n = NanoLeaf::CoordToOffset(ijk); + if (leaf->isActive(n)) { + begin = mData + leaf->first(n); + end = mData + leaf->last(n); + return end - begin; + } } - return 0; + begin = end = nullptr; + return 0u; // no leaf or inactive voxel } -}; // PointAccessor +}; // PointAccessor /// @brief Class to access values in channels at a specific voxel location. /// /// @note The ChannelT template parameter can be either const and non-const. -template -class ChannelAccessor : public DefaultReadAccessor +template +class ChannelAccessor : public DefaultReadAccessor { - using BaseT = DefaultReadAccessor; - const IndexGrid &mGrid; - ChannelT *mChannel; + static_assert(BuildTraits::is_index, "Expected an index build type"); + using BaseT = DefaultReadAccessor; + + const NanoGrid& mGrid; + ChannelT* mChannel; public: using ValueType = ChannelT; - using TreeType = IndexTree; - using AccessorType = ChannelAccessor; + using TreeType = NanoTree; + using AccessorType = ChannelAccessor; /// @brief Ctor from an IndexGrid and an integer ID of an internal channel /// that is assumed to exist as blind data in the IndexGrid. - __hostdev__ ChannelAccessor(const IndexGrid& grid, uint32_t channelID = 0u) + __hostdev__ ChannelAccessor(const NanoGrid& grid, uint32_t channelID = 0u) : BaseT(grid.tree().root()) , mGrid(grid) , mChannel(nullptr) { - NANOVDB_ASSERT(grid.gridType() == GridType::Index); + NANOVDB_ASSERT(isIndex(grid.gridType())); NANOVDB_ASSERT(grid.gridClass() == GridClass::IndexGrid); this->setChannel(channelID); } /// @brief Ctor from an IndexGrid and an external channel - __hostdev__ ChannelAccessor(const IndexGrid& grid, ChannelT *channelPtr) + __hostdev__ ChannelAccessor(const NanoGrid& grid, ChannelT* channelPtr) : BaseT(grid.tree().root()) , mGrid(grid) , mChannel(channelPtr) { - NANOVDB_ASSERT(grid.gridType() == GridType::Index); + NANOVDB_ASSERT(isIndex(grid.gridType())); NANOVDB_ASSERT(grid.gridClass() == GridClass::IndexGrid); - NANOVDB_ASSERT(mChannel); } + /// @brief return true if this access was initialized correctly + __hostdev__ operator bool() const { return mChannel != nullptr; } + /// @brief Return a const reference to the IndexGrid - __hostdev__ const IndexGrid &grid() const {return mGrid;} + __hostdev__ const NanoGrid& grid() const { return mGrid; } /// @brief Return a const reference to the tree of the IndexGrid - __hostdev__ const IndexTree &tree() const {return mGrid.tree();} + __hostdev__ const TreeType& tree() const { return mGrid.tree(); } /// @brief Return a vector of the axial voxel sizes - __hostdev__ const Vec3R& voxelSize() const { return mGrid.voxelSize(); } + __hostdev__ const Vec3d& voxelSize() const { return mGrid.voxelSize(); } /// @brief Return total number of values indexed by the IndexGrid __hostdev__ const uint64_t& valueCount() const { return mGrid.valueCount(); } /// @brief Change to an external channel - __hostdev__ void setChannel(ChannelT *channelPtr) - { - mChannel = channelPtr; - NANOVDB_ASSERT(mChannel); - } + /// @return Pointer to channel data + __hostdev__ ChannelT* setChannel(ChannelT* channelPtr) {return mChannel = channelPtr;} - /// @brief Change to an internal channel, assuming it exists as as blind data - /// in the IndexGrid. - __hostdev__ void setChannel(uint32_t channelID) + /// @brief Change to an internal channel, assuming it exists as as blind data + /// in the IndexGrid. + /// @return Pointer to channel data, which could be NULL if channelID is out of range or + /// if ChannelT does not match the value type of the blind data + __hostdev__ ChannelT* setChannel(uint32_t channelID) { - this->setChannel(reinterpret_cast(const_cast(mGrid.blindData(channelID)))); + return mChannel = const_cast(mGrid.template getBlindData(channelID)); } /// @brief Return the linear offset into a channel that maps to the specified coordinate - __hostdev__ uint64_t getIndex(const Coord& ijk) const {return BaseT::getValue(ijk);} - __hostdev__ uint64_t idx(int i, int j, int k) const {return BaseT::getValue(Coord(i,j,k));} + __hostdev__ uint64_t getIndex(const Coord& ijk) const { return BaseT::getValue(ijk); } + __hostdev__ uint64_t idx(int i, int j, int k) const { return BaseT::getValue(Coord(i, j, k)); } /// @brief Return the value from a cached channel that maps to the specified coordinate - __hostdev__ ChannelT& getValue(const Coord& ijk) const {return mChannel[BaseT::getValue(ijk)];} - __hostdev__ ChannelT& operator()(const Coord& ijk) const {return this->getValue(ijk);} - __hostdev__ ChannelT& operator()(int i, int j, int k) const {return this->getValue(Coord(i,j,k));} + __hostdev__ ChannelT& getValue(const Coord& ijk) const { return mChannel[BaseT::getValue(ijk)]; } + __hostdev__ ChannelT& operator()(const Coord& ijk) const { return this->getValue(ijk); } + __hostdev__ ChannelT& operator()(int i, int j, int k) const { return this->getValue(Coord(i, j, k)); } /// @brief return the state and updates the value of the specified voxel - __hostdev__ bool probeValue(const CoordType& ijk, typename remove_const::type &v) const + __hostdev__ bool probeValue(const Coord& ijk, typename remove_const::type& v) const { - uint64_t idx; + uint64_t idx; const bool isActive = BaseT::probeValue(ijk, idx); v = mChannel[idx]; return isActive; @@ -5602,17 +7803,15 @@ class ChannelAccessor : public DefaultReadAccessor /// @brief Return the value from a specified channel that maps to the specified coordinate /// /// @note The template parameter can be either const or non-const - template - __hostdev__ T& getValue(const Coord& ijk, T* channelPtr) const {return channelPtr[BaseT::getValue(ijk)];} + template + __hostdev__ T& getValue(const Coord& ijk, T* channelPtr) const { return channelPtr[BaseT::getValue(ijk)]; } }; // ChannelAccessor - -#if !defined(__CUDA_ARCH__) && !defined(__HIP__) - #if 0 // This MiniGridHandle class is only included as a stand-alone example. Note that aligned_alloc is a C++17 feature! -// Normally we recommend using GridHandle defined in util/GridHandle.h +// Normally we recommend using GridHandle defined in util/GridHandle.h but this minimal implementation could be an +// alternative when using the IO medthods defined below. struct MiniGridHandle { struct BufferType { uint8_t *data; @@ -5628,8 +7827,81 @@ struct MiniGridHandle { const uint8_t* data() const {return buffer.data;} };// MiniGridHandle #endif + namespace io { +/// @brief Define compression codecs +/// +/// @note NONE is the default, ZIP is slow but compact and BLOSC offers a great balance. +/// +/// @throw NanoVDB optionally supports ZIP and BLOSC compression and will throw an exception +/// if its support is required but missing. +enum class Codec : uint16_t { NONE = 0, + ZIP = 1, + BLOSC = 2, + END = 3 }; + +/// @brief Data encoded at the head of each segment of a file or stream. +/// +/// @note A file or stream is composed of one or more segments that each contain +// one or more grids. +struct FileHeader {// 16 bytes + uint64_t magic;// 8 bytes + Version version;// 4 bytes version numbers + uint16_t gridCount;// 2 bytes + Codec codec;// 2 bytes + bool isValid() const {return magic == NANOVDB_MAGIC_NUMBER || magic == NANOVDB_MAGIC_FILE;} +}; // FileHeader ( 16 bytes = 2 words ) + +// @brief Data encoded for each of the grids associated with a segment. +// Grid size in memory (uint64_t) | +// Grid size on disk (uint64_t) | +// Grid name hash key (uint64_t) | +// Numer of active voxels (uint64_t) | +// Grid type (uint32_t) | +// Grid class (uint32_t) | +// Characters in grid name (uint32_t) | +// AABB in world space (2*3*double) | one per grid in file +// AABB in index space (2*3*int) | +// Size of a voxel in world units (3*double) | +// Byte size of the grid name (uint32_t) | +// Number of nodes per level (4*uint32_t) | +// Numer of active tiles per level (3*uint32_t) | +// Codec for file compression (uint16_t) | +// Padding due to 8B alignment (uint16_t) | +// Version number (uint32_t) | +struct FileMetaData +{// 176 bytes + uint64_t gridSize, fileSize, nameKey, voxelCount; // 4 * 8 = 32B. + GridType gridType; // 4B. + GridClass gridClass; // 4B. + BBox worldBBox; // 2 * 3 * 8 = 48B. + CoordBBox indexBBox; // 2 * 3 * 4 = 24B. + Vec3d voxelSize; // 24B. + uint32_t nameSize; // 4B. + uint32_t nodeCount[4]; //4 x 4 = 16B + uint32_t tileCount[3];// 3 x 4 = 12B + Codec codec; // 2B + uint16_t padding;// 2B, due to 8B alignment from uint64_t + Version version;// 4B +}; // FileMetaData + +// the following code block uses std and therefore needs to be ignored by CUDA and HIP +#if !defined(__CUDA_ARCH__) && !defined(__HIP__) + +inline const char* toStr(Codec codec) +{ + static const char * LUT[] = { "NONE", "ZIP", "BLOSC" , "END" }; + static_assert(sizeof(LUT) / sizeof(char*) - 1 == int(Codec::END), "Unexpected size of LUT"); + return LUT[static_cast(codec)]; +} + +// Note that starting with version 32.6.0 it is possible to write and read raw grid buffers to +// files, e.g. os.write((const char*)&buffer.data(), buffer.size()) or more conveniently as +// handle.write(fileName). In addition to this simple approach we offer the methods below to +// write traditional uncompressed nanovdb files that unlike raw files include metadata that +// is used for tools like nanovdb_print. + /// /// @brief This is a standalone alternative to io::writeGrid(...,Codec::NONE) defined in util/IO.h /// Unlike the latter this function has no dependencies at all, not even NanoVDB.h, so it also @@ -5645,73 +7917,58 @@ namespace io { /// @throw std::invalid_argument if buffer does not point to a valid NanoVDB grid. /// /// @warning This is pretty ugly code that involves lots of pointer and bit manipulations - not for the faint of heart :) -template // StreamT class must support: "void write(char*, size_t)" -void writeUncompressedGrid(StreamT &os, const void *buffer) -{ - char header[192] = {0}, *dst = header;// combines io::Header + io::MetaData, see util/IO.h - const char *grid = (const char*)buffer, *tree = grid + 672, *root = tree + *(const uint64_t*)(tree + 24); - auto cpy = [&](const char *src, int n){for (auto *end=src+n; src!=end; ++src) *dst++ = *src;}; - if (*(const uint64_t*)(grid)!=0x304244566f6e614eUL) { - fprintf(stderr, "nanovdb::writeUncompressedGrid: invalid magic number\n"); exit(EXIT_FAILURE); - } else if (*(const uint32_t*)(grid+16)>>21!=32) { - fprintf(stderr, "nanovdb::writeUncompressedGrid: invalid major version\n"); exit(EXIT_FAILURE); - } - cpy(grid , 8);// uint64_t Header::magic - cpy(grid + 16, 4);// uint32_t Heder::version - *(uint16_t*)(dst) = 1; dst += 4;// uint16_t Header::gridCount=1 and uint16_t Header::codec=0 - cpy(grid + 32, 8);// uint64_t MetaData::gridSize - cpy(grid + 32, 8);// uint64_t MetaData::fileSize - dst += 8;// uint64_t MetaData::nameKey - cpy(tree + 56, 8);// uint64_t MetaData::voxelCount - cpy(grid + 636, 4);// uint32_t MetaData::gridType - cpy(grid + 632, 4);// uint32_t MetaData::gridClass - cpy(grid + 560, 48);// double[6] MetaData::worldBBox - cpy(root , 24);// int[6] MetaData::indexBBox - cpy(grid + 608, 24);// double[3] MetaData::voxelSize - const char *gridName = grid + 40;// shortGridName - if (*(const uint32_t*)(grid+20) & uint32_t(1)) {// has long grid name - gridName = grid + *(const int64_t*)(grid + 640) + 288*(*(const uint32_t*)(grid + 648) - 1); - gridName += *(const uint64_t*)gridName;// long grid name encoded in blind meta data - } - uint32_t nameSize = 1; // '\0' - for (const char *p = gridName; *p!='\0'; ++p) ++nameSize; - *(uint32_t*)(dst) = nameSize; dst += 4;// uint32_t MetaData::nameSize - cpy(tree + 32, 12);// uint32_t[3] MetaData::nodeCount - *(uint32_t*)(dst) = 1; dst += 4;// uint32_t MetaData::nodeCount[3]=1 - cpy(tree + 44, 12);// uint32_t[3] MetaData::tileCount - dst += 4;// uint16_t codec and padding - cpy(grid + 16, 4);// uint32_t MetaData::version - assert(dst - header == 192); - os.write(header, 192);// write header - os.write(gridName, nameSize);// write grid name - while(1) {// loop over all grids in the buffer (typically just one grid per buffer) - const uint64_t gridSize = *(const uint64_t*)(grid + 32); - os.write(grid, gridSize);// write grid <- bulk of writing! - if (*(const uint32_t*)(grid+24) >= *(const uint32_t*)(grid+28) - 1) break; - grid += gridSize; - } +template // StreamT class must support: "void write(const char*, size_t)" +void writeUncompressedGrid(StreamT& os, const GridData* gridData, bool raw = false) +{ + NANOVDB_ASSERT(gridData->mMagic == NANOVDB_MAGIC_NUMBER || gridData->mMagic == NANOVDB_MAGIC_GRID); + NANOVDB_ASSERT(gridData->mVersion.isCompatible()); + if (!raw) {// segment with a single grid: FileHeader, FileMetaData, gridName, Grid +#ifdef NANOVDB_USE_NEW_MAGIC_NUMBERS + FileHeader head{NANOVDB_MAGIC_FILE, gridData->mVersion, 1u, Codec::NONE}; +#else + FileHeader head{NANOVDB_MAGIC_NUMBER, gridData->mVersion, 1u, Codec::NONE}; +#endif + const char* gridName = gridData->gridName(); + uint32_t nameSize = 1; // '\0' + for (const char* p = gridName; *p != '\0'; ++p) ++nameSize; + const TreeData* treeData = (const TreeData*)gridData->treePtr(); + FileMetaData meta{gridData->mGridSize, gridData->mGridSize, 0u, treeData->mVoxelCount, + gridData->mGridType, gridData->mGridClass, gridData->mWorldBBox, + treeData->bbox(), gridData->mVoxelSize, nameSize, + {treeData->mNodeCount[0], treeData->mNodeCount[1], treeData->mNodeCount[2], 1u}, + {treeData->mTileCount[0], treeData->mTileCount[1], treeData->mTileCount[2]}, + Codec::NONE, 0u, gridData->mVersion }; // FileMetaData + os.write((const char*)&head, sizeof(FileHeader)); // write header + os.write((const char*)&meta, sizeof(FileMetaData)); // write meta data + os.write(gridName, nameSize); // write grid name + } + os.write((const char*)gridData, gridData->mGridSize);// write the grid }// writeUncompressedGrid /// @brief write multiple NanoVDB grids to a single file, without compression. +/// @note To write all grids in a single GridHandle simply use handle.write("fieNane") template class VecT> -void writeUncompressedGrids(const char* fileName, const VecT& handles) +void writeUncompressedGrids(const char* fileName, const VecT& handles, bool raw = false) { -#ifdef NANOVDB_USE_IOSTREAMS// use this to switch between std::ofstream or FILE implementations +#ifdef NANOVDB_USE_IOSTREAMS // use this to switch between std::ofstream or FILE implementations std::ofstream os(fileName, std::ios::out | std::ios::binary | std::ios::trunc); #else struct StreamT { - FILE *fptr; - StreamT(const char *name) {fptr = fopen(name, "wb");} - ~StreamT() {fclose(fptr);} - void write(const char *data, size_t n){fwrite(data, 1, n, fptr);} - bool is_open() const {return fptr != NULL;} + FILE* fptr; + StreamT(const char* name) { fptr = fopen(name, "wb"); } + ~StreamT() { fclose(fptr); } + void write(const char* data, size_t n) { fwrite(data, 1, n, fptr); } + bool is_open() const { return fptr != NULL; } } os(fileName); #endif if (!os.is_open()) { - fprintf(stderr, "nanovdb::writeUncompressedGrids: Unable to open file \"%s\"for output\n",fileName); exit(EXIT_FAILURE); + fprintf(stderr, "nanovdb::writeUncompressedGrids: Unable to open file \"%s\"for output\n", fileName); + exit(EXIT_FAILURE); + } + for (auto& h : handles) { + for (uint32_t n=0; n& handl /// /// @details StreamT class must support: "bool read(char*, size_t)" and "void skip(uint32_t)" template class VecT> -VecT readUncompressedGrids(StreamT& is, const typename GridHandleT::BufferType& buffer = typename GridHandleT::BufferType()) -{// header1, metadata11, grid11, metadata12, grid2 ... header2, metadata21, grid21, metadata22, grid22 ... - char header[16], metadata[176]; - VecT handles; - while(is.read(header, 16)) {// read all segments, e.g. header1, metadata11, grid11, metadata12, grid2 ... - if (*(uint64_t*)(header)!=0x304244566f6e614eUL) { - fprintf(stderr, "nanovdb::readUncompressedGrids: invalid magic number\n"); exit(EXIT_FAILURE); - } else if (*(uint32_t*)(header+8)>>21!=32) { - fprintf(stderr, "nanovdb::readUncompressedGrids: invalid major version\n"); exit(EXIT_FAILURE); - } else if (*(uint16_t*)(header+14)!=0) { - fprintf(stderr, "nanovdb::readUncompressedGrids: invalid codec\n"); exit(EXIT_FAILURE); - } - for (uint16_t i=0, e=*(uint16_t*)(header+12); i readUncompressedGrids(StreamT& is, const typename GridHandleT::BufferType& pool = typename GridHandleT::BufferType()) +{ + VecT handles; + GridData data; + is.read((char*)&data, 40);// we only need to load the first 40 bytes + if (data.mMagic == NANOVDB_MAGIC_GRID || data.isValid()) {// stream contains a raw grid buffer + uint64_t size = data.mGridSize, sum = 0u; + while(data.mGridIndex + 1u < data.mGridCount) { + is.skip(data.mGridSize - 40);// skip grid + is.read((char*)&data, 40);// read 40 bytes + sum += data.mGridSize; + } + is.skip(-int64_t(sum + 40));// rewind to start + auto buffer = GridHandleT::BufferType::create(size + sum, &pool); + is.read((char*)(buffer.data()), buffer.size()); + handles.emplace_back(std::move(buffer)); + } else {// Header0, MetaData0, gridName0, Grid0...HeaderN, MetaDataN, gridNameN, GridN + is.skip(-40);// rewind + FileHeader head; + while(is.read((char*)&head, sizeof(FileHeader))) { + if (!head.isValid()) { + fprintf(stderr, "nanovdb::readUncompressedGrids: invalid magic number = \"%s\"\n", (const char*)&(head.magic)); + exit(EXIT_FAILURE); + } else if (!head.version.isCompatible()) { + fprintf(stderr, "nanovdb::readUncompressedGrids: invalid major version = \"%s\"\n", head.version.c_str()); + exit(EXIT_FAILURE); + } else if (head.codec != Codec::NONE) { + fprintf(stderr, "nanovdb::readUncompressedGrids: invalid codec = \"%s\"\n", toStr(head.codec)); + exit(EXIT_FAILURE); + } + FileMetaData meta; + for (uint16_t i = 0; i < head.gridCount; ++i) { // read all grids in segment + is.read((char*)&meta, sizeof(FileMetaData));// read meta data + is.skip(meta.nameSize); // skip grid name + auto buffer = GridHandleT::BufferType::create(meta.gridSize, &pool); + is.read((char*)buffer.data(), meta.gridSize);// read grid + handles.emplace_back(std::move(buffer)); + }// loop over grids in segment + }// loop over segments + } + return handles; +} // readUncompressedGrids /// @brief Read a multiple un-compressed NanoVDB grids from a file and return them as a vector. template class VecT> -VecT readUncompressedGrids(const char *fileName, const typename GridHandleT::BufferType& buffer = typename GridHandleT::BufferType()) +VecT readUncompressedGrids(const char* fileName, const typename GridHandleT::BufferType& buffer = typename GridHandleT::BufferType()) { -#ifdef NANOVDB_USE_IOSTREAMS// use this to switch between std::ifstream or FILE implementations +#ifdef NANOVDB_USE_IOSTREAMS // use this to switch between std::ifstream or FILE implementations struct StreamT : public std::ifstream { - StreamT(const char *name) : std::ifstream(name, std::ios::in | std::ios::binary) {} - void skip(uint32_t off) {this->seekg(off, std::ios_base::cur);} + StreamT(const char* name) : std::ifstream(name, std::ios::in | std::ios::binary){} + void skip(int64_t off) { this->seekg(off, std::ios_base::cur); } }; #else struct StreamT { - FILE *fptr; - StreamT(const char *name) {fptr = fopen(name, "rb");} - ~StreamT() {fclose(fptr);} - bool read(char *data, size_t n){size_t m=fread(data, 1, n, fptr); return n==m;} - void skip(uint32_t off){fseek(fptr, off, SEEK_CUR);} - bool is_open() const {return fptr != NULL;} + FILE* fptr; + StreamT(const char* name) { fptr = fopen(name, "rb"); } + ~StreamT() { fclose(fptr); } + bool read(char* data, size_t n) { + size_t m = fread(data, 1, n, fptr); + return n == m; + } + void skip(int64_t off) { fseek(fptr, (long int)off, SEEK_CUR); } + bool is_open() const { return fptr != NULL; } }; #endif - StreamT is(fileName); - if (!is.is_open()) { - fprintf(stderr, "nanovdb::readUncompressedGrids: Unable to open file \"%s\"for input\n",fileName); exit(EXIT_FAILURE); - } - return readUncompressedGrids(is, buffer); -}// readUncompressedGrids + StreamT is(fileName); + if (!is.is_open()) { + fprintf(stderr, "nanovdb::readUncompressedGrids: Unable to open file \"%s\"for input\n", fileName); + exit(EXIT_FAILURE); + } + return readUncompressedGrids(is, buffer); +} // readUncompressedGrids + +#endif // if !defined(__CUDA_ARCH__) && !defined(__HIP__) } // namespace io -#endif// if !defined(__CUDA_ARCH__) && !defined(__HIP__) +// ----------------------------> Implementations of random access methods <-------------------------------------- + +/// @brief Implements Tree::getValue(Coord), i.e. return the value associated with a specific coordinate @c ijk. +/// @tparam BuildT Build type of the grid being called +/// @details The value at a coordinate maps to the background, a tile value or a leaf value. +template +struct GetValue +{ + __hostdev__ static auto get(const NanoRoot& root) { return root.mBackground; } + __hostdev__ static auto get(const typename NanoRoot::Tile& tile) { return tile.value; } + __hostdev__ static auto get(const NanoUpper& node, uint32_t n) { return node.mTable[n].value; } + __hostdev__ static auto get(const NanoLower& node, uint32_t n) { return node.mTable[n].value; } + __hostdev__ static auto get(const NanoLeaf& leaf, uint32_t n) { return leaf.getValue(n); } // works with all build types +}; // GetValue + +template +struct SetValue +{ + static_assert(!BuildTraits::is_special, "SetValue does not support special value types"); + using ValueT = typename NanoLeaf::ValueType; + __hostdev__ static auto set(NanoRoot&, const ValueT&) {} // no-op + __hostdev__ static auto set(typename NanoRoot::Tile& tile, const ValueT& v) { tile.value = v; } + __hostdev__ static auto set(NanoUpper& node, uint32_t n, const ValueT& v) { node.mTable[n].value = v; } + __hostdev__ static auto set(NanoLower& node, uint32_t n, const ValueT& v) { node.mTable[n].value = v; } + __hostdev__ static auto set(NanoLeaf& leaf, uint32_t n, const ValueT& v) { leaf.mValues[n] = v; } +}; // SetValue + +template +struct SetVoxel +{ + static_assert(!BuildTraits::is_special, "SetVoxel does not support special value types"); + using ValueT = typename NanoLeaf::ValueType; + __hostdev__ static auto set(NanoRoot&, const ValueT&) {} // no-op + __hostdev__ static auto set(typename NanoRoot::Tile&, const ValueT&) {} // no-op + __hostdev__ static auto set(NanoUpper&, uint32_t, const ValueT&) {} // no-op + __hostdev__ static auto set(NanoLower&, uint32_t, const ValueT&) {} // no-op + __hostdev__ static auto set(NanoLeaf& leaf, uint32_t n, const ValueT& v) { leaf.mValues[n] = v; } +}; // SetVoxel + +/// @brief Implements Tree::isActive(Coord) +/// @tparam BuildT Build type of the grid being called +template +struct GetState +{ + __hostdev__ static auto get(const NanoRoot&) { return false; } + __hostdev__ static auto get(const typename NanoRoot::Tile& tile) { return tile.state > 0; } + __hostdev__ static auto get(const NanoUpper& node, uint32_t n) { return node.mValueMask.isOn(n); } + __hostdev__ static auto get(const NanoLower& node, uint32_t n) { return node.mValueMask.isOn(n); } + __hostdev__ static auto get(const NanoLeaf& leaf, uint32_t n) { return leaf.mValueMask.isOn(n); } +}; // GetState + +/// @brief Implements Tree::getDim(Coord) +/// @tparam BuildT Build type of the grid being called +template +struct GetDim +{ + __hostdev__ static uint32_t get(const NanoRoot&) { return 0u; } // background + __hostdev__ static uint32_t get(const typename NanoRoot::Tile&) { return 4096u; } + __hostdev__ static uint32_t get(const NanoUpper&, uint32_t) { return 128u; } + __hostdev__ static uint32_t get(const NanoLower&, uint32_t) { return 8u; } + __hostdev__ static uint32_t get(const NanoLeaf&, uint32_t) { return 1u; } +}; // GetDim + +/// @brief Return the pointer to the leaf node that contains Coord. Implements Tree::probeLeaf(Coord) +/// @tparam BuildT Build type of the grid being called +template +struct GetLeaf +{ + __hostdev__ static const NanoLeaf* get(const NanoRoot&) { return nullptr; } + __hostdev__ static const NanoLeaf* get(const typename NanoRoot::Tile&) { return nullptr; } + __hostdev__ static const NanoLeaf* get(const NanoUpper&, uint32_t) { return nullptr; } + __hostdev__ static const NanoLeaf* get(const NanoLower&, uint32_t) { return nullptr; } + __hostdev__ static const NanoLeaf* get(const NanoLeaf& leaf, uint32_t) { return &leaf; } +}; // GetLeaf + +/// @brief Return point to the lower internal node where Coord maps to one of its values, i.e. terminates +/// @tparam BuildT Build type of the grid being called +template +struct GetLower +{ + __hostdev__ static const NanoLower* get(const NanoRoot&) { return nullptr; } + __hostdev__ static const NanoLower* get(const typename NanoRoot::Tile&) { return nullptr; } + __hostdev__ static const NanoLower* get(const NanoUpper&, uint32_t) { return nullptr; } + __hostdev__ static const NanoLower* get(const NanoLower& node, uint32_t) { return &node; } + __hostdev__ static const NanoLower* get(const NanoLeaf&, uint32_t) { return nullptr; } +}; // GetLower + +/// @brief Return point to the upper internal node where Coord maps to one of its values, i.e. terminates +/// @tparam BuildT Build type of the grid being called +template +struct GetUpper +{ + __hostdev__ static const NanoUpper* get(const NanoRoot&) { return nullptr; } + __hostdev__ static const NanoUpper* get(const typename NanoRoot::Tile&) { return nullptr; } + __hostdev__ static const NanoUpper* get(const NanoUpper& node, uint32_t) { return &node; } + __hostdev__ static const NanoUpper* get(const NanoLower& node, uint32_t) { return nullptr; } + __hostdev__ static const NanoUpper* get(const NanoLeaf&, uint32_t) { return nullptr; } +}; // GetUpper + +/// @brief Implements Tree::probeLeaf(Coord) +/// @tparam BuildT Build type of the grid being called +template +struct ProbeValue +{ + using ValueT = typename BuildToValueMap::Type; + __hostdev__ static bool get(const NanoRoot& root, ValueT& v) + { + v = root.mBackground; + return false; + } + __hostdev__ static bool get(const typename NanoRoot::Tile& tile, ValueT& v) + { + v = tile.value; + return tile.state > 0u; + } + __hostdev__ static bool get(const NanoUpper& node, uint32_t n, ValueT& v) + { + v = node.mTable[n].value; + return node.mValueMask.isOn(n); + } + __hostdev__ static bool get(const NanoLower& node, uint32_t n, ValueT& v) + { + v = node.mTable[n].value; + return node.mValueMask.isOn(n); + } + __hostdev__ static bool get(const NanoLeaf& leaf, uint32_t n, ValueT& v) + { + v = leaf.getValue(n); + return leaf.mValueMask.isOn(n); + } +}; // ProbeValue + +/// @brief Implements Tree::getNodeInfo(Coord) +/// @tparam BuildT Build type of the grid being called +template +struct GetNodeInfo +{ + using ValueType = typename NanoLeaf::ValueType; + using FloatType = typename NanoLeaf::FloatType; + struct NodeInfo + { + uint32_t level, dim; + ValueType minimum, maximum; + FloatType average, stdDevi; + CoordBBox bbox; + }; + __hostdev__ static NodeInfo get(const NanoRoot& root) + { + return NodeInfo{3u, NanoUpper::DIM, root.minimum(), root.maximum(), root.average(), root.stdDeviation(), root.bbox()}; + } + __hostdev__ static NodeInfo get(const typename NanoRoot::Tile& tile) + { + return NodeInfo{3u, NanoUpper::DIM, tile.value, tile.value, static_cast(tile.value), 0, CoordBBox::createCube(tile.origin(), NanoUpper::DIM)}; + } + __hostdev__ static NodeInfo get(const NanoUpper& node, uint32_t n) + { + return NodeInfo{2u, node.dim(), node.minimum(), node.maximum(), node.average(), node.stdDeviation(), node.bbox()}; + } + __hostdev__ static NodeInfo get(const NanoLower& node, uint32_t n) + { + return NodeInfo{1u, node.dim(), node.minimum(), node.maximum(), node.average(), node.stdDeviation(), node.bbox()}; + } + __hostdev__ static NodeInfo get(const NanoLeaf& leaf, uint32_t n) + { + return NodeInfo{0u, leaf.dim(), leaf.minimum(), leaf.maximum(), leaf.average(), leaf.stdDeviation(), leaf.bbox()}; + } +}; // GetNodeInfo } // namespace nanovdb diff --git a/nanovdb/nanovdb/PNanoVDB.h b/nanovdb/nanovdb/PNanoVDB.h index 950b50ceed..24fb68478c 100644 --- a/nanovdb/nanovdb/PNanoVDB.h +++ b/nanovdb/nanovdb/PNanoVDB.h @@ -1,2599 +1,3384 @@ - -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: MPL-2.0 - -/*! - \file PNanoVDB.h - - \author Andrew Reidmeyer - - \brief This file is a portable (e.g. pointer-less) C99/GLSL/HLSL port - of NanoVDB.h, which is compatible with most graphics APIs. -*/ - -#ifndef NANOVDB_PNANOVDB_H_HAS_BEEN_INCLUDED -#define NANOVDB_PNANOVDB_H_HAS_BEEN_INCLUDED - -// ------------------------------------------------ Configuration ----------------------------------------------------------- - -// platforms -//#define PNANOVDB_C -//#define PNANOVDB_HLSL -//#define PNANOVDB_GLSL - -// addressing mode -// PNANOVDB_ADDRESS_32 -// PNANOVDB_ADDRESS_64 -#if defined(PNANOVDB_C) -#ifndef PNANOVDB_ADDRESS_32 -#define PNANOVDB_ADDRESS_64 -#endif -#elif defined(PNANOVDB_HLSL) -#ifndef PNANOVDB_ADDRESS_64 -#define PNANOVDB_ADDRESS_32 -#endif -#elif defined(PNANOVDB_GLSL) -#ifndef PNANOVDB_ADDRESS_64 -#define PNANOVDB_ADDRESS_32 -#endif -#endif - -// bounds checking -//#define PNANOVDB_BUF_BOUNDS_CHECK - -// enable HDDA by default on HLSL/GLSL, make explicit on C -#if defined(PNANOVDB_C) -//#define PNANOVDB_HDDA -#ifdef PNANOVDB_HDDA -#ifndef PNANOVDB_CMATH -#define PNANOVDB_CMATH -#endif -#endif -#elif defined(PNANOVDB_HLSL) -#define PNANOVDB_HDDA -#elif defined(PNANOVDB_GLSL) -#define PNANOVDB_HDDA -#endif - -#ifdef PNANOVDB_CMATH -#include -#endif - -// ------------------------------------------------ Buffer ----------------------------------------------------------- - -#if defined(PNANOVDB_BUF_CUSTOM) -// NOP -#elif defined(PNANOVDB_C) -#define PNANOVDB_BUF_C -#elif defined(PNANOVDB_HLSL) -#define PNANOVDB_BUF_HLSL -#elif defined(PNANOVDB_GLSL) -#define PNANOVDB_BUF_GLSL -#endif - -#if defined(PNANOVDB_BUF_C) -#include -#if defined(_WIN32) -#define PNANOVDB_BUF_FORCE_INLINE static inline __forceinline -#else -#define PNANOVDB_BUF_FORCE_INLINE static inline __attribute__((always_inline)) -#endif -typedef struct pnanovdb_buf_t -{ - uint32_t* data; -#ifdef PNANOVDB_BUF_BOUNDS_CHECK - uint64_t size_in_words; -#endif -}pnanovdb_buf_t; -PNANOVDB_BUF_FORCE_INLINE pnanovdb_buf_t pnanovdb_make_buf(uint32_t* data, uint64_t size_in_words) -{ - pnanovdb_buf_t ret; - ret.data = data; -#ifdef PNANOVDB_BUF_BOUNDS_CHECK - ret.size_in_words = size_in_words; -#endif - return ret; -} -#if defined(PNANOVDB_ADDRESS_32) -PNANOVDB_BUF_FORCE_INLINE uint32_t pnanovdb_buf_read_uint32(pnanovdb_buf_t buf, uint32_t byte_offset) -{ - uint32_t wordaddress = (byte_offset >> 2u); -#ifdef PNANOVDB_BUF_BOUNDS_CHECK - return wordaddress < buf.size_in_words ? buf.data[wordaddress] : 0u; -#else - return buf.data[wordaddress]; -#endif -} -PNANOVDB_BUF_FORCE_INLINE uint64_t pnanovdb_buf_read_uint64(pnanovdb_buf_t buf, uint32_t byte_offset) -{ - uint64_t* data64 = (uint64_t*)buf.data; - uint32_t wordaddress64 = (byte_offset >> 3u); -#ifdef PNANOVDB_BUF_BOUNDS_CHECK - uint64_t size_in_words64 = buf.size_in_words >> 1u; - return wordaddress64 < size_in_words64 ? data64[wordaddress64] : 0llu; -#else - return data64[wordaddress64]; -#endif -} -#elif defined(PNANOVDB_ADDRESS_64) -PNANOVDB_BUF_FORCE_INLINE uint32_t pnanovdb_buf_read_uint32(pnanovdb_buf_t buf, uint64_t byte_offset) -{ - uint64_t wordaddress = (byte_offset >> 2u); -#ifdef PNANOVDB_BUF_BOUNDS_CHECK - return wordaddress < buf.size_in_words ? buf.data[wordaddress] : 0u; -#else - return buf.data[wordaddress]; -#endif -} -PNANOVDB_BUF_FORCE_INLINE uint64_t pnanovdb_buf_read_uint64(pnanovdb_buf_t buf, uint64_t byte_offset) -{ - uint64_t* data64 = (uint64_t*)buf.data; - uint64_t wordaddress64 = (byte_offset >> 3u); -#ifdef PNANOVDB_BUF_BOUNDS_CHECK - uint64_t size_in_words64 = buf.size_in_words >> 1u; - return wordaddress64 < size_in_words64 ? data64[wordaddress64] : 0llu; -#else - return data64[wordaddress64]; -#endif -} -#endif -typedef uint32_t pnanovdb_grid_type_t; -#define PNANOVDB_GRID_TYPE_GET(grid_typeIn, nameIn) pnanovdb_grid_type_constants[grid_typeIn].nameIn -#elif defined(PNANOVDB_BUF_HLSL) -#if defined(PNANOVDB_ADDRESS_32) -#define pnanovdb_buf_t StructuredBuffer -uint pnanovdb_buf_read_uint32(pnanovdb_buf_t buf, uint byte_offset) -{ - return buf[(byte_offset >> 2u)]; -} -uint2 pnanovdb_buf_read_uint64(pnanovdb_buf_t buf, uint byte_offset) -{ - uint2 ret; - ret.x = pnanovdb_buf_read_uint32(buf, byte_offset + 0u); - ret.y = pnanovdb_buf_read_uint32(buf, byte_offset + 4u); - return ret; -} -#elif defined(PNANOVDB_ADDRESS_64) -#define pnanovdb_buf_t StructuredBuffer -uint pnanovdb_buf_read_uint32(pnanovdb_buf_t buf, uint64_t byte_offset) -{ - return buf[uint(byte_offset >> 2u)]; -} -uint64_t pnanovdb_buf_read_uint64(pnanovdb_buf_t buf, uint64_t byte_offset) -{ - uint64_t ret; - ret = pnanovdb_buf_read_uint32(buf, byte_offset + 0u); - ret = ret + (uint64_t(pnanovdb_buf_read_uint32(buf, byte_offset + 4u)) << 32u); - return ret; -} -#endif -#define pnanovdb_grid_type_t uint -#define PNANOVDB_GRID_TYPE_GET(grid_typeIn, nameIn) pnanovdb_grid_type_constants[grid_typeIn].nameIn -#elif defined(PNANOVDB_BUF_GLSL) -struct pnanovdb_buf_t -{ - uint unused; // to satisfy min struct size? -}; -uint pnanovdb_buf_read_uint32(pnanovdb_buf_t buf, uint byte_offset) -{ - return pnanovdb_buf_data[(byte_offset >> 2u)]; -} -uvec2 pnanovdb_buf_read_uint64(pnanovdb_buf_t buf, uint byte_offset) -{ - uvec2 ret; - ret.x = pnanovdb_buf_read_uint32(buf, byte_offset + 0u); - ret.y = pnanovdb_buf_read_uint32(buf, byte_offset + 4u); - return ret; -} -#define pnanovdb_grid_type_t uint -#define PNANOVDB_GRID_TYPE_GET(grid_typeIn, nameIn) pnanovdb_grid_type_constants[grid_typeIn].nameIn -#endif - -// ------------------------------------------------ Basic Types ----------------------------------------------------------- - -// force inline -#if defined(PNANOVDB_C) -#if defined(_WIN32) -#define PNANOVDB_FORCE_INLINE static inline __forceinline -#else -#define PNANOVDB_FORCE_INLINE static inline __attribute__((always_inline)) -#endif -#elif defined(PNANOVDB_HLSL) -#define PNANOVDB_FORCE_INLINE -#elif defined(PNANOVDB_GLSL) -#define PNANOVDB_FORCE_INLINE -#endif - -// struct typedef, static const, inout -#if defined(PNANOVDB_C) -#define PNANOVDB_STRUCT_TYPEDEF(X) typedef struct X X; -#define PNANOVDB_STATIC_CONST static const -#define PNANOVDB_INOUT(X) X* -#define PNANOVDB_IN(X) const X* -#define PNANOVDB_DEREF(X) (*X) -#define PNANOVDB_REF(X) &X -#elif defined(PNANOVDB_HLSL) -#define PNANOVDB_STRUCT_TYPEDEF(X) -#define PNANOVDB_STATIC_CONST static const -#define PNANOVDB_INOUT(X) inout X -#define PNANOVDB_IN(X) X -#define PNANOVDB_DEREF(X) X -#define PNANOVDB_REF(X) X -#elif defined(PNANOVDB_GLSL) -#define PNANOVDB_STRUCT_TYPEDEF(X) -#define PNANOVDB_STATIC_CONST const -#define PNANOVDB_INOUT(X) inout X -#define PNANOVDB_IN(X) X -#define PNANOVDB_DEREF(X) X -#define PNANOVDB_REF(X) X -#endif - -// basic types, type conversion -#if defined(PNANOVDB_C) -#define PNANOVDB_NATIVE_64 -#include -#if !defined(PNANOVDB_MEMCPY_CUSTOM) -#include -#define pnanovdb_memcpy memcpy -#endif -typedef uint32_t pnanovdb_uint32_t; -typedef int32_t pnanovdb_int32_t; -typedef int32_t pnanovdb_bool_t; -#define PNANOVDB_FALSE 0 -#define PNANOVDB_TRUE 1 -typedef uint64_t pnanovdb_uint64_t; -typedef int64_t pnanovdb_int64_t; -typedef struct pnanovdb_coord_t -{ - pnanovdb_int32_t x, y, z; -}pnanovdb_coord_t; -typedef struct pnanovdb_vec3_t -{ - float x, y, z; -}pnanovdb_vec3_t; -PNANOVDB_FORCE_INLINE pnanovdb_int32_t pnanovdb_uint32_as_int32(pnanovdb_uint32_t v) { return (pnanovdb_int32_t)v; } -PNANOVDB_FORCE_INLINE pnanovdb_int64_t pnanovdb_uint64_as_int64(pnanovdb_uint64_t v) { return (pnanovdb_int64_t)v; } -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_int64_as_uint64(pnanovdb_int64_t v) { return (pnanovdb_uint64_t)v; } -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_int32_as_uint32(pnanovdb_int32_t v) { return (pnanovdb_uint32_t)v; } -PNANOVDB_FORCE_INLINE float pnanovdb_uint32_as_float(pnanovdb_uint32_t v) { float vf; pnanovdb_memcpy(&vf, &v, sizeof(vf)); return vf; } -PNANOVDB_FORCE_INLINE double pnanovdb_uint64_as_double(pnanovdb_uint64_t v) { double vf; pnanovdb_memcpy(&vf, &v, sizeof(vf)); return vf; } -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_uint64_low(pnanovdb_uint64_t v) { return (pnanovdb_uint32_t)v; } -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_uint64_high(pnanovdb_uint64_t v) { return (pnanovdb_uint32_t)(v >> 32u); } -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_uint32_as_uint64(pnanovdb_uint32_t x, pnanovdb_uint32_t y) { return ((pnanovdb_uint64_t)x) | (((pnanovdb_uint64_t)y) << 32u); } -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_uint32_as_uint64_low(pnanovdb_uint32_t x) { return ((pnanovdb_uint64_t)x); } -PNANOVDB_FORCE_INLINE pnanovdb_int32_t pnanovdb_uint64_is_equal(pnanovdb_uint64_t a, pnanovdb_uint64_t b) { return a == b; } -PNANOVDB_FORCE_INLINE pnanovdb_int32_t pnanovdb_int64_is_zero(pnanovdb_int64_t a) { return a == 0; } -#ifdef PNANOVDB_CMATH -PNANOVDB_FORCE_INLINE float pnanovdb_floor(float v) { return floorf(v); } -#endif -PNANOVDB_FORCE_INLINE pnanovdb_int32_t pnanovdb_float_to_int32(float v) { return (pnanovdb_int32_t)v; } -PNANOVDB_FORCE_INLINE float pnanovdb_int32_to_float(pnanovdb_int32_t v) { return (float)v; } -PNANOVDB_FORCE_INLINE float pnanovdb_uint32_to_float(pnanovdb_uint32_t v) { return (float)v; } -PNANOVDB_FORCE_INLINE float pnanovdb_min(float a, float b) { return a < b ? a : b; } -PNANOVDB_FORCE_INLINE float pnanovdb_max(float a, float b) { return a > b ? a : b; } -#elif defined(PNANOVDB_HLSL) -typedef uint pnanovdb_uint32_t; -typedef int pnanovdb_int32_t; -typedef bool pnanovdb_bool_t; -#define PNANOVDB_FALSE false -#define PNANOVDB_TRUE true -typedef int3 pnanovdb_coord_t; -typedef float3 pnanovdb_vec3_t; -pnanovdb_int32_t pnanovdb_uint32_as_int32(pnanovdb_uint32_t v) { return int(v); } -pnanovdb_uint32_t pnanovdb_int32_as_uint32(pnanovdb_int32_t v) { return uint(v); } -float pnanovdb_uint32_as_float(pnanovdb_uint32_t v) { return asfloat(v); } -float pnanovdb_floor(float v) { return floor(v); } -pnanovdb_int32_t pnanovdb_float_to_int32(float v) { return int(v); } -float pnanovdb_int32_to_float(pnanovdb_int32_t v) { return float(v); } -float pnanovdb_uint32_to_float(pnanovdb_uint32_t v) { return float(v); } -float pnanovdb_min(float a, float b) { return min(a, b); } -float pnanovdb_max(float a, float b) { return max(a, b); } -#if defined(PNANOVDB_ADDRESS_32) -typedef uint2 pnanovdb_uint64_t; -typedef int2 pnanovdb_int64_t; -pnanovdb_int64_t pnanovdb_uint64_as_int64(pnanovdb_uint64_t v) { return int2(v); } -pnanovdb_uint64_t pnanovdb_int64_as_uint64(pnanovdb_int64_t v) { return uint2(v); } -double pnanovdb_uint64_as_double(pnanovdb_uint64_t v) { return asdouble(v.x, v.y); } -pnanovdb_uint32_t pnanovdb_uint64_low(pnanovdb_uint64_t v) { return v.x; } -pnanovdb_uint32_t pnanovdb_uint64_high(pnanovdb_uint64_t v) { return v.y; } -pnanovdb_uint64_t pnanovdb_uint32_as_uint64(pnanovdb_uint32_t x, pnanovdb_uint32_t y) { return uint2(x, y); } -pnanovdb_uint64_t pnanovdb_uint32_as_uint64_low(pnanovdb_uint32_t x) { return uint2(x, 0); } -bool pnanovdb_uint64_is_equal(pnanovdb_uint64_t a, pnanovdb_uint64_t b) { return (a.x == b.x) && (a.y == b.y); } -bool pnanovdb_int64_is_zero(pnanovdb_int64_t a) { return a.x == 0 && a.y == 0; } -#else -typedef uint64_t pnanovdb_uint64_t; -typedef int64_t pnanovdb_int64_t; -pnanovdb_int64_t pnanovdb_uint64_as_int64(pnanovdb_uint64_t v) { return int64_t(v); } -pnanovdb_uint64_t pnanovdb_int64_as_uint64(pnanovdb_int64_t v) { return uint64_t(v); } -double pnanovdb_uint64_as_double(pnanovdb_uint64_t v) { return asdouble(uint(v), uint(v >> 32u)); } -pnanovdb_uint32_t pnanovdb_uint64_low(pnanovdb_uint64_t v) { return uint(v); } -pnanovdb_uint32_t pnanovdb_uint64_high(pnanovdb_uint64_t v) { return uint(v >> 32u); } -pnanovdb_uint64_t pnanovdb_uint32_as_uint64(pnanovdb_uint32_t x, pnanovdb_uint32_t y) { return uint64_t(x) + (uint64_t(y) << 32u); } -pnanovdb_uint64_t pnanovdb_uint32_as_uint64_low(pnanovdb_uint32_t x) { return uint64_t(x); } -bool pnanovdb_uint64_is_equal(pnanovdb_uint64_t a, pnanovdb_uint64_t b) { return a == b; } -bool pnanovdb_int64_is_zero(pnanovdb_int64_t a) { return a == 0; } -#endif -#elif defined(PNANOVDB_GLSL) -#define pnanovdb_uint32_t uint -#define pnanovdb_int32_t int -#define pnanovdb_bool_t bool -#define PNANOVDB_FALSE false -#define PNANOVDB_TRUE true -#define pnanovdb_uint64_t uvec2 -#define pnanovdb_int64_t ivec2 -#define pnanovdb_coord_t ivec3 -#define pnanovdb_vec3_t vec3 -pnanovdb_int32_t pnanovdb_uint32_as_int32(pnanovdb_uint32_t v) { return int(v); } -pnanovdb_int64_t pnanovdb_uint64_as_int64(pnanovdb_uint64_t v) { return ivec2(v); } -pnanovdb_uint64_t pnanovdb_int64_as_uint64(pnanovdb_int64_t v) { return uvec2(v); } -pnanovdb_uint32_t pnanovdb_int32_as_uint32(pnanovdb_int32_t v) { return uint(v); } -float pnanovdb_uint32_as_float(pnanovdb_uint32_t v) { return uintBitsToFloat(v); } -double pnanovdb_uint64_as_double(pnanovdb_uint64_t v) { return packDouble2x32(uvec2(v.x, v.y)); } -pnanovdb_uint32_t pnanovdb_uint64_low(pnanovdb_uint64_t v) { return v.x; } -pnanovdb_uint32_t pnanovdb_uint64_high(pnanovdb_uint64_t v) { return v.y; } -pnanovdb_uint64_t pnanovdb_uint32_as_uint64(pnanovdb_uint32_t x, pnanovdb_uint32_t y) { return uvec2(x, y); } -pnanovdb_uint64_t pnanovdb_uint32_as_uint64_low(pnanovdb_uint32_t x) { return uvec2(x, 0); } -bool pnanovdb_uint64_is_equal(pnanovdb_uint64_t a, pnanovdb_uint64_t b) { return (a.x == b.x) && (a.y == b.y); } -bool pnanovdb_int64_is_zero(pnanovdb_int64_t a) { return a.x == 0 && a.y == 0; } -float pnanovdb_floor(float v) { return floor(v); } -pnanovdb_int32_t pnanovdb_float_to_int32(float v) { return int(v); } -float pnanovdb_int32_to_float(pnanovdb_int32_t v) { return float(v); } -float pnanovdb_uint32_to_float(pnanovdb_uint32_t v) { return float(v); } -float pnanovdb_min(float a, float b) { return min(a, b); } -float pnanovdb_max(float a, float b) { return max(a, b); } -#endif - -// ------------------------------------------------ Coord/Vec3 Utilties ----------------------------------------------------------- - -#if defined(PNANOVDB_C) -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_vec3_uniform(float a) -{ - pnanovdb_vec3_t v; - v.x = a; - v.y = a; - v.z = a; - return v; -} -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_vec3_add(const pnanovdb_vec3_t a, const pnanovdb_vec3_t b) -{ - pnanovdb_vec3_t v; - v.x = a.x + b.x; - v.y = a.y + b.y; - v.z = a.z + b.z; - return v; -} -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_vec3_sub(const pnanovdb_vec3_t a, const pnanovdb_vec3_t b) -{ - pnanovdb_vec3_t v; - v.x = a.x - b.x; - v.y = a.y - b.y; - v.z = a.z - b.z; - return v; -} -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_vec3_mul(const pnanovdb_vec3_t a, const pnanovdb_vec3_t b) -{ - pnanovdb_vec3_t v; - v.x = a.x * b.x; - v.y = a.y * b.y; - v.z = a.z * b.z; - return v; -} -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_vec3_div(const pnanovdb_vec3_t a, const pnanovdb_vec3_t b) -{ - pnanovdb_vec3_t v; - v.x = a.x / b.x; - v.y = a.y / b.y; - v.z = a.z / b.z; - return v; -} -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_vec3_min(const pnanovdb_vec3_t a, const pnanovdb_vec3_t b) -{ - pnanovdb_vec3_t v; - v.x = a.x < b.x ? a.x : b.x; - v.y = a.y < b.y ? a.y : b.y; - v.z = a.z < b.z ? a.z : b.z; - return v; -} -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_vec3_max(const pnanovdb_vec3_t a, const pnanovdb_vec3_t b) -{ - pnanovdb_vec3_t v; - v.x = a.x > b.x ? a.x : b.x; - v.y = a.y > b.y ? a.y : b.y; - v.z = a.z > b.z ? a.z : b.z; - return v; -} -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_coord_to_vec3(const pnanovdb_coord_t coord) -{ - pnanovdb_vec3_t v; - v.x = pnanovdb_int32_to_float(coord.x); - v.y = pnanovdb_int32_to_float(coord.y); - v.z = pnanovdb_int32_to_float(coord.z); - return v; -} -PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_coord_uniform(const pnanovdb_int32_t a) -{ - pnanovdb_coord_t v; - v.x = a; - v.y = a; - v.z = a; - return v; -} -PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_coord_add(pnanovdb_coord_t a, pnanovdb_coord_t b) -{ - pnanovdb_coord_t v; - v.x = a.x + b.x; - v.y = a.y + b.y; - v.z = a.z + b.z; - return v; -} -#elif defined(PNANOVDB_HLSL) -pnanovdb_vec3_t pnanovdb_vec3_uniform(float a) { return float3(a, a, a); } -pnanovdb_vec3_t pnanovdb_vec3_add(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return a + b; } -pnanovdb_vec3_t pnanovdb_vec3_sub(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return a - b; } -pnanovdb_vec3_t pnanovdb_vec3_mul(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return a * b; } -pnanovdb_vec3_t pnanovdb_vec3_div(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return a / b; } -pnanovdb_vec3_t pnanovdb_vec3_min(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return min(a, b); } -pnanovdb_vec3_t pnanovdb_vec3_max(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return max(a, b); } -pnanovdb_vec3_t pnanovdb_coord_to_vec3(pnanovdb_coord_t coord) { return float3(coord); } -pnanovdb_coord_t pnanovdb_coord_uniform(pnanovdb_int32_t a) { return int3(a, a, a); } -pnanovdb_coord_t pnanovdb_coord_add(pnanovdb_coord_t a, pnanovdb_coord_t b) { return a + b; } -#elif defined(PNANOVDB_GLSL) -pnanovdb_vec3_t pnanovdb_vec3_uniform(float a) { return vec3(a, a, a); } -pnanovdb_vec3_t pnanovdb_vec3_add(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return a + b; } -pnanovdb_vec3_t pnanovdb_vec3_sub(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return a - b; } -pnanovdb_vec3_t pnanovdb_vec3_mul(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return a * b; } -pnanovdb_vec3_t pnanovdb_vec3_div(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return a / b; } -pnanovdb_vec3_t pnanovdb_vec3_min(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return min(a, b); } -pnanovdb_vec3_t pnanovdb_vec3_max(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return max(a, b); } -pnanovdb_vec3_t pnanovdb_coord_to_vec3(const pnanovdb_coord_t coord) { return vec3(coord); } -pnanovdb_coord_t pnanovdb_coord_uniform(pnanovdb_int32_t a) { return ivec3(a, a, a); } -pnanovdb_coord_t pnanovdb_coord_add(pnanovdb_coord_t a, pnanovdb_coord_t b) { return a + b; } -#endif - -// ------------------------------------------------ Address Type ----------------------------------------------------------- - -#if defined(PNANOVDB_ADDRESS_32) -struct pnanovdb_address_t -{ - pnanovdb_uint32_t byte_offset; -}; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_address_t) - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset(pnanovdb_address_t address, pnanovdb_uint32_t byte_offset) -{ - pnanovdb_address_t ret = address; - ret.byte_offset += byte_offset; - return ret; -} -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset_neg(pnanovdb_address_t address, pnanovdb_uint32_t byte_offset) -{ - pnanovdb_address_t ret = address; - ret.byte_offset -= byte_offset; - return ret; -} -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset_product(pnanovdb_address_t address, pnanovdb_uint32_t byte_offset, pnanovdb_uint32_t multiplier) -{ - pnanovdb_address_t ret = address; - ret.byte_offset += byte_offset * multiplier; - return ret; -} -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset64(pnanovdb_address_t address, pnanovdb_uint64_t byte_offset) -{ - pnanovdb_address_t ret = address; - // lose high bits on 32-bit - ret.byte_offset += pnanovdb_uint64_low(byte_offset); - return ret; -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_address_mask(pnanovdb_address_t address, pnanovdb_uint32_t mask) -{ - return address.byte_offset & mask; -} -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_mask_inv(pnanovdb_address_t address, pnanovdb_uint32_t mask) -{ - pnanovdb_address_t ret = address; - ret.byte_offset &= (~mask); - return ret; -} -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_null() -{ - pnanovdb_address_t ret = { 0 }; - return ret; -} -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_address_is_null(pnanovdb_address_t address) -{ - return address.byte_offset == 0u; -} -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_address_in_interval(pnanovdb_address_t address, pnanovdb_address_t min_address, pnanovdb_address_t max_address) -{ - return address.byte_offset >= min_address.byte_offset && address.byte_offset < max_address.byte_offset; -} -#elif defined(PNANOVDB_ADDRESS_64) -struct pnanovdb_address_t -{ - pnanovdb_uint64_t byte_offset; -}; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_address_t) - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset(pnanovdb_address_t address, pnanovdb_uint32_t byte_offset) -{ - pnanovdb_address_t ret = address; - ret.byte_offset += byte_offset; - return ret; -} -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset_neg(pnanovdb_address_t address, pnanovdb_uint32_t byte_offset) -{ - pnanovdb_address_t ret = address; - ret.byte_offset -= byte_offset; - return ret; -} -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset_product(pnanovdb_address_t address, pnanovdb_uint32_t byte_offset, pnanovdb_uint32_t multiplier) -{ - pnanovdb_address_t ret = address; - ret.byte_offset += pnanovdb_uint32_as_uint64_low(byte_offset) * pnanovdb_uint32_as_uint64_low(multiplier); - return ret; -} -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset64(pnanovdb_address_t address, pnanovdb_uint64_t byte_offset) -{ - pnanovdb_address_t ret = address; - ret.byte_offset += byte_offset; - return ret; -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_address_mask(pnanovdb_address_t address, pnanovdb_uint32_t mask) -{ - return pnanovdb_uint64_low(address.byte_offset) & mask; -} -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_mask_inv(pnanovdb_address_t address, pnanovdb_uint32_t mask) -{ - pnanovdb_address_t ret = address; - ret.byte_offset &= (~pnanovdb_uint32_as_uint64_low(mask)); - return ret; -} -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_null() -{ - pnanovdb_address_t ret = { 0 }; - return ret; -} -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_address_is_null(pnanovdb_address_t address) -{ - return address.byte_offset == 0llu; -} -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_address_in_interval(pnanovdb_address_t address, pnanovdb_address_t min_address, pnanovdb_address_t max_address) -{ - return address.byte_offset >= min_address.byte_offset && address.byte_offset < max_address.byte_offset; -} -#endif - -// ------------------------------------------------ High Level Buffer Read ----------------------------------------------------------- - -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_read_uint32(pnanovdb_buf_t buf, pnanovdb_address_t address) -{ - return pnanovdb_buf_read_uint32(buf, address.byte_offset); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_read_uint64(pnanovdb_buf_t buf, pnanovdb_address_t address) -{ - return pnanovdb_buf_read_uint64(buf, address.byte_offset); -} -PNANOVDB_FORCE_INLINE pnanovdb_int32_t pnanovdb_read_int32(pnanovdb_buf_t buf, pnanovdb_address_t address) -{ - return pnanovdb_uint32_as_int32(pnanovdb_read_uint32(buf, address)); -} -PNANOVDB_FORCE_INLINE float pnanovdb_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address) -{ - return pnanovdb_uint32_as_float(pnanovdb_read_uint32(buf, address)); -} -PNANOVDB_FORCE_INLINE pnanovdb_int64_t pnanovdb_read_int64(pnanovdb_buf_t buf, pnanovdb_address_t address) -{ - return pnanovdb_uint64_as_int64(pnanovdb_read_uint64(buf, address)); -} -PNANOVDB_FORCE_INLINE double pnanovdb_read_double(pnanovdb_buf_t buf, pnanovdb_address_t address) -{ - return pnanovdb_uint64_as_double(pnanovdb_read_uint64(buf, address)); -} -PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_read_coord(pnanovdb_buf_t buf, pnanovdb_address_t address) -{ - pnanovdb_coord_t ret; - ret.x = pnanovdb_uint32_as_int32(pnanovdb_read_uint32(buf, pnanovdb_address_offset(address, 0u))); - ret.y = pnanovdb_uint32_as_int32(pnanovdb_read_uint32(buf, pnanovdb_address_offset(address, 4u))); - ret.z = pnanovdb_uint32_as_int32(pnanovdb_read_uint32(buf, pnanovdb_address_offset(address, 8u))); - return ret; -} - -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_read_bit(pnanovdb_buf_t buf, pnanovdb_address_t address, pnanovdb_uint32_t bit_offset) -{ - pnanovdb_address_t word_address = pnanovdb_address_mask_inv(address, 3u); - pnanovdb_uint32_t bit_index = (pnanovdb_address_mask(address, 3u) << 3u) + bit_offset; - pnanovdb_uint32_t value_word = pnanovdb_buf_read_uint32(buf, word_address.byte_offset); - return ((value_word >> bit_index) & 1) != 0u; -} - -#if defined(PNANOVDB_C) -PNANOVDB_FORCE_INLINE short pnanovdb_read_half(pnanovdb_buf_t buf, pnanovdb_address_t address) -{ - pnanovdb_uint32_t raw = pnanovdb_read_uint32(buf, address); - return (short)(raw >> (pnanovdb_address_mask(address, 2) << 3)); -} -#elif defined(PNANOVDB_HLSL) -PNANOVDB_FORCE_INLINE float pnanovdb_read_half(pnanovdb_buf_t buf, pnanovdb_address_t address) -{ - pnanovdb_uint32_t raw = pnanovdb_read_uint32(buf, address); - return f16tof32(raw >> (pnanovdb_address_mask(address, 2) << 3)); -} -#elif defined(PNANOVDB_GLSL) -PNANOVDB_FORCE_INLINE float pnanovdb_read_half(pnanovdb_buf_t buf, pnanovdb_address_t address) -{ - pnanovdb_uint32_t raw = pnanovdb_read_uint32(buf, address); - return unpackHalf2x16(raw >> (pnanovdb_address_mask(address, 2) << 3)).x; -} -#endif - -// ------------------------------------------------ Core Structures ----------------------------------------------------------- - -#define PNANOVDB_MAGIC_NUMBER 0x304244566f6e614eUL// "NanoVDB0" in hex - little endian (uint64_t) - -#define PNANOVDB_MAJOR_VERSION_NUMBER 32// reflects changes to the ABI -#define PNANOVDB_MINOR_VERSION_NUMBER 4// reflects changes to the API but not ABI -#define PNANOVDB_PATCH_VERSION_NUMBER 2// reflects bug-fixes with no ABI or API changes - -#define PNANOVDB_GRID_TYPE_UNKNOWN 0 -#define PNANOVDB_GRID_TYPE_FLOAT 1 -#define PNANOVDB_GRID_TYPE_DOUBLE 2 -#define PNANOVDB_GRID_TYPE_INT16 3 -#define PNANOVDB_GRID_TYPE_INT32 4 -#define PNANOVDB_GRID_TYPE_INT64 5 -#define PNANOVDB_GRID_TYPE_VEC3F 6 -#define PNANOVDB_GRID_TYPE_VEC3D 7 -#define PNANOVDB_GRID_TYPE_MASK 8 -#define PNANOVDB_GRID_TYPE_HALF 9 -#define PNANOVDB_GRID_TYPE_UINT32 10 -#define PNANOVDB_GRID_TYPE_BOOLEAN 11 -#define PNANOVDB_GRID_TYPE_RGBA8 12 -#define PNANOVDB_GRID_TYPE_FP4 13 -#define PNANOVDB_GRID_TYPE_FP8 14 -#define PNANOVDB_GRID_TYPE_FP16 15 -#define PNANOVDB_GRID_TYPE_FPN 16 -#define PNANOVDB_GRID_TYPE_VEC4F 17 -#define PNANOVDB_GRID_TYPE_VEC4D 18 -#define PNANOVDB_GRID_TYPE_INDEX 19 -#define PNANOVDB_GRID_TYPE_END 20 - -#define PNANOVDB_GRID_CLASS_UNKNOWN 0 -#define PNANOVDB_GRID_CLASS_LEVEL_SET 1 // narrow band levelset, e.g. SDF -#define PNANOVDB_GRID_CLASS_FOG_VOLUME 2 // fog volume, e.g. density -#define PNANOVDB_GRID_CLASS_STAGGERED 3 // staggered MAC grid, e.g. velocity -#define PNANOVDB_GRID_CLASS_POINT_INDEX 4 // point index grid -#define PNANOVDB_GRID_CLASS_POINT_DATA 5 // point data grid -#define PNANOVDB_GRID_CLASS_TOPOLOGY 6 // grid with active states only (no values) -#define PNANOVDB_GRID_CLASS_VOXEL_VOLUME 7 // volume of geometric cubes, e.g. minecraft -#define PNANOVDB_GRID_CLASS_INDEX_GRID 8 // grid whose values are offsets, e.g. into an external array -#define PNANOVDB_GRID_CLASS_END 9 - -#define PNANOVDB_GRID_FLAGS_HAS_LONG_GRID_NAME (1 << 0) -#define PNANOVDB_GRID_FLAGS_HAS_BBOX (1 << 1) -#define PNANOVDB_GRID_FLAGS_HAS_MIN_MAX (1 << 2) -#define PNANOVDB_GRID_FLAGS_HAS_AVERAGE (1 << 3) -#define PNANOVDB_GRID_FLAGS_HAS_STD_DEVIATION (1 << 4) -#define PNANOVDB_GRID_FLAGS_IS_BREADTH_FIRST (1 << 5) -#define PNANOVDB_GRID_FLAGS_END (1 << 6) - -#define PNANOVDB_LEAF_TYPE_DEFAULT 0 -#define PNANOVDB_LEAF_TYPE_LITE 1 -#define PNANOVDB_LEAF_TYPE_FP 2 -#define PNANOVDB_LEAF_TYPE_INDEX 3 - -PNANOVDB_STATIC_CONST pnanovdb_uint32_t pnanovdb_grid_type_value_strides_bits[PNANOVDB_GRID_TYPE_END] = { 0, 32, 64, 16, 32, 64, 96, 192, 0, 16, 32, 1, 32, 4, 8, 16, 0, 128, 256, 0 }; -PNANOVDB_STATIC_CONST pnanovdb_uint32_t pnanovdb_grid_type_table_strides_bits[PNANOVDB_GRID_TYPE_END] = { 64, 64, 64, 64, 64, 64, 128, 192, 64, 64, 64, 64, 64, 64, 64, 64, 64, 128, 256, 64 }; -PNANOVDB_STATIC_CONST pnanovdb_uint32_t pnanovdb_grid_type_minmax_strides_bits[PNANOVDB_GRID_TYPE_END] = { 0, 32, 64, 16, 32, 64, 96, 192, 8, 16, 32, 8, 32, 32, 32, 32, 32, 128, 256, 64 }; -PNANOVDB_STATIC_CONST pnanovdb_uint32_t pnanovdb_grid_type_minmax_aligns_bits[PNANOVDB_GRID_TYPE_END] = { 0, 32, 64, 16, 32, 64, 32, 64, 8, 16, 32, 8, 32, 32, 32, 32, 32, 32, 64, 64 }; -PNANOVDB_STATIC_CONST pnanovdb_uint32_t pnanovdb_grid_type_stat_strides_bits[PNANOVDB_GRID_TYPE_END] = { 0, 32, 64, 32, 32, 64, 32, 64, 8, 32, 32, 8, 32, 32, 32, 32, 32, 32, 64, 64 }; -PNANOVDB_STATIC_CONST pnanovdb_uint32_t pnanovdb_grid_type_leaf_type[PNANOVDB_GRID_TYPE_END] = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 2, 2, 2, 2, 0, 0, 3 }; - -struct pnanovdb_map_t -{ - float matf[9]; - float invmatf[9]; - float vecf[3]; - float taperf; - double matd[9]; - double invmatd[9]; - double vecd[3]; - double taperd; -}; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_map_t) -struct pnanovdb_map_handle_t { pnanovdb_address_t address; }; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_map_handle_t) - -#define PNANOVDB_MAP_SIZE 264 - -#define PNANOVDB_MAP_OFF_MATF 0 -#define PNANOVDB_MAP_OFF_INVMATF 36 -#define PNANOVDB_MAP_OFF_VECF 72 -#define PNANOVDB_MAP_OFF_TAPERF 84 -#define PNANOVDB_MAP_OFF_MATD 88 -#define PNANOVDB_MAP_OFF_INVMATD 160 -#define PNANOVDB_MAP_OFF_VECD 232 -#define PNANOVDB_MAP_OFF_TAPERD 256 - -PNANOVDB_FORCE_INLINE float pnanovdb_map_get_matf(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index) { - return pnanovdb_read_float(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_MATF + 4u * index)); -} -PNANOVDB_FORCE_INLINE float pnanovdb_map_get_invmatf(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index) { - return pnanovdb_read_float(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_INVMATF + 4u * index)); -} -PNANOVDB_FORCE_INLINE float pnanovdb_map_get_vecf(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index) { - return pnanovdb_read_float(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_VECF + 4u * index)); -} -PNANOVDB_FORCE_INLINE float pnanovdb_map_get_taperf(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index) { - return pnanovdb_read_float(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_TAPERF)); -} -PNANOVDB_FORCE_INLINE double pnanovdb_map_get_matd(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index) { - return pnanovdb_read_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_MATD + 8u * index)); -} -PNANOVDB_FORCE_INLINE double pnanovdb_map_get_invmatd(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index) { - return pnanovdb_read_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_INVMATD + 8u * index)); -} -PNANOVDB_FORCE_INLINE double pnanovdb_map_get_vecd(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index) { - return pnanovdb_read_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_VECD + 8u * index)); -} -PNANOVDB_FORCE_INLINE double pnanovdb_map_get_taperd(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index) { - return pnanovdb_read_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_TAPERD)); -} - -struct pnanovdb_grid_t -{ - pnanovdb_uint64_t magic; // 8 bytes, 0 - pnanovdb_uint64_t checksum; // 8 bytes, 8 - pnanovdb_uint32_t version; // 4 bytes, 16 - pnanovdb_uint32_t flags; // 4 bytes, 20 - pnanovdb_uint32_t grid_index; // 4 bytes, 24 - pnanovdb_uint32_t grid_count; // 4 bytes, 28 - pnanovdb_uint64_t grid_size; // 8 bytes, 32 - pnanovdb_uint32_t grid_name[256 / 4]; // 256 bytes, 40 - pnanovdb_map_t map; // 264 bytes, 296 - double world_bbox[6]; // 48 bytes, 560 - double voxel_size[3]; // 24 bytes, 608 - pnanovdb_uint32_t grid_class; // 4 bytes, 632 - pnanovdb_uint32_t grid_type; // 4 bytes, 636 - pnanovdb_int64_t blind_metadata_offset; // 8 bytes, 640 - pnanovdb_uint32_t blind_metadata_count; // 4 bytes, 648 - pnanovdb_uint32_t pad[5]; // 20 bytes, 652 -}; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_grid_t) -struct pnanovdb_grid_handle_t { pnanovdb_address_t address; }; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_grid_handle_t) - -#define PNANOVDB_GRID_SIZE 672 - -#define PNANOVDB_GRID_OFF_MAGIC 0 -#define PNANOVDB_GRID_OFF_CHECKSUM 8 -#define PNANOVDB_GRID_OFF_VERSION 16 -#define PNANOVDB_GRID_OFF_FLAGS 20 -#define PNANOVDB_GRID_OFF_GRID_INDEX 24 -#define PNANOVDB_GRID_OFF_GRID_COUNT 28 -#define PNANOVDB_GRID_OFF_GRID_SIZE 32 -#define PNANOVDB_GRID_OFF_GRID_NAME 40 -#define PNANOVDB_GRID_OFF_MAP 296 -#define PNANOVDB_GRID_OFF_WORLD_BBOX 560 -#define PNANOVDB_GRID_OFF_VOXEL_SIZE 608 -#define PNANOVDB_GRID_OFF_GRID_CLASS 632 -#define PNANOVDB_GRID_OFF_GRID_TYPE 636 -#define PNANOVDB_GRID_OFF_BLIND_METADATA_OFFSET 640 -#define PNANOVDB_GRID_OFF_BLIND_METADATA_COUNT 648 - -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_grid_get_magic(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { - return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_MAGIC)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_grid_get_checksum(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { - return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_CHECKSUM)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_grid_get_version(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_VERSION)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_grid_get_flags(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_FLAGS)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_grid_get_grid_index(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_INDEX)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_grid_get_grid_count(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_COUNT)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_grid_get_grid_size(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { - return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_SIZE)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_grid_get_grid_name(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint32_t index) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_NAME + 4u * index)); -} -PNANOVDB_FORCE_INLINE pnanovdb_map_handle_t pnanovdb_grid_get_map(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { - pnanovdb_map_handle_t ret; - ret.address = pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_MAP); - return ret; -} -PNANOVDB_FORCE_INLINE double pnanovdb_grid_get_world_bbox(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint32_t index) { - return pnanovdb_read_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_WORLD_BBOX + 8u * index)); -} -PNANOVDB_FORCE_INLINE double pnanovdb_grid_get_voxel_size(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint32_t index) { - return pnanovdb_read_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_VOXEL_SIZE + 8u * index)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_grid_get_grid_class(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_CLASS)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_grid_get_grid_type(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_TYPE)); -} -PNANOVDB_FORCE_INLINE pnanovdb_int64_t pnanovdb_grid_get_blind_metadata_offset(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { - return pnanovdb_read_int64(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_BLIND_METADATA_OFFSET)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_grid_get_blind_metadata_count(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_BLIND_METADATA_COUNT)); -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_version_get_major(pnanovdb_uint32_t version) -{ - return (version >> 21u) & ((1u << 11u) - 1u); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_version_get_minor(pnanovdb_uint32_t version) -{ - return (version >> 10u) & ((1u << 11u) - 1u); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_version_get_patch(pnanovdb_uint32_t version) -{ - return version & ((1u << 10u) - 1u); -} - -struct pnanovdb_gridblindmetadata_t -{ - pnanovdb_int64_t byte_offset; // 8 bytes, 0 - pnanovdb_uint64_t element_count; // 8 bytes, 8 - pnanovdb_uint32_t flags; // 4 bytes, 16 - pnanovdb_uint32_t semantic; // 4 bytes, 20 - pnanovdb_uint32_t data_class; // 4 bytes, 24 - pnanovdb_uint32_t data_type; // 4 bytes, 28 - pnanovdb_uint32_t name[256 / 4]; // 256 bytes, 32 -}; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_gridblindmetadata_t) -struct pnanovdb_gridblindmetadata_handle_t { pnanovdb_address_t address; }; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_gridblindmetadata_handle_t) - -#define PNANOVDB_GRIDBLINDMETADATA_SIZE 288 - -#define PNANOVDB_GRIDBLINDMETADATA_OFF_BYTE_OFFSET 0 -#define PNANOVDB_GRIDBLINDMETADATA_OFF_ELEMENT_COUNT 8 -#define PNANOVDB_GRIDBLINDMETADATA_OFF_FLAGS 16 -#define PNANOVDB_GRIDBLINDMETADATA_OFF_SEMANTIC 20 -#define PNANOVDB_GRIDBLINDMETADATA_OFF_DATA_CLASS 24 -#define PNANOVDB_GRIDBLINDMETADATA_OFF_DATA_TYPE 28 -#define PNANOVDB_GRIDBLINDMETADATA_OFF_NAME 32 - -PNANOVDB_FORCE_INLINE pnanovdb_int64_t pnanovdb_gridblindmetadata_get_byte_offset(pnanovdb_buf_t buf, pnanovdb_gridblindmetadata_handle_t p) { - return pnanovdb_read_int64(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRIDBLINDMETADATA_OFF_BYTE_OFFSET)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_gridblindmetadata_get_element_count(pnanovdb_buf_t buf, pnanovdb_gridblindmetadata_handle_t p) { - return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRIDBLINDMETADATA_OFF_ELEMENT_COUNT)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_gridblindmetadata_get_flags(pnanovdb_buf_t buf, pnanovdb_gridblindmetadata_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRIDBLINDMETADATA_OFF_FLAGS)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_gridblindmetadata_get_semantic(pnanovdb_buf_t buf, pnanovdb_gridblindmetadata_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRIDBLINDMETADATA_OFF_SEMANTIC)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_gridblindmetadata_get_data_class(pnanovdb_buf_t buf, pnanovdb_gridblindmetadata_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRIDBLINDMETADATA_OFF_DATA_CLASS)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_gridblindmetadata_get_data_type(pnanovdb_buf_t buf, pnanovdb_gridblindmetadata_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRIDBLINDMETADATA_OFF_DATA_TYPE)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_gridblindmetadata_get_name(pnanovdb_buf_t buf, pnanovdb_gridblindmetadata_handle_t p, pnanovdb_uint32_t index) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRIDBLINDMETADATA_OFF_NAME + 4u * index)); -} - -struct pnanovdb_tree_t -{ - pnanovdb_uint64_t node_offset_leaf; - pnanovdb_uint64_t node_offset_lower; - pnanovdb_uint64_t node_offset_upper; - pnanovdb_uint64_t node_offset_root; - pnanovdb_uint32_t node_count_leaf; - pnanovdb_uint32_t node_count_lower; - pnanovdb_uint32_t node_count_upper; - pnanovdb_uint32_t tile_count_leaf; - pnanovdb_uint32_t tile_count_lower; - pnanovdb_uint32_t tile_count_upper; - pnanovdb_uint64_t voxel_count; -}; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_tree_t) -struct pnanovdb_tree_handle_t { pnanovdb_address_t address; }; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_tree_handle_t) - -#define PNANOVDB_TREE_SIZE 64 - -#define PNANOVDB_TREE_OFF_NODE_OFFSET_LEAF 0 -#define PNANOVDB_TREE_OFF_NODE_OFFSET_LOWER 8 -#define PNANOVDB_TREE_OFF_NODE_OFFSET_UPPER 16 -#define PNANOVDB_TREE_OFF_NODE_OFFSET_ROOT 24 -#define PNANOVDB_TREE_OFF_NODE_COUNT_LEAF 32 -#define PNANOVDB_TREE_OFF_NODE_COUNT_LOWER 36 -#define PNANOVDB_TREE_OFF_NODE_COUNT_UPPER 40 -#define PNANOVDB_TREE_OFF_TILE_COUNT_LEAF 44 -#define PNANOVDB_TREE_OFF_TILE_COUNT_LOWER 48 -#define PNANOVDB_TREE_OFF_TILE_COUNT_UPPER 52 -#define PNANOVDB_TREE_OFF_VOXEL_COUNT 56 - -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_tree_get_node_offset_leaf(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { - return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_OFFSET_LEAF)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_tree_get_node_offset_lower(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { - return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_OFFSET_LOWER)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_tree_get_node_offset_upper(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { - return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_OFFSET_UPPER)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_tree_get_node_offset_root(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { - return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_OFFSET_ROOT)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_tree_get_node_count_leaf(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_COUNT_LEAF)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_tree_get_node_count_lower(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_COUNT_LOWER)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_tree_get_node_count_upper(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_COUNT_UPPER)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_tree_get_tile_count_leaf(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_TILE_COUNT_LEAF)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_tree_get_tile_count_lower(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_TILE_COUNT_LOWER)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_tree_get_tile_count_upper(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_TILE_COUNT_UPPER)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_tree_get_voxel_count(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { - return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_VOXEL_COUNT)); -} - -struct pnanovdb_root_t -{ - pnanovdb_coord_t bbox_min; - pnanovdb_coord_t bbox_max; - pnanovdb_uint32_t table_size; - pnanovdb_uint32_t pad1; // background can start here - // background, min, max -}; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_root_t) -struct pnanovdb_root_handle_t { pnanovdb_address_t address; }; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_root_handle_t) - -#define PNANOVDB_ROOT_BASE_SIZE 28 - -#define PNANOVDB_ROOT_OFF_BBOX_MIN 0 -#define PNANOVDB_ROOT_OFF_BBOX_MAX 12 -#define PNANOVDB_ROOT_OFF_TABLE_SIZE 24 - -PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_root_get_bbox_min(pnanovdb_buf_t buf, pnanovdb_root_handle_t p) { - return pnanovdb_read_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_OFF_BBOX_MIN)); -} -PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_root_get_bbox_max(pnanovdb_buf_t buf, pnanovdb_root_handle_t p) { - return pnanovdb_read_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_OFF_BBOX_MAX)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_root_get_tile_count(pnanovdb_buf_t buf, pnanovdb_root_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_OFF_TABLE_SIZE)); -} - -struct pnanovdb_root_tile_t -{ - pnanovdb_uint64_t key; - pnanovdb_int64_t child; // signed byte offset from root to the child node, 0 means it is a constant tile, so use value - pnanovdb_uint32_t state; - pnanovdb_uint32_t pad1; // value can start here - // value -}; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_root_tile_t) -struct pnanovdb_root_tile_handle_t { pnanovdb_address_t address; }; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_root_tile_handle_t) - -#define PNANOVDB_ROOT_TILE_BASE_SIZE 20 - -#define PNANOVDB_ROOT_TILE_OFF_KEY 0 -#define PNANOVDB_ROOT_TILE_OFF_CHILD 8 -#define PNANOVDB_ROOT_TILE_OFF_STATE 16 - -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_root_tile_get_key(pnanovdb_buf_t buf, pnanovdb_root_tile_handle_t p) { - return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_TILE_OFF_KEY)); -} -PNANOVDB_FORCE_INLINE pnanovdb_int64_t pnanovdb_root_tile_get_child(pnanovdb_buf_t buf, pnanovdb_root_tile_handle_t p) { - return pnanovdb_read_int64(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_TILE_OFF_CHILD)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_root_tile_get_state(pnanovdb_buf_t buf, pnanovdb_root_tile_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_TILE_OFF_STATE)); -} - -struct pnanovdb_upper_t -{ - pnanovdb_coord_t bbox_min; - pnanovdb_coord_t bbox_max; - pnanovdb_uint64_t flags; - pnanovdb_uint32_t value_mask[1024]; - pnanovdb_uint32_t child_mask[1024]; - // min, max - // alignas(32) pnanovdb_uint32_t table[]; -}; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_upper_t) -struct pnanovdb_upper_handle_t { pnanovdb_address_t address; }; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_upper_handle_t) - -#define PNANOVDB_UPPER_TABLE_COUNT 32768 -#define PNANOVDB_UPPER_BASE_SIZE 8224 - -#define PNANOVDB_UPPER_OFF_BBOX_MIN 0 -#define PNANOVDB_UPPER_OFF_BBOX_MAX 12 -#define PNANOVDB_UPPER_OFF_FLAGS 24 -#define PNANOVDB_UPPER_OFF_VALUE_MASK 32 -#define PNANOVDB_UPPER_OFF_CHILD_MASK 4128 - -PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_upper_get_bbox_min(pnanovdb_buf_t buf, pnanovdb_upper_handle_t p) { - return pnanovdb_read_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_UPPER_OFF_BBOX_MIN)); -} -PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_upper_get_bbox_max(pnanovdb_buf_t buf, pnanovdb_upper_handle_t p) { - return pnanovdb_read_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_UPPER_OFF_BBOX_MAX)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_upper_get_flags(pnanovdb_buf_t buf, pnanovdb_upper_handle_t p) { - return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_UPPER_OFF_FLAGS)); -} -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_upper_get_value_mask(pnanovdb_buf_t buf, pnanovdb_upper_handle_t p, pnanovdb_uint32_t bit_index) { - pnanovdb_uint32_t value = pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_UPPER_OFF_VALUE_MASK + 4u * (bit_index >> 5u))); - return ((value >> (bit_index & 31u)) & 1) != 0u; -} -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_upper_get_child_mask(pnanovdb_buf_t buf, pnanovdb_upper_handle_t p, pnanovdb_uint32_t bit_index) { - pnanovdb_uint32_t value = pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_UPPER_OFF_CHILD_MASK + 4u * (bit_index >> 5u))); - return ((value >> (bit_index & 31u)) & 1) != 0u; -} - -struct pnanovdb_lower_t -{ - pnanovdb_coord_t bbox_min; - pnanovdb_coord_t bbox_max; - pnanovdb_uint64_t flags; - pnanovdb_uint32_t value_mask[128]; - pnanovdb_uint32_t child_mask[128]; - // min, max - // alignas(32) pnanovdb_uint32_t table[]; -}; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_lower_t) -struct pnanovdb_lower_handle_t { pnanovdb_address_t address; }; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_lower_handle_t) - -#define PNANOVDB_LOWER_TABLE_COUNT 4096 -#define PNANOVDB_LOWER_BASE_SIZE 1056 - -#define PNANOVDB_LOWER_OFF_BBOX_MIN 0 -#define PNANOVDB_LOWER_OFF_BBOX_MAX 12 -#define PNANOVDB_LOWER_OFF_FLAGS 24 -#define PNANOVDB_LOWER_OFF_VALUE_MASK 32 -#define PNANOVDB_LOWER_OFF_CHILD_MASK 544 - -PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_lower_get_bbox_min(pnanovdb_buf_t buf, pnanovdb_lower_handle_t p) { - return pnanovdb_read_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_LOWER_OFF_BBOX_MIN)); -} -PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_lower_get_bbox_max(pnanovdb_buf_t buf, pnanovdb_lower_handle_t p) { - return pnanovdb_read_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_LOWER_OFF_BBOX_MAX)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_lower_get_flags(pnanovdb_buf_t buf, pnanovdb_lower_handle_t p) { - return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_LOWER_OFF_FLAGS)); -} -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_lower_get_value_mask(pnanovdb_buf_t buf, pnanovdb_lower_handle_t p, pnanovdb_uint32_t bit_index) { - pnanovdb_uint32_t value = pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_LOWER_OFF_VALUE_MASK + 4u * (bit_index >> 5u))); - return ((value >> (bit_index & 31u)) & 1) != 0u; -} -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_lower_get_child_mask(pnanovdb_buf_t buf, pnanovdb_lower_handle_t p, pnanovdb_uint32_t bit_index) { - pnanovdb_uint32_t value = pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_LOWER_OFF_CHILD_MASK + 4u * (bit_index >> 5u))); - return ((value >> (bit_index & 31u)) & 1) != 0u; -} - -struct pnanovdb_leaf_t -{ - pnanovdb_coord_t bbox_min; - pnanovdb_uint32_t bbox_dif_and_flags; - pnanovdb_uint32_t value_mask[16]; - // min, max - // alignas(32) pnanovdb_uint32_t values[]; -}; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_leaf_t) -struct pnanovdb_leaf_handle_t { pnanovdb_address_t address; }; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_leaf_handle_t) - -#define PNANOVDB_LEAF_TABLE_COUNT 512 -#define PNANOVDB_LEAF_BASE_SIZE 80 - -#define PNANOVDB_LEAF_OFF_BBOX_MIN 0 -#define PNANOVDB_LEAF_OFF_BBOX_DIF_AND_FLAGS 12 -#define PNANOVDB_LEAF_OFF_VALUE_MASK 16 - -#define PNANOVDB_LEAF_TABLE_NEG_OFF_BBOX_DIF_AND_FLAGS 84 -#define PNANOVDB_LEAF_TABLE_NEG_OFF_MINIMUM 16 -#define PNANOVDB_LEAF_TABLE_NEG_OFF_QUANTUM 12 - -PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_leaf_get_bbox_min(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t p) { - return pnanovdb_read_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_LEAF_OFF_BBOX_MIN)); -} -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_leaf_get_bbox_dif_and_flags(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t p) { - return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_LEAF_OFF_BBOX_DIF_AND_FLAGS)); -} -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_leaf_get_value_mask(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t p, pnanovdb_uint32_t bit_index) { - pnanovdb_uint32_t value = pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_LEAF_OFF_VALUE_MASK + 4u * (bit_index >> 5u))); - return ((value >> (bit_index & 31u)) & 1) != 0u; -} - -struct pnanovdb_grid_type_constants_t -{ - pnanovdb_uint32_t root_off_background; - pnanovdb_uint32_t root_off_min; - pnanovdb_uint32_t root_off_max; - pnanovdb_uint32_t root_off_ave; - pnanovdb_uint32_t root_off_stddev; - pnanovdb_uint32_t root_size; - pnanovdb_uint32_t value_stride_bits; - pnanovdb_uint32_t table_stride; - pnanovdb_uint32_t root_tile_off_value; - pnanovdb_uint32_t root_tile_size; - pnanovdb_uint32_t upper_off_min; - pnanovdb_uint32_t upper_off_max; - pnanovdb_uint32_t upper_off_ave; - pnanovdb_uint32_t upper_off_stddev; - pnanovdb_uint32_t upper_off_table; - pnanovdb_uint32_t upper_size; - pnanovdb_uint32_t lower_off_min; - pnanovdb_uint32_t lower_off_max; - pnanovdb_uint32_t lower_off_ave; - pnanovdb_uint32_t lower_off_stddev; - pnanovdb_uint32_t lower_off_table; - pnanovdb_uint32_t lower_size; - pnanovdb_uint32_t leaf_off_min; - pnanovdb_uint32_t leaf_off_max; - pnanovdb_uint32_t leaf_off_ave; - pnanovdb_uint32_t leaf_off_stddev; - pnanovdb_uint32_t leaf_off_table; - pnanovdb_uint32_t leaf_size; -}; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_grid_type_constants_t) - -PNANOVDB_STATIC_CONST pnanovdb_grid_type_constants_t pnanovdb_grid_type_constants[PNANOVDB_GRID_TYPE_END] = -{ - {28, 28, 28, 28, 28, 32, 0, 8, 20, 32, 8224, 8224, 8224, 8224, 8224, 270368, 1056, 1056, 1056, 1056, 1056, 33824, 80, 80, 80, 80, 96, 96}, - {28, 32, 36, 40, 44, 64, 32, 8, 20, 32, 8224, 8228, 8232, 8236, 8256, 270400, 1056, 1060, 1064, 1068, 1088, 33856, 80, 84, 88, 92, 96, 2144}, - {32, 40, 48, 56, 64, 96, 64, 8, 24, 32, 8224, 8232, 8240, 8248, 8256, 270400, 1056, 1064, 1072, 1080, 1088, 33856, 80, 88, 96, 104, 128, 4224}, - {28, 30, 32, 36, 40, 64, 16, 8, 20, 32, 8224, 8226, 8228, 8232, 8256, 270400, 1056, 1058, 1060, 1064, 1088, 33856, 80, 82, 84, 88, 96, 1120}, - {28, 32, 36, 40, 44, 64, 32, 8, 20, 32, 8224, 8228, 8232, 8236, 8256, 270400, 1056, 1060, 1064, 1068, 1088, 33856, 80, 84, 88, 92, 96, 2144}, - {32, 40, 48, 56, 64, 96, 64, 8, 24, 32, 8224, 8232, 8240, 8248, 8256, 270400, 1056, 1064, 1072, 1080, 1088, 33856, 80, 88, 96, 104, 128, 4224}, - {28, 40, 52, 64, 68, 96, 96, 16, 20, 32, 8224, 8236, 8248, 8252, 8256, 532544, 1056, 1068, 1080, 1084, 1088, 66624, 80, 92, 104, 108, 128, 6272}, - {32, 56, 80, 104, 112, 128, 192, 24, 24, 64, 8224, 8248, 8272, 8280, 8288, 794720, 1056, 1080, 1104, 1112, 1120, 99424, 80, 104, 128, 136, 160, 12448}, - {28, 29, 30, 31, 32, 64, 0, 8, 20, 32, 8224, 8225, 8226, 8227, 8256, 270400, 1056, 1057, 1058, 1059, 1088, 33856, 80, 80, 80, 80, 96, 96}, - {28, 30, 32, 36, 40, 64, 16, 8, 20, 32, 8224, 8226, 8228, 8232, 8256, 270400, 1056, 1058, 1060, 1064, 1088, 33856, 80, 82, 84, 88, 96, 1120}, - {28, 32, 36, 40, 44, 64, 32, 8, 20, 32, 8224, 8228, 8232, 8236, 8256, 270400, 1056, 1060, 1064, 1068, 1088, 33856, 80, 84, 88, 92, 96, 2144}, - {28, 29, 30, 31, 32, 64, 1, 8, 20, 32, 8224, 8225, 8226, 8227, 8256, 270400, 1056, 1057, 1058, 1059, 1088, 33856, 80, 80, 80, 80, 96, 160}, - {28, 32, 36, 40, 44, 64, 32, 8, 20, 32, 8224, 8228, 8232, 8236, 8256, 270400, 1056, 1060, 1064, 1068, 1088, 33856, 80, 84, 88, 92, 96, 2144}, - {28, 32, 36, 40, 44, 64, 0, 8, 20, 32, 8224, 8228, 8232, 8236, 8256, 270400, 1056, 1060, 1064, 1068, 1088, 33856, 88, 90, 92, 94, 96, 352}, - {28, 32, 36, 40, 44, 64, 0, 8, 20, 32, 8224, 8228, 8232, 8236, 8256, 270400, 1056, 1060, 1064, 1068, 1088, 33856, 88, 90, 92, 94, 96, 608}, - {28, 32, 36, 40, 44, 64, 0, 8, 20, 32, 8224, 8228, 8232, 8236, 8256, 270400, 1056, 1060, 1064, 1068, 1088, 33856, 88, 90, 92, 94, 96, 1120}, - {28, 32, 36, 40, 44, 64, 0, 8, 20, 32, 8224, 8228, 8232, 8236, 8256, 270400, 1056, 1060, 1064, 1068, 1088, 33856, 88, 90, 92, 94, 96, 96}, - {28, 44, 60, 76, 80, 96, 128, 16, 20, 64, 8224, 8240, 8256, 8260, 8288, 532576, 1056, 1072, 1088, 1092, 1120, 66656, 80, 96, 112, 116, 128, 8320}, - {32, 64, 96, 128, 136, 160, 256, 32, 24, 64, 8224, 8256, 8288, 8296, 8320, 1056896, 1056, 1088, 1120, 1128, 1152, 132224, 80, 112, 144, 152, 160, 16544}, - {32, 40, 48, 56, 64, 96, 0, 8, 24, 32, 8224, 8232, 8240, 8248, 8256, 270400, 1056, 1064, 1072, 1080, 1088, 33856, 80, 80, 80, 80, 88, 96}, -}; - -// ------------------------------------------------ Basic Lookup ----------------------------------------------------------- - -PNANOVDB_FORCE_INLINE pnanovdb_gridblindmetadata_handle_t pnanovdb_grid_get_gridblindmetadata(pnanovdb_buf_t buf, pnanovdb_grid_handle_t grid, pnanovdb_uint32_t index) -{ - pnanovdb_gridblindmetadata_handle_t meta = { grid.address }; - pnanovdb_uint64_t byte_offset = pnanovdb_grid_get_blind_metadata_offset(buf, grid); - meta.address = pnanovdb_address_offset64(meta.address, byte_offset); - meta.address = pnanovdb_address_offset_product(meta.address, PNANOVDB_GRIDBLINDMETADATA_SIZE, index); - return meta; -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanodvb_grid_get_gridblindmetadata_value_address(pnanovdb_buf_t buf, pnanovdb_grid_handle_t grid, pnanovdb_uint32_t index) -{ - pnanovdb_gridblindmetadata_handle_t meta = pnanovdb_grid_get_gridblindmetadata(buf, grid, index); - pnanovdb_int64_t byte_offset = pnanovdb_gridblindmetadata_get_byte_offset(buf, meta); - pnanovdb_address_t address = grid.address; - address = pnanovdb_address_offset64(address, pnanovdb_int64_as_uint64(byte_offset)); - return address; -} - -PNANOVDB_FORCE_INLINE pnanovdb_tree_handle_t pnanovdb_grid_get_tree(pnanovdb_buf_t buf, pnanovdb_grid_handle_t grid) -{ - pnanovdb_tree_handle_t tree = { grid.address }; - tree.address = pnanovdb_address_offset(tree.address, PNANOVDB_GRID_SIZE); - return tree; -} - -PNANOVDB_FORCE_INLINE pnanovdb_root_handle_t pnanovdb_tree_get_root(pnanovdb_buf_t buf, pnanovdb_tree_handle_t tree) -{ - pnanovdb_root_handle_t root = { tree.address }; - pnanovdb_uint64_t byte_offset = pnanovdb_tree_get_node_offset_root(buf, tree); - root.address = pnanovdb_address_offset64(root.address, byte_offset); - return root; -} - -PNANOVDB_FORCE_INLINE pnanovdb_root_tile_handle_t pnanovdb_root_get_tile(pnanovdb_grid_type_t grid_type, pnanovdb_root_handle_t root, pnanovdb_uint32_t n) -{ - pnanovdb_root_tile_handle_t tile = { root.address }; - tile.address = pnanovdb_address_offset(tile.address, PNANOVDB_GRID_TYPE_GET(grid_type, root_size)); - tile.address = pnanovdb_address_offset_product(tile.address, PNANOVDB_GRID_TYPE_GET(grid_type, root_tile_size), n); - return tile; -} - -PNANOVDB_FORCE_INLINE pnanovdb_root_tile_handle_t pnanovdb_root_get_tile_zero(pnanovdb_grid_type_t grid_type, pnanovdb_root_handle_t root) -{ - pnanovdb_root_tile_handle_t tile = { root.address }; - tile.address = pnanovdb_address_offset(tile.address, PNANOVDB_GRID_TYPE_GET(grid_type, root_size)); - return tile; -} - -PNANOVDB_FORCE_INLINE pnanovdb_upper_handle_t pnanovdb_root_get_child(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, pnanovdb_root_tile_handle_t tile) -{ - pnanovdb_upper_handle_t upper = { root.address }; - upper.address = pnanovdb_address_offset64(upper.address, pnanovdb_int64_as_uint64(pnanovdb_root_tile_get_child(buf, tile))); - return upper; -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_coord_to_key(PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ -#if defined(PNANOVDB_NATIVE_64) - pnanovdb_uint64_t iu = pnanovdb_int32_as_uint32(PNANOVDB_DEREF(ijk).x) >> 12u; - pnanovdb_uint64_t ju = pnanovdb_int32_as_uint32(PNANOVDB_DEREF(ijk).y) >> 12u; - pnanovdb_uint64_t ku = pnanovdb_int32_as_uint32(PNANOVDB_DEREF(ijk).z) >> 12u; - return (ku) | (ju << 21u) | (iu << 42u); -#else - pnanovdb_uint32_t iu = pnanovdb_int32_as_uint32(PNANOVDB_DEREF(ijk).x) >> 12u; - pnanovdb_uint32_t ju = pnanovdb_int32_as_uint32(PNANOVDB_DEREF(ijk).y) >> 12u; - pnanovdb_uint32_t ku = pnanovdb_int32_as_uint32(PNANOVDB_DEREF(ijk).z) >> 12u; - pnanovdb_uint32_t key_x = ku | (ju << 21); - pnanovdb_uint32_t key_y = (iu << 10) | (ju >> 11); - return pnanovdb_uint32_as_uint64(key_x, key_y); -#endif -} - -PNANOVDB_FORCE_INLINE pnanovdb_root_tile_handle_t pnanovdb_root_find_tile(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - pnanovdb_uint32_t tile_count = pnanovdb_uint32_as_int32(pnanovdb_root_get_tile_count(buf, root)); - pnanovdb_root_tile_handle_t tile = pnanovdb_root_get_tile_zero(grid_type, root); - pnanovdb_uint64_t key = pnanovdb_coord_to_key(ijk); - for (pnanovdb_uint32_t i = 0u; i < tile_count; i++) - { - if (pnanovdb_uint64_is_equal(key, pnanovdb_root_tile_get_key(buf, tile))) - { - return tile; - } - tile.address = pnanovdb_address_offset(tile.address, PNANOVDB_GRID_TYPE_GET(grid_type, root_tile_size)); - } - pnanovdb_root_tile_handle_t null_handle = { pnanovdb_address_null() }; - return null_handle; -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_leaf_coord_to_offset(PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - return (((PNANOVDB_DEREF(ijk).x & 7) >> 0) << (2 * 3)) + - (((PNANOVDB_DEREF(ijk).y & 7) >> 0) << (3)) + - ((PNANOVDB_DEREF(ijk).z & 7) >> 0); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_leaf_get_min_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t node) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, leaf_off_min); - return pnanovdb_address_offset(node.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_leaf_get_max_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t node) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, leaf_off_max); - return pnanovdb_address_offset(node.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_leaf_get_ave_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t node) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, leaf_off_ave); - return pnanovdb_address_offset(node.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_leaf_get_stddev_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t node) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, leaf_off_stddev); - return pnanovdb_address_offset(node.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_leaf_get_table_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t node, pnanovdb_uint32_t n) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, leaf_off_table) + ((PNANOVDB_GRID_TYPE_GET(grid_type, value_stride_bits) * n) >> 3u); - return pnanovdb_address_offset(node.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_leaf_get_value_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - pnanovdb_uint32_t n = pnanovdb_leaf_coord_to_offset(ijk); - return pnanovdb_leaf_get_table_address(grid_type, buf, leaf, n); -} - -PNANOVDB_FORCE_INLINE float pnanovdb_leaf_fp_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk, pnanovdb_uint32_t value_log_bits) -{ - // value_log_bits // 2 3 4 - pnanovdb_uint32_t value_bits = 1u << value_log_bits; // 4 8 16 - pnanovdb_uint32_t value_mask = (1u << value_bits) - 1u; // 0xF 0xFF 0xFFFF - pnanovdb_uint32_t values_per_word_bits = 5u - value_log_bits; // 3 2 1 - pnanovdb_uint32_t values_per_word_mask = (1u << values_per_word_bits) - 1u; // 7 3 1 - - pnanovdb_uint32_t n = pnanovdb_leaf_coord_to_offset(ijk); - float minimum = pnanovdb_read_float(buf, pnanovdb_address_offset_neg(address, PNANOVDB_LEAF_TABLE_NEG_OFF_MINIMUM)); - float quantum = pnanovdb_read_float(buf, pnanovdb_address_offset_neg(address, PNANOVDB_LEAF_TABLE_NEG_OFF_QUANTUM)); - pnanovdb_uint32_t raw = pnanovdb_read_uint32(buf, pnanovdb_address_offset(address, ((n >> values_per_word_bits) << 2u))); - pnanovdb_uint32_t value_compressed = (raw >> ((n & values_per_word_mask) << value_log_bits)) & value_mask; - return pnanovdb_uint32_to_float(value_compressed) * quantum + minimum; -} - -PNANOVDB_FORCE_INLINE float pnanovdb_leaf_fp4_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - return pnanovdb_leaf_fp_read_float(buf, address, ijk, 2u); -} - -PNANOVDB_FORCE_INLINE float pnanovdb_leaf_fp8_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - return pnanovdb_leaf_fp_read_float(buf, address, ijk, 3u); -} - -PNANOVDB_FORCE_INLINE float pnanovdb_leaf_fp16_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - return pnanovdb_leaf_fp_read_float(buf, address, ijk, 4u); -} - -PNANOVDB_FORCE_INLINE float pnanovdb_leaf_fpn_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - pnanovdb_uint32_t bbox_dif_and_flags = pnanovdb_read_uint32(buf, pnanovdb_address_offset_neg(address, PNANOVDB_LEAF_TABLE_NEG_OFF_BBOX_DIF_AND_FLAGS)); - pnanovdb_uint32_t flags = bbox_dif_and_flags >> 24u; - pnanovdb_uint32_t value_log_bits = flags >> 5; // b = 0, 1, 2, 3, 4 corresponding to 1, 2, 4, 8, 16 bits - return pnanovdb_leaf_fp_read_float(buf, address, ijk, value_log_bits); -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_uint32_countbits(pnanovdb_uint32_t value) -{ -#if defined(PNANOVDB_C) -#if defined(_MSC_VER) && (_MSC_VER >= 1928) && defined(PNANOVDB_USE_INTRINSICS) - return __popcnt(value); -#elif (defined(__GNUC__) || defined(__clang__)) && defined(PNANOVDB_USE_INTRINSICS) - return __builtin_popcount(value); -#else - value = value - ((value >> 1) & 0x55555555); - value = (value & 0x33333333) + ((value >> 2) & 0x33333333); - value = (value + (value >> 4)) & 0x0F0F0F0F; - return (value * 0x01010101) >> 24; -#endif -#elif defined(PNANOVDB_HLSL) - return countbits(value); -#elif defined(PNANOVDB_GLSL) - return bitCount(value); -#endif -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_leaf_count_on_range(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t p, pnanovdb_uint32_t max_index) -{ - pnanovdb_uint32_t mask_idx_max = max_index >> 5u; - pnanovdb_uint32_t sum = 0u; - pnanovdb_uint32_t mask_val = 0u; - for (pnanovdb_uint32_t mask_idx = 0u; mask_idx < mask_idx_max; mask_idx++) - { - mask_val = pnanovdb_read_uint32( - buf, pnanovdb_address_offset(p.address, PNANOVDB_LEAF_OFF_VALUE_MASK + 4u * mask_idx)); - sum += pnanovdb_uint32_countbits(mask_val); - } - mask_val = pnanovdb_read_uint32( - buf, pnanovdb_address_offset(p.address, PNANOVDB_LEAF_OFF_VALUE_MASK + 4u * mask_idx_max)); - sum += pnanovdb_uint32_countbits(mask_val & ((1u << (max_index & 31u)) - 1u)); - return sum; -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_uint64_offset(pnanovdb_uint64_t a, pnanovdb_uint32_t b) -{ -#if defined(PNANOVDB_ADDRESS_32) - pnanovdb_uint32_t low = pnanovdb_uint64_low(a); - pnanovdb_uint32_t high = pnanovdb_uint64_high(a); - low += b; - if (low < b) - { - high += 1u; - } - return pnanovdb_uint32_as_uint64(low, high); -#else - return a + b; -#endif -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_index_get_min_index(pnanovdb_buf_t buf, pnanovdb_address_t min_address) -{ - return pnanovdb_read_uint64(buf, min_address); -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_index_get_max_index(pnanovdb_buf_t buf, pnanovdb_address_t max_address) -{ - return pnanovdb_uint64_offset(pnanovdb_read_uint64(buf, max_address), 1u); -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_index_get_ave_index(pnanovdb_buf_t buf, pnanovdb_address_t ave_address) -{ - return pnanovdb_uint64_offset(pnanovdb_read_uint64(buf, ave_address), 2u); -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_index_get_dev_index(pnanovdb_buf_t buf, pnanovdb_address_t dev_address) -{ - return pnanovdb_uint64_offset(pnanovdb_read_uint64(buf, dev_address), 3u); -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_index_get_value_index(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - pnanovdb_uint32_t n = pnanovdb_leaf_coord_to_offset(ijk); - pnanovdb_uint32_t bbox_dif_and_flags = pnanovdb_leaf_get_bbox_dif_and_flags(buf, leaf); - pnanovdb_address_t value_address = pnanovdb_leaf_get_table_address(grid_type, buf, leaf, 0u); - if ((bbox_dif_and_flags & 0x10000000) != 0u) - { - if (pnanovdb_leaf_get_value_mask(buf, leaf, n)) - { - n = pnanovdb_leaf_count_on_range(buf, leaf, n); - } - else - { - value_address = pnanovdb_address_null(); - n = 0; - } - } - return pnanovdb_uint64_offset(pnanovdb_read_uint64(buf, value_address), n); -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_lower_coord_to_offset(PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - return (((PNANOVDB_DEREF(ijk).x & 127) >> 3) << (2 * 4)) + - (((PNANOVDB_DEREF(ijk).y & 127) >> 3) << (4)) + - ((PNANOVDB_DEREF(ijk).z & 127) >> 3); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_min_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t node) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, lower_off_min); - return pnanovdb_address_offset(node.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_max_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t node) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, lower_off_max); - return pnanovdb_address_offset(node.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_ave_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t node) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, lower_off_ave); - return pnanovdb_address_offset(node.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_stddev_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t node) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, lower_off_stddev); - return pnanovdb_address_offset(node.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_table_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t node, pnanovdb_uint32_t n) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, lower_off_table) + PNANOVDB_GRID_TYPE_GET(grid_type, table_stride) * n; - return pnanovdb_address_offset(node.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_int64_t pnanovdb_lower_get_table_child(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t node, pnanovdb_uint32_t n) -{ - pnanovdb_address_t table_address = pnanovdb_lower_get_table_address(grid_type, buf, node, n); - return pnanovdb_read_int64(buf, table_address); -} - -PNANOVDB_FORCE_INLINE pnanovdb_leaf_handle_t pnanovdb_lower_get_child(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t lower, pnanovdb_uint32_t n) -{ - pnanovdb_leaf_handle_t leaf = { lower.address }; - leaf.address = pnanovdb_address_offset64(leaf.address, pnanovdb_int64_as_uint64(pnanovdb_lower_get_table_child(grid_type, buf, lower, n))); - return leaf; -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_value_address_and_level(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t lower, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_uint32_t) level) -{ - pnanovdb_uint32_t n = pnanovdb_lower_coord_to_offset(ijk); - pnanovdb_address_t value_address; - if (pnanovdb_lower_get_child_mask(buf, lower, n)) - { - pnanovdb_leaf_handle_t child = pnanovdb_lower_get_child(grid_type, buf, lower, n); - value_address = pnanovdb_leaf_get_value_address(grid_type, buf, child, ijk); - PNANOVDB_DEREF(level) = 0u; - } - else - { - value_address = pnanovdb_lower_get_table_address(grid_type, buf, lower, n); - PNANOVDB_DEREF(level) = 1u; - } - return value_address; -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_value_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t lower, PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - pnanovdb_uint32_t level; - return pnanovdb_lower_get_value_address_and_level(grid_type, buf, lower, ijk, PNANOVDB_REF(level)); -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_upper_coord_to_offset(PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - return (((PNANOVDB_DEREF(ijk).x & 4095) >> 7) << (2 * 5)) + - (((PNANOVDB_DEREF(ijk).y & 4095) >> 7) << (5)) + - ((PNANOVDB_DEREF(ijk).z & 4095) >> 7); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_min_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t node) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, upper_off_min); - return pnanovdb_address_offset(node.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_max_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t node) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, upper_off_max); - return pnanovdb_address_offset(node.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_ave_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t node) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, upper_off_ave); - return pnanovdb_address_offset(node.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_stddev_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t node) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, upper_off_stddev); - return pnanovdb_address_offset(node.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_table_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t node, pnanovdb_uint32_t n) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, upper_off_table) + PNANOVDB_GRID_TYPE_GET(grid_type, table_stride) * n; - return pnanovdb_address_offset(node.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_int64_t pnanovdb_upper_get_table_child(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t node, pnanovdb_uint32_t n) -{ - pnanovdb_address_t bufAddress = pnanovdb_upper_get_table_address(grid_type, buf, node, n); - return pnanovdb_read_int64(buf, bufAddress); -} - -PNANOVDB_FORCE_INLINE pnanovdb_lower_handle_t pnanovdb_upper_get_child(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t upper, pnanovdb_uint32_t n) -{ - pnanovdb_lower_handle_t lower = { upper.address }; - lower.address = pnanovdb_address_offset64(lower.address, pnanovdb_int64_as_uint64(pnanovdb_upper_get_table_child(grid_type, buf, upper, n))); - return lower; -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_value_address_and_level(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t upper, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_uint32_t) level) -{ - pnanovdb_uint32_t n = pnanovdb_upper_coord_to_offset(ijk); - pnanovdb_address_t value_address; - if (pnanovdb_upper_get_child_mask(buf, upper, n)) - { - pnanovdb_lower_handle_t child = pnanovdb_upper_get_child(grid_type, buf, upper, n); - value_address = pnanovdb_lower_get_value_address_and_level(grid_type, buf, child, ijk, level); - } - else - { - value_address = pnanovdb_upper_get_table_address(grid_type, buf, upper, n); - PNANOVDB_DEREF(level) = 2u; - } - return value_address; -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_value_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t upper, PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - pnanovdb_uint32_t level; - return pnanovdb_upper_get_value_address_and_level(grid_type, buf, upper, ijk, PNANOVDB_REF(level)); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_min_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, root_off_min); - return pnanovdb_address_offset(root.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_max_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, root_off_max); - return pnanovdb_address_offset(root.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_ave_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, root_off_ave); - return pnanovdb_address_offset(root.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_stddev_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, root_off_stddev); - return pnanovdb_address_offset(root.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_tile_get_value_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_tile_handle_t root_tile) -{ - pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, root_tile_off_value); - return pnanovdb_address_offset(root_tile.address, byte_offset); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_value_address_and_level(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_uint32_t) level) -{ - pnanovdb_root_tile_handle_t tile = pnanovdb_root_find_tile(grid_type, buf, root, ijk); - pnanovdb_address_t ret; - if (pnanovdb_address_is_null(tile.address)) - { - ret = pnanovdb_address_offset(root.address, PNANOVDB_GRID_TYPE_GET(grid_type, root_off_background)); - PNANOVDB_DEREF(level) = 4u; - } - else if (pnanovdb_int64_is_zero(pnanovdb_root_tile_get_child(buf, tile))) - { - ret = pnanovdb_address_offset(tile.address, PNANOVDB_GRID_TYPE_GET(grid_type, root_tile_off_value)); - PNANOVDB_DEREF(level) = 3u; - } - else - { - pnanovdb_upper_handle_t child = pnanovdb_root_get_child(grid_type, buf, root, tile); - ret = pnanovdb_upper_get_value_address_and_level(grid_type, buf, child, ijk, level); - } - return ret; -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_value_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - pnanovdb_uint32_t level; - return pnanovdb_root_get_value_address_and_level(grid_type, buf, root, ijk, PNANOVDB_REF(level)); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_value_address_bit(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_uint32_t) bit_index) -{ - pnanovdb_uint32_t level; - pnanovdb_address_t address = pnanovdb_root_get_value_address_and_level(grid_type, buf, root, ijk, PNANOVDB_REF(level)); - PNANOVDB_DEREF(bit_index) = level == 0u ? pnanovdb_int32_as_uint32(PNANOVDB_DEREF(ijk).x & 7) : 0u; - return address; -} - -PNANOVDB_FORCE_INLINE float pnanovdb_root_fp4_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk, pnanovdb_uint32_t level) -{ - float ret; - if (level == 0) - { - ret = pnanovdb_leaf_fp4_read_float(buf, address, ijk); - } - else - { - ret = pnanovdb_read_float(buf, address); - } - return ret; -} - -PNANOVDB_FORCE_INLINE float pnanovdb_root_fp8_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk, pnanovdb_uint32_t level) -{ - float ret; - if (level == 0) - { - ret = pnanovdb_leaf_fp8_read_float(buf, address, ijk); - } - else - { - ret = pnanovdb_read_float(buf, address); - } - return ret; -} - -PNANOVDB_FORCE_INLINE float pnanovdb_root_fp16_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk, pnanovdb_uint32_t level) -{ - float ret; - if (level == 0) - { - ret = pnanovdb_leaf_fp16_read_float(buf, address, ijk); - } - else - { - ret = pnanovdb_read_float(buf, address); - } - return ret; -} - -PNANOVDB_FORCE_INLINE float pnanovdb_root_fpn_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk, pnanovdb_uint32_t level) -{ - float ret; - if (level == 0) - { - ret = pnanovdb_leaf_fpn_read_float(buf, address, ijk); - } - else - { - ret = pnanovdb_read_float(buf, address); - } - return ret; -} - -// ------------------------------------------------ ReadAccessor ----------------------------------------------------------- - -struct pnanovdb_readaccessor_t -{ - pnanovdb_coord_t key; - pnanovdb_leaf_handle_t leaf; - pnanovdb_lower_handle_t lower; - pnanovdb_upper_handle_t upper; - pnanovdb_root_handle_t root; -}; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_readaccessor_t) - -PNANOVDB_FORCE_INLINE void pnanovdb_readaccessor_init(PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, pnanovdb_root_handle_t root) -{ - PNANOVDB_DEREF(acc).key.x = 0x7FFFFFFF; - PNANOVDB_DEREF(acc).key.y = 0x7FFFFFFF; - PNANOVDB_DEREF(acc).key.z = 0x7FFFFFFF; - PNANOVDB_DEREF(acc).leaf.address = pnanovdb_address_null(); - PNANOVDB_DEREF(acc).lower.address = pnanovdb_address_null(); - PNANOVDB_DEREF(acc).upper.address = pnanovdb_address_null(); - PNANOVDB_DEREF(acc).root = root; -} - -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_readaccessor_iscached0(PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, int dirty) -{ - if (pnanovdb_address_is_null(PNANOVDB_DEREF(acc).leaf.address)) { return PNANOVDB_FALSE; } - if ((dirty & ~((1u << 3) - 1u)) != 0) - { - PNANOVDB_DEREF(acc).leaf.address = pnanovdb_address_null(); - return PNANOVDB_FALSE; - } - return PNANOVDB_TRUE; -} -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_readaccessor_iscached1(PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, int dirty) -{ - if (pnanovdb_address_is_null(PNANOVDB_DEREF(acc).lower.address)) { return PNANOVDB_FALSE; } - if ((dirty & ~((1u << 7) - 1u)) != 0) - { - PNANOVDB_DEREF(acc).lower.address = pnanovdb_address_null(); - return PNANOVDB_FALSE; - } - return PNANOVDB_TRUE; -} -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_readaccessor_iscached2(PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, int dirty) -{ - if (pnanovdb_address_is_null(PNANOVDB_DEREF(acc).upper.address)) { return PNANOVDB_FALSE; } - if ((dirty & ~((1u << 12) - 1u)) != 0) - { - PNANOVDB_DEREF(acc).upper.address = pnanovdb_address_null(); - return PNANOVDB_FALSE; - } - return PNANOVDB_TRUE; -} -PNANOVDB_FORCE_INLINE int pnanovdb_readaccessor_computedirty(PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - return (PNANOVDB_DEREF(ijk).x ^ PNANOVDB_DEREF(acc).key.x) | (PNANOVDB_DEREF(ijk).y ^ PNANOVDB_DEREF(acc).key.y) | (PNANOVDB_DEREF(ijk).z ^ PNANOVDB_DEREF(acc).key.z); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_leaf_get_value_address_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) -{ - pnanovdb_uint32_t n = pnanovdb_leaf_coord_to_offset(ijk); - return pnanovdb_leaf_get_table_address(grid_type, buf, leaf, n); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_value_address_and_level_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t lower, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_INOUT(pnanovdb_uint32_t) level) -{ - pnanovdb_uint32_t n = pnanovdb_lower_coord_to_offset(ijk); - pnanovdb_address_t value_address; - if (pnanovdb_lower_get_child_mask(buf, lower, n)) - { - pnanovdb_leaf_handle_t child = pnanovdb_lower_get_child(grid_type, buf, lower, n); - PNANOVDB_DEREF(acc).leaf = child; - PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); - value_address = pnanovdb_leaf_get_value_address_and_cache(grid_type, buf, child, ijk, acc); - PNANOVDB_DEREF(level) = 0u; - } - else - { - value_address = pnanovdb_lower_get_table_address(grid_type, buf, lower, n); - PNANOVDB_DEREF(level) = 1u; - } - return value_address; -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_value_address_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t lower, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) -{ - pnanovdb_uint32_t level; - return pnanovdb_lower_get_value_address_and_level_and_cache(grid_type, buf, lower, ijk, acc, PNANOVDB_REF(level)); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_value_address_and_level_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t upper, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_INOUT(pnanovdb_uint32_t) level) -{ - pnanovdb_uint32_t n = pnanovdb_upper_coord_to_offset(ijk); - pnanovdb_address_t value_address; - if (pnanovdb_upper_get_child_mask(buf, upper, n)) - { - pnanovdb_lower_handle_t child = pnanovdb_upper_get_child(grid_type, buf, upper, n); - PNANOVDB_DEREF(acc).lower = child; - PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); - value_address = pnanovdb_lower_get_value_address_and_level_and_cache(grid_type, buf, child, ijk, acc, level); - } - else - { - value_address = pnanovdb_upper_get_table_address(grid_type, buf, upper, n); - PNANOVDB_DEREF(level) = 2u; - } - return value_address; -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_value_address_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t upper, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) -{ - pnanovdb_uint32_t level; - return pnanovdb_upper_get_value_address_and_level_and_cache(grid_type, buf, upper, ijk, acc, PNANOVDB_REF(level)); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_value_address_and_level_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_INOUT(pnanovdb_uint32_t) level) -{ - pnanovdb_root_tile_handle_t tile = pnanovdb_root_find_tile(grid_type, buf, root, ijk); - pnanovdb_address_t ret; - if (pnanovdb_address_is_null(tile.address)) - { - ret = pnanovdb_address_offset(root.address, PNANOVDB_GRID_TYPE_GET(grid_type, root_off_background)); - PNANOVDB_DEREF(level) = 4u; - } - else if (pnanovdb_int64_is_zero(pnanovdb_root_tile_get_child(buf, tile))) - { - ret = pnanovdb_address_offset(tile.address, PNANOVDB_GRID_TYPE_GET(grid_type, root_tile_off_value)); - PNANOVDB_DEREF(level) = 3u; - } - else - { - pnanovdb_upper_handle_t child = pnanovdb_root_get_child(grid_type, buf, root, tile); - PNANOVDB_DEREF(acc).upper = child; - PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); - ret = pnanovdb_upper_get_value_address_and_level_and_cache(grid_type, buf, child, ijk, acc, level); - } - return ret; -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_value_address_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) -{ - pnanovdb_uint32_t level; - return pnanovdb_root_get_value_address_and_level_and_cache(grid_type, buf, root, ijk, acc, PNANOVDB_REF(level)); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_readaccessor_get_value_address_and_level(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_uint32_t) level) -{ - int dirty = pnanovdb_readaccessor_computedirty(acc, ijk); - - pnanovdb_address_t value_address; - if (pnanovdb_readaccessor_iscached0(acc, dirty)) - { - value_address = pnanovdb_leaf_get_value_address_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).leaf, ijk, acc); - PNANOVDB_DEREF(level) = 0u; - } - else if (pnanovdb_readaccessor_iscached1(acc, dirty)) - { - value_address = pnanovdb_lower_get_value_address_and_level_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).lower, ijk, acc, level); - } - else if (pnanovdb_readaccessor_iscached2(acc, dirty)) - { - value_address = pnanovdb_upper_get_value_address_and_level_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).upper, ijk, acc, level); - } - else - { - value_address = pnanovdb_root_get_value_address_and_level_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).root, ijk, acc, level); - } - return value_address; -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_readaccessor_get_value_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - pnanovdb_uint32_t level; - return pnanovdb_readaccessor_get_value_address_and_level(grid_type, buf, acc, ijk, PNANOVDB_REF(level)); -} - -PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_readaccessor_get_value_address_bit(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_uint32_t) bit_index) -{ - pnanovdb_uint32_t level; - pnanovdb_address_t address = pnanovdb_readaccessor_get_value_address_and_level(grid_type, buf, acc, ijk, PNANOVDB_REF(level)); - PNANOVDB_DEREF(bit_index) = level == 0u ? pnanovdb_int32_as_uint32(PNANOVDB_DEREF(ijk).x & 7) : 0u; - return address; -} - -// ------------------------------------------------ ReadAccessor GetDim ----------------------------------------------------------- - -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_leaf_get_dim_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) -{ - return 1u; -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_lower_get_dim_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t lower, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) -{ - pnanovdb_uint32_t n = pnanovdb_lower_coord_to_offset(ijk); - pnanovdb_uint32_t ret; - if (pnanovdb_lower_get_child_mask(buf, lower, n)) - { - pnanovdb_leaf_handle_t child = pnanovdb_lower_get_child(grid_type, buf, lower, n); - PNANOVDB_DEREF(acc).leaf = child; - PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); - ret = pnanovdb_leaf_get_dim_and_cache(grid_type, buf, child, ijk, acc); - } - else - { - ret = (1u << (3u)); // node 0 dim - } - return ret; -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_upper_get_dim_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t upper, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) -{ - pnanovdb_uint32_t n = pnanovdb_upper_coord_to_offset(ijk); - pnanovdb_uint32_t ret; - if (pnanovdb_upper_get_child_mask(buf, upper, n)) - { - pnanovdb_lower_handle_t child = pnanovdb_upper_get_child(grid_type, buf, upper, n); - PNANOVDB_DEREF(acc).lower = child; - PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); - ret = pnanovdb_lower_get_dim_and_cache(grid_type, buf, child, ijk, acc); - } - else - { - ret = (1u << (4u + 3u)); // node 1 dim - } - return ret; -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_root_get_dim_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) -{ - pnanovdb_root_tile_handle_t tile = pnanovdb_root_find_tile(grid_type, buf, root, ijk); - pnanovdb_uint32_t ret; - if (pnanovdb_address_is_null(tile.address)) - { - ret = 1u << (5u + 4u + 3u); // background, node 2 dim - } - else if (pnanovdb_int64_is_zero(pnanovdb_root_tile_get_child(buf, tile))) - { - ret = 1u << (5u + 4u + 3u); // tile value, node 2 dim - } - else - { - pnanovdb_upper_handle_t child = pnanovdb_root_get_child(grid_type, buf, root, tile); - PNANOVDB_DEREF(acc).upper = child; - PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); - ret = pnanovdb_upper_get_dim_and_cache(grid_type, buf, child, ijk, acc); - } - return ret; -} - -PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_readaccessor_get_dim(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - int dirty = pnanovdb_readaccessor_computedirty(acc, ijk); - - pnanovdb_uint32_t dim; - if (pnanovdb_readaccessor_iscached0(acc, dirty)) - { - dim = pnanovdb_leaf_get_dim_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).leaf, ijk, acc); - } - else if (pnanovdb_readaccessor_iscached1(acc, dirty)) - { - dim = pnanovdb_lower_get_dim_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).lower, ijk, acc); - } - else if (pnanovdb_readaccessor_iscached2(acc, dirty)) - { - dim = pnanovdb_upper_get_dim_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).upper, ijk, acc); - } - else - { - dim = pnanovdb_root_get_dim_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).root, ijk, acc); - } - return dim; -} - -// ------------------------------------------------ ReadAccessor IsActive ----------------------------------------------------------- - -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_leaf_is_active_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) -{ - pnanovdb_uint32_t n = pnanovdb_leaf_coord_to_offset(ijk); - return pnanovdb_leaf_get_value_mask(buf, leaf, n); -} - -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_lower_is_active_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t lower, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) -{ - pnanovdb_uint32_t n = pnanovdb_lower_coord_to_offset(ijk); - pnanovdb_bool_t is_active; - if (pnanovdb_lower_get_child_mask(buf, lower, n)) - { - pnanovdb_leaf_handle_t child = pnanovdb_lower_get_child(grid_type, buf, lower, n); - PNANOVDB_DEREF(acc).leaf = child; - PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); - is_active = pnanovdb_leaf_is_active_and_cache(grid_type, buf, child, ijk, acc); - } - else - { - is_active = pnanovdb_lower_get_value_mask(buf, lower, n); - } - return is_active; -} - -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_upper_is_active_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t upper, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) -{ - pnanovdb_uint32_t n = pnanovdb_upper_coord_to_offset(ijk); - pnanovdb_bool_t is_active; - if (pnanovdb_upper_get_child_mask(buf, upper, n)) - { - pnanovdb_lower_handle_t child = pnanovdb_upper_get_child(grid_type, buf, upper, n); - PNANOVDB_DEREF(acc).lower = child; - PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); - is_active = pnanovdb_lower_is_active_and_cache(grid_type, buf, child, ijk, acc); - } - else - { - is_active = pnanovdb_upper_get_value_mask(buf, upper, n); - } - return is_active; -} - -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_root_is_active_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) -{ - pnanovdb_root_tile_handle_t tile = pnanovdb_root_find_tile(grid_type, buf, root, ijk); - pnanovdb_bool_t is_active; - if (pnanovdb_address_is_null(tile.address)) - { - is_active = PNANOVDB_FALSE; // background - } - else if (pnanovdb_int64_is_zero(pnanovdb_root_tile_get_child(buf, tile))) - { - pnanovdb_uint32_t state = pnanovdb_root_tile_get_state(buf, tile); - is_active = state != 0u; // tile value - } - else - { - pnanovdb_upper_handle_t child = pnanovdb_root_get_child(grid_type, buf, root, tile); - PNANOVDB_DEREF(acc).upper = child; - PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); - is_active = pnanovdb_upper_is_active_and_cache(grid_type, buf, child, ijk, acc); - } - return is_active; -} - -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_readaccessor_is_active(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_IN(pnanovdb_coord_t) ijk) -{ - int dirty = pnanovdb_readaccessor_computedirty(acc, ijk); - - pnanovdb_bool_t is_active; - if (pnanovdb_readaccessor_iscached0(acc, dirty)) - { - is_active = pnanovdb_leaf_is_active_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).leaf, ijk, acc); - } - else if (pnanovdb_readaccessor_iscached1(acc, dirty)) - { - is_active = pnanovdb_lower_is_active_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).lower, ijk, acc); - } - else if (pnanovdb_readaccessor_iscached2(acc, dirty)) - { - is_active = pnanovdb_upper_is_active_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).upper, ijk, acc); - } - else - { - is_active = pnanovdb_root_is_active_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).root, ijk, acc); - } - return is_active; -} - -// ------------------------------------------------ Map Transforms ----------------------------------------------------------- - -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_map_apply(pnanovdb_buf_t buf, pnanovdb_map_handle_t map, PNANOVDB_IN(pnanovdb_vec3_t) src) -{ - pnanovdb_vec3_t dst; - float sx = PNANOVDB_DEREF(src).x; - float sy = PNANOVDB_DEREF(src).y; - float sz = PNANOVDB_DEREF(src).z; - dst.x = sx * pnanovdb_map_get_matf(buf, map, 0) + sy * pnanovdb_map_get_matf(buf, map, 1) + sz * pnanovdb_map_get_matf(buf, map, 2) + pnanovdb_map_get_vecf(buf, map, 0); - dst.y = sx * pnanovdb_map_get_matf(buf, map, 3) + sy * pnanovdb_map_get_matf(buf, map, 4) + sz * pnanovdb_map_get_matf(buf, map, 5) + pnanovdb_map_get_vecf(buf, map, 1); - dst.z = sx * pnanovdb_map_get_matf(buf, map, 6) + sy * pnanovdb_map_get_matf(buf, map, 7) + sz * pnanovdb_map_get_matf(buf, map, 8) + pnanovdb_map_get_vecf(buf, map, 2); - return dst; -} - -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_map_apply_inverse(pnanovdb_buf_t buf, pnanovdb_map_handle_t map, PNANOVDB_IN(pnanovdb_vec3_t) src) -{ - pnanovdb_vec3_t dst; - float sx = PNANOVDB_DEREF(src).x - pnanovdb_map_get_vecf(buf, map, 0); - float sy = PNANOVDB_DEREF(src).y - pnanovdb_map_get_vecf(buf, map, 1); - float sz = PNANOVDB_DEREF(src).z - pnanovdb_map_get_vecf(buf, map, 2); - dst.x = sx * pnanovdb_map_get_invmatf(buf, map, 0) + sy * pnanovdb_map_get_invmatf(buf, map, 1) + sz * pnanovdb_map_get_invmatf(buf, map, 2); - dst.y = sx * pnanovdb_map_get_invmatf(buf, map, 3) + sy * pnanovdb_map_get_invmatf(buf, map, 4) + sz * pnanovdb_map_get_invmatf(buf, map, 5); - dst.z = sx * pnanovdb_map_get_invmatf(buf, map, 6) + sy * pnanovdb_map_get_invmatf(buf, map, 7) + sz * pnanovdb_map_get_invmatf(buf, map, 8); - return dst; -} - -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_map_apply_jacobi(pnanovdb_buf_t buf, pnanovdb_map_handle_t map, PNANOVDB_IN(pnanovdb_vec3_t) src) -{ - pnanovdb_vec3_t dst; - float sx = PNANOVDB_DEREF(src).x; - float sy = PNANOVDB_DEREF(src).y; - float sz = PNANOVDB_DEREF(src).z; - dst.x = sx * pnanovdb_map_get_matf(buf, map, 0) + sy * pnanovdb_map_get_matf(buf, map, 1) + sz * pnanovdb_map_get_matf(buf, map, 2); - dst.y = sx * pnanovdb_map_get_matf(buf, map, 3) + sy * pnanovdb_map_get_matf(buf, map, 4) + sz * pnanovdb_map_get_matf(buf, map, 5); - dst.z = sx * pnanovdb_map_get_matf(buf, map, 6) + sy * pnanovdb_map_get_matf(buf, map, 7) + sz * pnanovdb_map_get_matf(buf, map, 8); - return dst; -} - -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_map_apply_inverse_jacobi(pnanovdb_buf_t buf, pnanovdb_map_handle_t map, PNANOVDB_IN(pnanovdb_vec3_t) src) -{ - pnanovdb_vec3_t dst; - float sx = PNANOVDB_DEREF(src).x; - float sy = PNANOVDB_DEREF(src).y; - float sz = PNANOVDB_DEREF(src).z; - dst.x = sx * pnanovdb_map_get_invmatf(buf, map, 0) + sy * pnanovdb_map_get_invmatf(buf, map, 1) + sz * pnanovdb_map_get_invmatf(buf, map, 2); - dst.y = sx * pnanovdb_map_get_invmatf(buf, map, 3) + sy * pnanovdb_map_get_invmatf(buf, map, 4) + sz * pnanovdb_map_get_invmatf(buf, map, 5); - dst.z = sx * pnanovdb_map_get_invmatf(buf, map, 6) + sy * pnanovdb_map_get_invmatf(buf, map, 7) + sz * pnanovdb_map_get_invmatf(buf, map, 8); - return dst; -} - -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_grid_world_to_indexf(pnanovdb_buf_t buf, pnanovdb_grid_handle_t grid, PNANOVDB_IN(pnanovdb_vec3_t) src) -{ - pnanovdb_map_handle_t map = pnanovdb_grid_get_map(buf, grid); - return pnanovdb_map_apply_inverse(buf, map, src); -} - -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_grid_index_to_worldf(pnanovdb_buf_t buf, pnanovdb_grid_handle_t grid, PNANOVDB_IN(pnanovdb_vec3_t) src) -{ - pnanovdb_map_handle_t map = pnanovdb_grid_get_map(buf, grid); - return pnanovdb_map_apply(buf, map, src); -} - -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_grid_world_to_index_dirf(pnanovdb_buf_t buf, pnanovdb_grid_handle_t grid, PNANOVDB_IN(pnanovdb_vec3_t) src) -{ - pnanovdb_map_handle_t map = pnanovdb_grid_get_map(buf, grid); - return pnanovdb_map_apply_inverse_jacobi(buf, map, src); -} - -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_grid_index_to_world_dirf(pnanovdb_buf_t buf, pnanovdb_grid_handle_t grid, PNANOVDB_IN(pnanovdb_vec3_t) src) -{ - pnanovdb_map_handle_t map = pnanovdb_grid_get_map(buf, grid); - return pnanovdb_map_apply_jacobi(buf, map, src); -} - -// ------------------------------------------------ DitherLUT ----------------------------------------------------------- - -// This table was generated with -/************** - -static constexpr inline uint32 -SYSwang_inthash(uint32 key) -{ - // From http://www.concentric.net/~Ttwang/tech/inthash.htm - key += ~(key << 16); - key ^= (key >> 5); - key += (key << 3); - key ^= (key >> 13); - key += ~(key << 9); - key ^= (key >> 17); - return key; -} - -static void -ut_initDitherR(float *pattern, float offset, - int x, int y, int z, int res, int goalres) -{ - // These offsets are designed to maximize the difference between - // dither values in nearby voxels within a given 2x2x2 cell, without - // producing axis-aligned artifacts. The are organized in row-major - // order. - static const float theDitherOffset[] = {0,4,6,2,5,1,3,7}; - static const float theScale = 0.125F; - int key = (((z << res) + y) << res) + x; - - if (res == goalres) - { - pattern[key] = offset; - return; - } - - // Randomly flip (on each axis) the dithering patterns used by the - // subcells. This key is xor'd with the subcell index below before - // looking up in the dither offset list. - key = SYSwang_inthash(key) & 7; - - x <<= 1; - y <<= 1; - z <<= 1; - - offset *= theScale; - for (int i = 0; i < 8; i++) - ut_initDitherR(pattern, offset+theDitherOffset[i ^ key]*theScale, - x+(i&1), y+((i&2)>>1), z+((i&4)>>2), res+1, goalres); -} - -// This is a compact algorithm that accomplishes essentially the same thing -// as ut_initDither() above. We should eventually switch to use this and -// clean the dead code. -static fpreal32 * -ut_initDitherRecursive(int goalres) -{ - const int nfloat = 1 << (goalres*3); - float *pattern = new float[nfloat]; - ut_initDitherR(pattern, 1.0F, 0, 0, 0, 0, goalres); - - // This has built an even spacing from 1/nfloat to 1.0. - // however, our dither pattern should be 1/(nfloat+1) to nfloat/(nfloat+1) - // So we do a correction here. Note that the earlier calculations are - // done with powers of 2 so are exact, so it does make sense to delay - // the renormalization to this pass. - float correctionterm = nfloat / (nfloat+1.0F); - for (int i = 0; i < nfloat; i++) - pattern[i] *= correctionterm; - return pattern; -} - - theDitherMatrix = ut_initDitherRecursive(3); - - for (int i = 0; i < 512/8; i ++) - { - for (int j = 0; j < 8; j ++) - std::cout << theDitherMatrix[i*8+j] << "f, "; - std::cout << std::endl; - } - - **************/ - -PNANOVDB_STATIC_CONST float pnanovdb_dither_lut[512] = -{ - 0.14425f, 0.643275f, 0.830409f, 0.331384f, 0.105263f, 0.604289f, 0.167641f, 0.666667f, - 0.892788f, 0.393762f, 0.0818713f, 0.580897f, 0.853801f, 0.354776f, 0.916179f, 0.417154f, - 0.612086f, 0.11306f, 0.79922f, 0.300195f, 0.510721f, 0.0116959f, 0.947368f, 0.448343f, - 0.362573f, 0.861598f, 0.0506823f, 0.549708f, 0.261209f, 0.760234f, 0.19883f, 0.697856f, - 0.140351f, 0.639376f, 0.576998f, 0.0779727f, 0.522417f, 0.0233918f, 0.460039f, 0.959064f, - 0.888889f, 0.389864f, 0.327485f, 0.826511f, 0.272904f, 0.77193f, 0.709552f, 0.210526f, - 0.483431f, 0.982456f, 0.296296f, 0.795322f, 0.116959f, 0.615984f, 0.0545809f, 0.553606f, - 0.732943f, 0.233918f, 0.545809f, 0.0467836f, 0.865497f, 0.366472f, 0.803119f, 0.304094f, - 0.518519f, 0.0194932f, 0.45614f, 0.955166f, 0.729045f, 0.230019f, 0.54191f, 0.042885f, - 0.269006f, 0.768031f, 0.705653f, 0.206628f, 0.479532f, 0.978558f, 0.292398f, 0.791423f, - 0.237817f, 0.736842f, 0.424951f, 0.923977f, 0.136452f, 0.635478f, 0.323587f, 0.822612f, - 0.986355f, 0.487329f, 0.674464f, 0.175439f, 0.88499f, 0.385965f, 0.573099f, 0.0740741f, - 0.51462f, 0.0155945f, 0.202729f, 0.701754f, 0.148148f, 0.647174f, 0.834308f, 0.335283f, - 0.265107f, 0.764133f, 0.951267f, 0.452242f, 0.896686f, 0.397661f, 0.08577f, 0.584795f, - 0.8577f, 0.358674f, 0.920078f, 0.421053f, 0.740741f, 0.241715f, 0.678363f, 0.179337f, - 0.109162f, 0.608187f, 0.17154f, 0.670565f, 0.491228f, 0.990253f, 0.42885f, 0.927875f, - 0.0662768f, 0.565302f, 0.62768f, 0.128655f, 0.183236f, 0.682261f, 0.744639f, 0.245614f, - 0.814815f, 0.315789f, 0.378168f, 0.877193f, 0.931774f, 0.432749f, 0.495127f, 0.994152f, - 0.0350877f, 0.534113f, 0.97076f, 0.471735f, 0.214425f, 0.71345f, 0.526316f, 0.0272904f, - 0.783626f, 0.2846f, 0.222222f, 0.721248f, 0.962963f, 0.463938f, 0.276803f, 0.775828f, - 0.966862f, 0.467836f, 0.405458f, 0.904483f, 0.0701754f, 0.569201f, 0.881092f, 0.382066f, - 0.218324f, 0.717349f, 0.654971f, 0.155945f, 0.818713f, 0.319688f, 0.132554f, 0.631579f, - 0.0623782f, 0.561404f, 0.748538f, 0.249513f, 0.912281f, 0.413255f, 0.974659f, 0.475634f, - 0.810916f, 0.311891f, 0.499025f, 0.998051f, 0.163743f, 0.662768f, 0.226121f, 0.725146f, - 0.690058f, 0.191033f, 0.00389864f, 0.502924f, 0.557505f, 0.0584795f, 0.120858f, 0.619883f, - 0.440546f, 0.939571f, 0.752437f, 0.253411f, 0.307992f, 0.807018f, 0.869396f, 0.37037f, - 0.658869f, 0.159844f, 0.346979f, 0.846004f, 0.588694f, 0.0896686f, 0.152047f, 0.651072f, - 0.409357f, 0.908382f, 0.596491f, 0.0974659f, 0.339181f, 0.838207f, 0.900585f, 0.401559f, - 0.34308f, 0.842105f, 0.779727f, 0.280702f, 0.693957f, 0.194932f, 0.25731f, 0.756335f, - 0.592593f, 0.0935673f, 0.0311891f, 0.530214f, 0.444444f, 0.94347f, 0.506823f, 0.00779727f, - 0.68616f, 0.187135f, 0.124756f, 0.623782f, 0.288499f, 0.787524f, 0.350877f, 0.849903f, - 0.436647f, 0.935673f, 0.873294f, 0.374269f, 0.538012f, 0.0389864f, 0.60039f, 0.101365f, - 0.57115f, 0.0721248f, 0.758285f, 0.259259f, 0.719298f, 0.220273f, 0.532164f, 0.0331384f, - 0.321637f, 0.820663f, 0.00974659f, 0.508772f, 0.469786f, 0.968811f, 0.282651f, 0.781676f, - 0.539961f, 0.0409357f, 0.727096f, 0.22807f, 0.500975f, 0.00194932f, 0.563353f, 0.0643275f, - 0.290448f, 0.789474f, 0.477583f, 0.976608f, 0.251462f, 0.750487f, 0.31384f, 0.812865f, - 0.94152f, 0.442495f, 0.879142f, 0.380117f, 0.37232f, 0.871345f, 0.309942f, 0.808967f, - 0.192982f, 0.692008f, 0.130604f, 0.62963f, 0.621832f, 0.122807f, 0.559454f, 0.0604289f, - 0.660819f, 0.161793f, 0.723197f, 0.224172f, 0.403509f, 0.902534f, 0.840156f, 0.341131f, - 0.411306f, 0.910331f, 0.473684f, 0.97271f, 0.653021f, 0.153996f, 0.0916179f, 0.590643f, - 0.196881f, 0.695906f, 0.384016f, 0.883041f, 0.0955166f, 0.594542f, 0.157895f, 0.65692f, - 0.945419f, 0.446394f, 0.633528f, 0.134503f, 0.844055f, 0.345029f, 0.906433f, 0.407407f, - 0.165692f, 0.664717f, 0.103314f, 0.602339f, 0.126706f, 0.625731f, 0.189084f, 0.688109f, - 0.91423f, 0.415205f, 0.851852f, 0.352827f, 0.875244f, 0.376218f, 0.937622f, 0.438596f, - 0.317739f, 0.816764f, 0.255361f, 0.754386f, 0.996101f, 0.497076f, 0.933723f, 0.434698f, - 0.567251f, 0.0682261f, 0.504873f, 0.00584795f, 0.247563f, 0.746589f, 0.185185f, 0.684211f, - 0.037037f, 0.536062f, 0.0994152f, 0.598441f, 0.777778f, 0.278752f, 0.465887f, 0.964912f, - 0.785575f, 0.28655f, 0.847953f, 0.348928f, 0.0292398f, 0.528265f, 0.7154f, 0.216374f, - 0.39961f, 0.898636f, 0.961014f, 0.461988f, 0.0487329f, 0.547758f, 0.111111f, 0.610136f, - 0.649123f, 0.150097f, 0.212476f, 0.711501f, 0.797271f, 0.298246f, 0.859649f, 0.360624f, - 0.118908f, 0.617934f, 0.0565302f, 0.555556f, 0.329435f, 0.82846f, 0.516569f, 0.0175439f, - 0.867446f, 0.368421f, 0.805068f, 0.306043f, 0.578947f, 0.079922f, 0.267057f, 0.766082f, - 0.270955f, 0.76998f, 0.707602f, 0.208577f, 0.668616f, 0.169591f, 0.606238f, 0.107212f, - 0.520468f, 0.0214425f, 0.45809f, 0.957115f, 0.419103f, 0.918129f, 0.356725f, 0.855751f, - 0.988304f, 0.489279f, 0.426901f, 0.925926f, 0.450292f, 0.949318f, 0.512671f, 0.0136452f, - 0.239766f, 0.738791f, 0.676413f, 0.177388f, 0.699805f, 0.20078f, 0.263158f, 0.762183f, - 0.773879f, 0.274854f, 0.337232f, 0.836257f, 0.672515f, 0.173489f, 0.734893f, 0.235867f, - 0.0253411f, 0.524366f, 0.586745f, 0.0877193f, 0.423002f, 0.922027f, 0.48538f, 0.984405f, - 0.74269f, 0.243665f, 0.680312f, 0.181287f, 0.953216f, 0.454191f, 0.1423f, 0.641326f, - 0.493177f, 0.992203f, 0.430799f, 0.929825f, 0.204678f, 0.703704f, 0.890838f, 0.391813f, - 0.894737f, 0.395712f, 0.0838207f, 0.582846f, 0.0448343f, 0.54386f, 0.231969f, 0.730994f, - 0.146199f, 0.645224f, 0.832359f, 0.333333f, 0.793372f, 0.294347f, 0.980507f, 0.481481f, - 0.364522f, 0.863548f, 0.80117f, 0.302144f, 0.824561f, 0.325536f, 0.138402f, 0.637427f, - 0.614035f, 0.11501f, 0.0526316f, 0.551657f, 0.0760234f, 0.575049f, 0.88694f, 0.387914f, -}; - -PNANOVDB_FORCE_INLINE float pnanovdb_dither_lookup(pnanovdb_bool_t enabled, int offset) -{ - return enabled ? pnanovdb_dither_lut[offset & 511] : 0.5f; -} - -// ------------------------------------------------ HDDA ----------------------------------------------------------- - -#ifdef PNANOVDB_HDDA - -// Comment out to disable this explicit round-off check -#define PNANOVDB_ENFORCE_FORWARD_STEPPING - -#define PNANOVDB_HDDA_FLOAT_MAX 1e38f - -struct pnanovdb_hdda_t -{ - pnanovdb_int32_t dim; - float tmin; - float tmax; - pnanovdb_coord_t voxel; - pnanovdb_coord_t step; - pnanovdb_vec3_t delta; - pnanovdb_vec3_t next; -}; -PNANOVDB_STRUCT_TYPEDEF(pnanovdb_hdda_t) - -PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_hdda_pos_to_ijk(PNANOVDB_IN(pnanovdb_vec3_t) pos) -{ - pnanovdb_coord_t voxel; - voxel.x = pnanovdb_float_to_int32(pnanovdb_floor(PNANOVDB_DEREF(pos).x)); - voxel.y = pnanovdb_float_to_int32(pnanovdb_floor(PNANOVDB_DEREF(pos).y)); - voxel.z = pnanovdb_float_to_int32(pnanovdb_floor(PNANOVDB_DEREF(pos).z)); - return voxel; -} - -PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_hdda_pos_to_voxel(PNANOVDB_IN(pnanovdb_vec3_t) pos, int dim) -{ - pnanovdb_coord_t voxel; - voxel.x = pnanovdb_float_to_int32(pnanovdb_floor(PNANOVDB_DEREF(pos).x)) & (~(dim - 1)); - voxel.y = pnanovdb_float_to_int32(pnanovdb_floor(PNANOVDB_DEREF(pos).y)) & (~(dim - 1)); - voxel.z = pnanovdb_float_to_int32(pnanovdb_floor(PNANOVDB_DEREF(pos).z)) & (~(dim - 1)); - return voxel; -} - -PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_hdda_ray_start(PNANOVDB_IN(pnanovdb_vec3_t) origin, float tmin, PNANOVDB_IN(pnanovdb_vec3_t) direction) -{ - pnanovdb_vec3_t pos = pnanovdb_vec3_add( - pnanovdb_vec3_mul(PNANOVDB_DEREF(direction), pnanovdb_vec3_uniform(tmin)), - PNANOVDB_DEREF(origin) - ); - return pos; -} - -PNANOVDB_FORCE_INLINE void pnanovdb_hdda_init(PNANOVDB_INOUT(pnanovdb_hdda_t) hdda, PNANOVDB_IN(pnanovdb_vec3_t) origin, float tmin, PNANOVDB_IN(pnanovdb_vec3_t) direction, float tmax, int dim) -{ - PNANOVDB_DEREF(hdda).dim = dim; - PNANOVDB_DEREF(hdda).tmin = tmin; - PNANOVDB_DEREF(hdda).tmax = tmax; - - pnanovdb_vec3_t pos = pnanovdb_hdda_ray_start(origin, tmin, direction); - pnanovdb_vec3_t dir_inv = pnanovdb_vec3_div(pnanovdb_vec3_uniform(1.f), PNANOVDB_DEREF(direction)); - - PNANOVDB_DEREF(hdda).voxel = pnanovdb_hdda_pos_to_voxel(PNANOVDB_REF(pos), dim); - - // x - if (PNANOVDB_DEREF(direction).x == 0.f) - { - PNANOVDB_DEREF(hdda).next.x = PNANOVDB_HDDA_FLOAT_MAX; - PNANOVDB_DEREF(hdda).step.x = 0; - PNANOVDB_DEREF(hdda).delta.x = 0.f; - } - else if (dir_inv.x > 0.f) - { - PNANOVDB_DEREF(hdda).step.x = 1; - PNANOVDB_DEREF(hdda).next.x = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.x + dim - pos.x) * dir_inv.x; - PNANOVDB_DEREF(hdda).delta.x = dir_inv.x; - } - else - { - PNANOVDB_DEREF(hdda).step.x = -1; - PNANOVDB_DEREF(hdda).next.x = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.x - pos.x) * dir_inv.x; - PNANOVDB_DEREF(hdda).delta.x = -dir_inv.x; - } - - // y - if (PNANOVDB_DEREF(direction).y == 0.f) - { - PNANOVDB_DEREF(hdda).next.y = PNANOVDB_HDDA_FLOAT_MAX; - PNANOVDB_DEREF(hdda).step.y = 0; - PNANOVDB_DEREF(hdda).delta.y = 0.f; - } - else if (dir_inv.y > 0.f) - { - PNANOVDB_DEREF(hdda).step.y = 1; - PNANOVDB_DEREF(hdda).next.y = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.y + dim - pos.y) * dir_inv.y; - PNANOVDB_DEREF(hdda).delta.y = dir_inv.y; - } - else - { - PNANOVDB_DEREF(hdda).step.y = -1; - PNANOVDB_DEREF(hdda).next.y = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.y - pos.y) * dir_inv.y; - PNANOVDB_DEREF(hdda).delta.y = -dir_inv.y; - } - - // z - if (PNANOVDB_DEREF(direction).z == 0.f) - { - PNANOVDB_DEREF(hdda).next.z = PNANOVDB_HDDA_FLOAT_MAX; - PNANOVDB_DEREF(hdda).step.z = 0; - PNANOVDB_DEREF(hdda).delta.z = 0.f; - } - else if (dir_inv.z > 0.f) - { - PNANOVDB_DEREF(hdda).step.z = 1; - PNANOVDB_DEREF(hdda).next.z = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.z + dim - pos.z) * dir_inv.z; - PNANOVDB_DEREF(hdda).delta.z = dir_inv.z; - } - else - { - PNANOVDB_DEREF(hdda).step.z = -1; - PNANOVDB_DEREF(hdda).next.z = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.z - pos.z) * dir_inv.z; - PNANOVDB_DEREF(hdda).delta.z = -dir_inv.z; - } -} - -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_hdda_update(PNANOVDB_INOUT(pnanovdb_hdda_t) hdda, PNANOVDB_IN(pnanovdb_vec3_t) origin, PNANOVDB_IN(pnanovdb_vec3_t) direction, int dim) -{ - if (PNANOVDB_DEREF(hdda).dim == dim) - { - return PNANOVDB_FALSE; - } - PNANOVDB_DEREF(hdda).dim = dim; - - pnanovdb_vec3_t pos = pnanovdb_vec3_add( - pnanovdb_vec3_mul(PNANOVDB_DEREF(direction), pnanovdb_vec3_uniform(PNANOVDB_DEREF(hdda).tmin)), - PNANOVDB_DEREF(origin) - ); - pnanovdb_vec3_t dir_inv = pnanovdb_vec3_div(pnanovdb_vec3_uniform(1.f), PNANOVDB_DEREF(direction)); - - PNANOVDB_DEREF(hdda).voxel = pnanovdb_hdda_pos_to_voxel(PNANOVDB_REF(pos), dim); - - if (PNANOVDB_DEREF(hdda).step.x != 0) - { - PNANOVDB_DEREF(hdda).next.x = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.x - pos.x) * dir_inv.x; - if (PNANOVDB_DEREF(hdda).step.x > 0) - { - PNANOVDB_DEREF(hdda).next.x += dim * dir_inv.x; - } - } - if (PNANOVDB_DEREF(hdda).step.y != 0) - { - PNANOVDB_DEREF(hdda).next.y = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.y - pos.y) * dir_inv.y; - if (PNANOVDB_DEREF(hdda).step.y > 0) - { - PNANOVDB_DEREF(hdda).next.y += dim * dir_inv.y; - } - } - if (PNANOVDB_DEREF(hdda).step.z != 0) - { - PNANOVDB_DEREF(hdda).next.z = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.z - pos.z) * dir_inv.z; - if (PNANOVDB_DEREF(hdda).step.z > 0) - { - PNANOVDB_DEREF(hdda).next.z += dim * dir_inv.z; - } - } - - return PNANOVDB_TRUE; -} - -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_hdda_step(PNANOVDB_INOUT(pnanovdb_hdda_t) hdda) -{ - pnanovdb_bool_t ret; - if (PNANOVDB_DEREF(hdda).next.x < PNANOVDB_DEREF(hdda).next.y && PNANOVDB_DEREF(hdda).next.x < PNANOVDB_DEREF(hdda).next.z) - { -#ifdef PNANOVDB_ENFORCE_FORWARD_STEPPING - if (PNANOVDB_DEREF(hdda).next.x <= PNANOVDB_DEREF(hdda).tmin) - { - PNANOVDB_DEREF(hdda).next.x += PNANOVDB_DEREF(hdda).tmin - 0.999999f * PNANOVDB_DEREF(hdda).next.x + 1.0e-6f; - } -#endif - PNANOVDB_DEREF(hdda).tmin = PNANOVDB_DEREF(hdda).next.x; - PNANOVDB_DEREF(hdda).next.x += PNANOVDB_DEREF(hdda).dim * PNANOVDB_DEREF(hdda).delta.x; - PNANOVDB_DEREF(hdda).voxel.x += PNANOVDB_DEREF(hdda).dim * PNANOVDB_DEREF(hdda).step.x; - ret = PNANOVDB_DEREF(hdda).tmin <= PNANOVDB_DEREF(hdda).tmax; - } - else if (PNANOVDB_DEREF(hdda).next.y < PNANOVDB_DEREF(hdda).next.z) - { -#ifdef PNANOVDB_ENFORCE_FORWARD_STEPPING - if (PNANOVDB_DEREF(hdda).next.y <= PNANOVDB_DEREF(hdda).tmin) - { - PNANOVDB_DEREF(hdda).next.y += PNANOVDB_DEREF(hdda).tmin - 0.999999f * PNANOVDB_DEREF(hdda).next.y + 1.0e-6f; - } -#endif - PNANOVDB_DEREF(hdda).tmin = PNANOVDB_DEREF(hdda).next.y; - PNANOVDB_DEREF(hdda).next.y += PNANOVDB_DEREF(hdda).dim * PNANOVDB_DEREF(hdda).delta.y; - PNANOVDB_DEREF(hdda).voxel.y += PNANOVDB_DEREF(hdda).dim * PNANOVDB_DEREF(hdda).step.y; - ret = PNANOVDB_DEREF(hdda).tmin <= PNANOVDB_DEREF(hdda).tmax; - } - else - { -#ifdef PNANOVDB_ENFORCE_FORWARD_STEPPING - if (PNANOVDB_DEREF(hdda).next.z <= PNANOVDB_DEREF(hdda).tmin) - { - PNANOVDB_DEREF(hdda).next.z += PNANOVDB_DEREF(hdda).tmin - 0.999999f * PNANOVDB_DEREF(hdda).next.z + 1.0e-6f; - } -#endif - PNANOVDB_DEREF(hdda).tmin = PNANOVDB_DEREF(hdda).next.z; - PNANOVDB_DEREF(hdda).next.z += PNANOVDB_DEREF(hdda).dim * PNANOVDB_DEREF(hdda).delta.z; - PNANOVDB_DEREF(hdda).voxel.z += PNANOVDB_DEREF(hdda).dim * PNANOVDB_DEREF(hdda).step.z; - ret = PNANOVDB_DEREF(hdda).tmin <= PNANOVDB_DEREF(hdda).tmax; - } - return ret; -} - -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_hdda_ray_clip( - PNANOVDB_IN(pnanovdb_vec3_t) bbox_min, - PNANOVDB_IN(pnanovdb_vec3_t) bbox_max, - PNANOVDB_IN(pnanovdb_vec3_t) origin, PNANOVDB_INOUT(float) tmin, - PNANOVDB_IN(pnanovdb_vec3_t) direction, PNANOVDB_INOUT(float) tmax -) -{ - pnanovdb_vec3_t dir_inv = pnanovdb_vec3_div(pnanovdb_vec3_uniform(1.f), PNANOVDB_DEREF(direction)); - pnanovdb_vec3_t t0 = pnanovdb_vec3_mul(pnanovdb_vec3_sub(PNANOVDB_DEREF(bbox_min), PNANOVDB_DEREF(origin)), dir_inv); - pnanovdb_vec3_t t1 = pnanovdb_vec3_mul(pnanovdb_vec3_sub(PNANOVDB_DEREF(bbox_max), PNANOVDB_DEREF(origin)), dir_inv); - pnanovdb_vec3_t tmin3 = pnanovdb_vec3_min(t0, t1); - pnanovdb_vec3_t tmax3 = pnanovdb_vec3_max(t0, t1); - float tnear = pnanovdb_max(tmin3.x, pnanovdb_max(tmin3.y, tmin3.z)); - float tfar = pnanovdb_min(tmax3.x, pnanovdb_min(tmax3.y, tmax3.z)); - pnanovdb_bool_t hit = tnear <= tfar; - PNANOVDB_DEREF(tmin) = pnanovdb_max(PNANOVDB_DEREF(tmin), tnear); - PNANOVDB_DEREF(tmax) = pnanovdb_min(PNANOVDB_DEREF(tmax), tfar); - return hit; -} - -PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_hdda_zero_crossing( - pnanovdb_grid_type_t grid_type, - pnanovdb_buf_t buf, - PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, - PNANOVDB_IN(pnanovdb_vec3_t) origin, float tmin, - PNANOVDB_IN(pnanovdb_vec3_t) direction, float tmax, - PNANOVDB_INOUT(float) thit, - PNANOVDB_INOUT(float) v -) -{ - pnanovdb_coord_t bbox_min = pnanovdb_root_get_bbox_min(buf, PNANOVDB_DEREF(acc).root); - pnanovdb_coord_t bbox_max = pnanovdb_root_get_bbox_max(buf, PNANOVDB_DEREF(acc).root); - pnanovdb_vec3_t bbox_minf = pnanovdb_coord_to_vec3(bbox_min); - pnanovdb_vec3_t bbox_maxf = pnanovdb_coord_to_vec3(pnanovdb_coord_add(bbox_max, pnanovdb_coord_uniform(1))); - - pnanovdb_bool_t hit = pnanovdb_hdda_ray_clip(PNANOVDB_REF(bbox_minf), PNANOVDB_REF(bbox_maxf), origin, PNANOVDB_REF(tmin), direction, PNANOVDB_REF(tmax)); - if (!hit || tmax > 1.0e20f) - { - return PNANOVDB_FALSE; - } - - pnanovdb_vec3_t pos = pnanovdb_hdda_ray_start(origin, tmin, direction); - pnanovdb_coord_t ijk = pnanovdb_hdda_pos_to_ijk(PNANOVDB_REF(pos)); - - pnanovdb_address_t address = pnanovdb_readaccessor_get_value_address(PNANOVDB_GRID_TYPE_FLOAT, buf, acc, PNANOVDB_REF(ijk)); - float v0 = pnanovdb_read_float(buf, address); - - pnanovdb_int32_t dim = pnanovdb_uint32_as_int32(pnanovdb_readaccessor_get_dim(PNANOVDB_GRID_TYPE_FLOAT, buf, acc, PNANOVDB_REF(ijk))); - pnanovdb_hdda_t hdda; - pnanovdb_hdda_init(PNANOVDB_REF(hdda), origin, tmin, direction, tmax, dim); - while (pnanovdb_hdda_step(PNANOVDB_REF(hdda))) - { - pnanovdb_vec3_t pos_start = pnanovdb_hdda_ray_start(origin, hdda.tmin + 1.0001f, direction); - ijk = pnanovdb_hdda_pos_to_ijk(PNANOVDB_REF(pos_start)); - dim = pnanovdb_uint32_as_int32(pnanovdb_readaccessor_get_dim(PNANOVDB_GRID_TYPE_FLOAT, buf, acc, PNANOVDB_REF(ijk))); - pnanovdb_hdda_update(PNANOVDB_REF(hdda), origin, direction, dim); - if (hdda.dim > 1 || !pnanovdb_readaccessor_is_active(grid_type, buf, acc, PNANOVDB_REF(ijk))) - { - continue; - } - while (pnanovdb_hdda_step(PNANOVDB_REF(hdda)) && pnanovdb_readaccessor_is_active(grid_type, buf, acc, PNANOVDB_REF(hdda.voxel))) - { - ijk = hdda.voxel; - pnanovdb_address_t address = pnanovdb_readaccessor_get_value_address(PNANOVDB_GRID_TYPE_FLOAT, buf, acc, PNANOVDB_REF(ijk)); - PNANOVDB_DEREF(v) = pnanovdb_read_float(buf, address); - if (PNANOVDB_DEREF(v) * v0 < 0.f) - { - PNANOVDB_DEREF(thit) = hdda.tmin; - return PNANOVDB_TRUE; - } - } - } - return PNANOVDB_FALSE; -} - -#endif - -#endif // end of NANOVDB_PNANOVDB_H_HAS_BEEN_INCLUDED + +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +/*! + \file PNanoVDB.h + + \author Andrew Reidmeyer + + \brief This file is a portable (e.g. pointer-less) C99/GLSL/HLSL port + of NanoVDB.h, which is compatible with most graphics APIs. +*/ + +#ifndef NANOVDB_PNANOVDB_H_HAS_BEEN_INCLUDED +#define NANOVDB_PNANOVDB_H_HAS_BEEN_INCLUDED + +// ------------------------------------------------ Configuration ----------------------------------------------------------- + +// platforms +//#define PNANOVDB_C +//#define PNANOVDB_HLSL +//#define PNANOVDB_GLSL + +// addressing mode +// PNANOVDB_ADDRESS_32 +// PNANOVDB_ADDRESS_64 +#if defined(PNANOVDB_C) +#ifndef PNANOVDB_ADDRESS_32 +#define PNANOVDB_ADDRESS_64 +#endif +#elif defined(PNANOVDB_HLSL) +#ifndef PNANOVDB_ADDRESS_64 +#define PNANOVDB_ADDRESS_32 +#endif +#elif defined(PNANOVDB_GLSL) +#ifndef PNANOVDB_ADDRESS_64 +#define PNANOVDB_ADDRESS_32 +#endif +#endif + +// bounds checking +//#define PNANOVDB_BUF_BOUNDS_CHECK + +// enable HDDA by default on HLSL/GLSL, make explicit on C +#if defined(PNANOVDB_C) +//#define PNANOVDB_HDDA +#ifdef PNANOVDB_HDDA +#ifndef PNANOVDB_CMATH +#define PNANOVDB_CMATH +#endif +#endif +#elif defined(PNANOVDB_HLSL) +#define PNANOVDB_HDDA +#elif defined(PNANOVDB_GLSL) +#define PNANOVDB_HDDA +#endif + +#ifdef PNANOVDB_CMATH +#ifndef __CUDACC_RTC__ +#include +#endif +#endif + +// ------------------------------------------------ Buffer ----------------------------------------------------------- + +#if defined(PNANOVDB_BUF_CUSTOM) +// NOP +#elif defined(PNANOVDB_C) +#define PNANOVDB_BUF_C +#elif defined(PNANOVDB_HLSL) +#define PNANOVDB_BUF_HLSL +#elif defined(PNANOVDB_GLSL) +#define PNANOVDB_BUF_GLSL +#endif + +#if defined(PNANOVDB_BUF_C) +#ifndef __CUDACC_RTC__ +#include +#endif +#if defined(__CUDACC__) +#define PNANOVDB_BUF_FORCE_INLINE static __host__ __device__ __forceinline__ +#elif defined(_WIN32) +#define PNANOVDB_BUF_FORCE_INLINE static inline __forceinline +#else +#define PNANOVDB_BUF_FORCE_INLINE static inline __attribute__((always_inline)) +#endif +typedef struct pnanovdb_buf_t +{ + uint32_t* data; +#ifdef PNANOVDB_BUF_BOUNDS_CHECK + uint64_t size_in_words; +#endif +}pnanovdb_buf_t; +PNANOVDB_BUF_FORCE_INLINE pnanovdb_buf_t pnanovdb_make_buf(uint32_t* data, uint64_t size_in_words) +{ + pnanovdb_buf_t ret; + ret.data = data; +#ifdef PNANOVDB_BUF_BOUNDS_CHECK + ret.size_in_words = size_in_words; +#endif + return ret; +} +#if defined(PNANOVDB_ADDRESS_32) +PNANOVDB_BUF_FORCE_INLINE uint32_t pnanovdb_buf_read_uint32(pnanovdb_buf_t buf, uint32_t byte_offset) +{ + uint32_t wordaddress = (byte_offset >> 2u); +#ifdef PNANOVDB_BUF_BOUNDS_CHECK + return wordaddress < buf.size_in_words ? buf.data[wordaddress] : 0u; +#else + return buf.data[wordaddress]; +#endif +} +PNANOVDB_BUF_FORCE_INLINE uint64_t pnanovdb_buf_read_uint64(pnanovdb_buf_t buf, uint32_t byte_offset) +{ + uint64_t* data64 = (uint64_t*)buf.data; + uint32_t wordaddress64 = (byte_offset >> 3u); +#ifdef PNANOVDB_BUF_BOUNDS_CHECK + uint64_t size_in_words64 = buf.size_in_words >> 1u; + return wordaddress64 < size_in_words64 ? data64[wordaddress64] : 0llu; +#else + return data64[wordaddress64]; +#endif +} +PNANOVDB_BUF_FORCE_INLINE void pnanovdb_buf_write_uint32(pnanovdb_buf_t buf, uint32_t byte_offset, uint32_t value) +{ + uint32_t wordaddress = (byte_offset >> 2u); +#ifdef PNANOVDB_BUF_BOUNDS_CHECK + if (wordaddress < buf.size_in_words) + { + buf.data[wordaddress] = value; +} +#else + buf.data[wordaddress] = value; +#endif +} +PNANOVDB_BUF_FORCE_INLINE void pnanovdb_buf_write_uint64(pnanovdb_buf_t buf, uint32_t byte_offset, uint64_t value) +{ + uint64_t* data64 = (uint64_t*)buf.data; + uint32_t wordaddress64 = (byte_offset >> 3u); +#ifdef PNANOVDB_BUF_BOUNDS_CHECK + uint64_t size_in_words64 = buf.size_in_words >> 1u; + if (wordaddress64 < size_in_words64) + { + data64[wordaddress64] = value; + } +#else + data64[wordaddress64] = value; +#endif +} +#elif defined(PNANOVDB_ADDRESS_64) +PNANOVDB_BUF_FORCE_INLINE uint32_t pnanovdb_buf_read_uint32(pnanovdb_buf_t buf, uint64_t byte_offset) +{ + uint64_t wordaddress = (byte_offset >> 2u); +#ifdef PNANOVDB_BUF_BOUNDS_CHECK + return wordaddress < buf.size_in_words ? buf.data[wordaddress] : 0u; +#else + return buf.data[wordaddress]; +#endif +} +PNANOVDB_BUF_FORCE_INLINE uint64_t pnanovdb_buf_read_uint64(pnanovdb_buf_t buf, uint64_t byte_offset) +{ + uint64_t* data64 = (uint64_t*)buf.data; + uint64_t wordaddress64 = (byte_offset >> 3u); +#ifdef PNANOVDB_BUF_BOUNDS_CHECK + uint64_t size_in_words64 = buf.size_in_words >> 1u; + return wordaddress64 < size_in_words64 ? data64[wordaddress64] : 0llu; +#else + return data64[wordaddress64]; +#endif +} +PNANOVDB_BUF_FORCE_INLINE void pnanovdb_buf_write_uint32(pnanovdb_buf_t buf, uint64_t byte_offset, uint32_t value) +{ + uint64_t wordaddress = (byte_offset >> 2u); +#ifdef PNANOVDB_BUF_BOUNDS_CHECK + if (wordaddress < buf.size_in_words) + { + buf.data[wordaddress] = value; + } +#else + buf.data[wordaddress] = value; +#endif +} +PNANOVDB_BUF_FORCE_INLINE void pnanovdb_buf_write_uint64(pnanovdb_buf_t buf, uint64_t byte_offset, uint64_t value) +{ + uint64_t* data64 = (uint64_t*)buf.data; + uint64_t wordaddress64 = (byte_offset >> 3u); +#ifdef PNANOVDB_BUF_BOUNDS_CHECK + uint64_t size_in_words64 = buf.size_in_words >> 1u; + if (wordaddress64 < size_in_words64) + { + data64[wordaddress64] = value; + } +#else + data64[wordaddress64] = value; +#endif +} +#endif +typedef uint32_t pnanovdb_grid_type_t; +#define PNANOVDB_GRID_TYPE_GET(grid_typeIn, nameIn) pnanovdb_grid_type_constants[grid_typeIn].nameIn +#elif defined(PNANOVDB_BUF_HLSL) +#if defined(PNANOVDB_ADDRESS_32) +#define pnanovdb_buf_t StructuredBuffer +uint pnanovdb_buf_read_uint32(pnanovdb_buf_t buf, uint byte_offset) +{ + return buf[(byte_offset >> 2u)]; +} +uint2 pnanovdb_buf_read_uint64(pnanovdb_buf_t buf, uint byte_offset) +{ + uint2 ret; + ret.x = pnanovdb_buf_read_uint32(buf, byte_offset + 0u); + ret.y = pnanovdb_buf_read_uint32(buf, byte_offset + 4u); + return ret; +} +void pnanovdb_buf_write_uint32(pnanovdb_buf_t buf, uint byte_offset, uint value) +{ + // NOP, by default no write in HLSL +} +void pnanovdb_buf_write_uint64(pnanovdb_buf_t buf, uint byte_offset, uint2 value) +{ + // NOP, by default no write in HLSL +} +#elif defined(PNANOVDB_ADDRESS_64) +#define pnanovdb_buf_t StructuredBuffer +uint pnanovdb_buf_read_uint32(pnanovdb_buf_t buf, uint64_t byte_offset) +{ + return buf[uint(byte_offset >> 2u)]; +} +uint64_t pnanovdb_buf_read_uint64(pnanovdb_buf_t buf, uint64_t byte_offset) +{ + uint64_t ret; + ret = pnanovdb_buf_read_uint32(buf, byte_offset + 0u); + ret = ret + (uint64_t(pnanovdb_buf_read_uint32(buf, byte_offset + 4u)) << 32u); + return ret; +} +void pnanovdb_buf_write_uint32(pnanovdb_buf_t buf, uint64_t byte_offset, uint value) +{ + // NOP, by default no write in HLSL +} +void pnanovdb_buf_write_uint64(pnanovdb_buf_t buf, uint64_t byte_offset, uint64_t value) +{ + // NOP, by default no write in HLSL +} +#endif +#define pnanovdb_grid_type_t uint +#define PNANOVDB_GRID_TYPE_GET(grid_typeIn, nameIn) pnanovdb_grid_type_constants[grid_typeIn].nameIn +#elif defined(PNANOVDB_BUF_GLSL) +struct pnanovdb_buf_t +{ + uint unused; // to satisfy min struct size? +}; +uint pnanovdb_buf_read_uint32(pnanovdb_buf_t buf, uint byte_offset) +{ + return pnanovdb_buf_data[(byte_offset >> 2u)]; +} +uvec2 pnanovdb_buf_read_uint64(pnanovdb_buf_t buf, uint byte_offset) +{ + uvec2 ret; + ret.x = pnanovdb_buf_read_uint32(buf, byte_offset + 0u); + ret.y = pnanovdb_buf_read_uint32(buf, byte_offset + 4u); + return ret; +} +void pnanovdb_buf_write_uint32(pnanovdb_buf_t buf, uint byte_offset, uint value) +{ + // NOP, by default no write in HLSL +} +void pnanovdb_buf_write_uint64(pnanovdb_buf_t buf, uint byte_offset, uvec2 value) +{ + // NOP, by default no write in HLSL +} +#define pnanovdb_grid_type_t uint +#define PNANOVDB_GRID_TYPE_GET(grid_typeIn, nameIn) pnanovdb_grid_type_constants[grid_typeIn].nameIn +#endif + +// ------------------------------------------------ Basic Types ----------------------------------------------------------- + +// force inline +#if defined(PNANOVDB_C) +#if defined(__CUDACC__) +#define PNANOVDB_FORCE_INLINE static __host__ __device__ __forceinline__ +#elif defined(_WIN32) +#define PNANOVDB_FORCE_INLINE static inline __forceinline +#else +#define PNANOVDB_FORCE_INLINE static inline __attribute__((always_inline)) +#endif +#elif defined(PNANOVDB_HLSL) +#define PNANOVDB_FORCE_INLINE +#elif defined(PNANOVDB_GLSL) +#define PNANOVDB_FORCE_INLINE +#endif + +// struct typedef, static const, inout +#if defined(PNANOVDB_C) +#define PNANOVDB_STRUCT_TYPEDEF(X) typedef struct X X; +#define PNANOVDB_STATIC_CONST static const +#define PNANOVDB_INOUT(X) X* +#define PNANOVDB_IN(X) const X* +#define PNANOVDB_DEREF(X) (*X) +#define PNANOVDB_REF(X) &X +#elif defined(PNANOVDB_HLSL) +#define PNANOVDB_STRUCT_TYPEDEF(X) +#define PNANOVDB_STATIC_CONST static const +#define PNANOVDB_INOUT(X) inout X +#define PNANOVDB_IN(X) X +#define PNANOVDB_DEREF(X) X +#define PNANOVDB_REF(X) X +#elif defined(PNANOVDB_GLSL) +#define PNANOVDB_STRUCT_TYPEDEF(X) +#define PNANOVDB_STATIC_CONST const +#define PNANOVDB_INOUT(X) inout X +#define PNANOVDB_IN(X) X +#define PNANOVDB_DEREF(X) X +#define PNANOVDB_REF(X) X +#endif + +// basic types, type conversion +#if defined(PNANOVDB_C) +#define PNANOVDB_NATIVE_64 +#ifndef __CUDACC_RTC__ +#include +#endif +#if !defined(PNANOVDB_MEMCPY_CUSTOM) +#ifndef __CUDACC_RTC__ +#include +#endif +#define pnanovdb_memcpy memcpy +#endif +typedef uint32_t pnanovdb_uint32_t; +typedef int32_t pnanovdb_int32_t; +typedef int32_t pnanovdb_bool_t; +#define PNANOVDB_FALSE 0 +#define PNANOVDB_TRUE 1 +typedef uint64_t pnanovdb_uint64_t; +typedef int64_t pnanovdb_int64_t; +typedef struct pnanovdb_coord_t +{ + pnanovdb_int32_t x, y, z; +}pnanovdb_coord_t; +typedef struct pnanovdb_vec3_t +{ + float x, y, z; +}pnanovdb_vec3_t; +PNANOVDB_FORCE_INLINE pnanovdb_int32_t pnanovdb_uint32_as_int32(pnanovdb_uint32_t v) { return (pnanovdb_int32_t)v; } +PNANOVDB_FORCE_INLINE pnanovdb_int64_t pnanovdb_uint64_as_int64(pnanovdb_uint64_t v) { return (pnanovdb_int64_t)v; } +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_int64_as_uint64(pnanovdb_int64_t v) { return (pnanovdb_uint64_t)v; } +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_int32_as_uint32(pnanovdb_int32_t v) { return (pnanovdb_uint32_t)v; } +PNANOVDB_FORCE_INLINE float pnanovdb_uint32_as_float(pnanovdb_uint32_t v) { float vf; pnanovdb_memcpy(&vf, &v, sizeof(vf)); return vf; } +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_float_as_uint32(float v) { return *((pnanovdb_uint32_t*)(&v)); } +PNANOVDB_FORCE_INLINE double pnanovdb_uint64_as_double(pnanovdb_uint64_t v) { double vf; pnanovdb_memcpy(&vf, &v, sizeof(vf)); return vf; } +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_double_as_uint64(double v) { return *((pnanovdb_uint64_t*)(&v)); } +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_uint64_low(pnanovdb_uint64_t v) { return (pnanovdb_uint32_t)v; } +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_uint64_high(pnanovdb_uint64_t v) { return (pnanovdb_uint32_t)(v >> 32u); } +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_uint32_as_uint64(pnanovdb_uint32_t x, pnanovdb_uint32_t y) { return ((pnanovdb_uint64_t)x) | (((pnanovdb_uint64_t)y) << 32u); } +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_uint32_as_uint64_low(pnanovdb_uint32_t x) { return ((pnanovdb_uint64_t)x); } +PNANOVDB_FORCE_INLINE pnanovdb_int32_t pnanovdb_uint64_is_equal(pnanovdb_uint64_t a, pnanovdb_uint64_t b) { return a == b; } +PNANOVDB_FORCE_INLINE pnanovdb_int32_t pnanovdb_int64_is_zero(pnanovdb_int64_t a) { return a == 0; } +#ifdef PNANOVDB_CMATH +PNANOVDB_FORCE_INLINE float pnanovdb_floor(float v) { return floorf(v); } +#endif +PNANOVDB_FORCE_INLINE pnanovdb_int32_t pnanovdb_float_to_int32(float v) { return (pnanovdb_int32_t)v; } +PNANOVDB_FORCE_INLINE float pnanovdb_int32_to_float(pnanovdb_int32_t v) { return (float)v; } +PNANOVDB_FORCE_INLINE float pnanovdb_uint32_to_float(pnanovdb_uint32_t v) { return (float)v; } +PNANOVDB_FORCE_INLINE float pnanovdb_min(float a, float b) { return a < b ? a : b; } +PNANOVDB_FORCE_INLINE float pnanovdb_max(float a, float b) { return a > b ? a : b; } +#elif defined(PNANOVDB_HLSL) +typedef uint pnanovdb_uint32_t; +typedef int pnanovdb_int32_t; +typedef bool pnanovdb_bool_t; +#define PNANOVDB_FALSE false +#define PNANOVDB_TRUE true +typedef int3 pnanovdb_coord_t; +typedef float3 pnanovdb_vec3_t; +pnanovdb_int32_t pnanovdb_uint32_as_int32(pnanovdb_uint32_t v) { return int(v); } +pnanovdb_uint32_t pnanovdb_int32_as_uint32(pnanovdb_int32_t v) { return uint(v); } +float pnanovdb_uint32_as_float(pnanovdb_uint32_t v) { return asfloat(v); } +pnanovdb_uint32_t pnanovdb_float_as_uint32(float v) { return asuint(v); } +float pnanovdb_floor(float v) { return floor(v); } +pnanovdb_int32_t pnanovdb_float_to_int32(float v) { return int(v); } +float pnanovdb_int32_to_float(pnanovdb_int32_t v) { return float(v); } +float pnanovdb_uint32_to_float(pnanovdb_uint32_t v) { return float(v); } +float pnanovdb_min(float a, float b) { return min(a, b); } +float pnanovdb_max(float a, float b) { return max(a, b); } +#if defined(PNANOVDB_ADDRESS_32) +typedef uint2 pnanovdb_uint64_t; +typedef int2 pnanovdb_int64_t; +pnanovdb_int64_t pnanovdb_uint64_as_int64(pnanovdb_uint64_t v) { return int2(v); } +pnanovdb_uint64_t pnanovdb_int64_as_uint64(pnanovdb_int64_t v) { return uint2(v); } +double pnanovdb_uint64_as_double(pnanovdb_uint64_t v) { return asdouble(v.x, v.y); } +pnanovdb_uint64_t pnanovdb_double_as_uint64(double v) { uint2 ret; asuint(v, ret.x, ret.y); return ret; } +pnanovdb_uint32_t pnanovdb_uint64_low(pnanovdb_uint64_t v) { return v.x; } +pnanovdb_uint32_t pnanovdb_uint64_high(pnanovdb_uint64_t v) { return v.y; } +pnanovdb_uint64_t pnanovdb_uint32_as_uint64(pnanovdb_uint32_t x, pnanovdb_uint32_t y) { return uint2(x, y); } +pnanovdb_uint64_t pnanovdb_uint32_as_uint64_low(pnanovdb_uint32_t x) { return uint2(x, 0); } +bool pnanovdb_uint64_is_equal(pnanovdb_uint64_t a, pnanovdb_uint64_t b) { return (a.x == b.x) && (a.y == b.y); } +bool pnanovdb_int64_is_zero(pnanovdb_int64_t a) { return a.x == 0 && a.y == 0; } +#else +typedef uint64_t pnanovdb_uint64_t; +typedef int64_t pnanovdb_int64_t; +pnanovdb_int64_t pnanovdb_uint64_as_int64(pnanovdb_uint64_t v) { return int64_t(v); } +pnanovdb_uint64_t pnanovdb_int64_as_uint64(pnanovdb_int64_t v) { return uint64_t(v); } +double pnanovdb_uint64_as_double(pnanovdb_uint64_t v) { return asdouble(uint(v), uint(v >> 32u)); } +pnanovdb_uint64_t pnanovdb_double_as_uint64(double v) { uint2 ret; asuint(v, ret.x, ret.y); return uint64_t(ret.x) + (uint64_t(ret.y) << 32u); } +pnanovdb_uint32_t pnanovdb_uint64_low(pnanovdb_uint64_t v) { return uint(v); } +pnanovdb_uint32_t pnanovdb_uint64_high(pnanovdb_uint64_t v) { return uint(v >> 32u); } +pnanovdb_uint64_t pnanovdb_uint32_as_uint64(pnanovdb_uint32_t x, pnanovdb_uint32_t y) { return uint64_t(x) + (uint64_t(y) << 32u); } +pnanovdb_uint64_t pnanovdb_uint32_as_uint64_low(pnanovdb_uint32_t x) { return uint64_t(x); } +bool pnanovdb_uint64_is_equal(pnanovdb_uint64_t a, pnanovdb_uint64_t b) { return a == b; } +bool pnanovdb_int64_is_zero(pnanovdb_int64_t a) { return a == 0; } +#endif +#elif defined(PNANOVDB_GLSL) +#define pnanovdb_uint32_t uint +#define pnanovdb_int32_t int +#define pnanovdb_bool_t bool +#define PNANOVDB_FALSE false +#define PNANOVDB_TRUE true +#define pnanovdb_uint64_t uvec2 +#define pnanovdb_int64_t ivec2 +#define pnanovdb_coord_t ivec3 +#define pnanovdb_vec3_t vec3 +pnanovdb_int32_t pnanovdb_uint32_as_int32(pnanovdb_uint32_t v) { return int(v); } +pnanovdb_int64_t pnanovdb_uint64_as_int64(pnanovdb_uint64_t v) { return ivec2(v); } +pnanovdb_uint64_t pnanovdb_int64_as_uint64(pnanovdb_int64_t v) { return uvec2(v); } +pnanovdb_uint32_t pnanovdb_int32_as_uint32(pnanovdb_int32_t v) { return uint(v); } +float pnanovdb_uint32_as_float(pnanovdb_uint32_t v) { return uintBitsToFloat(v); } +pnanovdb_uint32_t pnanovdb_float_as_uint32(float v) { return floatBitsToUint(v); } +double pnanovdb_uint64_as_double(pnanovdb_uint64_t v) { return packDouble2x32(uvec2(v.x, v.y)); } +pnanovdb_uint64_t pnanovdb_double_as_uint64(double v) { return unpackDouble2x32(v); } +pnanovdb_uint32_t pnanovdb_uint64_low(pnanovdb_uint64_t v) { return v.x; } +pnanovdb_uint32_t pnanovdb_uint64_high(pnanovdb_uint64_t v) { return v.y; } +pnanovdb_uint64_t pnanovdb_uint32_as_uint64(pnanovdb_uint32_t x, pnanovdb_uint32_t y) { return uvec2(x, y); } +pnanovdb_uint64_t pnanovdb_uint32_as_uint64_low(pnanovdb_uint32_t x) { return uvec2(x, 0); } +bool pnanovdb_uint64_is_equal(pnanovdb_uint64_t a, pnanovdb_uint64_t b) { return (a.x == b.x) && (a.y == b.y); } +bool pnanovdb_int64_is_zero(pnanovdb_int64_t a) { return a.x == 0 && a.y == 0; } +float pnanovdb_floor(float v) { return floor(v); } +pnanovdb_int32_t pnanovdb_float_to_int32(float v) { return int(v); } +float pnanovdb_int32_to_float(pnanovdb_int32_t v) { return float(v); } +float pnanovdb_uint32_to_float(pnanovdb_uint32_t v) { return float(v); } +float pnanovdb_min(float a, float b) { return min(a, b); } +float pnanovdb_max(float a, float b) { return max(a, b); } +#endif + +// ------------------------------------------------ Coord/Vec3 Utilties ----------------------------------------------------------- + +#if defined(PNANOVDB_C) +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_vec3_uniform(float a) +{ + pnanovdb_vec3_t v; + v.x = a; + v.y = a; + v.z = a; + return v; +} +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_vec3_add(const pnanovdb_vec3_t a, const pnanovdb_vec3_t b) +{ + pnanovdb_vec3_t v; + v.x = a.x + b.x; + v.y = a.y + b.y; + v.z = a.z + b.z; + return v; +} +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_vec3_sub(const pnanovdb_vec3_t a, const pnanovdb_vec3_t b) +{ + pnanovdb_vec3_t v; + v.x = a.x - b.x; + v.y = a.y - b.y; + v.z = a.z - b.z; + return v; +} +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_vec3_mul(const pnanovdb_vec3_t a, const pnanovdb_vec3_t b) +{ + pnanovdb_vec3_t v; + v.x = a.x * b.x; + v.y = a.y * b.y; + v.z = a.z * b.z; + return v; +} +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_vec3_div(const pnanovdb_vec3_t a, const pnanovdb_vec3_t b) +{ + pnanovdb_vec3_t v; + v.x = a.x / b.x; + v.y = a.y / b.y; + v.z = a.z / b.z; + return v; +} +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_vec3_min(const pnanovdb_vec3_t a, const pnanovdb_vec3_t b) +{ + pnanovdb_vec3_t v; + v.x = a.x < b.x ? a.x : b.x; + v.y = a.y < b.y ? a.y : b.y; + v.z = a.z < b.z ? a.z : b.z; + return v; +} +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_vec3_max(const pnanovdb_vec3_t a, const pnanovdb_vec3_t b) +{ + pnanovdb_vec3_t v; + v.x = a.x > b.x ? a.x : b.x; + v.y = a.y > b.y ? a.y : b.y; + v.z = a.z > b.z ? a.z : b.z; + return v; +} +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_coord_to_vec3(const pnanovdb_coord_t coord) +{ + pnanovdb_vec3_t v; + v.x = pnanovdb_int32_to_float(coord.x); + v.y = pnanovdb_int32_to_float(coord.y); + v.z = pnanovdb_int32_to_float(coord.z); + return v; +} +PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_coord_uniform(const pnanovdb_int32_t a) +{ + pnanovdb_coord_t v; + v.x = a; + v.y = a; + v.z = a; + return v; +} +PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_coord_add(pnanovdb_coord_t a, pnanovdb_coord_t b) +{ + pnanovdb_coord_t v; + v.x = a.x + b.x; + v.y = a.y + b.y; + v.z = a.z + b.z; + return v; +} +#elif defined(PNANOVDB_HLSL) +pnanovdb_vec3_t pnanovdb_vec3_uniform(float a) { return float3(a, a, a); } +pnanovdb_vec3_t pnanovdb_vec3_add(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return a + b; } +pnanovdb_vec3_t pnanovdb_vec3_sub(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return a - b; } +pnanovdb_vec3_t pnanovdb_vec3_mul(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return a * b; } +pnanovdb_vec3_t pnanovdb_vec3_div(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return a / b; } +pnanovdb_vec3_t pnanovdb_vec3_min(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return min(a, b); } +pnanovdb_vec3_t pnanovdb_vec3_max(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return max(a, b); } +pnanovdb_vec3_t pnanovdb_coord_to_vec3(pnanovdb_coord_t coord) { return float3(coord); } +pnanovdb_coord_t pnanovdb_coord_uniform(pnanovdb_int32_t a) { return int3(a, a, a); } +pnanovdb_coord_t pnanovdb_coord_add(pnanovdb_coord_t a, pnanovdb_coord_t b) { return a + b; } +#elif defined(PNANOVDB_GLSL) +pnanovdb_vec3_t pnanovdb_vec3_uniform(float a) { return vec3(a, a, a); } +pnanovdb_vec3_t pnanovdb_vec3_add(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return a + b; } +pnanovdb_vec3_t pnanovdb_vec3_sub(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return a - b; } +pnanovdb_vec3_t pnanovdb_vec3_mul(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return a * b; } +pnanovdb_vec3_t pnanovdb_vec3_div(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return a / b; } +pnanovdb_vec3_t pnanovdb_vec3_min(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return min(a, b); } +pnanovdb_vec3_t pnanovdb_vec3_max(pnanovdb_vec3_t a, pnanovdb_vec3_t b) { return max(a, b); } +pnanovdb_vec3_t pnanovdb_coord_to_vec3(const pnanovdb_coord_t coord) { return vec3(coord); } +pnanovdb_coord_t pnanovdb_coord_uniform(pnanovdb_int32_t a) { return ivec3(a, a, a); } +pnanovdb_coord_t pnanovdb_coord_add(pnanovdb_coord_t a, pnanovdb_coord_t b) { return a + b; } +#endif + +// ------------------------------------------------ Uint64 Utils ----------------------------------------------------------- + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_uint32_countbits(pnanovdb_uint32_t value) +{ +#if defined(PNANOVDB_C) +#if defined(_MSC_VER) && (_MSC_VER >= 1928) && defined(PNANOVDB_USE_INTRINSICS) + return __popcnt(value); +#elif (defined(__GNUC__) || defined(__clang__)) && defined(PNANOVDB_USE_INTRINSICS) + return __builtin_popcount(value); +#else + value = value - ((value >> 1) & 0x55555555); + value = (value & 0x33333333) + ((value >> 2) & 0x33333333); + value = (value + (value >> 4)) & 0x0F0F0F0F; + return (value * 0x01010101) >> 24; +#endif +#elif defined(PNANOVDB_HLSL) + return countbits(value); +#elif defined(PNANOVDB_GLSL) + return bitCount(value); +#endif +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_uint64_countbits(pnanovdb_uint64_t value) +{ + return pnanovdb_uint32_countbits(pnanovdb_uint64_low(value)) + pnanovdb_uint32_countbits(pnanovdb_uint64_high(value)); +} + +#if defined(PNANOVDB_ADDRESS_32) +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_uint64_offset(pnanovdb_uint64_t a, pnanovdb_uint32_t b) +{ + pnanovdb_uint32_t low = pnanovdb_uint64_low(a); + pnanovdb_uint32_t high = pnanovdb_uint64_high(a); + low += b; + if (low < b) + { + high += 1u; + } + return pnanovdb_uint32_as_uint64(low, high); +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_uint64_dec(pnanovdb_uint64_t a) +{ + pnanovdb_uint32_t low = pnanovdb_uint64_low(a); + pnanovdb_uint32_t high = pnanovdb_uint64_high(a); + if (low == 0u) + { + high -= 1u; + } + low -= 1u; + return pnanovdb_uint32_as_uint64(low, high); +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_uint64_to_uint32_lsr(pnanovdb_uint64_t a, pnanovdb_uint32_t b) +{ + pnanovdb_uint32_t low = pnanovdb_uint64_low(a); + pnanovdb_uint32_t high = pnanovdb_uint64_high(a); + return (b >= 32u) ? + (high >> (b - 32)) : + ((low >> b) | ((b > 0) ? (high << (32u - b)) : 0u)); +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_uint64_bit_mask(pnanovdb_uint32_t bit_idx) +{ + pnanovdb_uint32_t mask_low = bit_idx < 32u ? 1u << bit_idx : 0u; + pnanovdb_uint32_t mask_high = bit_idx >= 32u ? 1u << (bit_idx - 32u) : 0u; + return pnanovdb_uint32_as_uint64(mask_low, mask_high); +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_uint64_and(pnanovdb_uint64_t a, pnanovdb_uint64_t b) +{ + return pnanovdb_uint32_as_uint64( + pnanovdb_uint64_low(a) & pnanovdb_uint64_low(b), + pnanovdb_uint64_high(a) & pnanovdb_uint64_high(b) + ); +} + +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_uint64_any_bit(pnanovdb_uint64_t a) +{ + return pnanovdb_uint64_low(a) != 0u || pnanovdb_uint64_high(a) != 0u; +} + +#else +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_uint64_offset(pnanovdb_uint64_t a, pnanovdb_uint32_t b) +{ + return a + b; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_uint64_dec(pnanovdb_uint64_t a) +{ + return a - 1u; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_uint64_to_uint32_lsr(pnanovdb_uint64_t a, pnanovdb_uint32_t b) +{ + return pnanovdb_uint64_low(a >> b); +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_uint64_bit_mask(pnanovdb_uint32_t bit_idx) +{ + return 1llu << bit_idx; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_uint64_and(pnanovdb_uint64_t a, pnanovdb_uint64_t b) +{ + return a & b; +} + +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_uint64_any_bit(pnanovdb_uint64_t a) +{ + return a != 0llu; +} +#endif + +// ------------------------------------------------ Address Type ----------------------------------------------------------- + +#if defined(PNANOVDB_ADDRESS_32) +struct pnanovdb_address_t +{ + pnanovdb_uint32_t byte_offset; +}; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_address_t) + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset(pnanovdb_address_t address, pnanovdb_uint32_t byte_offset) +{ + pnanovdb_address_t ret = address; + ret.byte_offset += byte_offset; + return ret; +} +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset_neg(pnanovdb_address_t address, pnanovdb_uint32_t byte_offset) +{ + pnanovdb_address_t ret = address; + ret.byte_offset -= byte_offset; + return ret; +} +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset_product(pnanovdb_address_t address, pnanovdb_uint32_t byte_offset, pnanovdb_uint32_t multiplier) +{ + pnanovdb_address_t ret = address; + ret.byte_offset += byte_offset * multiplier; + return ret; +} +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset64(pnanovdb_address_t address, pnanovdb_uint64_t byte_offset) +{ + pnanovdb_address_t ret = address; + // lose high bits on 32-bit + ret.byte_offset += pnanovdb_uint64_low(byte_offset); + return ret; +} +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset64_product(pnanovdb_address_t address, pnanovdb_uint64_t byte_offset, pnanovdb_uint32_t multiplier) +{ + pnanovdb_address_t ret = address; + ret.byte_offset += pnanovdb_uint64_low(byte_offset) * multiplier; + return ret; +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_address_mask(pnanovdb_address_t address, pnanovdb_uint32_t mask) +{ + return address.byte_offset & mask; +} +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_mask_inv(pnanovdb_address_t address, pnanovdb_uint32_t mask) +{ + pnanovdb_address_t ret = address; + ret.byte_offset &= (~mask); + return ret; +} +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_null() +{ + pnanovdb_address_t ret = { 0 }; + return ret; +} +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_address_is_null(pnanovdb_address_t address) +{ + return address.byte_offset == 0u; +} +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_address_in_interval(pnanovdb_address_t address, pnanovdb_address_t min_address, pnanovdb_address_t max_address) +{ + return address.byte_offset >= min_address.byte_offset && address.byte_offset < max_address.byte_offset; +} +#elif defined(PNANOVDB_ADDRESS_64) +struct pnanovdb_address_t +{ + pnanovdb_uint64_t byte_offset; +}; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_address_t) + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset(pnanovdb_address_t address, pnanovdb_uint32_t byte_offset) +{ + pnanovdb_address_t ret = address; + ret.byte_offset += byte_offset; + return ret; +} +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset_neg(pnanovdb_address_t address, pnanovdb_uint32_t byte_offset) +{ + pnanovdb_address_t ret = address; + ret.byte_offset -= byte_offset; + return ret; +} +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset_product(pnanovdb_address_t address, pnanovdb_uint32_t byte_offset, pnanovdb_uint32_t multiplier) +{ + pnanovdb_address_t ret = address; + ret.byte_offset += pnanovdb_uint32_as_uint64_low(byte_offset) * pnanovdb_uint32_as_uint64_low(multiplier); + return ret; +} +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset64(pnanovdb_address_t address, pnanovdb_uint64_t byte_offset) +{ + pnanovdb_address_t ret = address; + ret.byte_offset += byte_offset; + return ret; +} +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_offset64_product(pnanovdb_address_t address, pnanovdb_uint64_t byte_offset, pnanovdb_uint32_t multiplier) +{ + pnanovdb_address_t ret = address; + ret.byte_offset += byte_offset * pnanovdb_uint32_as_uint64_low(multiplier); + return ret; +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_address_mask(pnanovdb_address_t address, pnanovdb_uint32_t mask) +{ + return pnanovdb_uint64_low(address.byte_offset) & mask; +} +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_mask_inv(pnanovdb_address_t address, pnanovdb_uint32_t mask) +{ + pnanovdb_address_t ret = address; + ret.byte_offset &= (~pnanovdb_uint32_as_uint64_low(mask)); + return ret; +} +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_address_null() +{ + pnanovdb_address_t ret = { 0 }; + return ret; +} +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_address_is_null(pnanovdb_address_t address) +{ + return address.byte_offset == 0llu; +} +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_address_in_interval(pnanovdb_address_t address, pnanovdb_address_t min_address, pnanovdb_address_t max_address) +{ + return address.byte_offset >= min_address.byte_offset && address.byte_offset < max_address.byte_offset; +} +#endif + +// ------------------------------------------------ High Level Buffer Read ----------------------------------------------------------- + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_read_uint32(pnanovdb_buf_t buf, pnanovdb_address_t address) +{ + return pnanovdb_buf_read_uint32(buf, address.byte_offset); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_read_uint64(pnanovdb_buf_t buf, pnanovdb_address_t address) +{ + return pnanovdb_buf_read_uint64(buf, address.byte_offset); +} +PNANOVDB_FORCE_INLINE pnanovdb_int32_t pnanovdb_read_int32(pnanovdb_buf_t buf, pnanovdb_address_t address) +{ + return pnanovdb_uint32_as_int32(pnanovdb_read_uint32(buf, address)); +} +PNANOVDB_FORCE_INLINE float pnanovdb_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address) +{ + return pnanovdb_uint32_as_float(pnanovdb_read_uint32(buf, address)); +} +PNANOVDB_FORCE_INLINE pnanovdb_int64_t pnanovdb_read_int64(pnanovdb_buf_t buf, pnanovdb_address_t address) +{ + return pnanovdb_uint64_as_int64(pnanovdb_read_uint64(buf, address)); +} +PNANOVDB_FORCE_INLINE double pnanovdb_read_double(pnanovdb_buf_t buf, pnanovdb_address_t address) +{ + return pnanovdb_uint64_as_double(pnanovdb_read_uint64(buf, address)); +} +PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_read_coord(pnanovdb_buf_t buf, pnanovdb_address_t address) +{ + pnanovdb_coord_t ret; + ret.x = pnanovdb_uint32_as_int32(pnanovdb_read_uint32(buf, pnanovdb_address_offset(address, 0u))); + ret.y = pnanovdb_uint32_as_int32(pnanovdb_read_uint32(buf, pnanovdb_address_offset(address, 4u))); + ret.z = pnanovdb_uint32_as_int32(pnanovdb_read_uint32(buf, pnanovdb_address_offset(address, 8u))); + return ret; +} +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_read_vec3(pnanovdb_buf_t buf, pnanovdb_address_t address) +{ + pnanovdb_vec3_t ret; + ret.x = pnanovdb_read_float(buf, pnanovdb_address_offset(address, 0u)); + ret.y = pnanovdb_read_float(buf, pnanovdb_address_offset(address, 4u)); + ret.z = pnanovdb_read_float(buf, pnanovdb_address_offset(address, 8u)); + return ret; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_read_uint16(pnanovdb_buf_t buf, pnanovdb_address_t address) +{ + pnanovdb_uint32_t raw = pnanovdb_read_uint32(buf, pnanovdb_address_mask_inv(address, 3u)); + return (raw >> (pnanovdb_address_mask(address, 2) << 3)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_read_uint8(pnanovdb_buf_t buf, pnanovdb_address_t address) +{ + pnanovdb_uint32_t raw = pnanovdb_read_uint32(buf, pnanovdb_address_mask_inv(address, 3u)); + return (raw >> (pnanovdb_address_mask(address, 3) << 3)) & 255; +} +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_read_vec3u16(pnanovdb_buf_t buf, pnanovdb_address_t address) +{ + pnanovdb_vec3_t ret; + const float scale = 1.f / 65535.f; + ret.x = scale * pnanovdb_uint32_to_float(pnanovdb_read_uint16(buf, pnanovdb_address_offset(address, 0u))) - 0.5f; + ret.y = scale * pnanovdb_uint32_to_float(pnanovdb_read_uint16(buf, pnanovdb_address_offset(address, 2u))) - 0.5f; + ret.z = scale * pnanovdb_uint32_to_float(pnanovdb_read_uint16(buf, pnanovdb_address_offset(address, 4u))) - 0.5f; + return ret; +} +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_read_vec3u8(pnanovdb_buf_t buf, pnanovdb_address_t address) +{ + pnanovdb_vec3_t ret; + const float scale = 1.f / 255.f; + ret.x = scale * pnanovdb_uint32_to_float(pnanovdb_read_uint8(buf, pnanovdb_address_offset(address, 0u))) - 0.5f; + ret.y = scale * pnanovdb_uint32_to_float(pnanovdb_read_uint8(buf, pnanovdb_address_offset(address, 1u))) - 0.5f; + ret.z = scale * pnanovdb_uint32_to_float(pnanovdb_read_uint8(buf, pnanovdb_address_offset(address, 2u))) - 0.5f; + return ret; +} + +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_read_bit(pnanovdb_buf_t buf, pnanovdb_address_t address, pnanovdb_uint32_t bit_offset) +{ + pnanovdb_address_t word_address = pnanovdb_address_mask_inv(address, 3u); + pnanovdb_uint32_t bit_index = (pnanovdb_address_mask(address, 3u) << 3u) + bit_offset; + pnanovdb_uint32_t value_word = pnanovdb_buf_read_uint32(buf, word_address.byte_offset); + return ((value_word >> bit_index) & 1) != 0u; +} + +#if defined(PNANOVDB_C) +PNANOVDB_FORCE_INLINE short pnanovdb_read_half(pnanovdb_buf_t buf, pnanovdb_address_t address) +{ + pnanovdb_uint32_t raw = pnanovdb_read_uint32(buf, address); + return (short)(raw >> (pnanovdb_address_mask(address, 2) << 3)); +} +#elif defined(PNANOVDB_HLSL) +PNANOVDB_FORCE_INLINE float pnanovdb_read_half(pnanovdb_buf_t buf, pnanovdb_address_t address) +{ + pnanovdb_uint32_t raw = pnanovdb_read_uint32(buf, address); + return f16tof32(raw >> (pnanovdb_address_mask(address, 2) << 3)); +} +#elif defined(PNANOVDB_GLSL) +PNANOVDB_FORCE_INLINE float pnanovdb_read_half(pnanovdb_buf_t buf, pnanovdb_address_t address) +{ + pnanovdb_uint32_t raw = pnanovdb_read_uint32(buf, address); + return unpackHalf2x16(raw >> (pnanovdb_address_mask(address, 2) << 3)).x; +} +#endif + +// ------------------------------------------------ High Level Buffer Write ----------------------------------------------------------- + +PNANOVDB_FORCE_INLINE void pnanovdb_write_uint32(pnanovdb_buf_t buf, pnanovdb_address_t address, pnanovdb_uint32_t value) +{ + pnanovdb_buf_write_uint32(buf, address.byte_offset, value); +} +PNANOVDB_FORCE_INLINE void pnanovdb_write_uint64(pnanovdb_buf_t buf, pnanovdb_address_t address, pnanovdb_uint64_t value) +{ + pnanovdb_buf_write_uint64(buf, address.byte_offset, value); +} +PNANOVDB_FORCE_INLINE void pnanovdb_write_int32(pnanovdb_buf_t buf, pnanovdb_address_t address, pnanovdb_int32_t value) +{ + pnanovdb_write_uint32(buf, address, pnanovdb_int32_as_uint32(value)); +} +PNANOVDB_FORCE_INLINE void pnanovdb_write_int64(pnanovdb_buf_t buf, pnanovdb_address_t address, pnanovdb_int64_t value) +{ + pnanovdb_buf_write_uint64(buf, address.byte_offset, pnanovdb_int64_as_uint64(value)); +} +PNANOVDB_FORCE_INLINE void pnanovdb_write_float(pnanovdb_buf_t buf, pnanovdb_address_t address, float value) +{ + pnanovdb_write_uint32(buf, address, pnanovdb_float_as_uint32(value)); +} +PNANOVDB_FORCE_INLINE void pnanovdb_write_double(pnanovdb_buf_t buf, pnanovdb_address_t address, double value) +{ + pnanovdb_write_uint64(buf, address, pnanovdb_double_as_uint64(value)); +} +PNANOVDB_FORCE_INLINE void pnanovdb_write_coord(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) value) +{ + pnanovdb_write_uint32(buf, pnanovdb_address_offset(address, 0u), pnanovdb_int32_as_uint32(PNANOVDB_DEREF(value).x)); + pnanovdb_write_uint32(buf, pnanovdb_address_offset(address, 4u), pnanovdb_int32_as_uint32(PNANOVDB_DEREF(value).y)); + pnanovdb_write_uint32(buf, pnanovdb_address_offset(address, 8u), pnanovdb_int32_as_uint32(PNANOVDB_DEREF(value).z)); +} +PNANOVDB_FORCE_INLINE void pnanovdb_write_vec3(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_vec3_t) value) +{ + pnanovdb_write_float(buf, pnanovdb_address_offset(address, 0u), PNANOVDB_DEREF(value).x); + pnanovdb_write_float(buf, pnanovdb_address_offset(address, 4u), PNANOVDB_DEREF(value).y); + pnanovdb_write_float(buf, pnanovdb_address_offset(address, 8u), PNANOVDB_DEREF(value).z); +} + +// ------------------------------------------------ Core Structures ----------------------------------------------------------- + +#define PNANOVDB_MAGIC_NUMBER 0x304244566f6e614eUL// "NanoVDB0" in hex - little endian (uint64_t) +#define PNANOVDB_MAGIC_GRID 0x314244566f6e614eUL// "NanoVDB1" in hex - little endian (uint64_t) +#define PNANOVDB_MAGIC_FILE 0x324244566f6e614eUL// "NanoVDB2" in hex - little endian (uint64_t) + +#define PNANOVDB_MAJOR_VERSION_NUMBER 32// reflects changes to the ABI +#define PNANOVDB_MINOR_VERSION_NUMBER 6// reflects changes to the API but not ABI +#define PNANOVDB_PATCH_VERSION_NUMBER 0// reflects bug-fixes with no ABI or API changes + +#define PNANOVDB_GRID_TYPE_UNKNOWN 0 +#define PNANOVDB_GRID_TYPE_FLOAT 1 +#define PNANOVDB_GRID_TYPE_DOUBLE 2 +#define PNANOVDB_GRID_TYPE_INT16 3 +#define PNANOVDB_GRID_TYPE_INT32 4 +#define PNANOVDB_GRID_TYPE_INT64 5 +#define PNANOVDB_GRID_TYPE_VEC3F 6 +#define PNANOVDB_GRID_TYPE_VEC3D 7 +#define PNANOVDB_GRID_TYPE_MASK 8 +#define PNANOVDB_GRID_TYPE_HALF 9 +#define PNANOVDB_GRID_TYPE_UINT32 10 +#define PNANOVDB_GRID_TYPE_BOOLEAN 11 +#define PNANOVDB_GRID_TYPE_RGBA8 12 +#define PNANOVDB_GRID_TYPE_FP4 13 +#define PNANOVDB_GRID_TYPE_FP8 14 +#define PNANOVDB_GRID_TYPE_FP16 15 +#define PNANOVDB_GRID_TYPE_FPN 16 +#define PNANOVDB_GRID_TYPE_VEC4F 17 +#define PNANOVDB_GRID_TYPE_VEC4D 18 +#define PNANOVDB_GRID_TYPE_INDEX 19 +#define PNANOVDB_GRID_TYPE_ONINDEX 20 +#define PNANOVDB_GRID_TYPE_INDEXMASK 21 +#define PNANOVDB_GRID_TYPE_ONINDEXMASK 22 +#define PNANOVDB_GRID_TYPE_POINTINDEX 23 +#define PNANOVDB_GRID_TYPE_VEC3U8 24 +#define PNANOVDB_GRID_TYPE_VEC3U16 25 +#define PNANOVDB_GRID_TYPE_END 26 + +#define PNANOVDB_GRID_CLASS_UNKNOWN 0 +#define PNANOVDB_GRID_CLASS_LEVEL_SET 1 // narrow band level set, e.g. SDF +#define PNANOVDB_GRID_CLASS_FOG_VOLUME 2 // fog volume, e.g. density +#define PNANOVDB_GRID_CLASS_STAGGERED 3 // staggered MAC grid, e.g. velocity +#define PNANOVDB_GRID_CLASS_POINT_INDEX 4 // point index grid +#define PNANOVDB_GRID_CLASS_POINT_DATA 5 // point data grid +#define PNANOVDB_GRID_CLASS_TOPOLOGY 6 // grid with active states only (no values) +#define PNANOVDB_GRID_CLASS_VOXEL_VOLUME 7 // volume of geometric cubes, e.g. minecraft +#define PNANOVDB_GRID_CLASS_INDEX_GRID 8 // grid whose values are offsets, e.g. into an external array +#define PNANOVDB_GRID_CLASS_TENSOR_GRID 9 // grid which can have extra metadata and features +#define PNANOVDB_GRID_CLASS_END 10 + +#define PNANOVDB_GRID_FLAGS_HAS_LONG_GRID_NAME (1 << 0) +#define PNANOVDB_GRID_FLAGS_HAS_BBOX (1 << 1) +#define PNANOVDB_GRID_FLAGS_HAS_MIN_MAX (1 << 2) +#define PNANOVDB_GRID_FLAGS_HAS_AVERAGE (1 << 3) +#define PNANOVDB_GRID_FLAGS_HAS_STD_DEVIATION (1 << 4) +#define PNANOVDB_GRID_FLAGS_IS_BREADTH_FIRST (1 << 5) +#define PNANOVDB_GRID_FLAGS_END (1 << 6) + +#define PNANOVDB_LEAF_TYPE_DEFAULT 0 +#define PNANOVDB_LEAF_TYPE_LITE 1 +#define PNANOVDB_LEAF_TYPE_FP 2 +#define PNANOVDB_LEAF_TYPE_INDEX 3 +#define PNANOVDB_LEAF_TYPE_INDEXMASK 4 +#define PNANOVDB_LEAF_TYPE_POINTINDEX 5 + +// BuildType = Unknown, float, double, int16_t, int32_t, int64_t, Vec3f, Vec3d, Mask, ... +// bit count of values in leaf nodes, i.e. 8*sizeof(*nanovdb::LeafNode::mValues) or zero if no values are stored +PNANOVDB_STATIC_CONST pnanovdb_uint32_t pnanovdb_grid_type_value_strides_bits[PNANOVDB_GRID_TYPE_END] = { 0, 32, 64, 16, 32, 64, 96, 192, 0, 16, 32, 1, 32, 4, 8, 16, 0, 128, 256, 0, 0, 0, 0, 16, 24, 48 }; +// bit count of the Tile union in InternalNodes, i.e. 8*sizeof(nanovdb::InternalData::Tile) +PNANOVDB_STATIC_CONST pnanovdb_uint32_t pnanovdb_grid_type_table_strides_bits[PNANOVDB_GRID_TYPE_END] = { 64, 64, 64, 64, 64, 64, 128, 192, 64, 64, 64, 64, 64, 64, 64, 64, 64, 128, 256, 64, 64, 64, 64, 64, 64, 64 }; +// bit count of min/max values, i.e. 8*sizeof(nanovdb::LeafData::mMinimum) or zero if no min/max exists +PNANOVDB_STATIC_CONST pnanovdb_uint32_t pnanovdb_grid_type_minmax_strides_bits[PNANOVDB_GRID_TYPE_END] = { 0, 32, 64, 16, 32, 64, 96, 192, 8, 16, 32, 8, 32, 32, 32, 32, 32, 128, 256, 64, 64, 64, 64, 64, 24, 48 }; +// bit alignment of the value type, controlled by the smallest native type, which is why it is always 0, 8, 16, 32, or 64, e.g. for Vec3f it is 32 +PNANOVDB_STATIC_CONST pnanovdb_uint32_t pnanovdb_grid_type_minmax_aligns_bits[PNANOVDB_GRID_TYPE_END] = { 0, 32, 64, 16, 32, 64, 32, 64, 8, 16, 32, 8, 32, 32, 32, 32, 32, 32, 64, 64, 64, 64, 64, 64, 8, 16 }; +// bit alignment of the stats (avg/std-dev) types, e.g. 8*sizeof(nanovdb::LeafData::mAverage) +PNANOVDB_STATIC_CONST pnanovdb_uint32_t pnanovdb_grid_type_stat_strides_bits[PNANOVDB_GRID_TYPE_END] = { 0, 32, 64, 32, 32, 64, 32, 64, 8, 32, 32, 8, 32, 32, 32, 32, 32, 32, 64, 64, 64, 64, 64, 64, 32, 32 }; +// one of the 4 leaf types defined above, e.g. PNANOVDB_LEAF_TYPE_INDEX = 3 +PNANOVDB_STATIC_CONST pnanovdb_uint32_t pnanovdb_grid_type_leaf_type[PNANOVDB_GRID_TYPE_END] = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 2, 2, 2, 2, 0, 0, 3, 3, 4, 4, 5, 0, 0 }; + +struct pnanovdb_map_t +{ + float matf[9]; + float invmatf[9]; + float vecf[3]; + float taperf; + double matd[9]; + double invmatd[9]; + double vecd[3]; + double taperd; +}; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_map_t) +struct pnanovdb_map_handle_t { pnanovdb_address_t address; }; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_map_handle_t) + +#define PNANOVDB_MAP_SIZE 264 + +#define PNANOVDB_MAP_OFF_MATF 0 +#define PNANOVDB_MAP_OFF_INVMATF 36 +#define PNANOVDB_MAP_OFF_VECF 72 +#define PNANOVDB_MAP_OFF_TAPERF 84 +#define PNANOVDB_MAP_OFF_MATD 88 +#define PNANOVDB_MAP_OFF_INVMATD 160 +#define PNANOVDB_MAP_OFF_VECD 232 +#define PNANOVDB_MAP_OFF_TAPERD 256 + +PNANOVDB_FORCE_INLINE float pnanovdb_map_get_matf(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index) { + return pnanovdb_read_float(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_MATF + 4u * index)); +} +PNANOVDB_FORCE_INLINE float pnanovdb_map_get_invmatf(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index) { + return pnanovdb_read_float(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_INVMATF + 4u * index)); +} +PNANOVDB_FORCE_INLINE float pnanovdb_map_get_vecf(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index) { + return pnanovdb_read_float(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_VECF + 4u * index)); +} +PNANOVDB_FORCE_INLINE float pnanovdb_map_get_taperf(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index) { + return pnanovdb_read_float(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_TAPERF)); +} +PNANOVDB_FORCE_INLINE double pnanovdb_map_get_matd(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index) { + return pnanovdb_read_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_MATD + 8u * index)); +} +PNANOVDB_FORCE_INLINE double pnanovdb_map_get_invmatd(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index) { + return pnanovdb_read_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_INVMATD + 8u * index)); +} +PNANOVDB_FORCE_INLINE double pnanovdb_map_get_vecd(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index) { + return pnanovdb_read_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_VECD + 8u * index)); +} +PNANOVDB_FORCE_INLINE double pnanovdb_map_get_taperd(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index) { + return pnanovdb_read_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_TAPERD)); +} + +PNANOVDB_FORCE_INLINE void pnanovdb_map_set_matf(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index, float matf) { + pnanovdb_write_float(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_MATF + 4u * index), matf); +} +PNANOVDB_FORCE_INLINE void pnanovdb_map_set_invmatf(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index, float invmatf) { + pnanovdb_write_float(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_INVMATF + 4u * index), invmatf); +} +PNANOVDB_FORCE_INLINE void pnanovdb_map_set_vecf(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index, float vecf) { + pnanovdb_write_float(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_VECF + 4u * index), vecf); +} +PNANOVDB_FORCE_INLINE void pnanovdb_map_set_taperf(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index, float taperf) { + pnanovdb_write_float(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_TAPERF), taperf); +} +PNANOVDB_FORCE_INLINE void pnanovdb_map_set_matd(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index, double matd) { + pnanovdb_write_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_MATD + 8u * index), matd); +} +PNANOVDB_FORCE_INLINE void pnanovdb_map_set_invmatd(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index, double invmatd) { + pnanovdb_write_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_INVMATD + 8u * index), invmatd); +} +PNANOVDB_FORCE_INLINE void pnanovdb_map_set_vecd(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index, double vecd) { + pnanovdb_write_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_VECD + 8u * index), vecd); +} +PNANOVDB_FORCE_INLINE void pnanovdb_map_set_taperd(pnanovdb_buf_t buf, pnanovdb_map_handle_t p, pnanovdb_uint32_t index, double taperd) { + pnanovdb_write_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_MAP_OFF_TAPERD), taperd); +} + +struct pnanovdb_grid_t +{ + pnanovdb_uint64_t magic; // 8 bytes, 0 + pnanovdb_uint64_t checksum; // 8 bytes, 8 + pnanovdb_uint32_t version; // 4 bytes, 16 + pnanovdb_uint32_t flags; // 4 bytes, 20 + pnanovdb_uint32_t grid_index; // 4 bytes, 24 + pnanovdb_uint32_t grid_count; // 4 bytes, 28 + pnanovdb_uint64_t grid_size; // 8 bytes, 32 + pnanovdb_uint32_t grid_name[256 / 4]; // 256 bytes, 40 + pnanovdb_map_t map; // 264 bytes, 296 + double world_bbox[6]; // 48 bytes, 560 + double voxel_size[3]; // 24 bytes, 608 + pnanovdb_uint32_t grid_class; // 4 bytes, 632 + pnanovdb_uint32_t grid_type; // 4 bytes, 636 + pnanovdb_int64_t blind_metadata_offset; // 8 bytes, 640 + pnanovdb_uint32_t blind_metadata_count; // 4 bytes, 648 + pnanovdb_uint32_t pad[5]; // 20 bytes, 652 +}; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_grid_t) +struct pnanovdb_grid_handle_t { pnanovdb_address_t address; }; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_grid_handle_t) + +#define PNANOVDB_GRID_SIZE 672 + +#define PNANOVDB_GRID_OFF_MAGIC 0 +#define PNANOVDB_GRID_OFF_CHECKSUM 8 +#define PNANOVDB_GRID_OFF_VERSION 16 +#define PNANOVDB_GRID_OFF_FLAGS 20 +#define PNANOVDB_GRID_OFF_GRID_INDEX 24 +#define PNANOVDB_GRID_OFF_GRID_COUNT 28 +#define PNANOVDB_GRID_OFF_GRID_SIZE 32 +#define PNANOVDB_GRID_OFF_GRID_NAME 40 +#define PNANOVDB_GRID_OFF_MAP 296 +#define PNANOVDB_GRID_OFF_WORLD_BBOX 560 +#define PNANOVDB_GRID_OFF_VOXEL_SIZE 608 +#define PNANOVDB_GRID_OFF_GRID_CLASS 632 +#define PNANOVDB_GRID_OFF_GRID_TYPE 636 +#define PNANOVDB_GRID_OFF_BLIND_METADATA_OFFSET 640 +#define PNANOVDB_GRID_OFF_BLIND_METADATA_COUNT 648 + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_grid_get_magic(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { + return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_MAGIC)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_grid_get_checksum(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { + return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_CHECKSUM)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_grid_get_version(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_VERSION)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_grid_get_flags(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_FLAGS)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_grid_get_grid_index(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_INDEX)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_grid_get_grid_count(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_COUNT)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_grid_get_grid_size(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { + return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_SIZE)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_grid_get_grid_name(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint32_t index) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_NAME + 4u * index)); +} +PNANOVDB_FORCE_INLINE pnanovdb_map_handle_t pnanovdb_grid_get_map(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { + pnanovdb_map_handle_t ret; + ret.address = pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_MAP); + return ret; +} +PNANOVDB_FORCE_INLINE double pnanovdb_grid_get_world_bbox(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint32_t index) { + return pnanovdb_read_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_WORLD_BBOX + 8u * index)); +} +PNANOVDB_FORCE_INLINE double pnanovdb_grid_get_voxel_size(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint32_t index) { + return pnanovdb_read_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_VOXEL_SIZE + 8u * index)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_grid_get_grid_class(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_CLASS)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_grid_get_grid_type(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_TYPE)); +} +PNANOVDB_FORCE_INLINE pnanovdb_int64_t pnanovdb_grid_get_blind_metadata_offset(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { + return pnanovdb_read_int64(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_BLIND_METADATA_OFFSET)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_grid_get_blind_metadata_count(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_BLIND_METADATA_COUNT)); +} + +PNANOVDB_FORCE_INLINE void pnanovdb_grid_set_magic(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint64_t magic) { + pnanovdb_write_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_MAGIC), magic); +} +PNANOVDB_FORCE_INLINE void pnanovdb_grid_set_checksum(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint64_t checksum) { + pnanovdb_write_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_CHECKSUM), checksum); +} +PNANOVDB_FORCE_INLINE void pnanovdb_grid_set_version(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint32_t version) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_VERSION), version); +} +PNANOVDB_FORCE_INLINE void pnanovdb_grid_set_flags(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint32_t flags) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_FLAGS), flags); +} +PNANOVDB_FORCE_INLINE void pnanovdb_grid_set_grid_index(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint32_t grid_index) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_INDEX), grid_index); +} +PNANOVDB_FORCE_INLINE void pnanovdb_grid_set_grid_count(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint32_t grid_count) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_COUNT), grid_count); +} +PNANOVDB_FORCE_INLINE void pnanovdb_grid_set_grid_size(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint64_t grid_size) { + pnanovdb_write_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_SIZE), grid_size); +} +PNANOVDB_FORCE_INLINE void pnanovdb_grid_set_grid_name(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint32_t index, pnanovdb_uint32_t grid_name) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_NAME + 4u * index), grid_name); +} +PNANOVDB_FORCE_INLINE void pnanovdb_grid_set_world_bbox(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint32_t index, double world_bbox) { + pnanovdb_write_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_WORLD_BBOX + 8u * index), world_bbox); +} +PNANOVDB_FORCE_INLINE void pnanovdb_grid_set_voxel_size(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint32_t index, double voxel_size) { + pnanovdb_write_double(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_VOXEL_SIZE + 8u * index), voxel_size); +} +PNANOVDB_FORCE_INLINE void pnanovdb_grid_set_grid_class(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint32_t grid_class) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_CLASS), grid_class); +} +PNANOVDB_FORCE_INLINE void pnanovdb_grid_set_grid_type(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint32_t grid_type) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_GRID_TYPE), grid_type); +} +PNANOVDB_FORCE_INLINE void pnanovdb_grid_set_blind_metadata_offset(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint64_t blind_metadata_offset) { + pnanovdb_write_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_BLIND_METADATA_OFFSET), blind_metadata_offset); +} +PNANOVDB_FORCE_INLINE void pnanovdb_grid_set_blind_metadata_count(pnanovdb_buf_t buf, pnanovdb_grid_handle_t p, pnanovdb_uint32_t metadata_count) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRID_OFF_BLIND_METADATA_COUNT), metadata_count); +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_make_version(pnanovdb_uint32_t major, pnanovdb_uint32_t minor, pnanovdb_uint32_t patch_num) +{ + return (major << 21u) | (minor << 10u) | patch_num; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_version_get_major(pnanovdb_uint32_t version) +{ + return (version >> 21u) & ((1u << 11u) - 1u); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_version_get_minor(pnanovdb_uint32_t version) +{ + return (version >> 10u) & ((1u << 11u) - 1u); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_version_get_patch(pnanovdb_uint32_t version) +{ + return version & ((1u << 10u) - 1u); +} + +struct pnanovdb_gridblindmetadata_t +{ + pnanovdb_int64_t byte_offset; // 8 bytes, 0 + pnanovdb_uint64_t element_count; // 8 bytes, 8 + pnanovdb_uint32_t flags; // 4 bytes, 16 + pnanovdb_uint32_t semantic; // 4 bytes, 20 + pnanovdb_uint32_t data_class; // 4 bytes, 24 + pnanovdb_uint32_t data_type; // 4 bytes, 28 + pnanovdb_uint32_t name[256 / 4]; // 256 bytes, 32 +}; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_gridblindmetadata_t) +struct pnanovdb_gridblindmetadata_handle_t { pnanovdb_address_t address; }; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_gridblindmetadata_handle_t) + +#define PNANOVDB_GRIDBLINDMETADATA_SIZE 288 + +#define PNANOVDB_GRIDBLINDMETADATA_OFF_BYTE_OFFSET 0 +#define PNANOVDB_GRIDBLINDMETADATA_OFF_ELEMENT_COUNT 8 +#define PNANOVDB_GRIDBLINDMETADATA_OFF_FLAGS 16 +#define PNANOVDB_GRIDBLINDMETADATA_OFF_SEMANTIC 20 +#define PNANOVDB_GRIDBLINDMETADATA_OFF_DATA_CLASS 24 +#define PNANOVDB_GRIDBLINDMETADATA_OFF_DATA_TYPE 28 +#define PNANOVDB_GRIDBLINDMETADATA_OFF_NAME 32 + +PNANOVDB_FORCE_INLINE pnanovdb_int64_t pnanovdb_gridblindmetadata_get_byte_offset(pnanovdb_buf_t buf, pnanovdb_gridblindmetadata_handle_t p) { + return pnanovdb_read_int64(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRIDBLINDMETADATA_OFF_BYTE_OFFSET)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_gridblindmetadata_get_element_count(pnanovdb_buf_t buf, pnanovdb_gridblindmetadata_handle_t p) { + return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRIDBLINDMETADATA_OFF_ELEMENT_COUNT)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_gridblindmetadata_get_flags(pnanovdb_buf_t buf, pnanovdb_gridblindmetadata_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRIDBLINDMETADATA_OFF_FLAGS)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_gridblindmetadata_get_semantic(pnanovdb_buf_t buf, pnanovdb_gridblindmetadata_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRIDBLINDMETADATA_OFF_SEMANTIC)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_gridblindmetadata_get_data_class(pnanovdb_buf_t buf, pnanovdb_gridblindmetadata_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRIDBLINDMETADATA_OFF_DATA_CLASS)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_gridblindmetadata_get_data_type(pnanovdb_buf_t buf, pnanovdb_gridblindmetadata_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRIDBLINDMETADATA_OFF_DATA_TYPE)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_gridblindmetadata_get_name(pnanovdb_buf_t buf, pnanovdb_gridblindmetadata_handle_t p, pnanovdb_uint32_t index) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_GRIDBLINDMETADATA_OFF_NAME + 4u * index)); +} + +struct pnanovdb_tree_t +{ + pnanovdb_uint64_t node_offset_leaf; + pnanovdb_uint64_t node_offset_lower; + pnanovdb_uint64_t node_offset_upper; + pnanovdb_uint64_t node_offset_root; + pnanovdb_uint32_t node_count_leaf; + pnanovdb_uint32_t node_count_lower; + pnanovdb_uint32_t node_count_upper; + pnanovdb_uint32_t tile_count_leaf; + pnanovdb_uint32_t tile_count_lower; + pnanovdb_uint32_t tile_count_upper; + pnanovdb_uint64_t voxel_count; +}; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_tree_t) +struct pnanovdb_tree_handle_t { pnanovdb_address_t address; }; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_tree_handle_t) + +#define PNANOVDB_TREE_SIZE 64 + +#define PNANOVDB_TREE_OFF_NODE_OFFSET_LEAF 0 +#define PNANOVDB_TREE_OFF_NODE_OFFSET_LOWER 8 +#define PNANOVDB_TREE_OFF_NODE_OFFSET_UPPER 16 +#define PNANOVDB_TREE_OFF_NODE_OFFSET_ROOT 24 +#define PNANOVDB_TREE_OFF_NODE_COUNT_LEAF 32 +#define PNANOVDB_TREE_OFF_NODE_COUNT_LOWER 36 +#define PNANOVDB_TREE_OFF_NODE_COUNT_UPPER 40 +#define PNANOVDB_TREE_OFF_TILE_COUNT_LEAF 44 +#define PNANOVDB_TREE_OFF_TILE_COUNT_LOWER 48 +#define PNANOVDB_TREE_OFF_TILE_COUNT_UPPER 52 +#define PNANOVDB_TREE_OFF_VOXEL_COUNT 56 + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_tree_get_node_offset_leaf(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { + return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_OFFSET_LEAF)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_tree_get_node_offset_lower(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { + return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_OFFSET_LOWER)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_tree_get_node_offset_upper(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { + return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_OFFSET_UPPER)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_tree_get_node_offset_root(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { + return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_OFFSET_ROOT)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_tree_get_node_count_leaf(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_COUNT_LEAF)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_tree_get_node_count_lower(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_COUNT_LOWER)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_tree_get_node_count_upper(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_COUNT_UPPER)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_tree_get_tile_count_leaf(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_TILE_COUNT_LEAF)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_tree_get_tile_count_lower(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_TILE_COUNT_LOWER)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_tree_get_tile_count_upper(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_TILE_COUNT_UPPER)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_tree_get_voxel_count(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p) { + return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_VOXEL_COUNT)); +} + +PNANOVDB_FORCE_INLINE void pnanovdb_tree_set_node_offset_leaf(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p, pnanovdb_uint64_t node_offset_leaf) { + pnanovdb_write_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_OFFSET_LEAF), node_offset_leaf); +} +PNANOVDB_FORCE_INLINE void pnanovdb_tree_set_node_offset_lower(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p, pnanovdb_uint64_t node_offset_lower) { + pnanovdb_write_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_OFFSET_LOWER), node_offset_lower); +} +PNANOVDB_FORCE_INLINE void pnanovdb_tree_set_node_offset_upper(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p, pnanovdb_uint64_t node_offset_upper) { + pnanovdb_write_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_OFFSET_UPPER), node_offset_upper); +} +PNANOVDB_FORCE_INLINE void pnanovdb_tree_set_node_offset_root(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p, pnanovdb_uint64_t node_offset_root) { + pnanovdb_write_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_OFFSET_ROOT), node_offset_root); +} +PNANOVDB_FORCE_INLINE void pnanovdb_tree_set_node_count_leaf(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p, pnanovdb_uint32_t node_count_leaf) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_COUNT_LEAF), node_count_leaf); +} +PNANOVDB_FORCE_INLINE void pnanovdb_tree_set_node_count_lower(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p, pnanovdb_uint32_t node_count_lower) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_COUNT_LOWER), node_count_lower); +} +PNANOVDB_FORCE_INLINE void pnanovdb_tree_set_node_count_upper(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p, pnanovdb_uint32_t node_count_upper) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_NODE_COUNT_UPPER), node_count_upper); +} +PNANOVDB_FORCE_INLINE void pnanovdb_tree_set_tile_count_leaf(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p, pnanovdb_uint32_t tile_count_leaf) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_TILE_COUNT_LEAF), tile_count_leaf); +} +PNANOVDB_FORCE_INLINE void pnanovdb_tree_set_tile_count_lower(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p, pnanovdb_uint32_t tile_count_lower) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_TILE_COUNT_LOWER), tile_count_lower); +} +PNANOVDB_FORCE_INLINE void pnanovdb_tree_set_tile_count_upper(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p, pnanovdb_uint32_t tile_count_upper) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_TILE_COUNT_UPPER), tile_count_upper); +} +PNANOVDB_FORCE_INLINE void pnanovdb_tree_set_voxel_count(pnanovdb_buf_t buf, pnanovdb_tree_handle_t p, pnanovdb_uint64_t voxel_count) { + pnanovdb_write_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_TREE_OFF_VOXEL_COUNT), voxel_count); +} + +struct pnanovdb_root_t +{ + pnanovdb_coord_t bbox_min; + pnanovdb_coord_t bbox_max; + pnanovdb_uint32_t table_size; + pnanovdb_uint32_t pad1; // background can start here + // background, min, max +}; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_root_t) +struct pnanovdb_root_handle_t { pnanovdb_address_t address; }; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_root_handle_t) + +#define PNANOVDB_ROOT_BASE_SIZE 28 + +#define PNANOVDB_ROOT_OFF_BBOX_MIN 0 +#define PNANOVDB_ROOT_OFF_BBOX_MAX 12 +#define PNANOVDB_ROOT_OFF_TABLE_SIZE 24 + +PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_root_get_bbox_min(pnanovdb_buf_t buf, pnanovdb_root_handle_t p) { + return pnanovdb_read_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_OFF_BBOX_MIN)); +} +PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_root_get_bbox_max(pnanovdb_buf_t buf, pnanovdb_root_handle_t p) { + return pnanovdb_read_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_OFF_BBOX_MAX)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_root_get_tile_count(pnanovdb_buf_t buf, pnanovdb_root_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_OFF_TABLE_SIZE)); +} + +PNANOVDB_FORCE_INLINE void pnanovdb_root_set_bbox_min(pnanovdb_buf_t buf, pnanovdb_root_handle_t p, PNANOVDB_IN(pnanovdb_coord_t) bbox_min) { + pnanovdb_write_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_OFF_BBOX_MIN), bbox_min); +} +PNANOVDB_FORCE_INLINE void pnanovdb_root_set_bbox_max(pnanovdb_buf_t buf, pnanovdb_root_handle_t p, PNANOVDB_IN(pnanovdb_coord_t) bbox_max) { + pnanovdb_write_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_OFF_BBOX_MAX), bbox_max); +} +PNANOVDB_FORCE_INLINE void pnanovdb_root_set_tile_count(pnanovdb_buf_t buf, pnanovdb_root_handle_t p, pnanovdb_uint32_t tile_count) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_OFF_TABLE_SIZE), tile_count); +} + +struct pnanovdb_root_tile_t +{ + pnanovdb_uint64_t key; + pnanovdb_int64_t child; // signed byte offset from root to the child node, 0 means it is a constant tile, so use value + pnanovdb_uint32_t state; + pnanovdb_uint32_t pad1; // value can start here + // value +}; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_root_tile_t) +struct pnanovdb_root_tile_handle_t { pnanovdb_address_t address; }; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_root_tile_handle_t) + +#define PNANOVDB_ROOT_TILE_BASE_SIZE 20 + +#define PNANOVDB_ROOT_TILE_OFF_KEY 0 +#define PNANOVDB_ROOT_TILE_OFF_CHILD 8 +#define PNANOVDB_ROOT_TILE_OFF_STATE 16 + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_root_tile_get_key(pnanovdb_buf_t buf, pnanovdb_root_tile_handle_t p) { + return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_TILE_OFF_KEY)); +} +PNANOVDB_FORCE_INLINE pnanovdb_int64_t pnanovdb_root_tile_get_child(pnanovdb_buf_t buf, pnanovdb_root_tile_handle_t p) { + return pnanovdb_read_int64(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_TILE_OFF_CHILD)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_root_tile_get_state(pnanovdb_buf_t buf, pnanovdb_root_tile_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_TILE_OFF_STATE)); +} + +PNANOVDB_FORCE_INLINE void pnanovdb_root_tile_set_key(pnanovdb_buf_t buf, pnanovdb_root_tile_handle_t p, pnanovdb_uint64_t key) { + pnanovdb_write_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_TILE_OFF_KEY), key); +} +PNANOVDB_FORCE_INLINE void pnanovdb_root_tile_set_child(pnanovdb_buf_t buf, pnanovdb_root_tile_handle_t p, pnanovdb_int64_t child) { + pnanovdb_write_int64(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_TILE_OFF_CHILD), child); +} +PNANOVDB_FORCE_INLINE void pnanovdb_root_tile_set_state(pnanovdb_buf_t buf, pnanovdb_root_tile_handle_t p, pnanovdb_uint32_t state) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_ROOT_TILE_OFF_STATE), state); +} + +struct pnanovdb_upper_t +{ + pnanovdb_coord_t bbox_min; + pnanovdb_coord_t bbox_max; + pnanovdb_uint64_t flags; + pnanovdb_uint32_t value_mask[1024]; + pnanovdb_uint32_t child_mask[1024]; + // min, max + // alignas(32) pnanovdb_uint32_t table[]; +}; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_upper_t) +struct pnanovdb_upper_handle_t { pnanovdb_address_t address; }; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_upper_handle_t) + +#define PNANOVDB_UPPER_TABLE_COUNT 32768 +#define PNANOVDB_UPPER_BASE_SIZE 8224 + +#define PNANOVDB_UPPER_OFF_BBOX_MIN 0 +#define PNANOVDB_UPPER_OFF_BBOX_MAX 12 +#define PNANOVDB_UPPER_OFF_FLAGS 24 +#define PNANOVDB_UPPER_OFF_VALUE_MASK 32 +#define PNANOVDB_UPPER_OFF_CHILD_MASK 4128 + +PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_upper_get_bbox_min(pnanovdb_buf_t buf, pnanovdb_upper_handle_t p) { + return pnanovdb_read_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_UPPER_OFF_BBOX_MIN)); +} +PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_upper_get_bbox_max(pnanovdb_buf_t buf, pnanovdb_upper_handle_t p) { + return pnanovdb_read_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_UPPER_OFF_BBOX_MAX)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_upper_get_flags(pnanovdb_buf_t buf, pnanovdb_upper_handle_t p) { + return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_UPPER_OFF_FLAGS)); +} +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_upper_get_value_mask(pnanovdb_buf_t buf, pnanovdb_upper_handle_t p, pnanovdb_uint32_t bit_index) { + pnanovdb_uint32_t value = pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_UPPER_OFF_VALUE_MASK + 4u * (bit_index >> 5u))); + return ((value >> (bit_index & 31u)) & 1) != 0u; +} +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_upper_get_child_mask(pnanovdb_buf_t buf, pnanovdb_upper_handle_t p, pnanovdb_uint32_t bit_index) { + pnanovdb_uint32_t value = pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_UPPER_OFF_CHILD_MASK + 4u * (bit_index >> 5u))); + return ((value >> (bit_index & 31u)) & 1) != 0u; +} + +PNANOVDB_FORCE_INLINE void pnanovdb_upper_set_bbox_min(pnanovdb_buf_t buf, pnanovdb_upper_handle_t p, PNANOVDB_IN(pnanovdb_coord_t) bbox_min) { + pnanovdb_write_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_UPPER_OFF_BBOX_MIN), bbox_min); +} +PNANOVDB_FORCE_INLINE void pnanovdb_upper_set_bbox_max(pnanovdb_buf_t buf, pnanovdb_upper_handle_t p, PNANOVDB_IN(pnanovdb_coord_t) bbox_max) { + pnanovdb_write_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_UPPER_OFF_BBOX_MAX), bbox_max); +} +PNANOVDB_FORCE_INLINE void pnanovdb_upper_set_child_mask(pnanovdb_buf_t buf, pnanovdb_upper_handle_t p, pnanovdb_uint32_t bit_index, pnanovdb_bool_t value) { + pnanovdb_address_t addr = pnanovdb_address_offset(p.address, PNANOVDB_UPPER_OFF_CHILD_MASK + 4u * (bit_index >> 5u)); + pnanovdb_uint32_t valueMask = pnanovdb_read_uint32(buf, addr); + if (!value) { valueMask &= ~(1u << (bit_index & 31u)); } + if (value) valueMask |= (1u << (bit_index & 31u)); + pnanovdb_write_uint32(buf, addr, valueMask); +} + +struct pnanovdb_lower_t +{ + pnanovdb_coord_t bbox_min; + pnanovdb_coord_t bbox_max; + pnanovdb_uint64_t flags; + pnanovdb_uint32_t value_mask[128]; + pnanovdb_uint32_t child_mask[128]; + // min, max + // alignas(32) pnanovdb_uint32_t table[]; +}; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_lower_t) +struct pnanovdb_lower_handle_t { pnanovdb_address_t address; }; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_lower_handle_t) + +#define PNANOVDB_LOWER_TABLE_COUNT 4096 +#define PNANOVDB_LOWER_BASE_SIZE 1056 + +#define PNANOVDB_LOWER_OFF_BBOX_MIN 0 +#define PNANOVDB_LOWER_OFF_BBOX_MAX 12 +#define PNANOVDB_LOWER_OFF_FLAGS 24 +#define PNANOVDB_LOWER_OFF_VALUE_MASK 32 +#define PNANOVDB_LOWER_OFF_CHILD_MASK 544 + +PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_lower_get_bbox_min(pnanovdb_buf_t buf, pnanovdb_lower_handle_t p) { + return pnanovdb_read_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_LOWER_OFF_BBOX_MIN)); +} +PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_lower_get_bbox_max(pnanovdb_buf_t buf, pnanovdb_lower_handle_t p) { + return pnanovdb_read_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_LOWER_OFF_BBOX_MAX)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_lower_get_flags(pnanovdb_buf_t buf, pnanovdb_lower_handle_t p) { + return pnanovdb_read_uint64(buf, pnanovdb_address_offset(p.address, PNANOVDB_LOWER_OFF_FLAGS)); +} +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_lower_get_value_mask(pnanovdb_buf_t buf, pnanovdb_lower_handle_t p, pnanovdb_uint32_t bit_index) { + pnanovdb_uint32_t value = pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_LOWER_OFF_VALUE_MASK + 4u * (bit_index >> 5u))); + return ((value >> (bit_index & 31u)) & 1) != 0u; +} +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_lower_get_child_mask(pnanovdb_buf_t buf, pnanovdb_lower_handle_t p, pnanovdb_uint32_t bit_index) { + pnanovdb_uint32_t value = pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_LOWER_OFF_CHILD_MASK + 4u * (bit_index >> 5u))); + return ((value >> (bit_index & 31u)) & 1) != 0u; +} + +PNANOVDB_FORCE_INLINE void pnanovdb_lower_set_bbox_min(pnanovdb_buf_t buf, pnanovdb_lower_handle_t p, PNANOVDB_IN(pnanovdb_coord_t) bbox_min) { + pnanovdb_write_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_LOWER_OFF_BBOX_MIN), bbox_min); +} +PNANOVDB_FORCE_INLINE void pnanovdb_lower_set_bbox_max(pnanovdb_buf_t buf, pnanovdb_lower_handle_t p, PNANOVDB_IN(pnanovdb_coord_t) bbox_max) { + pnanovdb_write_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_LOWER_OFF_BBOX_MAX), bbox_max); +} +PNANOVDB_FORCE_INLINE void pnanovdb_lower_set_child_mask(pnanovdb_buf_t buf, pnanovdb_lower_handle_t p, pnanovdb_uint32_t bit_index, pnanovdb_bool_t value) { + pnanovdb_address_t addr = pnanovdb_address_offset(p.address, PNANOVDB_LOWER_OFF_CHILD_MASK + 4u * (bit_index >> 5u)); + pnanovdb_uint32_t valueMask = pnanovdb_read_uint32(buf, addr); + if (!value) { valueMask &= ~(1u << (bit_index & 31u)); } + if (value) valueMask |= (1u << (bit_index & 31u)); + pnanovdb_write_uint32(buf, addr, valueMask); +} + +struct pnanovdb_leaf_t +{ + pnanovdb_coord_t bbox_min; + pnanovdb_uint32_t bbox_dif_and_flags; + pnanovdb_uint32_t value_mask[16]; + // min, max + // alignas(32) pnanovdb_uint32_t values[]; +}; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_leaf_t) +struct pnanovdb_leaf_handle_t { pnanovdb_address_t address; }; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_leaf_handle_t) + +#define PNANOVDB_LEAF_TABLE_COUNT 512 +#define PNANOVDB_LEAF_BASE_SIZE 80 + +#define PNANOVDB_LEAF_OFF_BBOX_MIN 0 +#define PNANOVDB_LEAF_OFF_BBOX_DIF_AND_FLAGS 12 +#define PNANOVDB_LEAF_OFF_VALUE_MASK 16 + +#define PNANOVDB_LEAF_TABLE_NEG_OFF_BBOX_DIF_AND_FLAGS 84 +#define PNANOVDB_LEAF_TABLE_NEG_OFF_MINIMUM 16 +#define PNANOVDB_LEAF_TABLE_NEG_OFF_QUANTUM 12 + +PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_leaf_get_bbox_min(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t p) { + return pnanovdb_read_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_LEAF_OFF_BBOX_MIN)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_leaf_get_bbox_dif_and_flags(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t p) { + return pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_LEAF_OFF_BBOX_DIF_AND_FLAGS)); +} +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_leaf_get_value_mask(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t p, pnanovdb_uint32_t bit_index) { + pnanovdb_uint32_t value = pnanovdb_read_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_LEAF_OFF_VALUE_MASK + 4u * (bit_index >> 5u))); + return ((value >> (bit_index & 31u)) & 1) != 0u; +} + +PNANOVDB_FORCE_INLINE void pnanovdb_leaf_set_bbox_min(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t p, PNANOVDB_IN(pnanovdb_coord_t) bbox_min) { + pnanovdb_write_coord(buf, pnanovdb_address_offset(p.address, PNANOVDB_LEAF_OFF_BBOX_MIN), bbox_min); +} +PNANOVDB_FORCE_INLINE void pnanovdb_leaf_set_bbox_dif_and_flags(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t p, pnanovdb_uint32_t bbox_dif_and_flags) { + pnanovdb_write_uint32(buf, pnanovdb_address_offset(p.address, PNANOVDB_LEAF_OFF_BBOX_DIF_AND_FLAGS), bbox_dif_and_flags); +} + +struct pnanovdb_grid_type_constants_t +{ + pnanovdb_uint32_t root_off_background; + pnanovdb_uint32_t root_off_min; + pnanovdb_uint32_t root_off_max; + pnanovdb_uint32_t root_off_ave; + pnanovdb_uint32_t root_off_stddev; + pnanovdb_uint32_t root_size; + pnanovdb_uint32_t value_stride_bits; + pnanovdb_uint32_t table_stride; + pnanovdb_uint32_t root_tile_off_value; + pnanovdb_uint32_t root_tile_size; + pnanovdb_uint32_t upper_off_min; + pnanovdb_uint32_t upper_off_max; + pnanovdb_uint32_t upper_off_ave; + pnanovdb_uint32_t upper_off_stddev; + pnanovdb_uint32_t upper_off_table; + pnanovdb_uint32_t upper_size; + pnanovdb_uint32_t lower_off_min; + pnanovdb_uint32_t lower_off_max; + pnanovdb_uint32_t lower_off_ave; + pnanovdb_uint32_t lower_off_stddev; + pnanovdb_uint32_t lower_off_table; + pnanovdb_uint32_t lower_size; + pnanovdb_uint32_t leaf_off_min; + pnanovdb_uint32_t leaf_off_max; + pnanovdb_uint32_t leaf_off_ave; + pnanovdb_uint32_t leaf_off_stddev; + pnanovdb_uint32_t leaf_off_table; + pnanovdb_uint32_t leaf_size; +}; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_grid_type_constants_t) + +// The following table with offsets will nedd to be updates as new GridTypes are added in NanoVDB.h +PNANOVDB_STATIC_CONST pnanovdb_grid_type_constants_t pnanovdb_grid_type_constants[PNANOVDB_GRID_TYPE_END] = +{ +{28, 28, 28, 28, 28, 32, 0, 8, 20, 32, 8224, 8224, 8224, 8224, 8224, 270368, 1056, 1056, 1056, 1056, 1056, 33824, 80, 80, 80, 80, 96, 96}, +{28, 32, 36, 40, 44, 64, 32, 8, 20, 32, 8224, 8228, 8232, 8236, 8256, 270400, 1056, 1060, 1064, 1068, 1088, 33856, 80, 84, 88, 92, 96, 2144}, +{32, 40, 48, 56, 64, 96, 64, 8, 24, 32, 8224, 8232, 8240, 8248, 8256, 270400, 1056, 1064, 1072, 1080, 1088, 33856, 80, 88, 96, 104, 128, 4224}, +{28, 30, 32, 36, 40, 64, 16, 8, 20, 32, 8224, 8226, 8228, 8232, 8256, 270400, 1056, 1058, 1060, 1064, 1088, 33856, 80, 82, 84, 88, 96, 1120}, +{28, 32, 36, 40, 44, 64, 32, 8, 20, 32, 8224, 8228, 8232, 8236, 8256, 270400, 1056, 1060, 1064, 1068, 1088, 33856, 80, 84, 88, 92, 96, 2144}, +{32, 40, 48, 56, 64, 96, 64, 8, 24, 32, 8224, 8232, 8240, 8248, 8256, 270400, 1056, 1064, 1072, 1080, 1088, 33856, 80, 88, 96, 104, 128, 4224}, +{28, 40, 52, 64, 68, 96, 96, 16, 20, 32, 8224, 8236, 8248, 8252, 8256, 532544, 1056, 1068, 1080, 1084, 1088, 66624, 80, 92, 104, 108, 128, 6272}, +{32, 56, 80, 104, 112, 128, 192, 24, 24, 64, 8224, 8248, 8272, 8280, 8288, 794720, 1056, 1080, 1104, 1112, 1120, 99424, 80, 104, 128, 136, 160, 12448}, +{28, 29, 30, 31, 32, 64, 0, 8, 20, 32, 8224, 8225, 8226, 8227, 8256, 270400, 1056, 1057, 1058, 1059, 1088, 33856, 80, 80, 80, 80, 96, 96}, +{28, 30, 32, 36, 40, 64, 16, 8, 20, 32, 8224, 8226, 8228, 8232, 8256, 270400, 1056, 1058, 1060, 1064, 1088, 33856, 80, 82, 84, 88, 96, 1120}, +{28, 32, 36, 40, 44, 64, 32, 8, 20, 32, 8224, 8228, 8232, 8236, 8256, 270400, 1056, 1060, 1064, 1068, 1088, 33856, 80, 84, 88, 92, 96, 2144}, +{28, 29, 30, 31, 32, 64, 1, 8, 20, 32, 8224, 8225, 8226, 8227, 8256, 270400, 1056, 1057, 1058, 1059, 1088, 33856, 80, 80, 80, 80, 96, 160}, +{28, 32, 36, 40, 44, 64, 32, 8, 20, 32, 8224, 8228, 8232, 8236, 8256, 270400, 1056, 1060, 1064, 1068, 1088, 33856, 80, 84, 88, 92, 96, 2144}, +{28, 32, 36, 40, 44, 64, 0, 8, 20, 32, 8224, 8228, 8232, 8236, 8256, 270400, 1056, 1060, 1064, 1068, 1088, 33856, 88, 90, 92, 94, 96, 352}, +{28, 32, 36, 40, 44, 64, 0, 8, 20, 32, 8224, 8228, 8232, 8236, 8256, 270400, 1056, 1060, 1064, 1068, 1088, 33856, 88, 90, 92, 94, 96, 608}, +{28, 32, 36, 40, 44, 64, 0, 8, 20, 32, 8224, 8228, 8232, 8236, 8256, 270400, 1056, 1060, 1064, 1068, 1088, 33856, 88, 90, 92, 94, 96, 1120}, +{28, 32, 36, 40, 44, 64, 0, 8, 20, 32, 8224, 8228, 8232, 8236, 8256, 270400, 1056, 1060, 1064, 1068, 1088, 33856, 88, 90, 92, 94, 96, 96}, +{28, 44, 60, 76, 80, 96, 128, 16, 20, 64, 8224, 8240, 8256, 8260, 8288, 532576, 1056, 1072, 1088, 1092, 1120, 66656, 80, 96, 112, 116, 128, 8320}, +{32, 64, 96, 128, 136, 160, 256, 32, 24, 64, 8224, 8256, 8288, 8296, 8320, 1056896, 1056, 1088, 1120, 1128, 1152, 132224, 80, 112, 144, 152, 160, 16544}, +{32, 40, 48, 56, 64, 96, 0, 8, 24, 32, 8224, 8232, 8240, 8248, 8256, 270400, 1056, 1064, 1072, 1080, 1088, 33856, 80, 80, 80, 80, 80, 96}, +{32, 40, 48, 56, 64, 96, 0, 8, 24, 32, 8224, 8232, 8240, 8248, 8256, 270400, 1056, 1064, 1072, 1080, 1088, 33856, 80, 80, 80, 80, 80, 96}, +{32, 40, 48, 56, 64, 96, 0, 8, 24, 32, 8224, 8232, 8240, 8248, 8256, 270400, 1056, 1064, 1072, 1080, 1088, 33856, 80, 80, 80, 80, 80, 160}, +{32, 40, 48, 56, 64, 96, 0, 8, 24, 32, 8224, 8232, 8240, 8248, 8256, 270400, 1056, 1064, 1072, 1080, 1088, 33856, 80, 80, 80, 80, 80, 160}, +{32, 40, 48, 56, 64, 96, 16, 8, 24, 32, 8224, 8232, 8240, 8248, 8256, 270400, 1056, 1064, 1072, 1080, 1088, 33856, 80, 88, 96, 96, 96, 1120}, +{28, 31, 34, 40, 44, 64, 24, 8, 20, 32, 8224, 8227, 8232, 8236, 8256, 270400, 1056, 1059, 1064, 1068, 1088, 33856, 80, 83, 88, 92, 96, 1632}, +{28, 34, 40, 48, 52, 64, 48, 8, 20, 32, 8224, 8230, 8236, 8240, 8256, 270400, 1056, 1062, 1068, 1072, 1088, 33856, 80, 86, 92, 96, 128, 3200}, +}; + +// ------------------------------------------------ Basic Lookup ----------------------------------------------------------- + +PNANOVDB_FORCE_INLINE pnanovdb_gridblindmetadata_handle_t pnanovdb_grid_get_gridblindmetadata(pnanovdb_buf_t buf, pnanovdb_grid_handle_t grid, pnanovdb_uint32_t index) +{ + pnanovdb_gridblindmetadata_handle_t meta = { grid.address }; + pnanovdb_uint64_t byte_offset = pnanovdb_grid_get_blind_metadata_offset(buf, grid); + meta.address = pnanovdb_address_offset64(meta.address, byte_offset); + meta.address = pnanovdb_address_offset_product(meta.address, PNANOVDB_GRIDBLINDMETADATA_SIZE, index); + return meta; +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_grid_get_gridblindmetadata_value_address(pnanovdb_buf_t buf, pnanovdb_grid_handle_t grid, pnanovdb_uint32_t index) +{ + pnanovdb_gridblindmetadata_handle_t meta = pnanovdb_grid_get_gridblindmetadata(buf, grid, index); + pnanovdb_int64_t byte_offset = pnanovdb_gridblindmetadata_get_byte_offset(buf, meta); + pnanovdb_address_t address = pnanovdb_address_offset64(meta.address, pnanovdb_int64_as_uint64(byte_offset)); + return address; +} + +PNANOVDB_FORCE_INLINE pnanovdb_tree_handle_t pnanovdb_grid_get_tree(pnanovdb_buf_t buf, pnanovdb_grid_handle_t grid) +{ + pnanovdb_tree_handle_t tree = { grid.address }; + tree.address = pnanovdb_address_offset(tree.address, PNANOVDB_GRID_SIZE); + return tree; +} + +PNANOVDB_FORCE_INLINE pnanovdb_root_handle_t pnanovdb_tree_get_root(pnanovdb_buf_t buf, pnanovdb_tree_handle_t tree) +{ + pnanovdb_root_handle_t root = { tree.address }; + pnanovdb_uint64_t byte_offset = pnanovdb_tree_get_node_offset_root(buf, tree); + root.address = pnanovdb_address_offset64(root.address, byte_offset); + return root; +} + +PNANOVDB_FORCE_INLINE pnanovdb_root_tile_handle_t pnanovdb_root_get_tile(pnanovdb_grid_type_t grid_type, pnanovdb_root_handle_t root, pnanovdb_uint32_t n) +{ + pnanovdb_root_tile_handle_t tile = { root.address }; + tile.address = pnanovdb_address_offset(tile.address, PNANOVDB_GRID_TYPE_GET(grid_type, root_size)); + tile.address = pnanovdb_address_offset_product(tile.address, PNANOVDB_GRID_TYPE_GET(grid_type, root_tile_size), n); + return tile; +} + +PNANOVDB_FORCE_INLINE pnanovdb_root_tile_handle_t pnanovdb_root_get_tile_zero(pnanovdb_grid_type_t grid_type, pnanovdb_root_handle_t root) +{ + pnanovdb_root_tile_handle_t tile = { root.address }; + tile.address = pnanovdb_address_offset(tile.address, PNANOVDB_GRID_TYPE_GET(grid_type, root_size)); + return tile; +} + +PNANOVDB_FORCE_INLINE pnanovdb_upper_handle_t pnanovdb_root_get_child(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, pnanovdb_root_tile_handle_t tile) +{ + pnanovdb_upper_handle_t upper = { root.address }; + upper.address = pnanovdb_address_offset64(upper.address, pnanovdb_int64_as_uint64(pnanovdb_root_tile_get_child(buf, tile))); + return upper; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_coord_to_key(PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ +#if defined(PNANOVDB_NATIVE_64) + pnanovdb_uint64_t iu = pnanovdb_int32_as_uint32(PNANOVDB_DEREF(ijk).x) >> 12u; + pnanovdb_uint64_t ju = pnanovdb_int32_as_uint32(PNANOVDB_DEREF(ijk).y) >> 12u; + pnanovdb_uint64_t ku = pnanovdb_int32_as_uint32(PNANOVDB_DEREF(ijk).z) >> 12u; + return (ku) | (ju << 21u) | (iu << 42u); +#else + pnanovdb_uint32_t iu = pnanovdb_int32_as_uint32(PNANOVDB_DEREF(ijk).x) >> 12u; + pnanovdb_uint32_t ju = pnanovdb_int32_as_uint32(PNANOVDB_DEREF(ijk).y) >> 12u; + pnanovdb_uint32_t ku = pnanovdb_int32_as_uint32(PNANOVDB_DEREF(ijk).z) >> 12u; + pnanovdb_uint32_t key_x = ku | (ju << 21); + pnanovdb_uint32_t key_y = (iu << 10) | (ju >> 11); + return pnanovdb_uint32_as_uint64(key_x, key_y); +#endif +} + +PNANOVDB_FORCE_INLINE pnanovdb_root_tile_handle_t pnanovdb_root_find_tile(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + pnanovdb_uint32_t tile_count = pnanovdb_uint32_as_int32(pnanovdb_root_get_tile_count(buf, root)); + pnanovdb_root_tile_handle_t tile = pnanovdb_root_get_tile_zero(grid_type, root); + pnanovdb_uint64_t key = pnanovdb_coord_to_key(ijk); + for (pnanovdb_uint32_t i = 0u; i < tile_count; i++) + { + if (pnanovdb_uint64_is_equal(key, pnanovdb_root_tile_get_key(buf, tile))) + { + return tile; + } + tile.address = pnanovdb_address_offset(tile.address, PNANOVDB_GRID_TYPE_GET(grid_type, root_tile_size)); + } + pnanovdb_root_tile_handle_t null_handle = { pnanovdb_address_null() }; + return null_handle; +} + +// ----------------------------- Leaf Node --------------------------------------- + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_leaf_coord_to_offset(PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + return (((PNANOVDB_DEREF(ijk).x & 7) >> 0) << (2 * 3)) + + (((PNANOVDB_DEREF(ijk).y & 7) >> 0) << (3)) + + ((PNANOVDB_DEREF(ijk).z & 7) >> 0); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_leaf_get_min_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t node) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, leaf_off_min); + return pnanovdb_address_offset(node.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_leaf_get_max_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t node) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, leaf_off_max); + return pnanovdb_address_offset(node.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_leaf_get_ave_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t node) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, leaf_off_ave); + return pnanovdb_address_offset(node.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_leaf_get_stddev_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t node) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, leaf_off_stddev); + return pnanovdb_address_offset(node.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_leaf_get_table_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t node, pnanovdb_uint32_t n) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, leaf_off_table) + ((PNANOVDB_GRID_TYPE_GET(grid_type, value_stride_bits) * n) >> 3u); + return pnanovdb_address_offset(node.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_leaf_get_value_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + pnanovdb_uint32_t n = pnanovdb_leaf_coord_to_offset(ijk); + return pnanovdb_leaf_get_table_address(grid_type, buf, leaf, n); +} + +// ----------------------------- Leaf FP Types Specialization --------------------------------------- + +PNANOVDB_FORCE_INLINE float pnanovdb_leaf_fp_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk, pnanovdb_uint32_t value_log_bits) +{ + // value_log_bits // 2 3 4 + pnanovdb_uint32_t value_bits = 1u << value_log_bits; // 4 8 16 + pnanovdb_uint32_t value_mask = (1u << value_bits) - 1u; // 0xF 0xFF 0xFFFF + pnanovdb_uint32_t values_per_word_bits = 5u - value_log_bits; // 3 2 1 + pnanovdb_uint32_t values_per_word_mask = (1u << values_per_word_bits) - 1u; // 7 3 1 + + pnanovdb_uint32_t n = pnanovdb_leaf_coord_to_offset(ijk); + float minimum = pnanovdb_read_float(buf, pnanovdb_address_offset_neg(address, PNANOVDB_LEAF_TABLE_NEG_OFF_MINIMUM)); + float quantum = pnanovdb_read_float(buf, pnanovdb_address_offset_neg(address, PNANOVDB_LEAF_TABLE_NEG_OFF_QUANTUM)); + pnanovdb_uint32_t raw = pnanovdb_read_uint32(buf, pnanovdb_address_offset(address, ((n >> values_per_word_bits) << 2u))); + pnanovdb_uint32_t value_compressed = (raw >> ((n & values_per_word_mask) << value_log_bits)) & value_mask; + return pnanovdb_uint32_to_float(value_compressed) * quantum + minimum; +} + +PNANOVDB_FORCE_INLINE float pnanovdb_leaf_fp4_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + return pnanovdb_leaf_fp_read_float(buf, address, ijk, 2u); +} + +PNANOVDB_FORCE_INLINE float pnanovdb_leaf_fp8_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + return pnanovdb_leaf_fp_read_float(buf, address, ijk, 3u); +} + +PNANOVDB_FORCE_INLINE float pnanovdb_leaf_fp16_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + return pnanovdb_leaf_fp_read_float(buf, address, ijk, 4u); +} + +PNANOVDB_FORCE_INLINE float pnanovdb_leaf_fpn_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + pnanovdb_uint32_t bbox_dif_and_flags = pnanovdb_read_uint32(buf, pnanovdb_address_offset_neg(address, PNANOVDB_LEAF_TABLE_NEG_OFF_BBOX_DIF_AND_FLAGS)); + pnanovdb_uint32_t flags = bbox_dif_and_flags >> 24u; + pnanovdb_uint32_t value_log_bits = flags >> 5; // b = 0, 1, 2, 3, 4 corresponding to 1, 2, 4, 8, 16 bits + return pnanovdb_leaf_fp_read_float(buf, address, ijk, value_log_bits); +} + +// ----------------------------- Leaf Index Specialization --------------------------------------- + +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_leaf_index_has_stats(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf) +{ + return (pnanovdb_leaf_get_bbox_dif_and_flags(buf, leaf) & (1u << 28u)) != 0u; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_index_get_min_index(pnanovdb_buf_t buf, pnanovdb_address_t min_address) +{ + return pnanovdb_uint64_offset(pnanovdb_read_uint64(buf, min_address), 512u); +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_index_get_max_index(pnanovdb_buf_t buf, pnanovdb_address_t max_address) +{ + return pnanovdb_uint64_offset(pnanovdb_read_uint64(buf, max_address), 513u); +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_index_get_ave_index(pnanovdb_buf_t buf, pnanovdb_address_t ave_address) +{ + return pnanovdb_uint64_offset(pnanovdb_read_uint64(buf, ave_address), 514u); +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_index_get_dev_index(pnanovdb_buf_t buf, pnanovdb_address_t dev_address) +{ + return pnanovdb_uint64_offset(pnanovdb_read_uint64(buf, dev_address), 515u); +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_index_get_value_index(pnanovdb_buf_t buf, pnanovdb_address_t value_address, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + pnanovdb_uint32_t n = pnanovdb_leaf_coord_to_offset(ijk); + pnanovdb_uint64_t offset = pnanovdb_read_uint64(buf, value_address); + return pnanovdb_uint64_offset(offset, n); +} + +// ----------------------------- Leaf IndexMask Specialization --------------------------------------- + +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_leaf_indexmask_has_stats(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf) +{ + return pnanovdb_leaf_index_has_stats(buf, leaf); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_indexmask_get_min_index(pnanovdb_buf_t buf, pnanovdb_address_t min_address) +{ + return pnanovdb_leaf_index_get_min_index(buf, min_address); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_indexmask_get_max_index(pnanovdb_buf_t buf, pnanovdb_address_t max_address) +{ + return pnanovdb_leaf_index_get_max_index(buf, max_address); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_indexmask_get_ave_index(pnanovdb_buf_t buf, pnanovdb_address_t ave_address) +{ + return pnanovdb_leaf_index_get_ave_index(buf, ave_address); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_indexmask_get_dev_index(pnanovdb_buf_t buf, pnanovdb_address_t dev_address) +{ + return pnanovdb_leaf_index_get_dev_index(buf, dev_address); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_indexmask_get_value_index(pnanovdb_buf_t buf, pnanovdb_address_t value_address, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + return pnanovdb_leaf_index_get_value_index(buf, value_address, ijk); +} +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_leaf_indexmask_get_mask_bit(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, pnanovdb_uint32_t n) +{ + pnanovdb_uint32_t word_idx = n >> 5; + pnanovdb_uint32_t bit_idx = n & 31; + pnanovdb_uint32_t val_mask = + pnanovdb_read_uint32(buf, pnanovdb_address_offset(leaf.address, 96u + 4u * word_idx)); + return (val_mask & (1u << bit_idx)) != 0u; +} +PNANOVDB_FORCE_INLINE void pnanovdb_leaf_indexmask_set_mask_bit(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, pnanovdb_uint32_t n, pnanovdb_bool_t v) +{ + pnanovdb_uint32_t word_idx = n >> 5; + pnanovdb_uint32_t bit_idx = n & 31; + pnanovdb_uint32_t val_mask = + pnanovdb_read_uint32(buf, pnanovdb_address_offset(leaf.address, 96u + 4u * word_idx)); + if (v) + { + val_mask = val_mask | (1u << bit_idx); + } + else + { + val_mask = val_mask & ~(1u << bit_idx); + } + pnanovdb_write_uint32(buf, pnanovdb_address_offset(leaf.address, 96u + 4u * word_idx), val_mask); +} + +// ----------------------------- Leaf OnIndex Specialization --------------------------------------- + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_leaf_onindex_get_value_count(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf) +{ + pnanovdb_uint64_t val_mask = pnanovdb_read_uint64(buf, pnanovdb_address_offset(leaf.address, PNANOVDB_LEAF_OFF_VALUE_MASK + 8u * 7u)); + pnanovdb_uint64_t prefix_sum = pnanovdb_read_uint64( + buf, pnanovdb_address_offset(leaf.address, PNANOVDB_GRID_TYPE_GET(PNANOVDB_GRID_TYPE_ONINDEX, leaf_off_table) + 8u)); + return pnanovdb_uint64_countbits(val_mask) + (pnanovdb_uint64_to_uint32_lsr(prefix_sum, 54u) & 511u); +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_onindex_get_last_offset(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf) +{ + return pnanovdb_uint64_offset( + pnanovdb_read_uint64(buf, pnanovdb_address_offset(leaf.address, PNANOVDB_GRID_TYPE_GET(PNANOVDB_GRID_TYPE_ONINDEX, leaf_off_table))), + pnanovdb_leaf_onindex_get_value_count(buf, leaf) - 1u); +} + +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_leaf_onindex_has_stats(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf) +{ + return (pnanovdb_leaf_get_bbox_dif_and_flags(buf, leaf) & (1u << 28u)) != 0u; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_onindex_get_min_index(pnanovdb_buf_t buf, pnanovdb_address_t min_address) +{ + pnanovdb_leaf_handle_t leaf = { pnanovdb_address_offset_neg(min_address, PNANOVDB_GRID_TYPE_GET(PNANOVDB_GRID_TYPE_ONINDEX, leaf_off_table)) }; + pnanovdb_uint64_t idx = pnanovdb_uint32_as_uint64_low(0u); + if (pnanovdb_leaf_onindex_has_stats(buf, leaf)) + { + idx = pnanovdb_uint64_offset(pnanovdb_leaf_onindex_get_last_offset(buf, leaf), 1u); + } + return idx; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_onindex_get_max_index(pnanovdb_buf_t buf, pnanovdb_address_t max_address) +{ + pnanovdb_leaf_handle_t leaf = { pnanovdb_address_offset_neg(max_address, PNANOVDB_GRID_TYPE_GET(PNANOVDB_GRID_TYPE_ONINDEX, leaf_off_table)) }; + pnanovdb_uint64_t idx = pnanovdb_uint32_as_uint64_low(0u); + if (pnanovdb_leaf_onindex_has_stats(buf, leaf)) + { + idx = pnanovdb_uint64_offset(pnanovdb_leaf_onindex_get_last_offset(buf, leaf), 2u); + } + return idx; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_onindex_get_ave_index(pnanovdb_buf_t buf, pnanovdb_address_t ave_address) +{ + pnanovdb_leaf_handle_t leaf = { pnanovdb_address_offset_neg(ave_address, PNANOVDB_GRID_TYPE_GET(PNANOVDB_GRID_TYPE_ONINDEX, leaf_off_table)) }; + pnanovdb_uint64_t idx = pnanovdb_uint32_as_uint64_low(0u); + if (pnanovdb_leaf_onindex_has_stats(buf, leaf)) + { + idx = pnanovdb_uint64_offset(pnanovdb_leaf_onindex_get_last_offset(buf, leaf), 3u); + } + return idx; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_onindex_get_dev_index(pnanovdb_buf_t buf, pnanovdb_address_t dev_address) +{ + pnanovdb_leaf_handle_t leaf = { pnanovdb_address_offset_neg(dev_address, PNANOVDB_GRID_TYPE_GET(PNANOVDB_GRID_TYPE_ONINDEX, leaf_off_table)) }; + pnanovdb_uint64_t idx = pnanovdb_uint32_as_uint64_low(0u); + if (pnanovdb_leaf_onindex_has_stats(buf, leaf)) + { + idx = pnanovdb_uint64_offset(pnanovdb_leaf_onindex_get_last_offset(buf, leaf), 4u); + } + return idx; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_onindex_get_value_index(pnanovdb_buf_t buf, pnanovdb_address_t value_address, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + pnanovdb_uint32_t n = pnanovdb_leaf_coord_to_offset(ijk); + pnanovdb_leaf_handle_t leaf = { pnanovdb_address_offset_neg(value_address, PNANOVDB_GRID_TYPE_GET(PNANOVDB_GRID_TYPE_ONINDEX, leaf_off_table)) }; + + pnanovdb_uint32_t word_idx = n >> 6u; + pnanovdb_uint32_t bit_idx = n & 63u; + pnanovdb_uint64_t val_mask = pnanovdb_read_uint64(buf, pnanovdb_address_offset(leaf.address, PNANOVDB_LEAF_OFF_VALUE_MASK + 8u * word_idx)); + pnanovdb_uint64_t mask = pnanovdb_uint64_bit_mask(bit_idx); + pnanovdb_uint64_t value_index = pnanovdb_uint32_as_uint64_low(0u); + if (pnanovdb_uint64_any_bit(pnanovdb_uint64_and(val_mask, mask))) + { + pnanovdb_uint32_t sum = 0u; + sum += pnanovdb_uint64_countbits(pnanovdb_uint64_and(val_mask, pnanovdb_uint64_dec(mask))); + if (word_idx > 0u) + { + pnanovdb_uint64_t prefix_sum = pnanovdb_read_uint64(buf, pnanovdb_address_offset(value_address, 8u)); + sum += pnanovdb_uint64_to_uint32_lsr(prefix_sum, 9u * (word_idx - 1u)) & 511u; + } + pnanovdb_uint64_t offset = pnanovdb_read_uint64(buf, value_address); + value_index = pnanovdb_uint64_offset(offset, sum); + } + return value_index; +} + +// ----------------------------- Leaf OnIndexMask Specialization --------------------------------------- + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_leaf_onindexmask_get_value_count(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf) +{ + return pnanovdb_leaf_onindex_get_value_count(buf, leaf); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_onindexmask_get_last_offset(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf) +{ + return pnanovdb_leaf_onindex_get_last_offset(buf, leaf); +} +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_leaf_onindexmask_has_stats(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf) +{ + return pnanovdb_leaf_onindex_has_stats(buf, leaf); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_onindexmask_get_min_index(pnanovdb_buf_t buf, pnanovdb_address_t min_address) +{ + return pnanovdb_leaf_onindex_get_min_index(buf, min_address); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_onindexmask_get_max_index(pnanovdb_buf_t buf, pnanovdb_address_t max_address) +{ + return pnanovdb_leaf_onindex_get_max_index(buf, max_address); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_onindexmask_get_ave_index(pnanovdb_buf_t buf, pnanovdb_address_t ave_address) +{ + return pnanovdb_leaf_onindex_get_ave_index(buf, ave_address); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_onindexmask_get_dev_index(pnanovdb_buf_t buf, pnanovdb_address_t dev_address) +{ + return pnanovdb_leaf_onindex_get_dev_index(buf, dev_address); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_onindexmask_get_value_index(pnanovdb_buf_t buf, pnanovdb_address_t value_address, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + return pnanovdb_leaf_onindex_get_value_index(buf, value_address, ijk); +} +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_leaf_onindexmask_get_mask_bit(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, pnanovdb_uint32_t n) +{ + pnanovdb_uint32_t word_idx = n >> 5; + pnanovdb_uint32_t bit_idx = n & 31; + pnanovdb_uint32_t val_mask = + pnanovdb_read_uint32(buf, pnanovdb_address_offset(leaf.address, 96u + 4u * word_idx)); + return (val_mask & (1u << bit_idx)) != 0u; +} +PNANOVDB_FORCE_INLINE void pnanovdb_leaf_onindexmask_set_mask_bit(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, pnanovdb_uint32_t n, pnanovdb_bool_t v) +{ + pnanovdb_uint32_t word_idx = n >> 5; + pnanovdb_uint32_t bit_idx = n & 31; + pnanovdb_uint32_t val_mask = + pnanovdb_read_uint32(buf, pnanovdb_address_offset(leaf.address, 96u + 4u * word_idx)); + if (v) + { + val_mask = val_mask | (1u << bit_idx); + } + else + { + val_mask = val_mask & ~(1u << bit_idx); + } + pnanovdb_write_uint32(buf, pnanovdb_address_offset(leaf.address, 96u + 4u * word_idx), val_mask); +} + +// ----------------------------- Leaf PointIndex Specialization --------------------------------------- + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_pointindex_get_offset(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf) +{ + return pnanovdb_read_uint64(buf, pnanovdb_leaf_get_min_address(PNANOVDB_GRID_TYPE_POINTINDEX, buf, leaf)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_pointindex_get_point_count(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf) +{ + return pnanovdb_read_uint64(buf, pnanovdb_leaf_get_max_address(PNANOVDB_GRID_TYPE_POINTINDEX, buf, leaf)); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_pointindex_get_first(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, pnanovdb_uint32_t i) +{ + return pnanovdb_uint64_offset(pnanovdb_leaf_pointindex_get_offset(buf, leaf), + (i == 0u ? 0u : pnanovdb_read_uint16(buf, pnanovdb_leaf_get_table_address(PNANOVDB_GRID_TYPE_POINTINDEX, buf, leaf, i - 1u)))); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_pointindex_get_last(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, pnanovdb_uint32_t i) +{ + return pnanovdb_uint64_offset(pnanovdb_leaf_pointindex_get_offset(buf, leaf), + pnanovdb_read_uint16(buf, pnanovdb_leaf_get_table_address(PNANOVDB_GRID_TYPE_POINTINDEX, buf, leaf, i))); +} +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_leaf_pointindex_get_value(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, pnanovdb_uint32_t i) +{ + return pnanovdb_uint32_as_uint64_low(pnanovdb_read_uint16(buf, pnanovdb_leaf_get_table_address(PNANOVDB_GRID_TYPE_POINTINDEX, buf, leaf, i))); +} +PNANOVDB_FORCE_INLINE void pnanovdb_leaf_pointindex_set_value_only(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, pnanovdb_uint32_t i, pnanovdb_uint32_t value) +{ + pnanovdb_address_t addr = pnanovdb_leaf_get_table_address(PNANOVDB_GRID_TYPE_POINTINDEX, buf, leaf, i); + pnanovdb_uint32_t raw32 = pnanovdb_read_uint32(buf, pnanovdb_address_mask_inv(addr, 3u)); + if ((i & 1) == 0u) + { + raw32 = (raw32 & 0xFFFF0000) | (value & 0x0000FFFF); + } + else + { + raw32 = (raw32 & 0x0000FFFF) | (value << 16u); + } + pnanovdb_write_uint32(buf, addr, raw32); +} +PNANOVDB_FORCE_INLINE void pnanovdb_leaf_pointindex_set_on(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, pnanovdb_uint32_t i) +{ + pnanovdb_uint32_t word_idx = i >> 5; + pnanovdb_uint32_t bit_idx = i & 31; + pnanovdb_address_t addr = pnanovdb_address_offset(leaf.address, PNANOVDB_LEAF_OFF_VALUE_MASK + 4u * word_idx); + pnanovdb_uint32_t val_mask = pnanovdb_read_uint32(buf, addr); + val_mask = val_mask | (1u << bit_idx); + pnanovdb_write_uint32(buf, addr, val_mask); +} +PNANOVDB_FORCE_INLINE void pnanovdb_leaf_pointindex_set_value(pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, pnanovdb_uint32_t i, pnanovdb_uint32_t value) +{ + pnanovdb_leaf_pointindex_set_on(buf, leaf, i); + pnanovdb_leaf_pointindex_set_value_only(buf, leaf, i, value); +} + +// ------------------------------------------------ Lower Node ----------------------------------------------------------- + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_lower_coord_to_offset(PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + return (((PNANOVDB_DEREF(ijk).x & 127) >> 3) << (2 * 4)) + + (((PNANOVDB_DEREF(ijk).y & 127) >> 3) << (4)) + + ((PNANOVDB_DEREF(ijk).z & 127) >> 3); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_min_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t node) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, lower_off_min); + return pnanovdb_address_offset(node.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_max_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t node) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, lower_off_max); + return pnanovdb_address_offset(node.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_ave_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t node) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, lower_off_ave); + return pnanovdb_address_offset(node.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_stddev_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t node) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, lower_off_stddev); + return pnanovdb_address_offset(node.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_table_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t node, pnanovdb_uint32_t n) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, lower_off_table) + PNANOVDB_GRID_TYPE_GET(grid_type, table_stride) * n; + return pnanovdb_address_offset(node.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_int64_t pnanovdb_lower_get_table_child(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t node, pnanovdb_uint32_t n) +{ + pnanovdb_address_t table_address = pnanovdb_lower_get_table_address(grid_type, buf, node, n); + return pnanovdb_read_int64(buf, table_address); +} + +PNANOVDB_FORCE_INLINE pnanovdb_leaf_handle_t pnanovdb_lower_get_child(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t lower, pnanovdb_uint32_t n) +{ + pnanovdb_leaf_handle_t leaf = { lower.address }; + leaf.address = pnanovdb_address_offset64(leaf.address, pnanovdb_int64_as_uint64(pnanovdb_lower_get_table_child(grid_type, buf, lower, n))); + return leaf; +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_value_address_and_level(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t lower, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_uint32_t) level) +{ + pnanovdb_uint32_t n = pnanovdb_lower_coord_to_offset(ijk); + pnanovdb_address_t value_address; + if (pnanovdb_lower_get_child_mask(buf, lower, n)) + { + pnanovdb_leaf_handle_t child = pnanovdb_lower_get_child(grid_type, buf, lower, n); + value_address = pnanovdb_leaf_get_value_address(grid_type, buf, child, ijk); + PNANOVDB_DEREF(level) = 0u; + } + else + { + value_address = pnanovdb_lower_get_table_address(grid_type, buf, lower, n); + PNANOVDB_DEREF(level) = 1u; + } + return value_address; +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_value_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t lower, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + pnanovdb_uint32_t level; + return pnanovdb_lower_get_value_address_and_level(grid_type, buf, lower, ijk, PNANOVDB_REF(level)); +} + +// ------------------------------------------------ Upper Node ----------------------------------------------------------- + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_upper_coord_to_offset(PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + return (((PNANOVDB_DEREF(ijk).x & 4095) >> 7) << (2 * 5)) + + (((PNANOVDB_DEREF(ijk).y & 4095) >> 7) << (5)) + + ((PNANOVDB_DEREF(ijk).z & 4095) >> 7); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_min_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t node) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, upper_off_min); + return pnanovdb_address_offset(node.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_max_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t node) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, upper_off_max); + return pnanovdb_address_offset(node.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_ave_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t node) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, upper_off_ave); + return pnanovdb_address_offset(node.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_stddev_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t node) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, upper_off_stddev); + return pnanovdb_address_offset(node.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_table_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t node, pnanovdb_uint32_t n) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, upper_off_table) + PNANOVDB_GRID_TYPE_GET(grid_type, table_stride) * n; + return pnanovdb_address_offset(node.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_int64_t pnanovdb_upper_get_table_child(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t node, pnanovdb_uint32_t n) +{ + pnanovdb_address_t bufAddress = pnanovdb_upper_get_table_address(grid_type, buf, node, n); + return pnanovdb_read_int64(buf, bufAddress); +} + +PNANOVDB_FORCE_INLINE pnanovdb_lower_handle_t pnanovdb_upper_get_child(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t upper, pnanovdb_uint32_t n) +{ + pnanovdb_lower_handle_t lower = { upper.address }; + lower.address = pnanovdb_address_offset64(lower.address, pnanovdb_int64_as_uint64(pnanovdb_upper_get_table_child(grid_type, buf, upper, n))); + return lower; +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_value_address_and_level(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t upper, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_uint32_t) level) +{ + pnanovdb_uint32_t n = pnanovdb_upper_coord_to_offset(ijk); + pnanovdb_address_t value_address; + if (pnanovdb_upper_get_child_mask(buf, upper, n)) + { + pnanovdb_lower_handle_t child = pnanovdb_upper_get_child(grid_type, buf, upper, n); + value_address = pnanovdb_lower_get_value_address_and_level(grid_type, buf, child, ijk, level); + } + else + { + value_address = pnanovdb_upper_get_table_address(grid_type, buf, upper, n); + PNANOVDB_DEREF(level) = 2u; + } + return value_address; +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_value_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t upper, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + pnanovdb_uint32_t level; + return pnanovdb_upper_get_value_address_and_level(grid_type, buf, upper, ijk, PNANOVDB_REF(level)); +} + +PNANOVDB_FORCE_INLINE void pnanovdb_upper_set_table_child(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t node, pnanovdb_uint32_t n, pnanovdb_int64_t child) +{ + pnanovdb_address_t bufAddress = pnanovdb_upper_get_table_address(grid_type, buf, node, n); + pnanovdb_write_int64(buf, bufAddress, child); +} + +// ------------------------------------------------ Root ----------------------------------------------------------- + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_min_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, root_off_min); + return pnanovdb_address_offset(root.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_max_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, root_off_max); + return pnanovdb_address_offset(root.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_ave_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, root_off_ave); + return pnanovdb_address_offset(root.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_stddev_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, root_off_stddev); + return pnanovdb_address_offset(root.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_tile_get_value_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_tile_handle_t root_tile) +{ + pnanovdb_uint32_t byte_offset = PNANOVDB_GRID_TYPE_GET(grid_type, root_tile_off_value); + return pnanovdb_address_offset(root_tile.address, byte_offset); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_value_address_and_level(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_uint32_t) level) +{ + pnanovdb_root_tile_handle_t tile = pnanovdb_root_find_tile(grid_type, buf, root, ijk); + pnanovdb_address_t ret; + if (pnanovdb_address_is_null(tile.address)) + { + ret = pnanovdb_address_offset(root.address, PNANOVDB_GRID_TYPE_GET(grid_type, root_off_background)); + PNANOVDB_DEREF(level) = 4u; + } + else if (pnanovdb_int64_is_zero(pnanovdb_root_tile_get_child(buf, tile))) + { + ret = pnanovdb_address_offset(tile.address, PNANOVDB_GRID_TYPE_GET(grid_type, root_tile_off_value)); + PNANOVDB_DEREF(level) = 3u; + } + else + { + pnanovdb_upper_handle_t child = pnanovdb_root_get_child(grid_type, buf, root, tile); + ret = pnanovdb_upper_get_value_address_and_level(grid_type, buf, child, ijk, level); + } + return ret; +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_value_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + pnanovdb_uint32_t level; + return pnanovdb_root_get_value_address_and_level(grid_type, buf, root, ijk, PNANOVDB_REF(level)); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_value_address_bit(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_uint32_t) bit_index) +{ + pnanovdb_uint32_t level; + pnanovdb_address_t address = pnanovdb_root_get_value_address_and_level(grid_type, buf, root, ijk, PNANOVDB_REF(level)); + PNANOVDB_DEREF(bit_index) = level == 0u ? pnanovdb_int32_as_uint32(PNANOVDB_DEREF(ijk).x & 7) : 0u; + return address; +} + +PNANOVDB_FORCE_INLINE float pnanovdb_root_fp4_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk, pnanovdb_uint32_t level) +{ + float ret; + if (level == 0) + { + ret = pnanovdb_leaf_fp4_read_float(buf, address, ijk); + } + else + { + ret = pnanovdb_read_float(buf, address); + } + return ret; +} + +PNANOVDB_FORCE_INLINE float pnanovdb_root_fp8_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk, pnanovdb_uint32_t level) +{ + float ret; + if (level == 0) + { + ret = pnanovdb_leaf_fp8_read_float(buf, address, ijk); + } + else + { + ret = pnanovdb_read_float(buf, address); + } + return ret; +} + +PNANOVDB_FORCE_INLINE float pnanovdb_root_fp16_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk, pnanovdb_uint32_t level) +{ + float ret; + if (level == 0) + { + ret = pnanovdb_leaf_fp16_read_float(buf, address, ijk); + } + else + { + ret = pnanovdb_read_float(buf, address); + } + return ret; +} + +PNANOVDB_FORCE_INLINE float pnanovdb_root_fpn_read_float(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk, pnanovdb_uint32_t level) +{ + float ret; + if (level == 0) + { + ret = pnanovdb_leaf_fpn_read_float(buf, address, ijk); + } + else + { + ret = pnanovdb_read_float(buf, address); + } + return ret; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_root_index_get_value_index(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk, pnanovdb_uint32_t level) +{ + pnanovdb_uint64_t ret; + if (level == 0) + { + ret = pnanovdb_leaf_index_get_value_index(buf, address, ijk); + } + else + { + ret = pnanovdb_read_uint64(buf, address); + } + return ret; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_root_onindex_get_value_index(pnanovdb_buf_t buf, pnanovdb_address_t address, PNANOVDB_IN(pnanovdb_coord_t) ijk, pnanovdb_uint32_t level) +{ + pnanovdb_uint64_t ret; + if (level == 0) + { + ret = pnanovdb_leaf_onindex_get_value_index(buf, address, ijk); + } + else + { + ret = pnanovdb_read_uint64(buf, address); + } + return ret; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_root_pointindex_get_point_range( + pnanovdb_buf_t buf, + pnanovdb_address_t value_address, + PNANOVDB_IN(pnanovdb_coord_t) ijk, + pnanovdb_uint32_t level, + PNANOVDB_INOUT(pnanovdb_uint64_t)range_begin, + PNANOVDB_INOUT(pnanovdb_uint64_t)range_end +) +{ + pnanovdb_uint32_t local_range_begin = 0u; + pnanovdb_uint32_t local_range_end = 0u; + pnanovdb_uint64_t offset = pnanovdb_uint32_as_uint64_low(0u); + if (level == 0) + { + pnanovdb_uint32_t n = pnanovdb_leaf_coord_to_offset(ijk); + // recover leaf address + pnanovdb_leaf_handle_t leaf = { pnanovdb_address_offset_neg(value_address, PNANOVDB_GRID_TYPE_GET(PNANOVDB_GRID_TYPE_POINTINDEX, leaf_off_table) + 2u * n) }; + if (n > 0u) + { + local_range_begin = pnanovdb_read_uint16(buf, pnanovdb_address_offset_neg(value_address, 2u)); + } + local_range_end = pnanovdb_read_uint16(buf, value_address); + offset = pnanovdb_leaf_pointindex_get_offset(buf, leaf); + } + PNANOVDB_DEREF(range_begin) = pnanovdb_uint64_offset(offset, local_range_begin); + PNANOVDB_DEREF(range_end) = pnanovdb_uint64_offset(offset, local_range_end); + return pnanovdb_uint32_as_uint64_low(local_range_end - local_range_begin); +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint64_t pnanovdb_root_pointindex_get_point_address_range( + pnanovdb_buf_t buf, + pnanovdb_grid_type_t value_type, + pnanovdb_address_t value_address, + pnanovdb_address_t blindmetadata_value_address, + PNANOVDB_IN(pnanovdb_coord_t) ijk, + pnanovdb_uint32_t level, + PNANOVDB_INOUT(pnanovdb_address_t)address_begin, + PNANOVDB_INOUT(pnanovdb_address_t)address_end +) +{ + pnanovdb_uint64_t range_begin; + pnanovdb_uint64_t range_end; + pnanovdb_uint64_t range_size = pnanovdb_root_pointindex_get_point_range(buf, value_address, ijk, level, PNANOVDB_REF(range_begin), PNANOVDB_REF(range_end)); + + pnanovdb_uint32_t stride = 12u; // vec3f + if (value_type == PNANOVDB_GRID_TYPE_VEC3U8) + { + stride = 3u; + } + else if (value_type == PNANOVDB_GRID_TYPE_VEC3U16) + { + stride = 6u; + } + PNANOVDB_DEREF(address_begin) = pnanovdb_address_offset64_product(blindmetadata_value_address, range_begin, stride); + PNANOVDB_DEREF(address_end) = pnanovdb_address_offset64_product(blindmetadata_value_address, range_end, stride); + return range_size; +} + +// ------------------------------------------------ ReadAccessor ----------------------------------------------------------- + +struct pnanovdb_readaccessor_t +{ + pnanovdb_coord_t key; + pnanovdb_leaf_handle_t leaf; + pnanovdb_lower_handle_t lower; + pnanovdb_upper_handle_t upper; + pnanovdb_root_handle_t root; +}; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_readaccessor_t) + +PNANOVDB_FORCE_INLINE void pnanovdb_readaccessor_init(PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, pnanovdb_root_handle_t root) +{ + PNANOVDB_DEREF(acc).key.x = 0x7FFFFFFF; + PNANOVDB_DEREF(acc).key.y = 0x7FFFFFFF; + PNANOVDB_DEREF(acc).key.z = 0x7FFFFFFF; + PNANOVDB_DEREF(acc).leaf.address = pnanovdb_address_null(); + PNANOVDB_DEREF(acc).lower.address = pnanovdb_address_null(); + PNANOVDB_DEREF(acc).upper.address = pnanovdb_address_null(); + PNANOVDB_DEREF(acc).root = root; +} + +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_readaccessor_iscached0(PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, int dirty) +{ + if (pnanovdb_address_is_null(PNANOVDB_DEREF(acc).leaf.address)) { return PNANOVDB_FALSE; } + if ((dirty & ~((1u << 3) - 1u)) != 0) + { + PNANOVDB_DEREF(acc).leaf.address = pnanovdb_address_null(); + return PNANOVDB_FALSE; + } + return PNANOVDB_TRUE; +} +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_readaccessor_iscached1(PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, int dirty) +{ + if (pnanovdb_address_is_null(PNANOVDB_DEREF(acc).lower.address)) { return PNANOVDB_FALSE; } + if ((dirty & ~((1u << 7) - 1u)) != 0) + { + PNANOVDB_DEREF(acc).lower.address = pnanovdb_address_null(); + return PNANOVDB_FALSE; + } + return PNANOVDB_TRUE; +} +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_readaccessor_iscached2(PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, int dirty) +{ + if (pnanovdb_address_is_null(PNANOVDB_DEREF(acc).upper.address)) { return PNANOVDB_FALSE; } + if ((dirty & ~((1u << 12) - 1u)) != 0) + { + PNANOVDB_DEREF(acc).upper.address = pnanovdb_address_null(); + return PNANOVDB_FALSE; + } + return PNANOVDB_TRUE; +} +PNANOVDB_FORCE_INLINE int pnanovdb_readaccessor_computedirty(PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + return (PNANOVDB_DEREF(ijk).x ^ PNANOVDB_DEREF(acc).key.x) | (PNANOVDB_DEREF(ijk).y ^ PNANOVDB_DEREF(acc).key.y) | (PNANOVDB_DEREF(ijk).z ^ PNANOVDB_DEREF(acc).key.z); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_leaf_get_value_address_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) +{ + pnanovdb_uint32_t n = pnanovdb_leaf_coord_to_offset(ijk); + return pnanovdb_leaf_get_table_address(grid_type, buf, leaf, n); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_value_address_and_level_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t lower, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_INOUT(pnanovdb_uint32_t) level) +{ + pnanovdb_uint32_t n = pnanovdb_lower_coord_to_offset(ijk); + pnanovdb_address_t value_address; + if (pnanovdb_lower_get_child_mask(buf, lower, n)) + { + pnanovdb_leaf_handle_t child = pnanovdb_lower_get_child(grid_type, buf, lower, n); + PNANOVDB_DEREF(acc).leaf = child; + PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); + value_address = pnanovdb_leaf_get_value_address_and_cache(grid_type, buf, child, ijk, acc); + PNANOVDB_DEREF(level) = 0u; + } + else + { + value_address = pnanovdb_lower_get_table_address(grid_type, buf, lower, n); + PNANOVDB_DEREF(level) = 1u; + } + return value_address; +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_lower_get_value_address_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t lower, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) +{ + pnanovdb_uint32_t level; + return pnanovdb_lower_get_value_address_and_level_and_cache(grid_type, buf, lower, ijk, acc, PNANOVDB_REF(level)); +} + +PNANOVDB_FORCE_INLINE void pnanovdb_lower_set_table_child(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t node, pnanovdb_uint32_t n, pnanovdb_int64_t child) +{ + pnanovdb_address_t table_address = pnanovdb_lower_get_table_address(grid_type, buf, node, n); + pnanovdb_write_int64(buf, table_address, child); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_value_address_and_level_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t upper, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_INOUT(pnanovdb_uint32_t) level) +{ + pnanovdb_uint32_t n = pnanovdb_upper_coord_to_offset(ijk); + pnanovdb_address_t value_address; + if (pnanovdb_upper_get_child_mask(buf, upper, n)) + { + pnanovdb_lower_handle_t child = pnanovdb_upper_get_child(grid_type, buf, upper, n); + PNANOVDB_DEREF(acc).lower = child; + PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); + value_address = pnanovdb_lower_get_value_address_and_level_and_cache(grid_type, buf, child, ijk, acc, level); + } + else + { + value_address = pnanovdb_upper_get_table_address(grid_type, buf, upper, n); + PNANOVDB_DEREF(level) = 2u; + } + return value_address; +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_upper_get_value_address_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t upper, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) +{ + pnanovdb_uint32_t level; + return pnanovdb_upper_get_value_address_and_level_and_cache(grid_type, buf, upper, ijk, acc, PNANOVDB_REF(level)); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_value_address_and_level_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_INOUT(pnanovdb_uint32_t) level) +{ + pnanovdb_root_tile_handle_t tile = pnanovdb_root_find_tile(grid_type, buf, root, ijk); + pnanovdb_address_t ret; + if (pnanovdb_address_is_null(tile.address)) + { + ret = pnanovdb_address_offset(root.address, PNANOVDB_GRID_TYPE_GET(grid_type, root_off_background)); + PNANOVDB_DEREF(level) = 4u; + } + else if (pnanovdb_int64_is_zero(pnanovdb_root_tile_get_child(buf, tile))) + { + ret = pnanovdb_address_offset(tile.address, PNANOVDB_GRID_TYPE_GET(grid_type, root_tile_off_value)); + PNANOVDB_DEREF(level) = 3u; + } + else + { + pnanovdb_upper_handle_t child = pnanovdb_root_get_child(grid_type, buf, root, tile); + PNANOVDB_DEREF(acc).upper = child; + PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); + ret = pnanovdb_upper_get_value_address_and_level_and_cache(grid_type, buf, child, ijk, acc, level); + } + return ret; +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_root_get_value_address_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) +{ + pnanovdb_uint32_t level; + return pnanovdb_root_get_value_address_and_level_and_cache(grid_type, buf, root, ijk, acc, PNANOVDB_REF(level)); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_readaccessor_get_value_address_and_level(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_uint32_t) level) +{ + int dirty = pnanovdb_readaccessor_computedirty(acc, ijk); + + pnanovdb_address_t value_address; + if (pnanovdb_readaccessor_iscached0(acc, dirty)) + { + value_address = pnanovdb_leaf_get_value_address_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).leaf, ijk, acc); + PNANOVDB_DEREF(level) = 0u; + } + else if (pnanovdb_readaccessor_iscached1(acc, dirty)) + { + value_address = pnanovdb_lower_get_value_address_and_level_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).lower, ijk, acc, level); + } + else if (pnanovdb_readaccessor_iscached2(acc, dirty)) + { + value_address = pnanovdb_upper_get_value_address_and_level_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).upper, ijk, acc, level); + } + else + { + value_address = pnanovdb_root_get_value_address_and_level_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).root, ijk, acc, level); + } + return value_address; +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_readaccessor_get_value_address(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + pnanovdb_uint32_t level; + return pnanovdb_readaccessor_get_value_address_and_level(grid_type, buf, acc, ijk, PNANOVDB_REF(level)); +} + +PNANOVDB_FORCE_INLINE pnanovdb_address_t pnanovdb_readaccessor_get_value_address_bit(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_uint32_t) bit_index) +{ + pnanovdb_uint32_t level; + pnanovdb_address_t address = pnanovdb_readaccessor_get_value_address_and_level(grid_type, buf, acc, ijk, PNANOVDB_REF(level)); + PNANOVDB_DEREF(bit_index) = level == 0u ? pnanovdb_int32_as_uint32(PNANOVDB_DEREF(ijk).x & 7) : 0u; + return address; +} + +// ------------------------------------------------ ReadAccessor GetDim ----------------------------------------------------------- + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_leaf_get_dim_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) +{ + return 1u; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_lower_get_dim_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t lower, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) +{ + pnanovdb_uint32_t n = pnanovdb_lower_coord_to_offset(ijk); + pnanovdb_uint32_t ret; + if (pnanovdb_lower_get_child_mask(buf, lower, n)) + { + pnanovdb_leaf_handle_t child = pnanovdb_lower_get_child(grid_type, buf, lower, n); + PNANOVDB_DEREF(acc).leaf = child; + PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); + ret = pnanovdb_leaf_get_dim_and_cache(grid_type, buf, child, ijk, acc); + } + else + { + ret = (1u << (3u)); // node 0 dim + } + return ret; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_upper_get_dim_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t upper, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) +{ + pnanovdb_uint32_t n = pnanovdb_upper_coord_to_offset(ijk); + pnanovdb_uint32_t ret; + if (pnanovdb_upper_get_child_mask(buf, upper, n)) + { + pnanovdb_lower_handle_t child = pnanovdb_upper_get_child(grid_type, buf, upper, n); + PNANOVDB_DEREF(acc).lower = child; + PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); + ret = pnanovdb_lower_get_dim_and_cache(grid_type, buf, child, ijk, acc); + } + else + { + ret = (1u << (4u + 3u)); // node 1 dim + } + return ret; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_root_get_dim_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) +{ + pnanovdb_root_tile_handle_t tile = pnanovdb_root_find_tile(grid_type, buf, root, ijk); + pnanovdb_uint32_t ret; + if (pnanovdb_address_is_null(tile.address)) + { + ret = 1u << (5u + 4u + 3u); // background, node 2 dim + } + else if (pnanovdb_int64_is_zero(pnanovdb_root_tile_get_child(buf, tile))) + { + ret = 1u << (5u + 4u + 3u); // tile value, node 2 dim + } + else + { + pnanovdb_upper_handle_t child = pnanovdb_root_get_child(grid_type, buf, root, tile); + PNANOVDB_DEREF(acc).upper = child; + PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); + ret = pnanovdb_upper_get_dim_and_cache(grid_type, buf, child, ijk, acc); + } + return ret; +} + +PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_readaccessor_get_dim(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + int dirty = pnanovdb_readaccessor_computedirty(acc, ijk); + + pnanovdb_uint32_t dim; + if (pnanovdb_readaccessor_iscached0(acc, dirty)) + { + dim = pnanovdb_leaf_get_dim_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).leaf, ijk, acc); + } + else if (pnanovdb_readaccessor_iscached1(acc, dirty)) + { + dim = pnanovdb_lower_get_dim_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).lower, ijk, acc); + } + else if (pnanovdb_readaccessor_iscached2(acc, dirty)) + { + dim = pnanovdb_upper_get_dim_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).upper, ijk, acc); + } + else + { + dim = pnanovdb_root_get_dim_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).root, ijk, acc); + } + return dim; +} + +// ------------------------------------------------ ReadAccessor IsActive ----------------------------------------------------------- + +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_leaf_is_active_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_leaf_handle_t leaf, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) +{ + pnanovdb_uint32_t n = pnanovdb_leaf_coord_to_offset(ijk); + return pnanovdb_leaf_get_value_mask(buf, leaf, n); +} + +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_lower_is_active_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_lower_handle_t lower, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) +{ + pnanovdb_uint32_t n = pnanovdb_lower_coord_to_offset(ijk); + pnanovdb_bool_t is_active; + if (pnanovdb_lower_get_child_mask(buf, lower, n)) + { + pnanovdb_leaf_handle_t child = pnanovdb_lower_get_child(grid_type, buf, lower, n); + PNANOVDB_DEREF(acc).leaf = child; + PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); + is_active = pnanovdb_leaf_is_active_and_cache(grid_type, buf, child, ijk, acc); + } + else + { + is_active = pnanovdb_lower_get_value_mask(buf, lower, n); + } + return is_active; +} + +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_upper_is_active_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_upper_handle_t upper, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) +{ + pnanovdb_uint32_t n = pnanovdb_upper_coord_to_offset(ijk); + pnanovdb_bool_t is_active; + if (pnanovdb_upper_get_child_mask(buf, upper, n)) + { + pnanovdb_lower_handle_t child = pnanovdb_upper_get_child(grid_type, buf, upper, n); + PNANOVDB_DEREF(acc).lower = child; + PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); + is_active = pnanovdb_lower_is_active_and_cache(grid_type, buf, child, ijk, acc); + } + else + { + is_active = pnanovdb_upper_get_value_mask(buf, upper, n); + } + return is_active; +} + +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_root_is_active_and_cache(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, pnanovdb_root_handle_t root, PNANOVDB_IN(pnanovdb_coord_t) ijk, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc) +{ + pnanovdb_root_tile_handle_t tile = pnanovdb_root_find_tile(grid_type, buf, root, ijk); + pnanovdb_bool_t is_active; + if (pnanovdb_address_is_null(tile.address)) + { + is_active = PNANOVDB_FALSE; // background + } + else if (pnanovdb_int64_is_zero(pnanovdb_root_tile_get_child(buf, tile))) + { + pnanovdb_uint32_t state = pnanovdb_root_tile_get_state(buf, tile); + is_active = state != 0u; // tile value + } + else + { + pnanovdb_upper_handle_t child = pnanovdb_root_get_child(grid_type, buf, root, tile); + PNANOVDB_DEREF(acc).upper = child; + PNANOVDB_DEREF(acc).key = PNANOVDB_DEREF(ijk); + is_active = pnanovdb_upper_is_active_and_cache(grid_type, buf, child, ijk, acc); + } + return is_active; +} + +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_readaccessor_is_active(pnanovdb_grid_type_t grid_type, pnanovdb_buf_t buf, PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, PNANOVDB_IN(pnanovdb_coord_t) ijk) +{ + int dirty = pnanovdb_readaccessor_computedirty(acc, ijk); + + pnanovdb_bool_t is_active; + if (pnanovdb_readaccessor_iscached0(acc, dirty)) + { + is_active = pnanovdb_leaf_is_active_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).leaf, ijk, acc); + } + else if (pnanovdb_readaccessor_iscached1(acc, dirty)) + { + is_active = pnanovdb_lower_is_active_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).lower, ijk, acc); + } + else if (pnanovdb_readaccessor_iscached2(acc, dirty)) + { + is_active = pnanovdb_upper_is_active_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).upper, ijk, acc); + } + else + { + is_active = pnanovdb_root_is_active_and_cache(grid_type, buf, PNANOVDB_DEREF(acc).root, ijk, acc); + } + return is_active; +} + +// ------------------------------------------------ Map Transforms ----------------------------------------------------------- + +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_map_apply(pnanovdb_buf_t buf, pnanovdb_map_handle_t map, PNANOVDB_IN(pnanovdb_vec3_t) src) +{ + pnanovdb_vec3_t dst; + float sx = PNANOVDB_DEREF(src).x; + float sy = PNANOVDB_DEREF(src).y; + float sz = PNANOVDB_DEREF(src).z; + dst.x = sx * pnanovdb_map_get_matf(buf, map, 0) + sy * pnanovdb_map_get_matf(buf, map, 1) + sz * pnanovdb_map_get_matf(buf, map, 2) + pnanovdb_map_get_vecf(buf, map, 0); + dst.y = sx * pnanovdb_map_get_matf(buf, map, 3) + sy * pnanovdb_map_get_matf(buf, map, 4) + sz * pnanovdb_map_get_matf(buf, map, 5) + pnanovdb_map_get_vecf(buf, map, 1); + dst.z = sx * pnanovdb_map_get_matf(buf, map, 6) + sy * pnanovdb_map_get_matf(buf, map, 7) + sz * pnanovdb_map_get_matf(buf, map, 8) + pnanovdb_map_get_vecf(buf, map, 2); + return dst; +} + +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_map_apply_inverse(pnanovdb_buf_t buf, pnanovdb_map_handle_t map, PNANOVDB_IN(pnanovdb_vec3_t) src) +{ + pnanovdb_vec3_t dst; + float sx = PNANOVDB_DEREF(src).x - pnanovdb_map_get_vecf(buf, map, 0); + float sy = PNANOVDB_DEREF(src).y - pnanovdb_map_get_vecf(buf, map, 1); + float sz = PNANOVDB_DEREF(src).z - pnanovdb_map_get_vecf(buf, map, 2); + dst.x = sx * pnanovdb_map_get_invmatf(buf, map, 0) + sy * pnanovdb_map_get_invmatf(buf, map, 1) + sz * pnanovdb_map_get_invmatf(buf, map, 2); + dst.y = sx * pnanovdb_map_get_invmatf(buf, map, 3) + sy * pnanovdb_map_get_invmatf(buf, map, 4) + sz * pnanovdb_map_get_invmatf(buf, map, 5); + dst.z = sx * pnanovdb_map_get_invmatf(buf, map, 6) + sy * pnanovdb_map_get_invmatf(buf, map, 7) + sz * pnanovdb_map_get_invmatf(buf, map, 8); + return dst; +} + +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_map_apply_jacobi(pnanovdb_buf_t buf, pnanovdb_map_handle_t map, PNANOVDB_IN(pnanovdb_vec3_t) src) +{ + pnanovdb_vec3_t dst; + float sx = PNANOVDB_DEREF(src).x; + float sy = PNANOVDB_DEREF(src).y; + float sz = PNANOVDB_DEREF(src).z; + dst.x = sx * pnanovdb_map_get_matf(buf, map, 0) + sy * pnanovdb_map_get_matf(buf, map, 1) + sz * pnanovdb_map_get_matf(buf, map, 2); + dst.y = sx * pnanovdb_map_get_matf(buf, map, 3) + sy * pnanovdb_map_get_matf(buf, map, 4) + sz * pnanovdb_map_get_matf(buf, map, 5); + dst.z = sx * pnanovdb_map_get_matf(buf, map, 6) + sy * pnanovdb_map_get_matf(buf, map, 7) + sz * pnanovdb_map_get_matf(buf, map, 8); + return dst; +} + +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_map_apply_inverse_jacobi(pnanovdb_buf_t buf, pnanovdb_map_handle_t map, PNANOVDB_IN(pnanovdb_vec3_t) src) +{ + pnanovdb_vec3_t dst; + float sx = PNANOVDB_DEREF(src).x; + float sy = PNANOVDB_DEREF(src).y; + float sz = PNANOVDB_DEREF(src).z; + dst.x = sx * pnanovdb_map_get_invmatf(buf, map, 0) + sy * pnanovdb_map_get_invmatf(buf, map, 1) + sz * pnanovdb_map_get_invmatf(buf, map, 2); + dst.y = sx * pnanovdb_map_get_invmatf(buf, map, 3) + sy * pnanovdb_map_get_invmatf(buf, map, 4) + sz * pnanovdb_map_get_invmatf(buf, map, 5); + dst.z = sx * pnanovdb_map_get_invmatf(buf, map, 6) + sy * pnanovdb_map_get_invmatf(buf, map, 7) + sz * pnanovdb_map_get_invmatf(buf, map, 8); + return dst; +} + +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_grid_world_to_indexf(pnanovdb_buf_t buf, pnanovdb_grid_handle_t grid, PNANOVDB_IN(pnanovdb_vec3_t) src) +{ + pnanovdb_map_handle_t map = pnanovdb_grid_get_map(buf, grid); + return pnanovdb_map_apply_inverse(buf, map, src); +} + +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_grid_index_to_worldf(pnanovdb_buf_t buf, pnanovdb_grid_handle_t grid, PNANOVDB_IN(pnanovdb_vec3_t) src) +{ + pnanovdb_map_handle_t map = pnanovdb_grid_get_map(buf, grid); + return pnanovdb_map_apply(buf, map, src); +} + +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_grid_world_to_index_dirf(pnanovdb_buf_t buf, pnanovdb_grid_handle_t grid, PNANOVDB_IN(pnanovdb_vec3_t) src) +{ + pnanovdb_map_handle_t map = pnanovdb_grid_get_map(buf, grid); + return pnanovdb_map_apply_inverse_jacobi(buf, map, src); +} + +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_grid_index_to_world_dirf(pnanovdb_buf_t buf, pnanovdb_grid_handle_t grid, PNANOVDB_IN(pnanovdb_vec3_t) src) +{ + pnanovdb_map_handle_t map = pnanovdb_grid_get_map(buf, grid); + return pnanovdb_map_apply_jacobi(buf, map, src); +} + +// ------------------------------------------------ DitherLUT ----------------------------------------------------------- + +// This table was generated with +/************** + +static constexpr inline uint32 +SYSwang_inthash(uint32 key) +{ + // From http://www.concentric.net/~Ttwang/tech/inthash.htm + key += ~(key << 16); + key ^= (key >> 5); + key += (key << 3); + key ^= (key >> 13); + key += ~(key << 9); + key ^= (key >> 17); + return key; +} + +static void +ut_initDitherR(float *pattern, float offset, + int x, int y, int z, int res, int goalres) +{ + // These offsets are designed to maximize the difference between + // dither values in nearby voxels within a given 2x2x2 cell, without + // producing axis-aligned artifacts. The are organized in row-major + // order. + static const float theDitherOffset[] = {0,4,6,2,5,1,3,7}; + static const float theScale = 0.125F; + int key = (((z << res) + y) << res) + x; + + if (res == goalres) + { + pattern[key] = offset; + return; + } + + // Randomly flip (on each axis) the dithering patterns used by the + // subcells. This key is xor'd with the subcell index below before + // looking up in the dither offset list. + key = SYSwang_inthash(key) & 7; + + x <<= 1; + y <<= 1; + z <<= 1; + + offset *= theScale; + for (int i = 0; i < 8; i++) + ut_initDitherR(pattern, offset+theDitherOffset[i ^ key]*theScale, + x+(i&1), y+((i&2)>>1), z+((i&4)>>2), res+1, goalres); +} + +// This is a compact algorithm that accomplishes essentially the same thing +// as ut_initDither() above. We should eventually switch to use this and +// clean the dead code. +static fpreal32 * +ut_initDitherRecursive(int goalres) +{ + const int nfloat = 1 << (goalres*3); + float *pattern = new float[nfloat]; + ut_initDitherR(pattern, 1.0F, 0, 0, 0, 0, goalres); + + // This has built an even spacing from 1/nfloat to 1.0. + // however, our dither pattern should be 1/(nfloat+1) to nfloat/(nfloat+1) + // So we do a correction here. Note that the earlier calculations are + // done with powers of 2 so are exact, so it does make sense to delay + // the renormalization to this pass. + float correctionterm = nfloat / (nfloat+1.0F); + for (int i = 0; i < nfloat; i++) + pattern[i] *= correctionterm; + return pattern; +} + + theDitherMatrix = ut_initDitherRecursive(3); + + for (int i = 0; i < 512/8; i ++) + { + for (int j = 0; j < 8; j ++) + std::cout << theDitherMatrix[i*8+j] << "f, "; + std::cout << std::endl; + } + + **************/ + +PNANOVDB_STATIC_CONST float pnanovdb_dither_lut[512] = +{ + 0.14425f, 0.643275f, 0.830409f, 0.331384f, 0.105263f, 0.604289f, 0.167641f, 0.666667f, + 0.892788f, 0.393762f, 0.0818713f, 0.580897f, 0.853801f, 0.354776f, 0.916179f, 0.417154f, + 0.612086f, 0.11306f, 0.79922f, 0.300195f, 0.510721f, 0.0116959f, 0.947368f, 0.448343f, + 0.362573f, 0.861598f, 0.0506823f, 0.549708f, 0.261209f, 0.760234f, 0.19883f, 0.697856f, + 0.140351f, 0.639376f, 0.576998f, 0.0779727f, 0.522417f, 0.0233918f, 0.460039f, 0.959064f, + 0.888889f, 0.389864f, 0.327485f, 0.826511f, 0.272904f, 0.77193f, 0.709552f, 0.210526f, + 0.483431f, 0.982456f, 0.296296f, 0.795322f, 0.116959f, 0.615984f, 0.0545809f, 0.553606f, + 0.732943f, 0.233918f, 0.545809f, 0.0467836f, 0.865497f, 0.366472f, 0.803119f, 0.304094f, + 0.518519f, 0.0194932f, 0.45614f, 0.955166f, 0.729045f, 0.230019f, 0.54191f, 0.042885f, + 0.269006f, 0.768031f, 0.705653f, 0.206628f, 0.479532f, 0.978558f, 0.292398f, 0.791423f, + 0.237817f, 0.736842f, 0.424951f, 0.923977f, 0.136452f, 0.635478f, 0.323587f, 0.822612f, + 0.986355f, 0.487329f, 0.674464f, 0.175439f, 0.88499f, 0.385965f, 0.573099f, 0.0740741f, + 0.51462f, 0.0155945f, 0.202729f, 0.701754f, 0.148148f, 0.647174f, 0.834308f, 0.335283f, + 0.265107f, 0.764133f, 0.951267f, 0.452242f, 0.896686f, 0.397661f, 0.08577f, 0.584795f, + 0.8577f, 0.358674f, 0.920078f, 0.421053f, 0.740741f, 0.241715f, 0.678363f, 0.179337f, + 0.109162f, 0.608187f, 0.17154f, 0.670565f, 0.491228f, 0.990253f, 0.42885f, 0.927875f, + 0.0662768f, 0.565302f, 0.62768f, 0.128655f, 0.183236f, 0.682261f, 0.744639f, 0.245614f, + 0.814815f, 0.315789f, 0.378168f, 0.877193f, 0.931774f, 0.432749f, 0.495127f, 0.994152f, + 0.0350877f, 0.534113f, 0.97076f, 0.471735f, 0.214425f, 0.71345f, 0.526316f, 0.0272904f, + 0.783626f, 0.2846f, 0.222222f, 0.721248f, 0.962963f, 0.463938f, 0.276803f, 0.775828f, + 0.966862f, 0.467836f, 0.405458f, 0.904483f, 0.0701754f, 0.569201f, 0.881092f, 0.382066f, + 0.218324f, 0.717349f, 0.654971f, 0.155945f, 0.818713f, 0.319688f, 0.132554f, 0.631579f, + 0.0623782f, 0.561404f, 0.748538f, 0.249513f, 0.912281f, 0.413255f, 0.974659f, 0.475634f, + 0.810916f, 0.311891f, 0.499025f, 0.998051f, 0.163743f, 0.662768f, 0.226121f, 0.725146f, + 0.690058f, 0.191033f, 0.00389864f, 0.502924f, 0.557505f, 0.0584795f, 0.120858f, 0.619883f, + 0.440546f, 0.939571f, 0.752437f, 0.253411f, 0.307992f, 0.807018f, 0.869396f, 0.37037f, + 0.658869f, 0.159844f, 0.346979f, 0.846004f, 0.588694f, 0.0896686f, 0.152047f, 0.651072f, + 0.409357f, 0.908382f, 0.596491f, 0.0974659f, 0.339181f, 0.838207f, 0.900585f, 0.401559f, + 0.34308f, 0.842105f, 0.779727f, 0.280702f, 0.693957f, 0.194932f, 0.25731f, 0.756335f, + 0.592593f, 0.0935673f, 0.0311891f, 0.530214f, 0.444444f, 0.94347f, 0.506823f, 0.00779727f, + 0.68616f, 0.187135f, 0.124756f, 0.623782f, 0.288499f, 0.787524f, 0.350877f, 0.849903f, + 0.436647f, 0.935673f, 0.873294f, 0.374269f, 0.538012f, 0.0389864f, 0.60039f, 0.101365f, + 0.57115f, 0.0721248f, 0.758285f, 0.259259f, 0.719298f, 0.220273f, 0.532164f, 0.0331384f, + 0.321637f, 0.820663f, 0.00974659f, 0.508772f, 0.469786f, 0.968811f, 0.282651f, 0.781676f, + 0.539961f, 0.0409357f, 0.727096f, 0.22807f, 0.500975f, 0.00194932f, 0.563353f, 0.0643275f, + 0.290448f, 0.789474f, 0.477583f, 0.976608f, 0.251462f, 0.750487f, 0.31384f, 0.812865f, + 0.94152f, 0.442495f, 0.879142f, 0.380117f, 0.37232f, 0.871345f, 0.309942f, 0.808967f, + 0.192982f, 0.692008f, 0.130604f, 0.62963f, 0.621832f, 0.122807f, 0.559454f, 0.0604289f, + 0.660819f, 0.161793f, 0.723197f, 0.224172f, 0.403509f, 0.902534f, 0.840156f, 0.341131f, + 0.411306f, 0.910331f, 0.473684f, 0.97271f, 0.653021f, 0.153996f, 0.0916179f, 0.590643f, + 0.196881f, 0.695906f, 0.384016f, 0.883041f, 0.0955166f, 0.594542f, 0.157895f, 0.65692f, + 0.945419f, 0.446394f, 0.633528f, 0.134503f, 0.844055f, 0.345029f, 0.906433f, 0.407407f, + 0.165692f, 0.664717f, 0.103314f, 0.602339f, 0.126706f, 0.625731f, 0.189084f, 0.688109f, + 0.91423f, 0.415205f, 0.851852f, 0.352827f, 0.875244f, 0.376218f, 0.937622f, 0.438596f, + 0.317739f, 0.816764f, 0.255361f, 0.754386f, 0.996101f, 0.497076f, 0.933723f, 0.434698f, + 0.567251f, 0.0682261f, 0.504873f, 0.00584795f, 0.247563f, 0.746589f, 0.185185f, 0.684211f, + 0.037037f, 0.536062f, 0.0994152f, 0.598441f, 0.777778f, 0.278752f, 0.465887f, 0.964912f, + 0.785575f, 0.28655f, 0.847953f, 0.348928f, 0.0292398f, 0.528265f, 0.7154f, 0.216374f, + 0.39961f, 0.898636f, 0.961014f, 0.461988f, 0.0487329f, 0.547758f, 0.111111f, 0.610136f, + 0.649123f, 0.150097f, 0.212476f, 0.711501f, 0.797271f, 0.298246f, 0.859649f, 0.360624f, + 0.118908f, 0.617934f, 0.0565302f, 0.555556f, 0.329435f, 0.82846f, 0.516569f, 0.0175439f, + 0.867446f, 0.368421f, 0.805068f, 0.306043f, 0.578947f, 0.079922f, 0.267057f, 0.766082f, + 0.270955f, 0.76998f, 0.707602f, 0.208577f, 0.668616f, 0.169591f, 0.606238f, 0.107212f, + 0.520468f, 0.0214425f, 0.45809f, 0.957115f, 0.419103f, 0.918129f, 0.356725f, 0.855751f, + 0.988304f, 0.489279f, 0.426901f, 0.925926f, 0.450292f, 0.949318f, 0.512671f, 0.0136452f, + 0.239766f, 0.738791f, 0.676413f, 0.177388f, 0.699805f, 0.20078f, 0.263158f, 0.762183f, + 0.773879f, 0.274854f, 0.337232f, 0.836257f, 0.672515f, 0.173489f, 0.734893f, 0.235867f, + 0.0253411f, 0.524366f, 0.586745f, 0.0877193f, 0.423002f, 0.922027f, 0.48538f, 0.984405f, + 0.74269f, 0.243665f, 0.680312f, 0.181287f, 0.953216f, 0.454191f, 0.1423f, 0.641326f, + 0.493177f, 0.992203f, 0.430799f, 0.929825f, 0.204678f, 0.703704f, 0.890838f, 0.391813f, + 0.894737f, 0.395712f, 0.0838207f, 0.582846f, 0.0448343f, 0.54386f, 0.231969f, 0.730994f, + 0.146199f, 0.645224f, 0.832359f, 0.333333f, 0.793372f, 0.294347f, 0.980507f, 0.481481f, + 0.364522f, 0.863548f, 0.80117f, 0.302144f, 0.824561f, 0.325536f, 0.138402f, 0.637427f, + 0.614035f, 0.11501f, 0.0526316f, 0.551657f, 0.0760234f, 0.575049f, 0.88694f, 0.387914f, +}; + +PNANOVDB_FORCE_INLINE float pnanovdb_dither_lookup(pnanovdb_bool_t enabled, int offset) +{ + return enabled ? pnanovdb_dither_lut[offset & 511] : 0.5f; +} + +// ------------------------------------------------ HDDA ----------------------------------------------------------- + +#ifdef PNANOVDB_HDDA + +// Comment out to disable this explicit round-off check +#define PNANOVDB_ENFORCE_FORWARD_STEPPING + +#define PNANOVDB_HDDA_FLOAT_MAX 1e38f + +struct pnanovdb_hdda_t +{ + pnanovdb_int32_t dim; + float tmin; + float tmax; + pnanovdb_coord_t voxel; + pnanovdb_coord_t step; + pnanovdb_vec3_t delta; + pnanovdb_vec3_t next; +}; +PNANOVDB_STRUCT_TYPEDEF(pnanovdb_hdda_t) + +PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_hdda_pos_to_ijk(PNANOVDB_IN(pnanovdb_vec3_t) pos) +{ + pnanovdb_coord_t voxel; + voxel.x = pnanovdb_float_to_int32(pnanovdb_floor(PNANOVDB_DEREF(pos).x)); + voxel.y = pnanovdb_float_to_int32(pnanovdb_floor(PNANOVDB_DEREF(pos).y)); + voxel.z = pnanovdb_float_to_int32(pnanovdb_floor(PNANOVDB_DEREF(pos).z)); + return voxel; +} + +PNANOVDB_FORCE_INLINE pnanovdb_coord_t pnanovdb_hdda_pos_to_voxel(PNANOVDB_IN(pnanovdb_vec3_t) pos, int dim) +{ + pnanovdb_coord_t voxel; + voxel.x = pnanovdb_float_to_int32(pnanovdb_floor(PNANOVDB_DEREF(pos).x)) & (~(dim - 1)); + voxel.y = pnanovdb_float_to_int32(pnanovdb_floor(PNANOVDB_DEREF(pos).y)) & (~(dim - 1)); + voxel.z = pnanovdb_float_to_int32(pnanovdb_floor(PNANOVDB_DEREF(pos).z)) & (~(dim - 1)); + return voxel; +} + +PNANOVDB_FORCE_INLINE pnanovdb_vec3_t pnanovdb_hdda_ray_start(PNANOVDB_IN(pnanovdb_vec3_t) origin, float tmin, PNANOVDB_IN(pnanovdb_vec3_t) direction) +{ + pnanovdb_vec3_t pos = pnanovdb_vec3_add( + pnanovdb_vec3_mul(PNANOVDB_DEREF(direction), pnanovdb_vec3_uniform(tmin)), + PNANOVDB_DEREF(origin) + ); + return pos; +} + +PNANOVDB_FORCE_INLINE void pnanovdb_hdda_init(PNANOVDB_INOUT(pnanovdb_hdda_t) hdda, PNANOVDB_IN(pnanovdb_vec3_t) origin, float tmin, PNANOVDB_IN(pnanovdb_vec3_t) direction, float tmax, int dim) +{ + PNANOVDB_DEREF(hdda).dim = dim; + PNANOVDB_DEREF(hdda).tmin = tmin; + PNANOVDB_DEREF(hdda).tmax = tmax; + + pnanovdb_vec3_t pos = pnanovdb_hdda_ray_start(origin, tmin, direction); + pnanovdb_vec3_t dir_inv = pnanovdb_vec3_div(pnanovdb_vec3_uniform(1.f), PNANOVDB_DEREF(direction)); + + PNANOVDB_DEREF(hdda).voxel = pnanovdb_hdda_pos_to_voxel(PNANOVDB_REF(pos), dim); + + // x + if (PNANOVDB_DEREF(direction).x == 0.f) + { + PNANOVDB_DEREF(hdda).next.x = PNANOVDB_HDDA_FLOAT_MAX; + PNANOVDB_DEREF(hdda).step.x = 0; + PNANOVDB_DEREF(hdda).delta.x = 0.f; + } + else if (dir_inv.x > 0.f) + { + PNANOVDB_DEREF(hdda).step.x = 1; + PNANOVDB_DEREF(hdda).next.x = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.x + dim - pos.x) * dir_inv.x; + PNANOVDB_DEREF(hdda).delta.x = dir_inv.x; + } + else + { + PNANOVDB_DEREF(hdda).step.x = -1; + PNANOVDB_DEREF(hdda).next.x = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.x - pos.x) * dir_inv.x; + PNANOVDB_DEREF(hdda).delta.x = -dir_inv.x; + } + + // y + if (PNANOVDB_DEREF(direction).y == 0.f) + { + PNANOVDB_DEREF(hdda).next.y = PNANOVDB_HDDA_FLOAT_MAX; + PNANOVDB_DEREF(hdda).step.y = 0; + PNANOVDB_DEREF(hdda).delta.y = 0.f; + } + else if (dir_inv.y > 0.f) + { + PNANOVDB_DEREF(hdda).step.y = 1; + PNANOVDB_DEREF(hdda).next.y = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.y + dim - pos.y) * dir_inv.y; + PNANOVDB_DEREF(hdda).delta.y = dir_inv.y; + } + else + { + PNANOVDB_DEREF(hdda).step.y = -1; + PNANOVDB_DEREF(hdda).next.y = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.y - pos.y) * dir_inv.y; + PNANOVDB_DEREF(hdda).delta.y = -dir_inv.y; + } + + // z + if (PNANOVDB_DEREF(direction).z == 0.f) + { + PNANOVDB_DEREF(hdda).next.z = PNANOVDB_HDDA_FLOAT_MAX; + PNANOVDB_DEREF(hdda).step.z = 0; + PNANOVDB_DEREF(hdda).delta.z = 0.f; + } + else if (dir_inv.z > 0.f) + { + PNANOVDB_DEREF(hdda).step.z = 1; + PNANOVDB_DEREF(hdda).next.z = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.z + dim - pos.z) * dir_inv.z; + PNANOVDB_DEREF(hdda).delta.z = dir_inv.z; + } + else + { + PNANOVDB_DEREF(hdda).step.z = -1; + PNANOVDB_DEREF(hdda).next.z = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.z - pos.z) * dir_inv.z; + PNANOVDB_DEREF(hdda).delta.z = -dir_inv.z; + } +} + +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_hdda_update(PNANOVDB_INOUT(pnanovdb_hdda_t) hdda, PNANOVDB_IN(pnanovdb_vec3_t) origin, PNANOVDB_IN(pnanovdb_vec3_t) direction, int dim) +{ + if (PNANOVDB_DEREF(hdda).dim == dim) + { + return PNANOVDB_FALSE; + } + PNANOVDB_DEREF(hdda).dim = dim; + + pnanovdb_vec3_t pos = pnanovdb_vec3_add( + pnanovdb_vec3_mul(PNANOVDB_DEREF(direction), pnanovdb_vec3_uniform(PNANOVDB_DEREF(hdda).tmin)), + PNANOVDB_DEREF(origin) + ); + pnanovdb_vec3_t dir_inv = pnanovdb_vec3_div(pnanovdb_vec3_uniform(1.f), PNANOVDB_DEREF(direction)); + + PNANOVDB_DEREF(hdda).voxel = pnanovdb_hdda_pos_to_voxel(PNANOVDB_REF(pos), dim); + + if (PNANOVDB_DEREF(hdda).step.x != 0) + { + PNANOVDB_DEREF(hdda).next.x = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.x - pos.x) * dir_inv.x; + if (PNANOVDB_DEREF(hdda).step.x > 0) + { + PNANOVDB_DEREF(hdda).next.x += dim * dir_inv.x; + } + } + if (PNANOVDB_DEREF(hdda).step.y != 0) + { + PNANOVDB_DEREF(hdda).next.y = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.y - pos.y) * dir_inv.y; + if (PNANOVDB_DEREF(hdda).step.y > 0) + { + PNANOVDB_DEREF(hdda).next.y += dim * dir_inv.y; + } + } + if (PNANOVDB_DEREF(hdda).step.z != 0) + { + PNANOVDB_DEREF(hdda).next.z = PNANOVDB_DEREF(hdda).tmin + (PNANOVDB_DEREF(hdda).voxel.z - pos.z) * dir_inv.z; + if (PNANOVDB_DEREF(hdda).step.z > 0) + { + PNANOVDB_DEREF(hdda).next.z += dim * dir_inv.z; + } + } + + return PNANOVDB_TRUE; +} + +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_hdda_step(PNANOVDB_INOUT(pnanovdb_hdda_t) hdda) +{ + pnanovdb_bool_t ret; + if (PNANOVDB_DEREF(hdda).next.x < PNANOVDB_DEREF(hdda).next.y && PNANOVDB_DEREF(hdda).next.x < PNANOVDB_DEREF(hdda).next.z) + { +#ifdef PNANOVDB_ENFORCE_FORWARD_STEPPING + if (PNANOVDB_DEREF(hdda).next.x <= PNANOVDB_DEREF(hdda).tmin) + { + PNANOVDB_DEREF(hdda).next.x += PNANOVDB_DEREF(hdda).tmin - 0.999999f * PNANOVDB_DEREF(hdda).next.x + 1.0e-6f; + } +#endif + PNANOVDB_DEREF(hdda).tmin = PNANOVDB_DEREF(hdda).next.x; + PNANOVDB_DEREF(hdda).next.x += PNANOVDB_DEREF(hdda).dim * PNANOVDB_DEREF(hdda).delta.x; + PNANOVDB_DEREF(hdda).voxel.x += PNANOVDB_DEREF(hdda).dim * PNANOVDB_DEREF(hdda).step.x; + ret = PNANOVDB_DEREF(hdda).tmin <= PNANOVDB_DEREF(hdda).tmax; + } + else if (PNANOVDB_DEREF(hdda).next.y < PNANOVDB_DEREF(hdda).next.z) + { +#ifdef PNANOVDB_ENFORCE_FORWARD_STEPPING + if (PNANOVDB_DEREF(hdda).next.y <= PNANOVDB_DEREF(hdda).tmin) + { + PNANOVDB_DEREF(hdda).next.y += PNANOVDB_DEREF(hdda).tmin - 0.999999f * PNANOVDB_DEREF(hdda).next.y + 1.0e-6f; + } +#endif + PNANOVDB_DEREF(hdda).tmin = PNANOVDB_DEREF(hdda).next.y; + PNANOVDB_DEREF(hdda).next.y += PNANOVDB_DEREF(hdda).dim * PNANOVDB_DEREF(hdda).delta.y; + PNANOVDB_DEREF(hdda).voxel.y += PNANOVDB_DEREF(hdda).dim * PNANOVDB_DEREF(hdda).step.y; + ret = PNANOVDB_DEREF(hdda).tmin <= PNANOVDB_DEREF(hdda).tmax; + } + else + { +#ifdef PNANOVDB_ENFORCE_FORWARD_STEPPING + if (PNANOVDB_DEREF(hdda).next.z <= PNANOVDB_DEREF(hdda).tmin) + { + PNANOVDB_DEREF(hdda).next.z += PNANOVDB_DEREF(hdda).tmin - 0.999999f * PNANOVDB_DEREF(hdda).next.z + 1.0e-6f; + } +#endif + PNANOVDB_DEREF(hdda).tmin = PNANOVDB_DEREF(hdda).next.z; + PNANOVDB_DEREF(hdda).next.z += PNANOVDB_DEREF(hdda).dim * PNANOVDB_DEREF(hdda).delta.z; + PNANOVDB_DEREF(hdda).voxel.z += PNANOVDB_DEREF(hdda).dim * PNANOVDB_DEREF(hdda).step.z; + ret = PNANOVDB_DEREF(hdda).tmin <= PNANOVDB_DEREF(hdda).tmax; + } + return ret; +} + +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_hdda_ray_clip( + PNANOVDB_IN(pnanovdb_vec3_t) bbox_min, + PNANOVDB_IN(pnanovdb_vec3_t) bbox_max, + PNANOVDB_IN(pnanovdb_vec3_t) origin, PNANOVDB_INOUT(float) tmin, + PNANOVDB_IN(pnanovdb_vec3_t) direction, PNANOVDB_INOUT(float) tmax +) +{ + pnanovdb_vec3_t dir_inv = pnanovdb_vec3_div(pnanovdb_vec3_uniform(1.f), PNANOVDB_DEREF(direction)); + pnanovdb_vec3_t t0 = pnanovdb_vec3_mul(pnanovdb_vec3_sub(PNANOVDB_DEREF(bbox_min), PNANOVDB_DEREF(origin)), dir_inv); + pnanovdb_vec3_t t1 = pnanovdb_vec3_mul(pnanovdb_vec3_sub(PNANOVDB_DEREF(bbox_max), PNANOVDB_DEREF(origin)), dir_inv); + pnanovdb_vec3_t tmin3 = pnanovdb_vec3_min(t0, t1); + pnanovdb_vec3_t tmax3 = pnanovdb_vec3_max(t0, t1); + float tnear = pnanovdb_max(tmin3.x, pnanovdb_max(tmin3.y, tmin3.z)); + float tfar = pnanovdb_min(tmax3.x, pnanovdb_min(tmax3.y, tmax3.z)); + pnanovdb_bool_t hit = tnear <= tfar; + PNANOVDB_DEREF(tmin) = pnanovdb_max(PNANOVDB_DEREF(tmin), tnear); + PNANOVDB_DEREF(tmax) = pnanovdb_min(PNANOVDB_DEREF(tmax), tfar); + return hit; +} + +PNANOVDB_FORCE_INLINE pnanovdb_bool_t pnanovdb_hdda_zero_crossing( + pnanovdb_grid_type_t grid_type, + pnanovdb_buf_t buf, + PNANOVDB_INOUT(pnanovdb_readaccessor_t) acc, + PNANOVDB_IN(pnanovdb_vec3_t) origin, float tmin, + PNANOVDB_IN(pnanovdb_vec3_t) direction, float tmax, + PNANOVDB_INOUT(float) thit, + PNANOVDB_INOUT(float) v +) +{ + pnanovdb_coord_t bbox_min = pnanovdb_root_get_bbox_min(buf, PNANOVDB_DEREF(acc).root); + pnanovdb_coord_t bbox_max = pnanovdb_root_get_bbox_max(buf, PNANOVDB_DEREF(acc).root); + pnanovdb_vec3_t bbox_minf = pnanovdb_coord_to_vec3(bbox_min); + pnanovdb_vec3_t bbox_maxf = pnanovdb_coord_to_vec3(pnanovdb_coord_add(bbox_max, pnanovdb_coord_uniform(1))); + + pnanovdb_bool_t hit = pnanovdb_hdda_ray_clip(PNANOVDB_REF(bbox_minf), PNANOVDB_REF(bbox_maxf), origin, PNANOVDB_REF(tmin), direction, PNANOVDB_REF(tmax)); + if (!hit || tmax > 1.0e20f) + { + return PNANOVDB_FALSE; + } + + pnanovdb_vec3_t pos = pnanovdb_hdda_ray_start(origin, tmin, direction); + pnanovdb_coord_t ijk = pnanovdb_hdda_pos_to_ijk(PNANOVDB_REF(pos)); + + pnanovdb_address_t address = pnanovdb_readaccessor_get_value_address(PNANOVDB_GRID_TYPE_FLOAT, buf, acc, PNANOVDB_REF(ijk)); + float v0 = pnanovdb_read_float(buf, address); + + pnanovdb_int32_t dim = pnanovdb_uint32_as_int32(pnanovdb_readaccessor_get_dim(PNANOVDB_GRID_TYPE_FLOAT, buf, acc, PNANOVDB_REF(ijk))); + pnanovdb_hdda_t hdda; + pnanovdb_hdda_init(PNANOVDB_REF(hdda), origin, tmin, direction, tmax, dim); + while (pnanovdb_hdda_step(PNANOVDB_REF(hdda))) + { + pnanovdb_vec3_t pos_start = pnanovdb_hdda_ray_start(origin, hdda.tmin + 1.0001f, direction); + ijk = pnanovdb_hdda_pos_to_ijk(PNANOVDB_REF(pos_start)); + dim = pnanovdb_uint32_as_int32(pnanovdb_readaccessor_get_dim(PNANOVDB_GRID_TYPE_FLOAT, buf, acc, PNANOVDB_REF(ijk))); + pnanovdb_hdda_update(PNANOVDB_REF(hdda), origin, direction, dim); + if (hdda.dim > 1 || !pnanovdb_readaccessor_is_active(grid_type, buf, acc, PNANOVDB_REF(ijk))) + { + continue; + } + while (pnanovdb_hdda_step(PNANOVDB_REF(hdda)) && pnanovdb_readaccessor_is_active(grid_type, buf, acc, PNANOVDB_REF(hdda.voxel))) + { + ijk = hdda.voxel; + pnanovdb_address_t address = pnanovdb_readaccessor_get_value_address(PNANOVDB_GRID_TYPE_FLOAT, buf, acc, PNANOVDB_REF(ijk)); + PNANOVDB_DEREF(v) = pnanovdb_read_float(buf, address); + if (PNANOVDB_DEREF(v) * v0 < 0.f) + { + PNANOVDB_DEREF(thit) = hdda.tmin; + return PNANOVDB_TRUE; + } + } + } + return PNANOVDB_FALSE; +} + +#endif + +#endif // end of NANOVDB_PNANOVDB_H_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/cmd/convert/nanovdb_convert.cc b/nanovdb/nanovdb/cmd/convert/nanovdb_convert.cc index f3aa7ce8ea..7a3a5b5170 100644 --- a/nanovdb/nanovdb/cmd/convert/nanovdb_convert.cc +++ b/nanovdb/nanovdb/cmd/convert/nanovdb_convert.cc @@ -16,7 +16,7 @@ #include #include // this is required to read (and write) NanoVDB files on the host -#include +#include #include void usage [[noreturn]] (const std::string& progName, int exitStatus = EXIT_FAILURE) @@ -201,36 +201,26 @@ int main(int argc, char* argv[]) auto openToNano = [&](const openvdb::GridBase::Ptr& base) { - if (auto floatGrid = openvdb::GridBase::grid(base)) { + using SrcGridT = openvdb::FloatGrid; + if (auto floatGrid = openvdb::GridBase::grid(base)) { + nanovdb::CreateNanoGrid s(*floatGrid); + s.setStats(sMode); + s.setChecksum(cMode); + s.enableDithering(dither); + s.setVerbose(verbose ? 1 : 0); switch (qMode) { - case nanovdb::GridType::Fp4: { - nanovdb::OpenToNanoVDB s; - s.enableDithering(dither); - return s(*floatGrid, sMode, cMode, verbose ? 1 : 0); - } - case nanovdb::GridType::Fp8: { - nanovdb::OpenToNanoVDB s; - s.enableDithering(dither); - return s(*floatGrid, sMode, cMode, verbose ? 1 : 0); - } - case nanovdb::GridType::Fp16: { - nanovdb::OpenToNanoVDB s; - s.enableDithering(dither); - return s(*floatGrid, sMode, cMode, verbose ? 1 : 0); - } - case nanovdb::GridType::FpN: { + case nanovdb::GridType::Fp4: + return s.getHandle(); + case nanovdb::GridType::Fp8: + return s.getHandle(); + case nanovdb::GridType::Fp16: + return s.getHandle(); + case nanovdb::GridType::FpN: if (absolute) { - nanovdb::OpenToNanoVDB s; - s.enableDithering(dither); - s.oracle() = nanovdb::AbsDiff(tolerance); - return s(*floatGrid, sMode, cMode, verbose ? 1 : 0); + return s.getHandle(nanovdb::AbsDiff(tolerance)); } else { - nanovdb::OpenToNanoVDB s; - s.enableDithering(dither); - s.oracle() = nanovdb::RelDiff(tolerance); - return s(*floatGrid, sMode, cMode, verbose ? 1 : 0); + return s.getHandle(nanovdb::RelDiff(tolerance)); } - } default: break; }// end of switch @@ -251,13 +241,15 @@ int main(int argc, char* argv[]) file.open(false); //disable delayed loading if (gridName.empty()) {// convert all grid in the file auto grids = file.getGrids(); + std::vector > handles; for (auto& grid : *grids) { if (verbose) { std::cout << "Converting OpenVDB grid named \"" << grid->getName() << "\" to NanoVDB" << std::endl; } - auto handle = openToNano(grid); - nanovdb::io::writeGrid(os, handle, codec); + handles.push_back(openToNano(grid)); } // loop over OpenVDB grids in file + auto handle = nanovdb::mergeGrids(handles); + nanovdb::io::writeGrid(os, handle, codec); } else {// convert only grid with matching name auto grid = file.readGrid(gridName); if (verbose) { @@ -280,9 +272,11 @@ int main(int argc, char* argv[]) if (gridName.empty()) { auto handles = nanovdb::io::readGrids(inputFile, verbose); for (auto &h : handles) { - if (verbose) - std::cout << "Converting NanoVDB grid named \"" << h.gridMetaData()->shortGridName() << "\" to OpenVDB" << std::endl; - grids->push_back(nanoToOpenVDB(h)); + for (uint32_t i = 0; i < h.gridCount(); ++i) { + if (verbose) + std::cout << "Converting NanoVDB grid named \"" << h.gridMetaData(i)->shortGridName() << "\" to OpenVDB" << std::endl; + grids->push_back(nanoToOpenVDB(h, 0, i)); + } } } else { auto handle = nanovdb::io::readGrid(inputFile, gridName); diff --git a/nanovdb/nanovdb/cmd/print/nanovdb_print.cc b/nanovdb/nanovdb/cmd/print/nanovdb_print.cc index 5f24f52c4e..5336a07190 100644 --- a/nanovdb/nanovdb/cmd/print/nanovdb_print.cc +++ b/nanovdb/nanovdb/cmd/print/nanovdb_print.cc @@ -103,7 +103,7 @@ int main(int argc, char* argv[]) if (size > n) n = size; }; - auto vec3RToStr = [](const nanovdb::Vec3R& v) { + auto Vec3dToStr = [](const nanovdb::Vec3d& v) { std::stringstream ss; ss << std::setprecision(3); ss << "(" << v[0] << "," << v[1] << "," << v[2] << ")"; @@ -148,7 +148,7 @@ int main(int argc, char* argv[]) for (auto& file : fileNames) { auto list = nanovdb::io::readGridMetaData(file); if (!gridName.empty()) { - std::vector tmp; + std::vector tmp; for (auto& m : list) { if (nameKey == m.nameKey && gridName == m.gridName) tmp.emplace_back(m); @@ -186,7 +186,7 @@ int main(int argc, char* argv[]) width(configWidth, nodesToStr(m.nodeCount)); width(tileWidth, nodesToStr(m.tileCount)); width(voxelsWidth, std::to_string(m.voxelCount)); - width(voxelSizeWidth, vec3RToStr(m.voxelSize)); + width(voxelSizeWidth, Vec3dToStr(m.voxelSize)); } std::cout << "\nThe file \"" << file << "\" contains the following "; if (list.size()>1) { @@ -227,7 +227,7 @@ int main(int argc, char* argv[]) << std::left << std::setw(codecWidth) << nanovdb::io::toStr(m.codec) << std::left << std::setw(sizeWidth) << format(m.gridSize) << std::left << std::setw(fileWidth) << format(m.fileSize) - << std::left << std::setw(voxelSizeWidth) << vec3RToStr(m.voxelSize); + << std::left << std::setw(voxelSizeWidth) << Vec3dToStr(m.voxelSize); } std::cout << std::left << std::setw(voxelsWidth) << m.voxelCount << std::left << std::setw(resWidth) << resToStr(m.indexBBox); @@ -316,7 +316,7 @@ int main(int argc, char* argv[]) exitStatus = EXIT_FAILURE; } catch (...) { - std::cerr << "Exception oof unexpected type caught" << std::endl; + std::cerr << "Exception of unexpected type caught" << std::endl; exitStatus = EXIT_FAILURE; } diff --git a/nanovdb/nanovdb/cmd/validate/nanovdb_validate.cc b/nanovdb/nanovdb/cmd/validate/nanovdb_validate.cc index ae70dd310c..faec25aa4d 100644 --- a/nanovdb/nanovdb/cmd/validate/nanovdb_validate.cc +++ b/nanovdb/nanovdb/cmd/validate/nanovdb_validate.cc @@ -77,7 +77,7 @@ int main(int argc, char* argv[]) for (auto& file : fileNames) { auto list = nanovdb::io::readGridMetaData(file); if (!gridName.empty()) { - std::vector tmp; + std::vector tmp; for (auto& m : list) { if (nameKey == m.nameKey && gridName == m.gridName) tmp.emplace_back(m); diff --git a/nanovdb/nanovdb/examples/CMakeLists.txt b/nanovdb/nanovdb/examples/CMakeLists.txt index dc0d243495..e86c17bc32 100644 --- a/nanovdb/nanovdb/examples/CMakeLists.txt +++ b/nanovdb/nanovdb/examples/CMakeLists.txt @@ -83,11 +83,12 @@ endfunction() # ----------------------------------------------------------------------- -if(NANOVDB_BUILD_BENCHMARK) - add_subdirectory(benchmark) -endif() +#if(NANOVDB_BUILD_BENCHMARK) +# add_subdirectory(benchmark) +#endif() nanovdb_example(NAME "ex_make_custom_nanovdb") +nanovdb_example(NAME "ex_make_custom_nanovdb_cuda") nanovdb_example(NAME "ex_make_funny_nanovdb") nanovdb_example(NAME "ex_make_typed_grids") nanovdb_example(NAME "ex_make_nanovdb_sphere") @@ -100,6 +101,7 @@ nanovdb_example(NAME "ex_read_nanovdb_sphere_accessor") nanovdb_example(NAME "ex_read_nanovdb_sphere_accessor_cuda") nanovdb_example(NAME "ex_index_grid_cuda") nanovdb_example(NAME "ex_nodemanager_cuda") +nanovdb_example(NAME "ex_voxels_to_grid_cuda") nanovdb_example(NAME "ex_modify_nanovdb_thrust") nanovdb_example(NAME "ex_map_pool_buffer") nanovdb_example(NAME "ex_bump_pool_buffer") diff --git a/nanovdb/nanovdb/examples/benchmark/BenchKernels_dense.cu b/nanovdb/nanovdb/examples/benchmark/BenchKernels_dense.cu deleted file mode 100644 index 0c8175dfb3..0000000000 --- a/nanovdb/nanovdb/examples/benchmark/BenchKernels_dense.cu +++ /dev/null @@ -1,108 +0,0 @@ - -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: MPL-2.0 - -/// @file BenchKernels_dense.cu -/// -/// @author Ken Museth -/// -/// @brief CUDA kernel for a simple ray-tracing benchmark test. - -#include "DenseGrid.h" -#include // for CUDA memory management -#include // for nanovdb::Ray -#include // for nanovdb::DDA - -#include "Image.h" -#include "Camera.h" - -// Comment out to disable timing of the CUDA kernel -#define CUDA_TIMING - -// This is called by the device -__global__ void render_kernel(const nanovdb::DenseGrid& grid, - const nanovdb::Camera& camera, - nanovdb::Image& img) -{ - using RealT = float; - using Vec3T = nanovdb::Vec3; - using CoordT = nanovdb::Coord; - using RayT = nanovdb::Ray; - using ColorRGB = nanovdb::Image::ColorRGB; - - const int w = blockIdx.x * blockDim.x + threadIdx.x; - const int h = blockIdx.y * blockDim.y + threadIdx.y; - if (w >= img.width() || h >= img.height()) return; - RayT ray = camera.getRay(img.u(w), img.v(h));// ray in world space - ray = ray.worldToIndexF(grid);// ray in index space - if (ray.clip(grid.indexBBox().expandBy(-1))) {// clip to the index bounding box - nanovdb::DDA dda(ray); - const float v0 = grid.getValue(dda.voxel()); - while( dda.step() ) { - CoordT ijk = dda.voxel(); - const float v1 = grid.getValue(ijk); - if (v0*v1>0) continue; -#if 1// second-order central difference - Vec3T grad(grid.getValue(ijk.offsetBy(1,0,0)) - grid.getValue(ijk.offsetBy(-1,0,0)), - grid.getValue(ijk.offsetBy(0,1,0)) - grid.getValue(ijk.offsetBy(0,-1,0)), - grid.getValue(ijk.offsetBy(0,0,1)) - grid.getValue(ijk.offsetBy(0,0,-1))); -#else// first order single-sided difference - Vec3T grad(-v0); - ijk[0] += 1; - grad[0] += grid.getValue(ijk); - ijk[0] -= 1; - ijk[1] += 1; - grad[1] += grid.getValue(ijk); - ijk[1] -= 1; - ijk[2] += 1; - grad[2] += grid.getValue(ijk); -#endif - grad *= rnorm3df(grad[0], grad[1], grad[2]); - img(w, h) = ColorRGB(abs(grad.dot(ray.dir())), 0, 0); - return; - } - } - const int checkerboard = 1 << 7; - img(w, h) = ((h & checkerboard) ^ (w & checkerboard)) ? ColorRGB(1, 1, 1) : ColorRGB(0, 0, 0); -} - -// This is called by the host -extern "C" float launch_kernels(const nanovdb::DenseGridHandle& gridHandle, - nanovdb::ImageHandle& imgHandle, - const nanovdb::Camera* camera, - cudaStream_t stream) -{ - const auto* img = imgHandle.image(); // host image! - auto round = [](int a, int b) { return (a + b - 1) / b; }; - const dim3 threadsPerBlock(8, 8), numBlocks(round(img->width(), threadsPerBlock.x), round(img->height(), threadsPerBlock.y)); - auto* deviceGrid = gridHandle.deviceGrid(); // note this cannot be de-referenced since it points to a memory address on the GPU! - auto* deviceImage = imgHandle.deviceImage(); // note this cannot be de-referenced since it points to a memory address on the GPU! - assert(deviceGrid && deviceImage); - -#ifdef CUDA_TIMING - cudaEvent_t start, stop; - cudaEventCreate(&start); - cudaEventCreate(&stop); - cudaEventRecord(start, stream); -#endif - - // kernal syntax: <<>> - render_kernel<<>>(*deviceGrid, *camera, *deviceImage); - - float elapsedTime = 0.0f; -#ifdef CUDA_TIMING - cudaEventRecord(stop, stream); - cudaEventSynchronize(stop); - - cudaEventElapsedTime(&elapsedTime, start, stop); - //printf("DenseGrid: GPU kernel with %i rays ... completed in %5.3f milliseconds\n", imgHandle.image()->size(), elapsedTime); - cudaError_t errCode = cudaGetLastError(); - if (errCode != cudaSuccess) { - fprintf(stderr, "CUDA Runtime Error: %s %s %d\n", cudaGetErrorString(errCode), __FILE__, __LINE__); - exit(errCode); - } - cudaEventDestroy(start); - cudaEventDestroy(stop); -#endif - return elapsedTime; -} \ No newline at end of file diff --git a/nanovdb/nanovdb/examples/benchmark/BenchKernels_nano.cu b/nanovdb/nanovdb/examples/benchmark/BenchKernels_nano.cu deleted file mode 100644 index 13d2f0088f..0000000000 --- a/nanovdb/nanovdb/examples/benchmark/BenchKernels_nano.cu +++ /dev/null @@ -1,112 +0,0 @@ - -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: MPL-2.0 - -/// @file BenchKernels.cu -/// -/// @author Ken Museth -/// -/// @brief CUDA kernel for a simple ray-tracing benchmark test. - -#include // for nanovdb::GridHandle -#include // for CUDA memory management -#include // for nanovdb::Ray -#include // for nanovdb::ZeroCrossing - -#include "Image.h" -#include "Camera.h" - -// Comment out to disable timing of the CUDA kernel -#define CUDA_TIMING - -// This is called by the device -template -__global__ void render_kernel(const nanovdb::NanoGrid& grid, - const nanovdb::Camera& camera, - nanovdb::Image& img) -{ - using RealT = float; - using Vec3T = nanovdb::Vec3; - using CoordT = nanovdb::Coord; - using RayT = nanovdb::Ray; - using ColorRGB = nanovdb::Image::ColorRGB; - - const int w = blockIdx.x * blockDim.x + threadIdx.x; - const int h = blockIdx.y * blockDim.y + threadIdx.y; - if (w >= img.width() || h >= img.height()) - return; - - const auto& tree = grid.tree(); - const auto& bbox = tree.bbox(); - RayT ray = camera.getRay(img.u(w), img.v(h)); - ray = ray.worldToIndexF(grid); - - auto acc = tree.getAccessor(); - CoordT ijk; - float t; - float v0; - if (nanovdb::ZeroCrossing(ray, acc, ijk, v0, t)) { -#if 1// second-order central difference - Vec3T grad(acc.getValue(ijk.offsetBy(1,0,0)) - acc.getValue(ijk.offsetBy(-1,0,0)), - acc.getValue(ijk.offsetBy(0,1,0)) - acc.getValue(ijk.offsetBy(0,-1,0)), - acc.getValue(ijk.offsetBy(0,0,1)) - acc.getValue(ijk.offsetBy(0,0,-1))); -#else// first order single-sided difference - Vec3T grad(-v0); - ijk[0] += 1; - grad[0] += acc.getValue(ijk); - ijk[0] -= 1; - ijk[1] += 1; - grad[1] += acc.getValue(ijk); - ijk[1] -= 1; - ijk[2] += 1; - grad[2] += acc.getValue(ijk); -#endif - grad *= rnorm3df(grad[0], grad[1], grad[2]); - img(w, h) = ColorRGB(abs(grad.dot(ray.dir())), 0, 0); - } else { - const int checkerboard = 1 << 7; - img(w, h) = ((h & checkerboard) ^ (w & checkerboard)) ? ColorRGB(1, 1, 1) : ColorRGB(0, 0, 0); - } -} - -// This is called by the host -extern "C" float launch_kernels(const nanovdb::GridHandle& gridHandle, - nanovdb::ImageHandle& imgHandle, - const nanovdb::Camera* camera, - cudaStream_t stream) -{ - using BuildT = nanovdb::FpN; - const auto* img = imgHandle.image(); // host image! - auto round = [](int a, int b) { return (a + b - 1) / b; }; - const dim3 threadsPerBlock(8, 8), numBlocks(round(img->width(), threadsPerBlock.x), round(img->height(), threadsPerBlock.y)); - auto* deviceGrid = gridHandle.deviceGrid(); // note this cannot be de-referenced since it points to a memory address on the GPU! - auto* deviceImage = imgHandle.deviceImage(); // note this cannot be de-referenced since it points to a memory address on the GPU! - assert(deviceGrid && deviceImage); - -#ifdef CUDA_TIMING - cudaEvent_t start, stop; - cudaEventCreate(&start); - cudaEventCreate(&stop); - cudaEventRecord(start, stream); -#endif - - // kernal syntax: <<>> - render_kernel<<>>(*deviceGrid, *camera, *deviceImage); - - float elapsedTime = 0.0f; -#ifdef CUDA_TIMING - cudaEventRecord(stop, stream); - cudaEventSynchronize(stop); - - cudaEventElapsedTime(&elapsedTime, start, stop); - //printf("NanoVDB: GPU kernel with %i rays ... completed in %5.3f milliseconds\n", imgHandle.image()->size(), elapsedTime); - cudaError_t errCode = cudaGetLastError(); - if (errCode != cudaSuccess) { - fprintf(stderr, "CUDA Runtime Error: %s %s %d\n", cudaGetErrorString(errCode), __FILE__, __LINE__); - exit(errCode); - } - cudaEventDestroy(start); - cudaEventDestroy(stop); -#endif - return elapsedTime; -} \ No newline at end of file diff --git a/nanovdb/nanovdb/examples/benchmark/Benchmark.cc b/nanovdb/nanovdb/examples/benchmark/Benchmark.cc deleted file mode 100644 index ba3b46dcdb..0000000000 --- a/nanovdb/nanovdb/examples/benchmark/Benchmark.cc +++ /dev/null @@ -1,702 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: MPL-2.0 - -/// @file Benchmark.cc -/// -/// @author Ken Museth -/// -/// @brief A simple ray-tracing benchmark test. - -#include -#include -#include -#include -#include -#include "Image.h" -#include "Camera.h" -#include "../ex_util/CpuTimer.h" - -#include "DenseGrid.h" - -#if defined(NANOVDB_USE_CUDA) -#include -#endif - -#if defined(NANOVDB_USE_OPENVDB) -#include - -#include -#include -#include -#include -#include -#include -#include -#endif - -#if defined(NANOVDB_USE_TBB) -#include -#include -#endif - -#include - -namespace nanovdb { - -inline std::ostream& -operator<<(std::ostream& os, const CoordBBox& b) -{ - os << "(" << b[0][0] << "," << b[0][1] << "," << b[0][2] << ") ->" - << "(" << b[1][0] << "," << b[1][1] << "," << b[1][2] << ")"; - return os; -} - -inline std::ostream& -operator<<(std::ostream& os, const Coord& ijk) -{ - os << "(" << ijk[0] << "," << ijk[1] << "," << ijk[2] << ")"; - return os; -} - -template -inline std::ostream& -operator<<(std::ostream& os, const Vec3& v) -{ - os << "(" << v[0] << "," << v[1] << "," << v[2] << ")"; - return os; -} - -} - -// define the environment variable VDB_DATA_PATH to use models from the web -// e.g. setenv VDB_DATA_PATH /home/kmu/dev/data/vdb -// or export VDB_DATA_PATH=/Users/ken/dev/data/vdb - -// define the environment variable VDB_SCRATCH_PATH to specify the directory where image are saved - -// The fixture for testing class. -class Benchmark : public ::testing::Test -{ -protected: - Benchmark() {} - - ~Benchmark() override {} - - // If the constructor and destructor are not enough for setting up - // and cleaning up each test, you can define the following methods: - - void SetUp() override - { - // Code here will be called immediately after the constructor (right - // before each test). - } - - void TearDown() override - { - // Code here will be called immediately after each test (right - // before the destructor). - } - std::string getEnvVar(const std::string& name, const std::string def = "") const - { - const char* str = std::getenv(name.c_str()); - return str == nullptr ? def : std::string(str); - } - -#if defined(NANOVDB_USE_OPENVDB) - openvdb::FloatGrid::Ptr getSrcGrid(int verbose = 1) - { - openvdb::FloatGrid::Ptr grid; - const std::string path = this->getEnvVar("VDB_DATA_PATH"); - if (path.empty()) { // create a narrow-band level set sphere - std::cout << "\tSet the environment variable \"VDB_DATA_PATH\" to a directory\n" - << "\tcontaining OpenVDB level sets files. They can be downloaded\n" - << "\there: https://www.openvdb.org/download/" << std::endl; - const float radius = 50.0f; - const openvdb::Vec3f center(0.0f, 0.0f, 0.0f); - const float voxelSize = 0.1f, width = 3.0f; - if (verbose > 0) { - std::stringstream ss; - ss << "Generating level set sphere with a radius of " << radius << " voxels"; - mTimer.start(ss.str()); - } -#if 1 // choose between a sphere or one of five platonic solids - grid = openvdb::tools::createLevelSetSphere(radius, center, voxelSize, width); - grid->setName("ls_sphere"); -#else - const int faces[5] = {4, 6, 8, 12, 20}; - grid = openvdb::tools::createLevelSetPlatonic(faces[4], radius, center, voxelSize, width); - grid->setName("ls_platonic"); -#endif - } else { - openvdb::initialize(); - const std::vector models = {"armadillo.vdb", "buddha.vdb", "bunny.vdb", "crawler.vdb", "dragon.vdb", "iss.vdb", "space.vdb", "torus_knot_helix.vdb", "utahteapot.vdb", "bunny_cloud.vdb", "wdas_cloud.vdb"}; - const std::string fileName = path + "/" + models[4]; // - if (verbose > 0) - mTimer.start("Reading grid from the file \"" + fileName + "\""); - openvdb::io::File file(fileName); - file.open(false); //disable delayed loading - grid = openvdb::gridPtrCast(file.readGrid(file.beginName().gridName())); - } - if (verbose > 0) - mTimer.stop(); - if (verbose > 1) - grid->print(std::cout, 3); - return grid; - } -#endif - nanovdb::CpuTimer<> mTimer; -}; // Benchmark - -TEST_F(Benchmark, Ray) -{ - using RealT = float; - using Vec3T = nanovdb::Vec3; - using CoordT = nanovdb::Coord; - using CoordBBoxT = nanovdb::BBox; - using BBoxT = nanovdb::BBox; - using RayT = nanovdb::Ray; - - {// clip ray against an index bbox - // test bbox clip - const Vec3T dir(-1.0, 2.0, 3.0); - const Vec3T eye(2.0, 1.0, 1.0); - RealT t0 = 0.1, t1 = 12589.0; - RayT ray(eye, dir, t0, t1); - - // intersects the two faces of the box perpendicular to the y-axis! - EXPECT_TRUE(ray.clip(CoordBBoxT(CoordT(0, 2, 2), CoordT(2, 4, 6)))); - //std::cerr << "t0 = " << ray.t0() << ", ray.t1() = " << ray.t1() << std::endl; - //std::cerr << "ray(0.5) = " << ray(0.5) << std::endl; - //std::cerr << "ray(1.5) = " << ray(1.5) << std::endl; - //std::cerr << "ray(2.0) = " << ray(2.0) << std::endl; - EXPECT_EQ(0.5, ray.t0()); - EXPECT_EQ(2.0, ray.t1()); - EXPECT_EQ(ray(0.5)[1], 2); //lower y component of intersection - EXPECT_EQ(ray(2.0)[1], 5); //higher y component of intersection - - ray.reset(eye, dir, t0, t1); - // intersects the lower edge anlong the z-axis of the box - EXPECT_TRUE(ray.clip(BBoxT(Vec3T(1.5, 2.0, 2.0), Vec3T(4.5, 4.0, 6.0)))); - EXPECT_EQ(0.5, ray.t0()); - EXPECT_EQ(0.5, ray.t1()); - EXPECT_EQ(ray(0.5)[0], 1.5); //lower y component of intersection - EXPECT_EQ(ray(0.5)[1], 2.0); //higher y component of intersection - - ray.reset(eye, dir, t0, t1); - // no intersections - EXPECT_TRUE(!ray.clip(CoordBBoxT(CoordT(4, 2, 2), CoordT(6, 4, 6)))); - EXPECT_EQ(t0, ray.t0()); - EXPECT_EQ(t1, ray.t1()); - } - {// clip ray against an real bbox - // test bbox clip - const Vec3T dir(-1.0, 2.0, 3.0); - const Vec3T eye(2.0, 1.0, 1.0); - RealT t0 = 0.1, t1 = 12589.0; - RayT ray(eye, dir, t0, t1); - - // intersects the two faces of the box perpendicular to the y-axis! - EXPECT_TRUE( ray.clip(CoordBBoxT(CoordT(0, 2, 2), CoordT(2, 4, 6)).asReal()) ); - //std::cerr << "t0 = " << ray.t0() << ", ray.t1() = " << ray.t1() << std::endl; - //std::cerr << "ray(0.5) = " << ray(0.5) << std::endl; - //std::cerr << "ray(1.5) = " << ray(1.5) << std::endl; - //std::cerr << "ray(2.0) = " << ray(2.0) << std::endl; - EXPECT_EQ(0.5, ray.t0()); - EXPECT_EQ(2.0, ray.t1()); - EXPECT_EQ(ray(0.5)[1], 2); //lower y component of intersection - EXPECT_EQ(ray(1.5)[1], 4); //higher y component of intersection - - ray.reset(eye, dir, t0, t1); - // intersects the lower edge along the z-axis of the box - EXPECT_TRUE( ray.clip(BBoxT(Vec3T(1.5, 2.0, 2.0), Vec3T(4.5, 4.0, 6.0))) ); - EXPECT_EQ(0.5, ray.t0()); - EXPECT_EQ(0.5, ray.t1()); - EXPECT_EQ(ray(0.5)[0], 1.5); //lower y component of intersection - EXPECT_EQ(ray(0.5)[1], 2.0); //higher y component of intersection - - ray.reset(eye, dir, t0, t1); - // no intersections - EXPECT_TRUE(!ray.clip(CoordBBoxT(CoordT(4, 2, 2), CoordT(6, 4, 6)).asReal()) ); - EXPECT_EQ(t0, ray.t0()); - EXPECT_EQ(t1, ray.t1()); - } -} - -TEST_F(Benchmark, HDDA) -{ - using RealT = float; - using CoordT = nanovdb::Coord; - using RayT = nanovdb::Ray; - using Vec3T = RayT::Vec3T; - - { // basic test - using DDAT = nanovdb::HDDA; - const RayT::Vec3T dir(1.0, 0.0, 0.0); - const RayT::Vec3T eye(-1.0, 0.0, 0.0); - const RayT ray(eye, dir); - DDAT dda(ray, 1 << (3 + 4 + 5)); - EXPECT_EQ(nanovdb::Delta::value(), dda.time()); - EXPECT_EQ(1.0, dda.next()); - dda.step(); - EXPECT_EQ(1.0, dda.time()); - EXPECT_EQ(4096 + 1.0, dda.next()); - } - { // Check for the notorious +-0 issue! - using DDAT = nanovdb::HDDA; - - const Vec3T dir1(1.0, 0.0, 0.0); - const Vec3T eye1(2.0, 0.0, 0.0); - const RayT ray1(eye1, dir1); - DDAT dda1(ray1, 1 << 3); - dda1.step(); - - const Vec3T dir2(1.0, -0.0, -0.0); - const Vec3T eye2(2.0, 0.0, 0.0); - const RayT ray2(eye2, dir2); - DDAT dda2(ray2, 1 << 3); - dda2.step(); - - const Vec3T dir3(1.0, -1e-9, -1e-9); - const Vec3T eye3(2.0, 0.0, 0.0); - const RayT ray3(eye3, dir3); - DDAT dda3(ray3, 1 << 3); - dda3.step(); - - const Vec3T dir4(1.0, -1e-9, -1e-9); - const Vec3T eye4(2.0, 0.0, 0.0); - const RayT ray4(eye3, dir4); - DDAT dda4(ray4, 1 << 3); - dda4.step(); - - EXPECT_EQ(dda1.time(), dda2.time()); - EXPECT_EQ(dda2.time(), dda3.time()); - EXPECT_EQ(dda3.time(), dda4.time()); - EXPECT_EQ(dda1.next(), dda2.next()); - EXPECT_EQ(dda2.next(), dda3.next()); - EXPECT_EQ(dda3.next(), dda4.next()); - } - { // test voxel traversal along both directions of each axis - using DDAT = nanovdb::HDDA; - const Vec3T eye(0, 0, 0); - for (int s = -1; s <= 1; s += 2) { - for (int a = 0; a < 3; ++a) { - const int d[3] = {s * (a == 0), s * (a == 1), s * (a == 2)}; - const Vec3T dir(d[0], d[1], d[2]); - RayT ray(eye, dir); - DDAT dda(ray, 1 << 0); - for (int i = 1; i <= 10; ++i) { - EXPECT_TRUE(dda.step()); - EXPECT_EQ(i, dda.time()); - } - } - } - } - { // test Node traversal along both directions of each axis - using DDAT = nanovdb::HDDA; - const Vec3T eye(0, 0, 0); - - for (int s = -1; s <= 1; s += 2) { - for (int a = 0; a < 3; ++a) { - const int d[3] = {s * (a == 0), s * (a == 1), s * (a == 2)}; - const Vec3T dir(d[0], d[1], d[2]); - RayT ray(eye, dir); - DDAT dda(ray, 1 << 3); - for (int i = 1; i <= 10; ++i) { - EXPECT_TRUE(dda.step()); - EXPECT_EQ(8 * i, dda.time()); - } - } - } - } - { // test accelerated Node traversal along both directions of each axis - using DDAT = nanovdb::HDDA; - const Vec3T eye(0, 0, 0); - - for (int s = -1; s <= 1; s += 2) { - for (int a = 0; a < 3; ++a) { - const int d[3] = {s * (a == 0), s * (a == 1), s * (a == 2)}; - const Vec3T dir(2 * d[0], 2 * d[1], 2 * d[2]); - RayT ray(eye, dir); - DDAT dda(ray, 1 << 3); - double next = 0; - for (int i = 1; i <= 10; ++i) { - EXPECT_TRUE(dda.step()); - EXPECT_EQ(4 * i, dda.time()); - if (i > 1) { - EXPECT_EQ(dda.time(), next); - } - next = dda.next(); - } - } - } - } -} // HDDA - - -TEST_F(Benchmark, DenseGrid) -{ - {// CoordT = nanovdb::Coord - using GridT = nanovdb::DenseGrid; - const nanovdb::Coord min(-10,0,10), max(10,20,30), pos(0,5,20); - const nanovdb::CoordBBox bbox( min, max ); - auto handle = GridT::create(min, max); - auto *grid = handle.grid(); - EXPECT_TRUE(grid); - EXPECT_TRUE(grid->test(min)); - EXPECT_TRUE(grid->test(max)); - EXPECT_TRUE(grid->test(pos)); - EXPECT_EQ( uint64_t(21*21*21), bbox.volume() ); - EXPECT_EQ( bbox.volume(), grid->size() ); - EXPECT_EQ( 0u, grid->coordToOffset(min) ); - float *p = grid->values(); - for (uint64_t i=0; isize(); ++i) { - *p++ = 0.0f; - } - EXPECT_EQ( 0.0f, grid->getValue(min) ); - EXPECT_EQ( 0.0f, grid->getValue(pos) ); - EXPECT_EQ( 0.0f, grid->getValue(max) ); - grid->setValue(pos, 1.0f); - EXPECT_EQ( 0.0f, grid->getValue(min) ); - EXPECT_EQ( 1.0f, grid->getValue(pos) ); - EXPECT_EQ( 0.0f, grid->getValue(max) ); - for (auto it = bbox.begin(); it; ++it) { - auto &ijk = *it; - EXPECT_TRUE(grid->test(ijk)); - if (ijk == pos) { - EXPECT_EQ( 1.0f, grid->getValue(ijk) ); - } else { - EXPECT_EQ( 0.0f, grid->getValue(ijk) ); - } - } - EXPECT_EQ(nanovdb::GridType::Float, grid->gridType()); - EXPECT_EQ(nanovdb::GridClass::Unknown, grid->gridClass()); - EXPECT_EQ(bbox, grid->indexBBox()); - EXPECT_EQ(nanovdb::Vec3d(min[0], min[1], min[2]), grid->worldBBox()[0]); - EXPECT_EQ(nanovdb::Vec3d(max[0]+1, max[1]+1, max[2]+1), grid->worldBBox()[1]); - EXPECT_EQ(nanovdb::Vec3d(1.0), grid->voxelSize()); - nanovdb::io::writeDense(handle, "data/dense.vol"); - //nanovdb::io::writeDense(*grid, "data/dense.vol"); - } - {// CoordT = nanovdb::Coord - auto handle = nanovdb::io::readDense<>("data/dense.vol"); - const nanovdb::Coord min(-10,0,10), max(10,20,30), pos(0,5,20); - const nanovdb::CoordBBox bbox( min, max ); - auto *grid = handle.grid(); - EXPECT_TRUE(grid); - EXPECT_TRUE(grid->test(min)); - EXPECT_TRUE(grid->test(max)); - EXPECT_TRUE(grid->test(pos)); - EXPECT_EQ( uint64_t(21*21*21), bbox.volume() ); - EXPECT_EQ( bbox.volume(), grid->size() ); - EXPECT_EQ( 0u, grid->coordToOffset(min) ); - EXPECT_EQ( 0.0f, grid->getValue(min) ); - EXPECT_EQ( 1.0f, grid->getValue(pos) ); - EXPECT_EQ( 0.0f, grid->getValue(max) ); - EXPECT_EQ(min, grid->min()); - EXPECT_EQ(max, grid->max()); - for (auto it = bbox.begin(); it; ++it) { - auto &ijk = *it; - EXPECT_TRUE(grid->test(ijk)); - if (ijk == pos) { - EXPECT_EQ( 1.0f, grid->getValue(ijk) ); - } else { - EXPECT_EQ( 0.0f, grid->getValue(ijk) ); - } - } - EXPECT_EQ(nanovdb::GridType::Float, grid->gridType()); - EXPECT_EQ(nanovdb::GridClass::Unknown, grid->gridClass()); - EXPECT_EQ(bbox, grid->indexBBox()); - EXPECT_EQ(nanovdb::Vec3d(min[0], min[1], min[2]), grid->worldBBox()[0]); - EXPECT_EQ(nanovdb::Vec3d(max[0]+1, max[1]+1, max[2]+1), grid->worldBBox()[1]); - EXPECT_EQ(nanovdb::Vec3d(1.0), grid->voxelSize()); - } -} - -#if defined(NANOVDB_USE_OPENVDB) -TEST_F(Benchmark, OpenVDB_CPU) -{ - using GridT = openvdb::FloatGrid; - using CoordT = openvdb::Coord; - using ColorRGB = nanovdb::Image::ColorRGB; - using RealT = float; - using Vec3T = openvdb::math::Vec3; - using RayT = openvdb::math::Ray; - - const std::string image_path = this->getEnvVar("VDB_SCRATCH_PATH", "."); - - auto srcGrid = this->getSrcGrid(); - mTimer.start("Generating NanoVDB grid"); - auto handle = nanovdb::openToNanoVDB(*srcGrid, nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Disable, /*verbose=*/0); - mTimer.restart("Writing NanoVDB grid"); -#if defined(NANOVDB_USE_BLOSC) - nanovdb::io::writeGrid("data/test.nvdb", handle, nanovdb::io::Codec::BLOSC); -#elif defined(NANOVDB_USE_ZIP) - nanovdb::io::writeGrid("data/test.nvdb", handle, nanovdb::io::Codec::ZIP); -#else - nanovdb::io::writeGrid("data/test.nvdb", handle, nanovdb::io::Codec::NONE); -#endif - mTimer.stop(); - - {// convert and write DenseGRid - mTimer.start("Generating DenseGrid"); - auto dHandle = nanovdb::convertToDense(*handle.grid()); - mTimer.restart("Writing DenseGrid"); - nanovdb::io::writeDense(dHandle, "data/test.vol"); - mTimer.stop(); - } - - const int width = 1280, height = 720; - const RealT vfov = 25.0f, aspect = RealT(width) / height, radius = 300.0f; - const auto bbox = srcGrid->evalActiveVoxelBoundingBox(); - const openvdb::Vec3d center(0.5 * (bbox.max()[0] + bbox.min()[0]), - 0.5 * (bbox.max()[1] + bbox.min()[1]), - 0.5 * (bbox.max()[2] + bbox.min()[2])); - const Vec3T lookat = srcGrid->indexToWorld(center), up(0, -1, 0); - auto eye = [&lookat, &radius](int angle) { - const RealT theta = angle * openvdb::math::pi() / 180.0f; - return lookat + radius * Vec3T(sin(theta), 0, cos(theta)); - }; - - nanovdb::Camera camera(eye(0), lookat, up, vfov, aspect); - - nanovdb::ImageHandle<> imgHandle(width, height); - auto* img = imgHandle.image(); - - auto kernel2D = [&](const tbb::blocked_range2d& r) { - openvdb::tools::LevelSetRayIntersector, GridT::TreeType::RootNodeType::ChildNodeType::LEVEL, RayT> tester(*srcGrid); - const RealT wScale = 1.0f / width, hScale = 1.0f / height; - auto acc = srcGrid->getAccessor(); - CoordT ijk; - Vec3T xyz; - float v; - for (int w = r.rows().begin(); w != r.rows().end(); ++w) { - for (int h = r.cols().begin(); h != r.cols().end(); ++h) { - const RayT wRay = camera.getRay(w * wScale, h * hScale); - RayT iRay = wRay.applyInverseMap(*srcGrid->transform().baseMap()); - if (tester.intersectsIS(iRay, xyz)) { - ijk = openvdb::Coord::floor(xyz); - v = acc.getValue(ijk); - Vec3T grad(-v); - ijk[0] += 1; - grad[0] += acc.getValue(ijk); - ijk[0] -= 1; - ijk[1] += 1; - grad[1] += acc.getValue(ijk); - ijk[1] -= 1; - ijk[2] += 1; - grad[2] += acc.getValue(ijk); - grad.normalize(); - (*img)(w, h) = ColorRGB(std::abs(grad.dot(iRay.dir())), 0, 0); - } else { - const int checkerboard = 1 << 7; - (*img)(w, h) = ((h & checkerboard) ^ (w & checkerboard)) ? ColorRGB(1, 1, 1) : ColorRGB(0, 0, 0); - } - } - } - }; // kernel - - for (int angle = 0; angle < 6; ++angle) { - camera.update(eye(angle), lookat, up, vfov, aspect); - std::stringstream ss; - ss << "OpenVDB: CPU kernel with " << img->size() << " rays"; - tbb::blocked_range2d range2D(0, img->width(), 0, img->height()); - mTimer.start(ss.str()); -#if 1 - tbb::parallel_for(range2D, kernel2D); -#else - kernel2D(range2D); -#endif - mTimer.stop(); - //mTimer.start("Write image to file"); - ss.str(""); - ss.clear(); - ss << image_path << "/openvdb_cpu_" << std::setfill('0') << std::setw(3) << angle << ".ppm"; - img->writePPM(ss.str(), "Benchmark test"); - //mTimer.stop(); - } // loop over angle -} // OpenVDB_CPU -#endif - - - -TEST_F(Benchmark, DenseGrid_CPU) -{ - using CoordT = nanovdb::Coord; - using ColorRGB = nanovdb::Image::ColorRGB; - using RealT = float; - using Vec3T = nanovdb::Vec3; - using RayT = nanovdb::Ray; - - auto handle = nanovdb::io::readDense("data/test.vol"); - auto* grid = handle.grid(); - EXPECT_TRUE(grid); - EXPECT_TRUE(grid->isLevelSet()); - - const int width = 1280, height = 720; - const RealT vfov = 25.0f, aspect = RealT(width) / height, radius = 300.0f; - const auto bbox = grid->worldBBox(); - const Vec3T lookat(0.5 * (bbox.min() + bbox.max())), up(0, -1, 0); - auto eye = [&lookat, &radius](int angle) { - const RealT theta = angle * openvdb::math::pi() / 180.0f; - return lookat + radius * Vec3T(sin(theta), 0, cos(theta)); - }; - - nanovdb::Camera camera(eye(0), lookat, up, vfov, aspect); - - nanovdb::ImageHandle<> imgHandle(width, height); - auto* img = imgHandle.image(); - - auto kernel2D = [&](int x0, int y0, int x1, int y1) { - const RealT wScale = 1.0f / width, hScale = 1.0f / height; - for (int w = x0; w != x1; ++w) { - for (int h = y0; h != y1; ++h) { - RayT ray = camera.getRay(w * wScale, h * hScale); - ray = ray.worldToIndexF(*grid); - if (!ray.clip(grid->indexBBox().expandBy(-1))) continue; - nanovdb::DDA dda(ray); - CoordT ijk = dda.voxel(); - EXPECT_TRUE(grid->test(ijk)); - const float v0 = grid->getValue(ijk); - bool hit = false; - while( !hit && dda.step() ) { - ijk = dda.voxel(); - EXPECT_TRUE(grid->test(ijk)); - const float v1 = grid->getValue(ijk); - if (v0*v1>0) continue; - Vec3T grad(-v1); - ijk[0] += 1; - grad[0] += grid->getValue(ijk); - ijk[0] -= 1; - ijk[1] += 1; - grad[1] += grid->getValue(ijk); - ijk[1] -= 1; - ijk[2] += 1; - grad[2] += grid->getValue(ijk); - grad.normalize(); - (*img)(w, h) = ColorRGB(std::abs(grad.dot(ray.dir())), 0, 0); - hit = true; - } - if (!hit) { - const int checkerboard = 1 << 7; - (*img)(w, h) = ((h & checkerboard) ^ (w & checkerboard)) ? ColorRGB(1, 1, 1) : ColorRGB(0, 0, 0); - } - } - } - }; // kernel - - for (int angle = 0; angle < 6; ++angle) { - camera.update(eye(angle), lookat, up, vfov, aspect); - std::stringstream ss; - ss << "DenseGrid: CPU kernel with " << img->size() << " rays"; - mTimer.start(ss.str()); -#if defined(NANOVDB_USE_TBB) - tbb::blocked_range2d range(0, img->width(), 0, img->height()); - tbb::parallel_for(range, [&](const tbb::blocked_range2d& r) { - kernel2D(r.rows().begin(), r.cols().begin(), r.rows().end(), r.cols().end()); - }); -#else - kernel2D(0, 0, img->width(), img->height()); -#endif - mTimer.stop(); - //mTimer.start("Write image to file"); - ss.str(""); - ss.clear(); - ss << "./dense_cpu_" << std::setfill('0') << std::setw(3) << angle << ".ppm"; - img->writePPM(ss.str(), "Benchmark test"); - //mTimer.stop(); - } // loop over angle -} // DenseGrid_CPU - -#if defined(NANOVDB_USE_CUDA) - -extern "C" void launch_kernels(const nanovdb::GridHandle&, - nanovdb::ImageHandle&, - const nanovdb::Camera*, - cudaStream_t stream); - -TEST_F(Benchmark, NanoVDB_GPU) -{ - using BufferT = nanovdb::CudaDeviceBuffer; - using RealT = float; - using Vec3T = nanovdb::Vec3; - using CameraT = nanovdb::Camera; - - const std::string image_path = this->getEnvVar("VDB_SCRATCH_PATH", "."); - - // The first CUDA run time call initializes the CUDA sub-system (loads the runtime API) which takes time! - int deviceCount; - cudaGetDeviceCount(&deviceCount); - for (int device = 0; device < deviceCount; ++device) { - cudaDeviceProp deviceProp; - cudaGetDeviceProperties(&deviceProp, device); - printf("Device %d has compute capability %d.%d.\n", - device, - deviceProp.major, - deviceProp.minor); - } - cudaSetDevice(0); - - cudaStream_t stream; - cudaCheck(cudaStreamCreate(&stream)); - -#if defined(NANOVDB_USE_OPENVDB) - auto handle = nanovdb::io::readGrid("data/test.nvdb"); -#else - auto handle = nanovdb::createLevelSetTorus(100.0f, 50.0f); -#endif - //auto handle = nanovdb::io::readGrid("data/test.nvdb"); - const auto* grid = handle.grid(); - EXPECT_TRUE(grid); - EXPECT_TRUE(grid->isLevelSet()); - EXPECT_FALSE(grid->isFogVolume()); - handle.deviceUpload(stream, false); - - std::cout << "\nRay-tracing NanoVDB grid named \"" << grid->gridName() << "\"" << std::endl; - - const int width = 1280, height = 720; - const RealT vfov = 25.0f, aspect = RealT(width) / height, radius = 300.0f; - const auto bbox = grid->worldBBox(); - const Vec3T lookat(0.5 * (bbox.min() + bbox.max())), up(0, -1, 0); - auto eye = [&lookat, &radius](int angle) { - const RealT theta = angle * openvdb::math::pi() / 180.0f; - return lookat + radius * Vec3T(sin(theta), 0, cos(theta)); - }; - CameraT *host_camera, *dev_camera; - cudaCheck(cudaMalloc((void**)&dev_camera, sizeof(CameraT))); // un-managed memory on the device - cudaCheck(cudaMallocHost((void**)&host_camera, sizeof(CameraT))); - - nanovdb::ImageHandle imgHandle(width, height); - auto* img = imgHandle.image(); - imgHandle.deviceUpload(stream, false); - - for (int angle = 0; angle < 6; ++angle) { - std::stringstream ss; - ss << "NanoVDB: GPU kernel with " << img->size() << " rays"; - host_camera->update(eye(angle), lookat, up, vfov, aspect); - cudaCheck(cudaMemcpyAsync(dev_camera, host_camera, sizeof(CameraT), cudaMemcpyHostToDevice, stream)); - mTimer.start(ss.str()); - launch_kernels(handle, imgHandle, dev_camera, stream); - mTimer.stop(); - - //mTimer.start("Write image to file"); - imgHandle.deviceDownload(stream); - ss.str(""); - ss.clear(); - ss << image_path << "/nanovdb_gpu_" << std::setfill('0') << std::setw(3) << angle << ".ppm"; - img->writePPM(ss.str(), "Benchmark test"); - //mTimer.stop(); - - } //frame number angle - - cudaCheck(cudaStreamDestroy(stream)); - cudaCheck(cudaFree(host_camera)); - cudaCheck(cudaFree(dev_camera)); -} // NanoVDB_GPU -#endif - - -int main(int argc, char** argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/nanovdb/nanovdb/examples/benchmark/Benchmark_dense.cc b/nanovdb/nanovdb/examples/benchmark/Benchmark_dense.cc deleted file mode 100644 index 041ca8bada..0000000000 --- a/nanovdb/nanovdb/examples/benchmark/Benchmark_dense.cc +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: MPL-2.0 - -/// @file Benchmark_nano.cc -/// -/// @author Ken Museth -/// -/// @brief A super lightweight and portable ray-tracing benchmark -/// that only depends on NanoVDB (not OpenVDB) and CUDA. - -#include -#include -#include "Image.h" -#include "Camera.h" -#include "DenseGrid.h" -#include "../ex_util/CpuTimer.h" - -#include // for std::setfill and std::setw - -extern "C" float launch_kernels(const nanovdb::DenseGridHandle&, - nanovdb::ImageHandle&, - const nanovdb::Camera*, - cudaStream_t stream); - -int main(int argc, char** argv) -{ - using BufferT = nanovdb::CudaDeviceBuffer; - using RealT = float; - using Vec3T = nanovdb::Vec3; - using CameraT = nanovdb::Camera; - nanovdb::CpuTimer<> timer; - - if (argc!=2) { - std::cerr << "Usage: " << argv[0] << " path/level_set.vol" << std::endl; - //std::cerr << "To generate an input file: nanovdb_convert dragon.vdb dragon.nvdb\n"; - return 1; - } - - // The first CUDA run time call initializes the CUDA sub-system (loads the runtime API) which takes time! - int deviceCount; - cudaGetDeviceCount(&deviceCount); - for (int device = 0; device < deviceCount; ++device) { - cudaDeviceProp deviceProp; - cudaGetDeviceProperties(&deviceProp, device); - printf("Device %d has compute capability %d.%d.\n", - device, - deviceProp.major, - deviceProp.minor); - } - cudaSetDevice(0); - - cudaStream_t stream; - cudaCheck(cudaStreamCreate(&stream)); - - auto handle = nanovdb::io::readDense(argv[1]); - - const auto* grid = handle.grid(); - if (!grid || !grid->isLevelSet()) { - std::cerr << "Error loading NanoVDB level set from file" << std::endl; - return 1; - } - handle.deviceUpload(stream, false); - std::cout << "\nRay-tracing DenseGrid of size " - << (grid->gridSize() >> 20) << " MB" << std::endl; - - const int width = 1280, height = 720; - const RealT vfov = 25.0f, aspect = RealT(width) / height, radius = 300.0f; - const auto bbox = grid->worldBBox(); - const Vec3T lookat(0.5 * (bbox.min() + bbox.max())), up(0, -1, 0); - auto eye = [&lookat, &radius](int angle) { - const RealT theta = angle * M_PI / 180.0f; - return lookat + radius * Vec3T(sin(theta), 0, cos(theta)); - }; - CameraT *host_camera, *dev_camera; - cudaCheck(cudaMalloc((void**)&dev_camera, sizeof(CameraT))); // un-managed memory on the device - cudaCheck(cudaMallocHost((void**)&host_camera, sizeof(CameraT))); - - nanovdb::ImageHandle imgHandle(width, height); - auto* img = imgHandle.image(); - imgHandle.deviceUpload(stream, false); - - float elapsedTime = 0.0f; - const int maxAngle = 360; - for (int angle = 0; angle < maxAngle; ++angle) { - host_camera->update(eye(angle), lookat, up, vfov, aspect); - cudaCheck(cudaMemcpyAsync(dev_camera, host_camera, sizeof(CameraT), cudaMemcpyHostToDevice, stream)); - elapsedTime += launch_kernels(handle, imgHandle, dev_camera, stream); - - //timer.start("Write image to file"); - imgHandle.deviceDownload(stream); - std::stringstream ss; - ss << "./dense_gpu_" << std::setfill('0') << std::setw(3) << angle << ".ppm"; - img->writePPM(ss.str(), "Benchmark test"); - //timer.stop(); - - } //frame number angle - - cudaCheck(cudaStreamDestroy(stream)); - cudaCheck(cudaFree(host_camera)); - cudaCheck(cudaFree(dev_camera)); - - printf("\nRay-traced %i different frames, each with %i rays, in %5.3f ms.\nThis corresponds to an average of %5.3f ms per frame or %5.3f FPS!\n", - maxAngle, imgHandle.image()->size(), elapsedTime, elapsedTime/maxAngle, 1000.0f*maxAngle/elapsedTime); - - return 0; -} \ No newline at end of file diff --git a/nanovdb/nanovdb/examples/benchmark/Benchmark_nano.cc b/nanovdb/nanovdb/examples/benchmark/Benchmark_nano.cc deleted file mode 100644 index aed38ddd07..0000000000 --- a/nanovdb/nanovdb/examples/benchmark/Benchmark_nano.cc +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: MPL-2.0 - -/// @file Benchmark_nano.cc -/// -/// @author Ken Museth -/// -/// @brief A super lightweight and portable ray-tracing benchmark -/// that only depends on NanoVDB (not OpenVDB) and CUDA. - -#include -#include -#include "Image.h" -#include "Camera.h" -#include "../ex_util/CpuTimer.h" - -#include // for std::setfill and std::setw - -extern "C" float launch_kernels(const nanovdb::GridHandle&, - nanovdb::ImageHandle&, - const nanovdb::Camera*, - cudaStream_t stream); - -int main(int argc, char** argv) -{ - using BufferT = nanovdb::CudaDeviceBuffer; - using ValueT = float; - using BuildT = nanovdb::FpN; - using Vec3T = nanovdb::Vec3; - using CameraT = nanovdb::Camera; - nanovdb::CpuTimer<> timer; - - if (argc!=2) { - std::cerr << "Usage: " << argv[0] << " path/level_set.nvdb" << std::endl; - std::cerr << "To generate an input file: nanovdb_convert dragon.vdb dragon.nvdb\n"; - return 1; - } - - // The first CUDA run time call initializes the CUDA sub-system (loads the runtime API) which takes time! - int deviceCount; - cudaGetDeviceCount(&deviceCount); - for (int device = 0; device < deviceCount; ++device) { - cudaDeviceProp deviceProp; - cudaGetDeviceProperties(&deviceProp, device); - printf("Device %d has compute capability %d.%d.\n", - device, - deviceProp.major, - deviceProp.minor); - } - cudaSetDevice(0); - int driverVersion, runtimeVersion; - cudaDriverGetVersion(&driverVersion); - cudaRuntimeGetVersion(&runtimeVersion); - printf("CUDA driver version:\t%i.%i\n", driverVersion/1000, (driverVersion%1000)/10); - printf("CUDA runtime version:\t%i.%i\n", runtimeVersion/1000, (runtimeVersion%1000)/10); - - cudaStream_t stream; - cudaCheck(cudaStreamCreate(&stream)); - - auto handle = nanovdb::io::readGrid(argv[1]); - - const auto* grid = handle.grid(); - if (!grid || !grid->isLevelSet()) { - std::cerr << "Error loading NanoVDB level set from file" << std::endl; - return 1; - } - handle.deviceUpload(stream, false); - std::cout << "\nRay-tracing NanoVDB grid named \"" << grid->gridName() << "\" of size " - << (grid->gridSize() >> 20) << " MB" << std::endl; - - const int width = 1280, height = 720; - const ValueT vfov = 25.0f, aspect = ValueT(width) / height, radius = 300.0f; - const auto bbox = grid->worldBBox(); - const Vec3T lookat(0.5 * (bbox.min() + bbox.max())), up(0, -1, 0); - auto eye = [&lookat, &radius](int angle) { - const ValueT theta = angle * M_PI / 180.0f; - return lookat + radius * Vec3T(sin(theta), 0, cos(theta)); - }; - CameraT *host_camera, *dev_camera; - cudaCheck(cudaMalloc((void**)&dev_camera, sizeof(CameraT))); // un-managed memory on the device - cudaCheck(cudaMallocHost((void**)&host_camera, sizeof(CameraT))); - - nanovdb::ImageHandle imgHandle(width, height); - auto* img = imgHandle.image(); - imgHandle.deviceUpload(stream, false); - - float elapsedTime = 0.0f; - const int maxAngle = 360; - for (int angle = 0; angle < maxAngle; ++angle) { - host_camera->update(eye(angle), lookat, up, vfov, aspect); - cudaCheck(cudaMemcpyAsync(dev_camera, host_camera, sizeof(CameraT), cudaMemcpyHostToDevice, stream)); - elapsedTime += launch_kernels(handle, imgHandle, dev_camera, stream); - - //timer.start("Write image to file"); - imgHandle.deviceDownload(stream); -#if 1 - std::stringstream ss; - ss << "./nanovdb_gpu_" << std::setfill('0') << std::setw(3) << angle << ".ppm"; - img->writePPM(ss.str(), "Benchmark test"); -#endif - //timer.stop(); - - } //frame number angle - - cudaCheck(cudaStreamDestroy(stream)); - cudaCheck(cudaFree(host_camera)); - cudaCheck(cudaFree(dev_camera)); - - printf("\nRay-traced %i different frames, each with %i rays, in %5.3f ms.\nThis corresponds to an average of %5.3f ms per frame or %5.3f FPS!\n", - maxAngle, imgHandle.image()->size(), elapsedTime, elapsedTime/maxAngle, 1000.0f*maxAngle/elapsedTime); - - return 0; -} \ No newline at end of file diff --git a/nanovdb/nanovdb/examples/benchmark/CMakeLists.txt b/nanovdb/nanovdb/examples/benchmark/CMakeLists.txt deleted file mode 100644 index d7a06d8930..0000000000 --- a/nanovdb/nanovdb/examples/benchmark/CMakeLists.txt +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: MPL-2.0 -# -#[=======================================================================[ - - CMake Configuration for NanoVDB Benchmark - -#]=======================================================================] - -cmake_minimum_required(VERSION 3.18) -project(NanoVDBBenchmark LANGUAGES CXX) - -include(GNUInstallDirs) - -############################################################################### - -message(STATUS "----------------------------------------------------") -message(STATUS "---------- Configuring NanoVDB Benchmark -----------") -message(STATUS "----------------------------------------------------") - -############################################################################### - -if(WIN32 AND NANOVDB_CUDA_KEEP_PTX) - file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/x64/Release") -endif() - -# ----------------------------------------------------------------------- -# TODO: Benchmark should probably not require gtest. -if(NOT TARGET GTest::GTest) - message(WARNING " - GTest required to build benchmark. Skipping.") - return() -endif() - -# ----------------------------------------------------------------------- -# many of the sample projects depend on a data directory. This allows Debug -# launching from the cmake binary working directory. -file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/data") - -set(BENCHMARK_SOURCES Benchmark.cc Camera.h Image.h) - -if(NANOVDB_USE_CUDA) - list(APPEND BENCHMARK_SOURCES BenchKernels_nano.cu) - - add_executable(benchmark_nano Benchmark_nano.cc BenchKernels_nano.cu) - add_executable(benchmark_dense Benchmark_dense.cc BenchKernels_dense.cu) - - target_link_libraries(benchmark_nano PRIVATE nanovdb) - target_link_libraries(benchmark_dense PRIVATE nanovdb) - - # Propagate MSVC Runtime from the OpenVDB library (if VDB_MSVC_RUNTIME_SELECTION - # has been set then we're building and linking against OpenVDB, see the root - # NanoVDB CMakeLists.txt) - if(VDB_MSVC_RUNTIME_SELECTION) - set_target_properties(benchmark_nano PROPERTIES - MSVC_RUNTIME_LIBRARY ${VDB_MSVC_RUNTIME_SELECTION}) - set_target_properties(benchmark_dense PROPERTIES - MSVC_RUNTIME_LIBRARY ${VDB_MSVC_RUNTIME_SELECTION}) - endif() -endif() - -# ----------------------------------------------------------------------- - -add_executable(benchmark ${BENCHMARK_SOURCES}) -target_link_libraries(benchmark PRIVATE nanovdb GTest::GTest GTest::Main) - -# Propagate MSVC Runtime from the OpenVDB library (if VDB_MSVC_RUNTIME_SELECTION -# has been set then we're building and linking against OpenVDB, see the root -# NanoVDB CMakeLists.txt) -if(VDB_MSVC_RUNTIME_SELECTION) - set_target_properties(benchmark PROPERTIES - MSVC_RUNTIME_LIBRARY ${VDB_MSVC_RUNTIME_SELECTION}) -endif() - -install(TARGETS benchmark DESTINATION ${CMAKE_INSTALL_DOCDIR}/examples) diff --git a/nanovdb/nanovdb/examples/benchmark/Camera.h b/nanovdb/nanovdb/examples/benchmark/Camera.h deleted file mode 100644 index 88e4580562..0000000000 --- a/nanovdb/nanovdb/examples/benchmark/Camera.h +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: MPL-2.0 - -/// @file Camera.h -/// -/// @author Ken Museth -/// -/// @brief A simple camera class. - -#ifndef NANOVDB_CAMERA_H_HAS_BEEN_INCLUDED -#define NANOVDB_CAMERA_H_HAS_BEEN_INCLUDED - -#include // for Vec3 -#include - -namespace nanovdb { - -/// @brief A minimal perspective camera for ray generation -template, typename RayT = Ray> -class Camera -{ - Vec3T mEye, mW, mU, mV; - - __hostdev__ void init(RealT vfov, RealT aspect) - { - const RealT halfHeight = RealT(tan(vfov * 3.14159265358979323846 / 360)); - const RealT halfWidth = aspect * halfHeight; - mW = halfWidth * mU + halfHeight * mV + mW; // remove eye here and in getRay - mU *= 2 * halfWidth; - mV *= 2 * halfHeight; - } - -public: - /// @brief default Ctor. - Camera() = default; - - /// @brief Ctor. // vfov is top to bottom in degrees - /// @note up is assumed to be a unit-vector - __hostdev__ Camera(const Vec3T& eye, const Vec3T& lookat, const Vec3T& up, RealT vfov, RealT aspect) - : mEye(eye) - , mW((eye - lookat).normalize()) - , mU(up.cross(mW)) - , mV(up) - { - this->init(vfov, aspect); - } - __hostdev__ void update(const Vec3T& eye, const Vec3T& lookat, const Vec3T& up, RealT vfov, RealT aspect) - { - mEye = eye; - mV = up; - mW = mEye - lookat; - mW.normalize(); - mU = mV.cross(mW); - this->init(vfov, aspect); - } - /// @brief {u,v} are are assumed to be [0,1] - __hostdev__ RayT getRay(RealT u, RealT v) const { - auto dir = u * mU + v * mV - mW; - dir.normalize(); - return RayT(mEye, dir); - } - - __hostdev__ const Vec3T& P() const { return mEye; } - __hostdev__ const Vec3T& U() const { return mU; } - __hostdev__ const Vec3T& V() const { return mV; } - __hostdev__ const Vec3T& W() const { return mW; } - -}; // Camera - -} // namespace nanovdb - -#endif // NANOVDB_CAMERA_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/examples/benchmark/DenseGrid.h b/nanovdb/nanovdb/examples/benchmark/DenseGrid.h deleted file mode 100644 index 1fd00630af..0000000000 --- a/nanovdb/nanovdb/examples/benchmark/DenseGrid.h +++ /dev/null @@ -1,487 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: MPL-2.0 - -/// @file DenseGrid.h -/// -/// @author Ken Museth -/// -/// @brief Simple dense grid class. - -#ifndef NANOVDB_DENSEGRID_H_HAS_BEEN_INCLUDED -#define NANOVDB_DENSEGRID_H_HAS_BEEN_INCLUDED - -#include // for uint64_t -#include // for std::ifstream -#include // for default Buffer -#include -#include // for Map, GridClass, GridType and and Coord - - -// use 4x4x4 tiles for better cache coherence -// else it uses dense indexing which is slow! -// 0 means disable, 1 is 2x2x2, 2 is 4x4x4 and 3 is 8x8x8 -#define LOG2_TILE_SIZE 2 - -namespace nanovdb { - -// forward decleration -template -class DenseGridHandle; - -#define DENSE_MAGIC_NUMBER 0x42445665736e6544UL // "DenseVDB" in hex - little endian (uint64_t) - - -struct DenseData -{ - Map mMap;// defined in NanoVDB.h - CoordBBox mIndexBBox;// min/max of bbox - BBox mWorldBBox;// 48B. floating-point AABB of active values in WORLD SPACE (2 x 3 doubles) - Vec3R mVoxelSize; - GridClass mGridClass;// defined in NanoVDB.h - GridType mGridType; // defined in NanoVDB.h - uint64_t mY, mX;//strides in the y and x direction - uint64_t mSize; - - __hostdev__ Coord dim() const { return mIndexBBox.dim(); } - - // Affine transformations based on double precision - template - __hostdev__ Vec3T applyMap(const Vec3T& xyz) const { return mMap.applyMap(xyz); } // Pos: index -> world - template - __hostdev__ Vec3T applyInverseMap(const Vec3T& xyz) const { return mMap.applyInverseMap(xyz); } // Pos: world -> index - template - __hostdev__ Vec3T applyJacobian(const Vec3T& xyz) const { return mMap.applyJacobian(xyz); } // Dir: index -> world - template - __hostdev__ Vec3T applyInverseJacobian(const Vec3T& xyz) const { return mMap.applyInverseJacobian(xyz); } // Dir: world -> index - template - __hostdev__ Vec3T applyIJT(const Vec3T& xyz) const { return mMap.applyIJT(xyz); } - // Affine transformations based on single precision - template - __hostdev__ Vec3T applyMapF(const Vec3T& xyz) const { return mMap.applyMapF(xyz); } // Pos: index -> world - template - __hostdev__ Vec3T applyInverseMapF(const Vec3T& xyz) const { return mMap.applyInverseMapF(xyz); } // Pos: world -> index - template - __hostdev__ Vec3T applyJacobianF(const Vec3T& xyz) const { return mMap.applyJacobianF(xyz); } // Dir: index -> world - template - __hostdev__ Vec3T applyInverseJacobianF(const Vec3T& xyz) const { return mMap.applyInverseJacobianF(xyz); } // Dir: world -> index - template - __hostdev__ Vec3T applyIJTF(const Vec3T& xyz) const { return mMap.applyIJTF(xyz); } -}; -/// @brief Simple dense grid class -/// @note ZYX is the memory-layout in VDB. It leads to nested -/// for-loops of the order x, y, z. -template -class DenseGrid : private DenseData -{ -#if LOG2_TILE_SIZE > 0 - static constexpr uint32_t TileLog2 = LOG2_TILE_SIZE, TileMask = (1 << TileLog2) - 1, TileDim = 1 << (3*TileLog2); -#endif - using DenseData = DenseData; - -public: - using ValueType = ValueT; - - template - inline static DenseGridHandle create(Coord min, // min inclusive index coordinate - Coord max, // max inclusive index coordinate - double dx = 1.0, //voxel size - const Vec3d& p0 = Vec3d(0.0), // origin - GridClass gridClass = GridClass::Unknown, - const BufferT& allocator = BufferT()); - - __hostdev__ DenseGrid(const DenseGrid&) = delete; - __hostdev__ ~DenseGrid() = delete; - __hostdev__ DenseGrid& operator=(const DenseGrid&) = delete; - - __hostdev__ uint64_t size() const { return mIndexBBox.volume(); } - __hostdev__ inline uint64_t coordToOffset(const Coord &ijk) const; - __hostdev__ inline bool test(const Coord &ijk) const; - __hostdev__ uint64_t memUsage() const {return mSize;} - __hostdev__ uint64_t gridSize() const {return this->memUsage();} - __hostdev__ const Coord& min() const { return mIndexBBox[0]; } - __hostdev__ const Coord& max() const { return mIndexBBox[1]; } - __hostdev__ inline bool isValidType() const; - - /// @brief Return a const reference to the Map for this grid - __hostdev__ const Map& map() const { return DenseData::mMap; } - - // @brief Return a const reference to the size of a voxel in world units - __hostdev__ const Vec3R& voxelSize() const { return DenseData::mVoxelSize; } - - /// @brief world to index space transformation - template - __hostdev__ Vec3T worldToIndex(const Vec3T& xyz) const { return this->applyInverseMap(xyz); } - - /// @brief world to index space transformation - template - __hostdev__ Vec3T indexToWorld(const Vec3T& xyz) const { return this->applyMap(xyz); } - - /// @brief transformation from index space direction to world space direction - /// @warning assumes dir to be normalized - template - __hostdev__ Vec3T indexToWorldDir(const Vec3T& dir) const { return this->applyJacobian(dir); } - - /// @brief transformation from world space direction to index space direction - /// @warning assumes dir to be normalized - template - __hostdev__ Vec3T worldToIndexDir(const Vec3T& dir) const { return this->applyInverseJacobian(dir); } - - /// @brief transform the gradient from index space to world space. - /// @details Applies the inverse jacobian transform map. - template - __hostdev__ Vec3T indexToWorldGrad(const Vec3T& grad) const { return this->applyIJT(grad); } - - /// @brief world to index space transformation - template - __hostdev__ Vec3T worldToIndexF(const Vec3T& xyz) const { return this->applyInverseMapF(xyz); } - - /// @brief index to world space transformation - template - __hostdev__ Vec3T indexToWorldF(const Vec3T& xyz) const { return this->applyMapF(xyz); } - - /// @brief transformation from index space direction to world space direction - /// @warning assumes dir to be normalized - template - __hostdev__ Vec3T indexToWorldDirF(const Vec3T& dir) const { return this->applyJacobianF(dir); } - - /// @brief transformation from world space direction to index space direction - /// @warning assumes dir to be normalized - template - __hostdev__ Vec3T worldToIndexDirF(const Vec3T& dir) const { return this->applyInverseJacobianF(dir); } - - /// @brief Transforms the gradient from index space to world space. - /// @details Applies the inverse jacobian transform map. - template - __hostdev__ Vec3T indexToWorldGradF(const Vec3T& grad) const { return DenseData::applyIJTF(grad); } - - /// @brief Computes a AABB of active values in world space - __hostdev__ const BBox& worldBBox() const { return DenseData::mWorldBBox; } - - __hostdev__ bool isLevelSet() const { return DenseData::mGridClass == GridClass::LevelSet; } - __hostdev__ bool isFogVolume() const { return DenseData::mGridClass == GridClass::FogVolume; } - - /// @brief Computes a AABB of active values in index space - /// - /// @note This method is returning a floating point bounding box and not a CoordBBox. This makes - /// it more useful for clipping rays. - __hostdev__ const CoordBBox& indexBBox() const { return mIndexBBox; } - - __hostdev__ const GridType& gridType() const { return DenseData::mGridType; } - __hostdev__ const GridClass& gridClass() const { return DenseData::mGridClass; } - - __hostdev__ DenseData* data() { return reinterpret_cast(this); } - __hostdev__ const DenseData* data() const { return reinterpret_cast(this); } - - __hostdev__ ValueT* values() { return reinterpret_cast(this+1);} - __hostdev__ const ValueT* values() const { return reinterpret_cast(this+1); } - - __hostdev__ inline const ValueT& getValue(const Coord &ijk) const; - __hostdev__ inline void setValue(const Coord &ijk, const ValueT &v); -}; // Grid - -template -template -DenseGridHandle -DenseGrid::create(Coord min, - Coord max, - double dx, //voxel size - const Vec3d& p0, // origin - GridClass gridClass, - const BufferT& allocator) -{ - if (dx <= 0) { - throw std::runtime_error("GridBuilder: voxel size is zero or negative"); - } - max += Coord(1,1,1);// now max is exclusive - -#if LOG2_TILE_SIZE > 0 - const uint64_t dim[3] = {(uint64_t(max[0] - min[0]) + TileMask) >> TileLog2, - (uint64_t(max[1] - min[1]) + TileMask) >> TileLog2, - (uint64_t(max[2] - min[2]) + TileMask) >> TileLog2}; - const uint64_t size = sizeof(DenseGrid) + sizeof(ValueT)*TileDim*dim[0]*dim[1]*dim[2]; -#else - const uint64_t dim[3] = {uint64_t(max[0] - min[0]), - uint64_t(max[1] - min[1]), - uint64_t(max[2] - min[2])}; - const uint64_t size = sizeof(DenseGrid) + sizeof(ValueT)*dim[0]*dim[1]*dim[2]; -#endif - - DenseGridHandle handle(allocator.create(size)); - DenseGrid* grid = reinterpret_cast(handle.data()); - grid->mSize = size; - const double Tx = p0[0], Ty = p0[1], Tz = p0[2]; - const double mat[4][4] = { - {dx, 0.0, 0.0, 0.0}, // row 0 - {0.0, dx, 0.0, 0.0}, // row 1 - {0.0, 0.0, dx, 0.0}, // row 2 - {Tx, Ty, Tz, 1.0}, // row 3 - }; - const double invMat[4][4] = { - {1 / dx, 0.0, 0.0, 0.0}, // row 0 - {0.0, 1 / dx, 0.0, 0.0}, // row 1 - {0.0, 0.0, 1 / dx, 0.0}, // row 2 - {-Tx, -Ty, -Tz, 1.0}, // row 3 - }; - - grid->mMap.set(mat, invMat, 1.0); - for (int i=0; i<3; ++i) { - grid->mIndexBBox[0][i] = min[i]; - grid->mIndexBBox[1][i] = max[i] - 1; - } - grid->mWorldBBox[0] = grid->mWorldBBox[1] = grid->mMap.applyMap(Vec3d(min[0], min[1], min[2])); - grid->mWorldBBox.expand(grid->mMap.applyMap(Vec3d(min[0], min[1], max[2]))); - grid->mWorldBBox.expand(grid->mMap.applyMap(Vec3d(min[0], max[1], min[2]))); - grid->mWorldBBox.expand(grid->mMap.applyMap(Vec3d(max[0], min[1], min[2]))); - grid->mWorldBBox.expand(grid->mMap.applyMap(Vec3d(max[0], max[1], min[2]))); - grid->mWorldBBox.expand(grid->mMap.applyMap(Vec3d(max[0], min[1], max[2]))); - grid->mWorldBBox.expand(grid->mMap.applyMap(Vec3d(min[0], max[1], max[2]))); - grid->mWorldBBox.expand(grid->mMap.applyMap(Vec3d(max[0], max[1], max[2]))); - grid->mVoxelSize = grid->mMap.applyMap(Vec3d(1)) - grid->mMap.applyMap(Vec3d(0)); - if (gridClass == GridClass::LevelSet && !is_floating_point::value) - throw std::runtime_error("Level sets are expected to be floating point types"); - if (gridClass == GridClass::FogVolume && !is_floating_point::value) - throw std::runtime_error("Fog volumes are expected to be floating point types"); - grid->mGridClass = gridClass; - grid->mGridType = mapToGridType(); - grid->mY = dim[2]; - grid->mX = dim[2] * dim[1]; - return handle; -} - -template -bool DenseGrid::test(const Coord &ijk) const -{ - return (ijk[0]>=mIndexBBox[0][0]) && (ijk[0]<=mIndexBBox[1][0]) && - (ijk[1]>=mIndexBBox[0][1]) && (ijk[1]<=mIndexBBox[1][1]) && - (ijk[2]>=mIndexBBox[0][2]) && (ijk[2]<=mIndexBBox[1][2]); -} - -template -uint64_t DenseGrid::coordToOffset(const Coord &ijk) const -{ - assert(this->test(ijk)); -#if LOG2_TILE_SIZE > 0 - const uint32_t x = ijk[0] - mIndexBBox[0][0]; - const uint32_t y = ijk[1] - mIndexBBox[0][1]; - const uint32_t z = ijk[2] - mIndexBBox[0][2]; - return ((mX*(x>>TileLog2) + mY*(y>>TileLog2) + (z>>TileLog2))<<(3*TileLog2)) + - ((x&TileMask)<<(2*TileLog2)) + ((y&TileMask)< -const ValueT& DenseGrid::getValue(const Coord &ijk) const -{ - return this->values()[this->coordToOffset(ijk)]; -} - -template -void DenseGrid::setValue(const Coord &ijk, const ValueT &value) -{ - this->values()[this->coordToOffset(ijk)] = value; -} - -template -bool DenseGrid::isValidType() const -{ - return std::is_same::value ? mGridType == GridType::Float : false; -} - -///////////////////////////////////////////// - -namespace io{ - -template -void writeDense(const DenseGrid &grid, const char* fileName) -{ - std::ofstream os(fileName, std::ios::out | std::ios::binary | std::ios::trunc); - if (!os.is_open()) { - throw std::runtime_error("Unable to open file for output"); - } - const uint64_t tmp[2] = {DENSE_MAGIC_NUMBER, grid.memUsage()}; - os.write(reinterpret_cast(tmp), 2*sizeof(uint64_t)); - os.write(reinterpret_cast(&grid), tmp[1]); -} - -template -void writeDense(const DenseGridHandle &handle, const char* fileName) -{ - std::ofstream os(fileName, std::ios::out | std::ios::binary | std::ios::trunc); - if (!os.is_open()) { - throw std::runtime_error("Unable to open file for output"); - } - const uint64_t tmp[2] = {DENSE_MAGIC_NUMBER, handle.size()}; - os.write(reinterpret_cast(tmp), 2*sizeof(uint64_t)); - os.write(reinterpret_cast(handle.data()), tmp[1]); -} - -template -DenseGridHandle -readDense(const char* fileName, const BufferT& allocator = BufferT()) -{ - std::ifstream is(fileName, std::ios::in | std::ios::binary); - if (!is.is_open()) { - throw std::runtime_error("Unable to open file for input"); - } - uint64_t tmp[2]; - is.read(reinterpret_cast(tmp), 2*sizeof(uint64_t)); - if (tmp[0] != DENSE_MAGIC_NUMBER) { - throw std::runtime_error("This is not a dense NanoVDB file!"); - } - DenseGridHandle handle(allocator.create(tmp[1])); - is.read(reinterpret_cast(handle.data()), tmp[1]); - return handle; -} -}// namespace io -///////////////////////////////////////////// - -/// @brief Converts a NanoVDB grid to a DenseGrid -template -DenseGridHandle convertToDense(const GridT &grid, const BufferT& allocator = BufferT()) -{ - using ValueT = typename GridT::ValueType; - using DenseT = DenseGrid; - const Coord min = grid.indexBBox().min(), max = grid.indexBBox().max() + Coord(1,1,1);// max is exclusive! -#if LOG2_TILE_SIZE > 0 - static constexpr uint32_t TileLog2 = LOG2_TILE_SIZE, TileMask = (1 << TileLog2) - 1, TileDim = 1 << (3*TileLog2); - const uint64_t dim[3] = {(uint64_t(max[0] - min[0]) + TileMask) >> TileLog2, - (uint64_t(max[1] - min[1]) + TileMask) >> TileLog2, - (uint64_t(max[2] - min[2]) + TileMask) >> TileLog2}; - const uint64_t size = sizeof(DenseT) + sizeof(ValueT)*TileDim*dim[0]*dim[1]*dim[2]; -#else - const uint64_t dim[3] = {uint64_t(max[0] - min[0]), - uint64_t(max[1] - min[1]), - uint64_t(max[2] - min[2])}; - const uint64_t size = sizeof(DenseT) + sizeof(ValueT)*dim[0]*dim[1]*dim[2]; -#endif - - DenseGridHandle handle( allocator.create(size) ); - auto *dense = reinterpret_cast(handle.data()); - auto *data = dense->data(); - - // copy DenseData - data->mMap = grid.map(); - data->mIndexBBox = grid.indexBBox(); - data->mWorldBBox = grid.worldBBox(); - data->mVoxelSize = grid.voxelSize(); - data->mGridClass = grid.gridClass(); - data->mGridType = grid.gridType(); - data->mY = dim[2]; - data->mX = dim[2] * dim[1]; - data->mSize = size; - - // copy values - auto kernel = [&](const Range<1,int> &r) { - auto acc = grid.getAccessor(); - Coord ijk; - for (ijk[0] = r.begin(); ijk[0] < r.end(); ++ijk[0]) { - for (ijk[1] = min[1]; ijk[1] < max[1]; ++ijk[1]) { - for (ijk[2] = min[2]; ijk[2] < max[2]; ++ijk[2]) { - dense->setValue(ijk, acc.getValue(ijk)); - } - } - } - }; - Range<1,int> range(min[0], max[0]); -#if 1 - forEach(range, kernel); -#else - kernel(range); -#endif - - return handle; -} -///////////////////////////////////////////// - -template -class DenseGridHandle -{ - BufferT mBuffer; - -public: - DenseGridHandle(BufferT&& resources) { mBuffer = std::move(resources); } - - DenseGridHandle() = default; - /// @brief Disallow copy-construction - DenseGridHandle(const DenseGridHandle&) = delete; - /// @brief Disallow copy assignment operation - DenseGridHandle& operator=(const DenseGridHandle&) = delete; - /// @brief Move copy assignment operation - DenseGridHandle& operator=(DenseGridHandle&& other) noexcept - { - mBuffer = std::move(other.mBuffer); - return *this; - } - /// @brief Move copy-constructor - DenseGridHandle(DenseGridHandle&& other) noexcept { mBuffer = std::move(other.mBuffer); } - /// @brief Default destructor - ~DenseGridHandle() { this->reset(); } - - void reset() { mBuffer.clear(); } - - BufferT& buffer() { return mBuffer; } - const BufferT& buffer() const { return mBuffer; } - - /// @brief Returns a non-const pointer to the data. - /// - /// @warning Note that the return pointer can be NULL if the DenseGridHandle was not initialized - uint8_t* data() {return mBuffer.data();} - - /// @brief Returns a const pointer to the data. - /// - /// @warning Note that the return pointer can be NULL if the DenseGridHandle was not initialized - const uint8_t* data() const {return mBuffer.data();} - - /// @brief Returns the size in bytes of the raw memory buffer managed by this DenseGridHandle's allocator. - uint64_t size() const { return mBuffer.size();} - - /// @brief Returns a const pointer to the NanoVDB grid encoded in the DenseGridHandle. - /// - /// @warning Note that the return pointer can be NULL if the DenseGridHandle was not initialized or the template - /// parameter does not match! - template - const DenseGrid* grid() const - { - using GridT = const DenseGrid; - GridT* grid = reinterpret_cast(mBuffer.data()); - return (grid && grid->isValidType()) ? grid : nullptr; - } - - template - DenseGrid* grid() - { - using GridT = DenseGrid; - GridT* grid = reinterpret_cast(mBuffer.data()); - return (grid && grid->isValidType()) ? grid : nullptr; - } - - template - typename std::enable_if::hasDeviceDual, const DenseGrid*>::type - deviceGrid() const - { - using GridT = const DenseGrid; - bool isValidType = reinterpret_cast(mBuffer.data())->isValidType(); - GridT* grid = reinterpret_cast(mBuffer.deviceData()); - return (grid && isValidType) ? grid : nullptr; - } - - template - typename std::enable_if::hasDeviceDual, void>::type - deviceUpload(void* stream = nullptr, bool sync = true) { - mBuffer.deviceUpload(stream, sync); - } - - template - typename std::enable_if::hasDeviceDual, void>::type - deviceDownload(void* stream = nullptr, bool sync = true) { - mBuffer.deviceDownload(stream, sync); - } -}; // DenseGridHandle - -} // namespace nanovdb - -#endif // NANOVDB_DENSEGRID_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/examples/benchmark/Image.h b/nanovdb/nanovdb/examples/benchmark/Image.h deleted file mode 100644 index 4f427fdf6e..0000000000 --- a/nanovdb/nanovdb/examples/benchmark/Image.h +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: MPL-2.0 - -/*! - \file Image.h - - \author Ken Museth - - \date January 8, 2020 - - \brief A simple image class that uses pinned memory for fast GPU transfer - - \warning This class is only included to support benchmark-tests. -*/ - -#ifndef NANOVDB_IMAGE_H_HAS_BEEN_INCLUDED -#define NANOVDB_IMAGE_H_HAS_BEEN_INCLUDED - -#include // for uint8_t -#include // for std::string -#include // for std::ofstream -#include - -#include - -#if defined(NANOVDB_USE_TBB) -#include -#include -#endif - -namespace nanovdb { - -struct ImageData -{ - int mWidth, mHeight, mSize; - float mScale[2]; - ImageData(int w, int h) - : mWidth(w) - , mHeight(h) - , mSize(w * h) - , mScale{1.0f / w, 1.0f / h} - { - } -}; - -/// @note Can only be constructed by an ImageHandle -class Image : private ImageData -{ - using DataT = ImageData; - -public: - struct ColorRGB - { - uint8_t r, g, b; - __hostdev__ ColorRGB(float _r, float _g, float _b) - : r(uint8_t(_r * 255.0f)) - , g(uint8_t(_g * 255.0f)) - , b(uint8_t(_b * 255.0f)) - { - } - }; - void clear(int log2 = 7); - __hostdev__ int width() const { return DataT::mWidth; } - __hostdev__ int height() const { return DataT::mHeight; } - __hostdev__ int size() const { return DataT::mSize; } - __hostdev__ float u(int w) const { return w * mScale[0]; } - __hostdev__ float v(int h) const { return h * mScale[1]; } - __hostdev__ inline ColorRGB& operator()(int w, int h); - void writePPM(const std::string& fileName, const std::string& comment = "width height 255"); -}; // Image - -template -class ImageHandle -{ - BufferT mBuffer; - -public: - ImageHandle(int width, int height, int log2 = 7); - - const Image* image() const { return reinterpret_cast(mBuffer.data()); } - - Image* image() { return reinterpret_cast(mBuffer.data()); } - - template - typename std::enable_if::hasDeviceDual, const Image*>::type - deviceImage() const { return reinterpret_cast(mBuffer.deviceData()); } - - template - typename std::enable_if::hasDeviceDual, Image*>::type - deviceImage() { return reinterpret_cast(mBuffer.deviceData()); } - - template - typename std::enable_if::hasDeviceDual, void>::type - deviceUpload(void* stream = nullptr, bool sync = true) { mBuffer.deviceUpload(stream, sync); } - - template - typename std::enable_if::hasDeviceDual, void>::type - deviceDownload(void* stream = nullptr, bool sync = true) { mBuffer.deviceDownload(stream, sync); } -}; - -template -ImageHandle::ImageHandle(int width, int height, int log2) - : mBuffer(sizeof(ImageData) + width * height * sizeof(Image::ColorRGB)) -{ - ImageData data(width, height); - *reinterpret_cast(mBuffer.data()) = data; - this->image()->clear(log2); // clear pixels or set background -} - -inline void Image::clear(int log2) -{ - ColorRGB* ptr = &(*this)(0, 0); - if (log2 < 0) { - for (auto* end = ptr + ImageData::mSize; ptr != end;) - *ptr++ = ColorRGB(0, 0, 0); - } else { - const int checkerboard = 1 << log2; - - auto kernel2D = [&](int x0, int y0, int x1, int y1) { - for (int h = y0; h != y1; ++h) { - const int n = h & checkerboard; - ColorRGB* p = ptr + h * ImageData::mWidth; - for (int w = x0; w != x1; ++w) { - *(p + w) = (n ^ (w & checkerboard)) ? ColorRGB(1, 1, 1) : ColorRGB(0, 0, 0); - } - } - }; - -#if defined(NANOVDB_USE_TBB) - tbb::blocked_range2d range(0, ImageData::mWidth, 0, ImageData::mHeight); - tbb::parallel_for(range, [&](const tbb::blocked_range2d& r) { - kernel2D(r.rows().begin(), r.cols().begin(), r.rows().end(), r.cols().end()); - }); -#else - kernel2D(0, 0, ImageData::mWidth, ImageData::mHeight); -#endif - } -} - -inline Image::ColorRGB& Image::operator()(int w, int h) -{ - assert(w < ImageData::mWidth); - assert(h < ImageData::mHeight); - return *(reinterpret_cast((uint8_t*)this + sizeof(ImageData)) + w + h * ImageData::mWidth); -} - -inline void Image::writePPM(const std::string& fileName, const std::string& comment) -{ - std::ofstream os(fileName, std::ios::out | std::ios::binary); - if (os.fail()) - throw std::runtime_error("Unable to open file named \"" + fileName + "\" for output"); - os << "P6\n#" << comment << "\n" - << this->width() << " " << this->height() << "\n255\n"; - os.write((const char*)&(*this)(0, 0), this->size() * sizeof(ColorRGB)); -} - -} // namespace nanovdb - -#endif // end of NANOVDB_IMAGE_H_HAS_BEEN_INCLUDED \ No newline at end of file diff --git a/nanovdb/nanovdb/examples/ex_bump_pool_buffer/bump_pool_buffer.cc b/nanovdb/nanovdb/examples/ex_bump_pool_buffer/bump_pool_buffer.cc index 33f37281fe..12edb019d5 100644 --- a/nanovdb/nanovdb/examples/ex_bump_pool_buffer/bump_pool_buffer.cc +++ b/nanovdb/nanovdb/examples/ex_bump_pool_buffer/bump_pool_buffer.cc @@ -110,8 +110,8 @@ int main() std::vector> gridHdls; // create two grids... - gridHdls.push_back(nanovdb::createLevelSetSphere(100.0f, nanovdb::Vec3f(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3R(0), "spheref", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, -1.0f, false, bufferContext)); - gridHdls.push_back(nanovdb::createLevelSetSphere(100.0, nanovdb::Vec3d( 20, 0, 0), 1.0, 3.0, nanovdb::Vec3R(0), "sphered", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, -1.0f, false, bufferContext)); + gridHdls.push_back(nanovdb::createLevelSetSphere(100.0, nanovdb::Vec3d(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "spheref", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, bufferContext)); + gridHdls.push_back(nanovdb::createLevelSetSphere(100.0, nanovdb::Vec3d( 20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphered", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, bufferContext)); // Get a (raw) pointer to the NanoVDB grid form the GridManager. auto* dstGrid = gridHdls[0].grid(); diff --git a/nanovdb/nanovdb/examples/ex_collide_level_set/common.h b/nanovdb/nanovdb/examples/ex_collide_level_set/common.h index ad3ce160ae..dc54e8f5f1 100644 --- a/nanovdb/nanovdb/examples/ex_collide_level_set/common.h +++ b/nanovdb/nanovdb/examples/ex_collide_level_set/common.h @@ -3,6 +3,7 @@ #pragma once +#define _USE_MATH_DEFINES #include #include #include diff --git a/nanovdb/nanovdb/examples/ex_collide_level_set/main.cc b/nanovdb/nanovdb/examples/ex_collide_level_set/main.cc index 637b5d98fb..876c08e16a 100644 --- a/nanovdb/nanovdb/examples/ex_collide_level_set/main.cc +++ b/nanovdb/nanovdb/examples/ex_collide_level_set/main.cc @@ -5,7 +5,7 @@ #include #include #include -#include +#include #if defined(NANOVDB_USE_CUDA) using BufferT = nanovdb::CudaDeviceBuffer; @@ -26,7 +26,7 @@ int main(int ac, char** av) handle = nanovdb::io::readGrid(av[1]); std::cout << "Loaded NanoVDB grid[" << handle.gridMetaData()->shortGridName() << "]...\n"; } else { - handle = nanovdb::createLevelSetSphere(100.0f, nanovdb::Vec3f(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3R(0), "sphere"); + handle = nanovdb::createLevelSetSphere(100.0f, nanovdb::Vec3d(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphere"); } if (handle.gridMetaData()->isLevelSet() == false) { diff --git a/nanovdb/nanovdb/examples/ex_collide_level_set/nanovdb.cu b/nanovdb/nanovdb/examples/ex_collide_level_set/nanovdb.cu index 994637c27c..71a976eca4 100644 --- a/nanovdb/nanovdb/examples/ex_collide_level_set/nanovdb.cu +++ b/nanovdb/nanovdb/examples/ex_collide_level_set/nanovdb.cu @@ -1,12 +1,12 @@ // Copyright Contributors to the OpenVDB Project // SPDX-License-Identifier: MPL-2.0 +#define _USE_MATH_DEFINES #include #include #include -#include -#include +#include #include #include @@ -44,8 +44,7 @@ void runNanoVDB(nanovdb::GridHandle& handle, int numIterations, int num using namespace nanovdb; auto* h_grid = handle.grid(); - if (!h_grid) - throw std::runtime_error("GridHandle does not contain a FloatGrid"); + if (!h_grid) throw std::runtime_error("GridHandle does not contain a FloatGrid"); Vec3f* h_positions = reinterpret_cast(positionBuffer.data()); computeFill(false, h_positions, 0, sizeof(Vec3f) * numPoints); diff --git a/nanovdb/nanovdb/examples/ex_collide_level_set/openvdb.cc b/nanovdb/nanovdb/examples/ex_collide_level_set/openvdb.cc index 0c07a02035..ec67f754bd 100644 --- a/nanovdb/nanovdb/examples/ex_collide_level_set/openvdb.cc +++ b/nanovdb/nanovdb/examples/ex_collide_level_set/openvdb.cc @@ -3,13 +3,14 @@ #if defined(NANOVDB_USE_OPENVDB) +#define _USE_MATH_DEFINES #include #include #include #include -#include +#include #include #include "common.h" @@ -20,6 +21,8 @@ using BufferT = nanovdb::CudaDeviceBuffer; using BufferT = nanovdb::HostBuffer; #endif +openvdb::GridBase::Ptr nanoToOpenVDB(nanovdb::GridHandle& handle); + void runOpenVDB(nanovdb::GridHandle& handle, int numIterations, int numPoints, BufferT& positionBuffer, BufferT& velocityBuffer) { using GridT = openvdb::FloatGrid; @@ -36,4 +39,4 @@ void runOpenVDB(nanovdb::GridHandle& handle, int numIterations, int num // Not yet implemented... } -#endif +#endif \ No newline at end of file diff --git a/nanovdb/nanovdb/examples/ex_index_grid_cuda/index_grid_cuda.cc b/nanovdb/nanovdb/examples/ex_index_grid_cuda/index_grid_cuda.cc index 45cfdff394..b81d71c22b 100644 --- a/nanovdb/nanovdb/examples/ex_index_grid_cuda/index_grid_cuda.cc +++ b/nanovdb/nanovdb/examples/ex_index_grid_cuda/index_grid_cuda.cc @@ -1,35 +1,37 @@ // Copyright Contributors to the OpenVDB Project // SPDX-License-Identifier: MPL-2.0 -#include // nanovdb::IndexGridBuilder +#include #include // for nanovdb::createLevelSetSphere -#include // for nanovdb::CudaDeviceBuffer +#include // for nanovdb::CudaDeviceBuffer -extern "C" void launch_kernels(const nanovdb::NanoGrid*,// device grid - const nanovdb::NanoGrid*,// host grid +extern "C" void launch_kernels(const nanovdb::NanoGrid*,// device grid + const nanovdb::NanoGrid*,// host grid cudaStream_t stream); /// @brief This examples depends on NanoVDB and CUDA. -int main() +int main(int, char**) { + using SrcGridT = nanovdb::FloatGrid; + using DstBuildT = nanovdb::ValueOnIndex; + using BufferT = nanovdb::CudaDeviceBuffer; try { // Create an NanoVDB grid of a sphere at the origin with radius 100 and voxel size 1. auto srcHandle = nanovdb::createLevelSetSphere(); auto *srcGrid = srcHandle.grid(); // Converts the FloatGrid to an IndexGrid using CUDA for memory management. - nanovdb::IndexGridBuilder builder(*srcGrid, /*only active values*/true); - auto idxHandle = builder.getHandle("IndexGrid_test", /*number of channels*/1u); + auto idxHandle = nanovdb::createNanoGrid(*srcGrid, 1u, false , false);// 1 channel, no tiles or stats cudaStream_t stream; // Create a CUDA stream to allow for asynchronous copy of pinned CUDA memory. cudaStreamCreate(&stream); idxHandle.deviceUpload(stream, false); // Copy the NanoVDB grid to the GPU asynchronously - auto* cpuGrid = idxHandle.grid(); // get a (raw) pointer to a NanoVDB grid of value type float on the CPU - auto* gpuGrid = idxHandle.deviceGrid(); // get a (raw) pointer to a NanoVDB grid of value type float on the GPU + auto* cpuGrid = idxHandle.grid(); // get a (raw) pointer to a NanoVDB grid of value type float on the CPU + auto* gpuGrid = idxHandle.deviceGrid(); // get a (raw) pointer to a NanoVDB grid of value type float on the GPU - if (!gpuGrid || !cpuGrid) - throw std::runtime_error("GridHandle did not contain a grid with value type float"); + if (!gpuGrid) throw std::runtime_error("GridHandle did not contain a device grid with value type float"); + if (!cpuGrid) throw std::runtime_error("GridHandle did not contain a host grid with value type float"); launch_kernels(cpuGrid, cpuGrid, stream); // Call a host method to print a grid value on both the CPU and GPU diff --git a/nanovdb/nanovdb/examples/ex_index_grid_cuda/index_grid_cuda.cu b/nanovdb/nanovdb/examples/ex_index_grid_cuda/index_grid_cuda_kernel.cu similarity index 79% rename from nanovdb/nanovdb/examples/ex_index_grid_cuda/index_grid_cuda.cu rename to nanovdb/nanovdb/examples/ex_index_grid_cuda/index_grid_cuda_kernel.cu index 6a2770d3a6..5bb29979cf 100644 --- a/nanovdb/nanovdb/examples/ex_index_grid_cuda/index_grid_cuda.cu +++ b/nanovdb/nanovdb/examples/ex_index_grid_cuda/index_grid_cuda_kernel.cu @@ -2,12 +2,13 @@ // SPDX-License-Identifier: MPL-2.0 #include // this defined the core tree data structure of NanoVDB accessable on both the host and device +#include // required since GridHandle has device code #include // for printf // This is called by the host only -void cpu_kernel(const nanovdb::NanoGrid* cpuGrid) +void cpu_kernel(const nanovdb::NanoGrid* cpuGrid) { - nanovdb::ChannelAccessor acc(*cpuGrid); + nanovdb::ChannelAccessor acc(*cpuGrid); //printf("\nNanoVDB CPU: channels=%u values=%lu\n", acc.grid().blindDataCount(), acc.root().maximum()); printf("NanoVDB CPU; %lu\n", acc.idx( 0, 0, 0)); printf("NanoVDB CPU; %lu\n", acc.idx( 99, 0, 0)); @@ -18,9 +19,9 @@ void cpu_kernel(const nanovdb::NanoGrid* cpuGrid) } // This is called by the device only -__global__ void gpu_kernel(const nanovdb::NanoGrid* gpuGrid) +__global__ void gpu_kernel(const nanovdb::NanoGrid* gpuGrid) { - nanovdb::ChannelAccessor acc(*gpuGrid); + nanovdb::ChannelAccessor acc(*gpuGrid); //printf("\nNanoVDB GPU: channels=%u values=%lu\n", gpuGrid->blindDataCount(), acc.root().maximum()); printf("NanoVDB GPU; %lu\n", acc.idx( 0, 0, 0)); printf("NanoVDB GPU; %lu\n", acc.idx( 99, 0, 0)); @@ -31,9 +32,9 @@ __global__ void gpu_kernel(const nanovdb::NanoGrid* gpuGrid } // This is called by the client code on the host -extern "C" void launch_kernels(const nanovdb::NanoGrid* gpuGrid, - const nanovdb::NanoGrid* cpuGrid, - cudaStream_t stream) +extern "C" void launch_kernels(const nanovdb::NanoGrid* gpuGrid, + const nanovdb::NanoGrid* cpuGrid, + cudaStream_t stream) { gpu_kernel<<<1, 1, 0, stream>>>(gpuGrid); // Launch the device kernel asynchronously diff --git a/nanovdb/nanovdb/examples/ex_make_custom_nanovdb/make_custom_nanovdb.cc b/nanovdb/nanovdb/examples/ex_make_custom_nanovdb/make_custom_nanovdb.cc index d5a37f1b81..aea2812a4b 100644 --- a/nanovdb/nanovdb/examples/ex_make_custom_nanovdb/make_custom_nanovdb.cc +++ b/nanovdb/nanovdb/examples/ex_make_custom_nanovdb/make_custom_nanovdb.cc @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 #include +#include #include @@ -11,14 +12,14 @@ int main() { try { - nanovdb::GridBuilder builder(0.0f); - auto acc = builder.getAccessor(); + nanovdb::build::Grid grid(0.0f); + auto acc = grid.getAccessor(); acc.setValue(nanovdb::Coord(1, 2, 3), 1.0f); - printf("GridBuilder: (%i,%i,%i)=%4.2f\t", 1, 2, 3, acc.getValue(nanovdb::Coord(1, 2, 3))); - printf("GridBuilder: (%i,%i,%i)=%4.2f\n", 1, 2,-3, acc.getValue(nanovdb::Coord(1, 2,-3))); + printf("build::Grid: (%i,%i,%i)=%4.2f\t", 1, 2, 3, acc.getValue(nanovdb::Coord(1, 2, 3))); + printf("build::Grid: (%i,%i,%i)=%4.2f\n", 1, 2,-3, acc.getValue(nanovdb::Coord(1, 2,-3))); - auto handle = builder.getHandle<>(); + auto handle = nanovdb::createNanoGrid(grid); auto* dstGrid = handle.grid(); // Get a (raw) pointer to the NanoVDB grid form the GridManager. if (!dstGrid) throw std::runtime_error("GridHandle does not contain a grid with value type float"); diff --git a/nanovdb/nanovdb/examples/ex_make_custom_nanovdb_cuda/make_custom_nanovdb_cuda.cc b/nanovdb/nanovdb/examples/ex_make_custom_nanovdb_cuda/make_custom_nanovdb_cuda.cc new file mode 100644 index 0000000000..7b4da85f0a --- /dev/null +++ b/nanovdb/nanovdb/examples/ex_make_custom_nanovdb_cuda/make_custom_nanovdb_cuda.cc @@ -0,0 +1,47 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +#undef NANOVDB_USE_OPENVDB // Prevents include/openvdb/points/AttributeArray.h:1841:25: error: ‘stride’ cannot be used as a function + +#include +#include +#include + +#include + +extern "C" void launch_kernels(const nanovdb::NanoGrid*,// GPU grid + const nanovdb::NanoGrid*,// CPU grid + cudaStream_t stream); + +/// @brief Creates a NanoVDB grids with custom values and access them. +/// +/// @note This example only depends on NanoVDB. +int main() +{ + try { + using GridT = nanovdb::build::Grid; + GridT grid(0.0f);// empty grid with a background value of zero + auto acc = grid.getAccessor(); + acc.setValue(nanovdb::Coord(1, 2, 3), 1.0f); + printf("build::Grid: (%i,%i,%i)=%4.2f\n", 1, 2,-3, acc.getValue(nanovdb::Coord(1, 2,-3))); + printf("build::Grid: (%i,%i,%i)=%4.2f\n", 1, 2, 3, acc.getValue(nanovdb::Coord(1, 2, 3))); + + // convert build::grid to a nanovdb::GridHandle using a Cuda buffer + auto handle = nanovdb::createNanoGrid(grid); + + auto* cpuGrid = handle.grid(); //get a (raw) pointer to a NanoVDB grid of value type float on the CPU + if (!cpuGrid) throw std::runtime_error("GridHandle does not contain a grid with value type float"); + + cudaStream_t stream; // Create a CUDA stream to allow for asynchronous copy of pinned CUDA memory. + cudaStreamCreate(&stream); + handle.deviceUpload(stream, false); // Copy the NanoVDB grid to the GPU asynchronously + auto* gpuGrid = handle.deviceGrid(); // get a (raw) pointer to a NanoVDB grid of value type float on the GPU + + launch_kernels(gpuGrid, cpuGrid, stream); // Call a host method to print a grid values on both the CPU and GPU + cudaStreamDestroy(stream); // Destroy the CUDA stream + } + catch (const std::exception& e) { + std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl; + } + return 0; +} \ No newline at end of file diff --git a/nanovdb/nanovdb/examples/ex_make_custom_nanovdb_cuda/make_custom_nanovdb_cuda_kernel.cu b/nanovdb/nanovdb/examples/ex_make_custom_nanovdb_cuda/make_custom_nanovdb_cuda_kernel.cu new file mode 100644 index 0000000000..ae3556ad7a --- /dev/null +++ b/nanovdb/nanovdb/examples/ex_make_custom_nanovdb_cuda/make_custom_nanovdb_cuda_kernel.cu @@ -0,0 +1,36 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +#include // this defined the core tree data structure of NanoVDB accessable on both the host and device +#include // required since GridHandle has device code +#include // for printf + +// This is called by the host only +void cpu_kernel(const nanovdb::NanoGrid* cpuGrid) +{ + auto cpuAcc = cpuGrid->getAccessor(); + for (int k=-3; k<=3; k+=6) { + printf("NanoVDB cpu: (%i,%i,%i)=%4.2f\n", 1, 2, k, cpuAcc.getValue(nanovdb::Coord(1, 2, k))); + } +} + +// This is called by the device only +__global__ void gpu_kernel(const nanovdb::NanoGrid* deviceGrid) +{ + if (threadIdx.x != 0 && threadIdx.x != 6) return; + int k = threadIdx.x - 3; + auto gpuAcc = deviceGrid->getAccessor(); + printf("NanoVDB gpu: (%i,%i,%i)=%4.2f\n", 1, 2, k, gpuAcc.getValue(nanovdb::Coord(1, 2, k))); +} + +// This is called by the client code on the host +extern "C" void launch_kernels(const nanovdb::NanoGrid* deviceGrid, + const nanovdb::NanoGrid* cpuGrid, + cudaStream_t stream) +{ + // Launch the device kernel asynchronously + gpu_kernel<<<1, 64, 0, stream>>>(deviceGrid); + + // Launch the host "kernel" (synchronously) + cpu_kernel(cpuGrid); +} diff --git a/nanovdb/nanovdb/examples/ex_make_funny_nanovdb/make_funny_nanovdb.cc b/nanovdb/nanovdb/examples/ex_make_funny_nanovdb/make_funny_nanovdb.cc index 7909285540..e9b7350bb8 100644 --- a/nanovdb/nanovdb/examples/ex_make_funny_nanovdb/make_funny_nanovdb.cc +++ b/nanovdb/nanovdb/examples/ex_make_funny_nanovdb/make_funny_nanovdb.cc @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 #include +#include #include #include @@ -11,22 +12,20 @@ /// @note This example only depends on NanoVDB. int main() { + using namespace nanovdb; try { const float background = 5.0f; - nanovdb::GridBuilder builder(background, nanovdb::GridClass::LevelSet); - auto acc = builder.getAccessor(); const int size = 500; - auto func = [&](const nanovdb::Coord &ijk){ + auto func = [&](const Coord &ijk){ float v = 40.0f + 50.0f*(cos(ijk[0]*0.1f)*sin(ijk[1]*0.1f) + cos(ijk[1]*0.1f)*sin(ijk[2]*0.1f) + cos(ijk[2]*0.1f)*sin(ijk[0]*0.1f)); - v = nanovdb::Max(v, nanovdb::Vec3f(ijk).length() - size);// CSG intersection with a sphere + v = Max(v, Vec3f(ijk).length() - size);// CSG intersection with a sphere return v > background ? background : v < -background ? -background : v;// clamp value }; - builder(func, nanovdb::CoordBBox(nanovdb::Coord(-size),nanovdb::Coord(size))); - - auto handle = builder.getHandle<>(); - nanovdb::io::writeGrid("data/funny.nvdb", handle, nanovdb::io::Codec::BLOSC); + build::Grid grid(background, "funny", GridClass::LevelSet); + grid(func, CoordBBox(Coord(-size), Coord(size))); + io::writeGrid("data/funny.nvdb", createNanoGrid(grid), io::Codec::BLOSC); } catch (const std::exception& e) { std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl; diff --git a/nanovdb/nanovdb/examples/ex_make_typed_grids/make_typed_grids.cc b/nanovdb/nanovdb/examples/ex_make_typed_grids/make_typed_grids.cc index dae0a9413a..f9d4666784 100644 --- a/nanovdb/nanovdb/examples/ex_make_typed_grids/make_typed_grids.cc +++ b/nanovdb/nanovdb/examples/ex_make_typed_grids/make_typed_grids.cc @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 #include +#include #include // Helper struct to create a default value for the type. @@ -36,11 +37,9 @@ void buildGridForType(std::vector>& gridHandles, T const& try { - nanovdb::GridBuilder builder(bgValue); - auto acc = builder.getAccessor(); - + nanovdb::build::Grid grid(bgValue, typeNameStr); + auto acc = grid.getAccessor(); const int radius = 16; - for (int z = -radius; z <= radius; ++z) { for (int y = -radius; y <= radius; ++y) { for (int x = -radius; x <= radius; ++x) { @@ -50,8 +49,7 @@ void buildGridForType(std::vector>& gridHandles, T const& } } } - - gridHandles.push_back(builder.template getHandle<>(1.0, nanovdb::Vec3d(0), typeNameStr)); + gridHandles.push_back(nanovdb::createNanoGrid(grid)); } catch (const std::exception& e) { std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl; @@ -83,8 +81,11 @@ int main() */ buildGridForType(gridHandles, float(0), double(0), int16_t(0), int32_t(0), int64_t(0), uint32_t(0), nanovdb::Vec3f(0) /*, nanovdb::Vec3d(0)*/ /*, bool(false)*/ /*, uint16_t(0)*/); - - nanovdb::io::writeGrids("data/custom_types.nvdb", gridHandles); +#if 0 + nanovdb::io::writeGrids("data/custom_types.nvdb", gridHandles); +#else + nanovdb::io::writeUncompressedGrids("data/custom_types.nvdb", gridHandles); +#endif } catch (const std::exception& e) { std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl; diff --git a/nanovdb/nanovdb/examples/ex_map_pool_buffer/map_pool_buffer.cc b/nanovdb/nanovdb/examples/ex_map_pool_buffer/map_pool_buffer.cc index 2a5de0844e..526ed9c8cf 100644 --- a/nanovdb/nanovdb/examples/ex_map_pool_buffer/map_pool_buffer.cc +++ b/nanovdb/nanovdb/examples/ex_map_pool_buffer/map_pool_buffer.cc @@ -148,8 +148,8 @@ int main() std::vector> gridHdls; // create two grids... - gridHdls.push_back(nanovdb::createLevelSetSphere(100.0f, nanovdb::Vec3f(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3R(0), "spheref", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, -1.0f, false, bufferContext)); - gridHdls.push_back(nanovdb::createLevelSetSphere(100.0, nanovdb::Vec3d( 20, 0, 0), 1.0, 3.0, nanovdb::Vec3R(0), "sphered", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, -1.0f, false, bufferContext)); + gridHdls.push_back(nanovdb::createLevelSetSphere(100.0, nanovdb::Vec3d(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "spheref", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, bufferContext)); + gridHdls.push_back(nanovdb::createLevelSetSphere(100.0, nanovdb::Vec3d( 20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphered", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, bufferContext)); // share grid[0]'s buffer into a parent-scope handle to prevent deletion. anotherHdl = nanovdb::GridHandle(bufferContext.copy(gridHdls[0].buffer().mId)); diff --git a/nanovdb/nanovdb/examples/ex_modify_nanovdb_thrust/modify_nanovdb_thrust.cc b/nanovdb/nanovdb/examples/ex_modify_nanovdb_thrust/modify_nanovdb_thrust.cc new file mode 100644 index 0000000000..dbda5b3d73 --- /dev/null +++ b/nanovdb/nanovdb/examples/ex_modify_nanovdb_thrust/modify_nanovdb_thrust.cc @@ -0,0 +1,43 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +/// @brief This examples demonstrates how values in a NanpVDB grid can be +/// modified on the device. It depends on NanoVDB and CUDA thrust. + +#include +#include + +extern "C" void scaleActiveVoxels(nanovdb::FloatGrid *grid_d, uint64_t leafCount, float scale); + +int main() +{ + try { + // Create an NanoVDB grid of a sphere at the origin with radius 100 and voxel size 1. + auto handle = nanovdb::createLevelSetSphere(100.0f); + using GridT = nanovdb::FloatGrid; + + handle.deviceUpload(0, false); // Copy the NanoVDB grid to the GPU asynchronously + + const GridT* grid = handle.grid(); // get a (raw) const pointer to a NanoVDB grid of value type float on the CPU + GridT* deviceGrid = handle.deviceGrid(); // get a (raw) pointer to a NanoVDB grid of value type float on the GPU + + if (!deviceGrid || !grid) { + throw std::runtime_error("GridHandle did not contain a grid with value type float"); + } + if (!grid->isSequential<0>()) { + throw std::runtime_error("Grid does not support sequential access to leaf nodes!"); + } + + std::cout << "Value before scaling = " << grid->tree().getValue(nanovdb::Coord(101,0,0)) << std::endl; + + scaleActiveVoxels(deviceGrid, grid->tree().nodeCount(0), 2.0f); + + handle.deviceDownload(0, true); // Copy the NanoVDB grid to the CPU synchronously + + std::cout << "Value after scaling = " << grid->tree().getValue(nanovdb::Coord(101,0,0)) << std::endl; + } + catch (const std::exception& e) { + std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl; + } + return 0; +} \ No newline at end of file diff --git a/nanovdb/nanovdb/examples/ex_modify_nanovdb_thrust/modify_nanovdb_thrust.cu b/nanovdb/nanovdb/examples/ex_modify_nanovdb_thrust/modify_nanovdb_thrust.cu index 6e8c45fd71..1078b8aa1b 100644 --- a/nanovdb/nanovdb/examples/ex_modify_nanovdb_thrust/modify_nanovdb_thrust.cu +++ b/nanovdb/nanovdb/examples/ex_modify_nanovdb_thrust/modify_nanovdb_thrust.cu @@ -7,10 +7,10 @@ #include #include -#include -#include +#include +#include -void scaleActiveVoxels(nanovdb::FloatGrid *grid_d, uint64_t leafCount, float scale) +extern "C" void scaleActiveVoxels(nanovdb::FloatGrid *grid_d, uint64_t leafCount, float scale) { auto kernel = [grid_d, scale] __device__ (const uint64_t n) { auto *leaf_d = grid_d->tree().getFirstNode<0>() + (n >> 9);// this only works if grid->isSequential<0>() == true @@ -23,37 +23,4 @@ void scaleActiveVoxels(nanovdb::FloatGrid *grid_d, uint64_t leafCount, float sca thrust::counting_iterator iter(0); thrust::for_each(iter, iter + 512*leafCount, kernel); -} - -int main() -{ - try { - // Create an NanoVDB grid of a sphere at the origin with radius 100 and voxel size 1. - auto handle = nanovdb::createLevelSetSphere(100.0f); - using GridT = nanovdb::FloatGrid; - - handle.deviceUpload(0, false); // Copy the NanoVDB grid to the GPU asynchronously - - const GridT* grid = handle.grid(); // get a (raw) const pointer to a NanoVDB grid of value type float on the CPU - GridT* deviceGrid = handle.deviceGrid(); // get a (raw) pointer to a NanoVDB grid of value type float on the GPU - - if (!deviceGrid || !grid) { - throw std::runtime_error("GridHandle did not contain a grid with value type float"); - } - if (!grid->isSequential<0>()) { - throw std::runtime_error("Grid does not support sequential access to leaf nodes!"); - } - - std::cout << "Value before scaling = " << grid->tree().getValue(nanovdb::Coord(101,0,0)) << std::endl; - - scaleActiveVoxels(deviceGrid, grid->tree().nodeCount(0), 2.0f); - - handle.deviceDownload(0, true); // Copy the NanoVDB grid to the CPU synchronously - - std::cout << "Value after scaling = " << grid->tree().getValue(nanovdb::Coord(101,0,0)) << std::endl; - } - catch (const std::exception& e) { - std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl; - } - return 0; } \ No newline at end of file diff --git a/nanovdb/nanovdb/examples/ex_nodemanager_cuda/nodemanager_cuda.cc b/nanovdb/nanovdb/examples/ex_nodemanager_cuda/nodemanager_cuda.cc index bf8e250d5b..68906b90e8 100644 --- a/nanovdb/nanovdb/examples/ex_nodemanager_cuda/nodemanager_cuda.cc +++ b/nanovdb/nanovdb/examples/ex_nodemanager_cuda/nodemanager_cuda.cc @@ -2,26 +2,31 @@ // SPDX-License-Identifier: MPL-2.0 #include // replace with your own dependencies for generating the OpenVDB grid -#include // converter from OpenVDB to NanoVDB (includes NanoVDB.h and GridManager.h) -#include +#include // converter from OpenVDB to NanoVDB (includes NanoVDB.h and GridManager.h) +#include #include extern "C" void launch_kernels(const nanovdb::NodeManager*, const nanovdb::NodeManager*, cudaStream_t stream); +extern "C" void cudaCreateNodeManager(const nanovdb::NanoGrid*, + nanovdb::NodeManagerHandle*); + /// @brief This examples depends on OpenVDB, NanoVDB and CUDA. int main() { + using SrcGridT = openvdb::FloatGrid; + using BufferT = nanovdb::CudaDeviceBuffer; try { cudaStream_t stream; // Create a CUDA stream to allow for asynchronous copy of pinned CUDA memory. cudaStreamCreate(&stream); // Create an OpenVDB grid of a sphere at the origin with radius 100 and voxel size 1. - auto srcGrid = openvdb::tools::createLevelSetSphere(100.0f, openvdb::Vec3f(0.0f), 1.0f); + auto srcGrid = openvdb::tools::createLevelSetSphere(100.0f, openvdb::Vec3f(0.0f), 1.0f); // Converts the OpenVDB to NanoVDB and returns a GridHandle that uses CUDA for memory management. - auto gridHandle = nanovdb::openToNanoVDB(*srcGrid); + auto gridHandle = nanovdb::createNanoGrid(*srcGrid); gridHandle.deviceUpload(stream, false); // Copy the NanoVDB grid to the GPU asynchronously auto* grid = gridHandle.grid(); // get a (raw) pointer to a NanoVDB grid of value type float on the CPU auto* deviceGrid = gridHandle.deviceGrid(); // get a (raw) pointer to a NanoVDB grid of value type float on the GPU @@ -29,10 +34,16 @@ int main() throw std::runtime_error("GridHandle did not contain a grid with value type float"); } - auto nodeHandle = nanovdb::createNodeManager(*grid); - nodeHandle.deviceUpload(deviceGrid, stream, false); + auto nodeHandle = nanovdb::createNodeManager(*grid); auto *nodeMgr = nodeHandle.template mgr(); +#if 0// this approach copies a NodeManager from host to device + nodeHandle.deviceUpload(deviceGrid, stream, false); auto *deviceNodeMgr = nodeHandle.template deviceMgr(); +#else// the approach below constructs a new NodeManager directly for a device grid + nanovdb::NodeManagerHandle nodeHandle2; + cudaCreateNodeManager(deviceGrid, &nodeHandle2); + auto *deviceNodeMgr = nodeHandle2.template deviceMgr(); +#endif if (!deviceNodeMgr || !nodeMgr) { throw std::runtime_error("NodeManagerHandle did not contain a grid with value type float"); } diff --git a/nanovdb/nanovdb/examples/ex_nodemanager_cuda/nodemanager_cuda.cu b/nanovdb/nanovdb/examples/ex_nodemanager_cuda/nodemanager_cuda_kernel.cu similarity index 69% rename from nanovdb/nanovdb/examples/ex_nodemanager_cuda/nodemanager_cuda.cu rename to nanovdb/nanovdb/examples/ex_nodemanager_cuda/nodemanager_cuda_kernel.cu index 0dd65c9008..b06c87b4e5 100644 --- a/nanovdb/nanovdb/examples/ex_nodemanager_cuda/nodemanager_cuda.cu +++ b/nanovdb/nanovdb/examples/ex_nodemanager_cuda/nodemanager_cuda_kernel.cu @@ -3,6 +3,8 @@ #include // this defined the core tree data structure of NanoVDB accessable on both the host and device #include +#include // required since GridHandle has device code +#include #include // for printf // This is called by the host only @@ -25,4 +27,11 @@ extern "C" void launch_kernels(const nanovdb::NodeManager* deviceMgr, gpu_kernel<<<1, 1, 0, stream>>>(deviceMgr); // Launch the device kernel asynchronously cpu_kernel(cpuMgr); // Launch the host "kernel" (synchronously) +} + +// Simple wrapper that makes sure nanovdb::cudaCreateNodeManager is initiated +extern "C" void cudaCreateNodeManager(const nanovdb::NanoGrid *d_grid, + nanovdb::NodeManagerHandle *handle) +{ + *handle = std::move(nanovdb::cudaCreateNodeManager(d_grid)); } \ No newline at end of file diff --git a/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb/openvdb_to_nanovdb.cc b/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb/openvdb_to_nanovdb.cc index 433e8da590..870114db39 100644 --- a/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb/openvdb_to_nanovdb.cc +++ b/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb/openvdb_to_nanovdb.cc @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 #include // replace with your own dependencies for generating the OpenVDB grid -#include // converter from OpenVDB to NanoVDB (includes NanoVDB.h and GridManager.h) +#include // converter from OpenVDB to NanoVDB (includes NanoVDB.h and GridManager.h) #include /// @brief Convert an openvdb level set sphere into a nanovdb, access a single value in both grids, and save NanoVDB to file. @@ -13,9 +13,7 @@ int main() try { // Create an OpenVDB grid of a sphere at the origin with radius 100 and voxel size 1. auto srcGrid = openvdb::tools::createLevelSetSphere(100.0f, openvdb::Vec3f(0.0f), 1.0f); - - auto handle = nanovdb::openToNanoVDB(*srcGrid); // Convert from OpenVDB to NanoVDB and return a shared pointer to a GridHandle. - + auto handle = nanovdb::createNanoGrid(*srcGrid); // Convert from OpenVDB to NanoVDB and return a shared pointer to a GridHandle. auto* dstGrid = handle.grid(); // Get a (raw) pointer to the NanoVDB grid form the GridManager. if (!dstGrid) throw std::runtime_error("GridHandle does not contain a grid with value type float"); diff --git a/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_accessor/openvdb_to_nanovdb_accessor.cc b/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_accessor/openvdb_to_nanovdb_accessor.cc index 773c639b63..4851732882 100644 --- a/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_accessor/openvdb_to_nanovdb_accessor.cc +++ b/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_accessor/openvdb_to_nanovdb_accessor.cc @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 #include // replace with your own dependencies for generating the OpenVDB grid -#include // converter from OpenVDB to NanoVDB (includes NanoVDB.h and GridManager.h) +#include // converter from OpenVDB to NanoVDB (includes NanoVDB.h and GridManager.h) #include // Convert an openvdb level set sphere into a nanovdb, use accessors to print out multiple values from both @@ -15,7 +15,7 @@ int main() auto srcGrid = openvdb::tools::createLevelSetSphere(100.0f, openvdb::Vec3f(0.0f), 1.0f); // Convert the OpenVDB grid, srcGrid, into a NanoVDB grid handle. - auto handle = nanovdb::openToNanoVDB(*srcGrid); + auto handle = nanovdb::createNanoGrid(*srcGrid); // Define a (raw) pointer to the NanoVDB grid on the host. Note we match the value type of the srcGrid! auto* dstGrid = handle.grid(); diff --git a/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_cuda/openvdb_to_nanovdb_cuda.cc b/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_cuda/openvdb_to_nanovdb_cuda.cc index ea1172b1a4..ae4d435dfc 100644 --- a/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_cuda/openvdb_to_nanovdb_cuda.cc +++ b/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_cuda/openvdb_to_nanovdb_cuda.cc @@ -2,22 +2,23 @@ // SPDX-License-Identifier: MPL-2.0 #include // replace with your own dependencies for generating the OpenVDB grid -#include // converter from OpenVDB to NanoVDB (includes NanoVDB.h and GridManager.h) -#include +#include // converter from OpenVDB to NanoVDB (includes NanoVDB.h and GridManager.h) +#include extern "C" void launch_kernels(const nanovdb::NanoGrid*, const nanovdb::NanoGrid*, cudaStream_t stream); /// @brief This examples depends on OpenVDB, NanoVDB and CUDA. -int main() +int main(int, char**) { + using SrcGridT = openvdb::FloatGrid; try { // Create an OpenVDB grid of a sphere at the origin with radius 100 and voxel size 1. - auto srcGrid = openvdb::tools::createLevelSetSphere(100.0f, openvdb::Vec3f(0.0f), 1.0f); + auto srcGrid = openvdb::tools::createLevelSetSphere(100.0f, openvdb::Vec3f(0.0f), 1.0f); // Converts the OpenVDB to NanoVDB and returns a GridHandle that uses CUDA for memory management. - auto handle = nanovdb::openToNanoVDB(*srcGrid); + auto handle = nanovdb::createNanoGrid(*srcGrid); cudaStream_t stream; // Create a CUDA stream to allow for asynchronous copy of pinned CUDA memory. cudaStreamCreate(&stream); diff --git a/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_cuda/openvdb_to_nanovdb_cuda.cu b/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_cuda/openvdb_to_nanovdb_cuda_kernel.cu similarity index 90% rename from nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_cuda/openvdb_to_nanovdb_cuda.cu rename to nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_cuda/openvdb_to_nanovdb_cuda_kernel.cu index 321f89fe64..543b0e3027 100644 --- a/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_cuda/openvdb_to_nanovdb_cuda.cu +++ b/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_cuda/openvdb_to_nanovdb_cuda_kernel.cu @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 #include // this defined the core tree data structure of NanoVDB accessable on both the host and device +#include // required since GridHandle has device code #include // for printf // This is called by the host only diff --git a/nanovdb/nanovdb/examples/ex_raytrace_fog_volume/common.h b/nanovdb/nanovdb/examples/ex_raytrace_fog_volume/common.h index 44ed1c739b..edc4c27a32 100644 --- a/nanovdb/nanovdb/examples/ex_raytrace_fog_volume/common.h +++ b/nanovdb/nanovdb/examples/ex_raytrace_fog_volume/common.h @@ -3,6 +3,7 @@ #pragma once +#define _USE_MATH_DEFINES #include #include #include diff --git a/nanovdb/nanovdb/examples/ex_raytrace_fog_volume/main.cc b/nanovdb/nanovdb/examples/ex_raytrace_fog_volume/main.cc index 9179ae758d..29752239f1 100644 --- a/nanovdb/nanovdb/examples/ex_raytrace_fog_volume/main.cc +++ b/nanovdb/nanovdb/examples/ex_raytrace_fog_volume/main.cc @@ -5,9 +5,9 @@ #include #include #include -#include #if defined(NANOVDB_USE_CUDA) +#include using BufferT = nanovdb::CudaDeviceBuffer; #else using BufferT = nanovdb::HostBuffer; @@ -26,7 +26,7 @@ int main(int ac, char** av) handle = nanovdb::io::readGrid(av[1]); std::cout << "Loaded NanoVDB grid[" << handle.gridMetaData()->shortGridName() << "]...\n"; } else { - handle = nanovdb::createFogVolumeSphere(100.0f, nanovdb::Vec3f(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphere"); + handle = nanovdb::createFogVolumeSphere(100.0f, nanovdb::Vec3d(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphere"); } if (handle.gridMetaData()->isFogVolume() == false) { diff --git a/nanovdb/nanovdb/examples/ex_raytrace_fog_volume/nanovdb.cu b/nanovdb/nanovdb/examples/ex_raytrace_fog_volume/nanovdb.cu index 663dcddd34..c65dfff85a 100644 --- a/nanovdb/nanovdb/examples/ex_raytrace_fog_volume/nanovdb.cu +++ b/nanovdb/nanovdb/examples/ex_raytrace_fog_volume/nanovdb.cu @@ -1,20 +1,21 @@ // Copyright Contributors to the OpenVDB Project // SPDX-License-Identifier: MPL-2.0 +#define _USE_MATH_DEFINES #include #include -#include -#include -#include - -#include "common.h" - #if defined(NANOVDB_USE_CUDA) +#include using BufferT = nanovdb::CudaDeviceBuffer; #else using BufferT = nanovdb::HostBuffer; #endif +#include +#include +#include + +#include "common.h" void runNanoVDB(nanovdb::GridHandle& handle, int numIterations, int width, int height, BufferT& imageBuffer) { diff --git a/nanovdb/nanovdb/examples/ex_raytrace_fog_volume/openvdb.cc b/nanovdb/nanovdb/examples/ex_raytrace_fog_volume/openvdb.cc index c2870c4dfa..aaa9aa6a63 100644 --- a/nanovdb/nanovdb/examples/ex_raytrace_fog_volume/openvdb.cc +++ b/nanovdb/nanovdb/examples/ex_raytrace_fog_volume/openvdb.cc @@ -3,6 +3,7 @@ #if defined(NANOVDB_USE_OPENVDB) +#define _USE_MATH_DEFINES #include #include @@ -10,7 +11,7 @@ #include #include -#include +#include #include #include "common.h" @@ -92,4 +93,4 @@ void runOpenVDB(nanovdb::GridHandle& handle, int numIterations, int wid } } -#endif +#endif \ No newline at end of file diff --git a/nanovdb/nanovdb/examples/ex_raytrace_level_set/common.h b/nanovdb/nanovdb/examples/ex_raytrace_level_set/common.h index 44ed1c739b..edc4c27a32 100644 --- a/nanovdb/nanovdb/examples/ex_raytrace_level_set/common.h +++ b/nanovdb/nanovdb/examples/ex_raytrace_level_set/common.h @@ -3,6 +3,7 @@ #pragma once +#define _USE_MATH_DEFINES #include #include #include diff --git a/nanovdb/nanovdb/examples/ex_raytrace_level_set/main.cc b/nanovdb/nanovdb/examples/ex_raytrace_level_set/main.cc index 35254bd283..5e066c20d7 100644 --- a/nanovdb/nanovdb/examples/ex_raytrace_level_set/main.cc +++ b/nanovdb/nanovdb/examples/ex_raytrace_level_set/main.cc @@ -5,7 +5,7 @@ #include #include #include -#include +#include #if defined(NANOVDB_USE_CUDA) using BufferT = nanovdb::CudaDeviceBuffer; @@ -26,7 +26,7 @@ int main(int ac, char** av) handle = nanovdb::io::readGrid(av[1]); std::cout << "Loaded NanoVDB grid[" << handle.gridMetaData()->shortGridName() << "]...\n"; } else { - handle = nanovdb::createLevelSetSphere(100.0f, nanovdb::Vec3f(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphere"); + handle = nanovdb::createLevelSetSphere(100.0f, nanovdb::Vec3d(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphere"); } if (handle.gridMetaData()->isLevelSet() == false) { diff --git a/nanovdb/nanovdb/examples/ex_raytrace_level_set/nanovdb.cu b/nanovdb/nanovdb/examples/ex_raytrace_level_set/nanovdb.cu index 61e0076943..14c8bd678d 100644 --- a/nanovdb/nanovdb/examples/ex_raytrace_level_set/nanovdb.cu +++ b/nanovdb/nanovdb/examples/ex_raytrace_level_set/nanovdb.cu @@ -1,22 +1,22 @@ // Copyright Contributors to the OpenVDB Project // SPDX-License-Identifier: MPL-2.0 +#define _USE_MATH_DEFINES #include #include -#include -#include -#include -#include -#include - -#include "common.h" - #if defined(NANOVDB_USE_CUDA) +#include using BufferT = nanovdb::CudaDeviceBuffer; #else using BufferT = nanovdb::HostBuffer; #endif +#include +#include +#include +#include + +#include "common.h" void runNanoVDB(nanovdb::GridHandle& handle, int numIterations, int width, int height, BufferT& imageBuffer) { diff --git a/nanovdb/nanovdb/examples/ex_raytrace_level_set/openvdb.cc b/nanovdb/nanovdb/examples/ex_raytrace_level_set/openvdb.cc index 4d75990d64..c8a28e60eb 100644 --- a/nanovdb/nanovdb/examples/ex_raytrace_level_set/openvdb.cc +++ b/nanovdb/nanovdb/examples/ex_raytrace_level_set/openvdb.cc @@ -3,6 +3,7 @@ #if defined(NANOVDB_USE_OPENVDB) +#define _USE_MATH_DEFINES #include #include @@ -10,7 +11,7 @@ #include #include -#include +#include #include #include "common.h" @@ -95,4 +96,4 @@ void runOpenVDB(nanovdb::GridHandle& handle, int numI } } -#endif +#endif \ No newline at end of file diff --git a/nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda.cc b/nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda.cc deleted file mode 100644 index ca8b360a56..0000000000 --- a/nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda.cc +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: MPL-2.0 - -#include // this is required to read (and write) NanoVDB files on the host -#include // required for CUDA memory management - -extern "C" void launch_kernels(const nanovdb::NanoGrid*, - const nanovdb::NanoGrid*, - cudaStream_t stream); - -/// @brief Read a NanoVDB grid from a file and print out multiple values on both the cpu and gpu. -/// -/// @note Note This example does NOT depend on OpenVDB, only NanoVDB and CUDA. -int main() -{ - try { - // returns a GridHandle using CUDA for memory management. - auto handle = nanovdb::io::readGrid("data/sphere.nvdb"); - - cudaStream_t stream; // Create a CUDA stream to allow for asynchronous copy of pinned CUDA memory. - cudaStreamCreate(&stream); - - handle.deviceUpload(stream, false); // Copy the NanoVDB grid to the GPU asynchronously - - auto* cpuGrid = handle.grid(); // get a (raw) pointer to a NanoVDB grid of value type float on the CPU - auto* deviceGrid = handle.deviceGrid(); // get a (raw) pointer to a NanoVDB grid of value type float on the GPU - - if (!deviceGrid || !cpuGrid) - throw std::runtime_error("GridHandle did not contain a grid with value type float"); - - launch_kernels(deviceGrid, cpuGrid, stream); // Call a host method to print a grid values on both the CPU and GPU - - cudaStreamDestroy(stream); // Destroy the CUDA stream - } - catch (const std::exception& e) { - std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl; - } - - return 0; -} \ No newline at end of file diff --git a/nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda.cu b/nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda.cu index 31301e2d17..4343e01420 100644 --- a/nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda.cu +++ b/nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda.cu @@ -1,36 +1,41 @@ // Copyright Contributors to the OpenVDB Project // SPDX-License-Identifier: MPL-2.0 -#include // this defined the core tree data structure of NanoVDB accessable on both the host and device -#include // for printf +#include // this is required to read (and write) NanoVDB files on the host +#include // required for CUDA memory management +#include -// This is called by the host only -void cpu_kernel(const nanovdb::NanoGrid* cpuGrid) -{ - auto cpuAcc = cpuGrid->getAccessor(); - for (int i = 97; i < 104; ++i) { - printf("(%3i,0,0) NanoVDB cpu: % -4.2f\n", i, cpuAcc.getValue(nanovdb::Coord(i, 0, 0))); - } -} +extern "C" void launch_kernels(const nanovdb::NanoGrid*, + const nanovdb::NanoGrid*, + cudaStream_t stream); -// This is called by the device only -__global__ void gpu_kernel(const nanovdb::NanoGrid* deviceGrid) -{ - if (threadIdx.x > 6) - return; - int i = 97 + threadIdx.x; - auto gpuAcc = deviceGrid->getAccessor(); - printf("(%3i,0,0) NanoVDB gpu: % -4.2f\n", i, gpuAcc.getValue(nanovdb::Coord(i, 0, 0))); -} - -// This is called by the client code on the host -extern "C" void launch_kernels(const nanovdb::NanoGrid* deviceGrid, - const nanovdb::NanoGrid* cpuGrid, - cudaStream_t stream) +/// @brief Read a NanoVDB grid from a file and print out multiple values on both the cpu and gpu. +/// +/// @note Note This example does NOT depend on OpenVDB, only NanoVDB and CUDA. +int main(int, char**) { - // Launch the device kernel asynchronously - gpu_kernel<<<1, 64, 0, stream>>>(deviceGrid); + try { + // returns a GridHandle using CUDA for memory management. + auto handle = nanovdb::io::readGrid("data/sphere.nvdb"); + + cudaStream_t stream; // Create a CUDA stream to allow for asynchronous copy of pinned CUDA memory. + cudaStreamCreate(&stream); + + handle.deviceUpload(stream, false); // Copy the NanoVDB grid to the GPU asynchronously + + auto* cpuGrid = handle.grid(); // get a (raw) pointer to a NanoVDB grid of value type float on the CPU + auto* deviceGrid = handle.deviceGrid(); // get a (raw) pointer to a NanoVDB grid of value type float on the GPU + + if (!deviceGrid || !cpuGrid) + throw std::runtime_error("GridHandle did not contain a grid with value type float"); + + launch_kernels(deviceGrid, cpuGrid, stream); // Call a host method to print a grid values on both the CPU and GPU + + cudaStreamDestroy(stream); // Destroy the CUDA stream + } + catch (const std::exception& e) { + std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl; + } - // Launch the host "kernel" (synchronously) - cpu_kernel(cpuGrid); + return 0; } \ No newline at end of file diff --git a/nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda_kernel.cu b/nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda_kernel.cu new file mode 100644 index 0000000000..31301e2d17 --- /dev/null +++ b/nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda_kernel.cu @@ -0,0 +1,36 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +#include // this defined the core tree data structure of NanoVDB accessable on both the host and device +#include // for printf + +// This is called by the host only +void cpu_kernel(const nanovdb::NanoGrid* cpuGrid) +{ + auto cpuAcc = cpuGrid->getAccessor(); + for (int i = 97; i < 104; ++i) { + printf("(%3i,0,0) NanoVDB cpu: % -4.2f\n", i, cpuAcc.getValue(nanovdb::Coord(i, 0, 0))); + } +} + +// This is called by the device only +__global__ void gpu_kernel(const nanovdb::NanoGrid* deviceGrid) +{ + if (threadIdx.x > 6) + return; + int i = 97 + threadIdx.x; + auto gpuAcc = deviceGrid->getAccessor(); + printf("(%3i,0,0) NanoVDB gpu: % -4.2f\n", i, gpuAcc.getValue(nanovdb::Coord(i, 0, 0))); +} + +// This is called by the client code on the host +extern "C" void launch_kernels(const nanovdb::NanoGrid* deviceGrid, + const nanovdb::NanoGrid* cpuGrid, + cudaStream_t stream) +{ + // Launch the device kernel asynchronously + gpu_kernel<<<1, 64, 0, stream>>>(deviceGrid); + + // Launch the host "kernel" (synchronously) + cpu_kernel(cpuGrid); +} \ No newline at end of file diff --git a/nanovdb/nanovdb/examples/ex_util/CpuTimer.h b/nanovdb/nanovdb/examples/ex_util/CpuTimer.h deleted file mode 100644 index 5f58fd3ebc..0000000000 --- a/nanovdb/nanovdb/examples/ex_util/CpuTimer.h +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: MPL-2.0 - -/// @file CpuTimer.h -/// -/// @author Ken Museth -/// -/// @brief A simple timing class - -#ifndef NANOVDB_CPU_TIMER_H_HAS_BEEN_INCLUDED -#define NANOVDB_CPU_TIMER_H_HAS_BEEN_INCLUDED - -#include -#include - -namespace nanovdb { - -template -class CpuTimer -{ - std::chrono::high_resolution_clock::time_point mStart; -public: - CpuTimer() {} - void start(const std::string &msg, std::ostream& os = std::cerr) { - os << msg << " ... " << std::flush; - mStart = std::chrono::high_resolution_clock::now(); - } - void restart(const std::string &msg, std::ostream& os = std::cerr) { - this->stop(); - os << msg << " ... " << std::flush; - mStart = std::chrono::high_resolution_clock::now(); - } - void stop(std::ostream& os = std::cerr) - { - auto end = std::chrono::high_resolution_clock::now(); - auto diff = std::chrono::duration_cast(end - mStart).count(); - os << "completed in " << diff; - if (std::is_same::value) {// resolved at compile-time - os << " microseconds" << std::endl; - } else if (std::is_same::value) { - os << " milliseconds" << std::endl; - } else if (std::is_same::value) { - os << " seconds" << std::endl; - } else { - os << " unknown time unit" << std::endl; - } - } -};// CpuTimer - -} // namespace nanovdb - -#endif // NANOVDB_CPU_TIMER_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/examples/ex_vox_to_nanovdb/VoxToNanoVDB.h b/nanovdb/nanovdb/examples/ex_vox_to_nanovdb/VoxToNanoVDB.h index 33348e4dc8..98bacb538e 100644 --- a/nanovdb/nanovdb/examples/ex_vox_to_nanovdb/VoxToNanoVDB.h +++ b/nanovdb/nanovdb/examples/ex_vox_to_nanovdb/VoxToNanoVDB.h @@ -4,6 +4,7 @@ #pragma once #include +#include #define OGT_VOX_IMPLEMENTATION #include "ogt_vox.h" @@ -28,7 +29,7 @@ inline const ogt_vox_scene* load_vox_scene(const char* filename, uint32_t scene_ uint32_t buffer_size = ftell(fp); fseek(fp, 0, SEEK_SET); uint8_t* buffer = new uint8_t[buffer_size]; - fread(buffer, buffer_size, 1, fp); + size_t bytes = fread(buffer, buffer_size, 1, fp); fclose(fp); const ogt_vox_scene* scene = ogt_vox_read_scene_with_flags(buffer, buffer_size, scene_read_flags); delete[] buffer; // the buffer can be safely deleted once the scene is instantiated. @@ -131,8 +132,8 @@ nanovdb::GridHandle convertVoxToNanoVDB(const std::string& inFilename, try { if (const auto* scene = detail::load_vox_scene(inFilename.c_str())) { // we just merge into one grid... - nanovdb::GridBuilder builder; - auto acc = builder.getAccessor(); + nanovdb::build::Grid grid(nanovdb::Rgba8(),modelName,nanovdb::GridClass::VoxelVolume); + auto acc = grid.getAccessor(); auto processModelFn = [&](int modelIndex, const ogt_vox_transform& xform) { const auto* model = scene->models[modelIndex]; @@ -143,7 +144,7 @@ nanovdb::GridHandle convertVoxToNanoVDB(const std::string& inFilename, for (uint32_t x = 0; x < model->size_x; ++x, ++voxel_index) { if (uint8_t color_index = model->voxel_data[voxel_index]) { ogt_vox_rgba rgba = scene->palette.color[color_index]; - auto ijk = nanovdb::Coord::Floor(detail::matMult4x4((float*)&xform, nanovdb::Vec4f(x, y, z, 1))); + auto ijk = nanovdb::Coord::Floor(detail::matMult4x4((float*)&xform, nanovdb::Vec4f(x, y, z, 1))); acc.setValue(nanovdb::Coord(ijk[0], ijk[2], -ijk[1]), *reinterpret_cast(&rgba)); } } @@ -184,8 +185,7 @@ nanovdb::GridHandle convertVoxToNanoVDB(const std::string& inFilename, printf("scene processing end.\n"); ogt_vox_destroy_scene(scene); - builder.setGridClass(nanovdb::GridClass::VoxelVolume); - return builder.getHandle<>(1.0f, nanovdb::Vec3d(0), modelName); + return nanovdb::createNanoGrid(grid); } else { std::ostringstream ss; ss << "Invalid file \"" << inFilename << "\""; diff --git a/nanovdb/nanovdb/examples/ex_voxels_to_grid_cuda/ex_voxels_to_grid_cuda.cu b/nanovdb/nanovdb/examples/ex_voxels_to_grid_cuda/ex_voxels_to_grid_cuda.cu new file mode 100644 index 0000000000..efb8b12879 --- /dev/null +++ b/nanovdb/nanovdb/examples/ex_voxels_to_grid_cuda/ex_voxels_to_grid_cuda.cu @@ -0,0 +1,53 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +#include + +/// @brief Demonstrates how to create a NanoVDB grid from voxel coordinates on the GPU +int main() +{ + using namespace nanovdb; + + try { + // Define list of voxel coordinates and copy them to the device + const size_t numVoxels = 3; + Coord coords[numVoxels] = {Coord(1, 2, 3), Coord(-1,3,6), Coord(-90,100,5678)}, *d_coords = nullptr; + cudaCheck(cudaMalloc(&d_coords, numVoxels * sizeof(Coord))); + cudaCheck(cudaMemcpy(d_coords, coords, numVoxels * sizeof(Coord), cudaMemcpyHostToDevice));// coords CPU -> GPU + + // Generate a NanoVDB grid that contains the list of voxels on the device + auto handle = cudaVoxelsToGrid(d_coords, numVoxels); + auto *grid = handle.deviceGrid(); + + // Define a list of values and copy them to the device + float values[numVoxels] = {1.4f, 6.7f, -5.0f}, *d_values; + cudaCheck(cudaMalloc(&d_values, numVoxels * sizeof(float))); + cudaCheck(cudaMemcpy(d_values, values, numVoxels * sizeof(float), cudaMemcpyHostToDevice));// values CPU -> GPU + + // Launch a device kernel that sets the values of voxels define above and prints them + const unsigned int numThreads = 128, numBlocks = (numVoxels + numThreads - 1) / numThreads; + cudaLambdaKernel<<>>(numVoxels, [=] __device__(size_t tid) { + using OpT = SetVoxel;// defines type of random-access operation (set value) + const Coord &ijk = d_coords[tid]; + grid->tree().set(ijk, d_values[tid]);// normally one should use a ValueAccessor + printf("GPU: voxel # %lu, grid(%4i,%4i,%4i) = %5.1f\n", tid, ijk[0], ijk[1], ijk[2], grid->tree().getValue(ijk)); + }); cudaCheckError(); + + // Copy grid from GPU to CPU and print the voxel values for validation + handle.deviceDownload();// creates a copy on the CPU + grid = handle.grid(); + for (size_t i=0; itree().getValue(ijk)); + } + + // free arrays allocated on the device + cudaCheck(cudaFree(d_coords)); + cudaCheck(cudaFree(d_values)); + } + catch (const std::exception& e) { + std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl; + } + + return 0; +} \ No newline at end of file diff --git a/nanovdb/nanovdb/examples/ex_write_nanovdb_grids/write_nanovdb_grids.cc b/nanovdb/nanovdb/examples/ex_write_nanovdb_grids/write_nanovdb_grids.cc index 0722fcf6e2..314fe4ea57 100644 --- a/nanovdb/nanovdb/examples/ex_write_nanovdb_grids/write_nanovdb_grids.cc +++ b/nanovdb/nanovdb/examples/ex_write_nanovdb_grids/write_nanovdb_grids.cc @@ -31,4 +31,4 @@ int main() std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl; } return 0; -} \ No newline at end of file +} diff --git a/nanovdb/nanovdb/unittest/CMakeLists.txt b/nanovdb/nanovdb/unittest/CMakeLists.txt index 3c4358358f..b0a32be445 100644 --- a/nanovdb/nanovdb/unittest/CMakeLists.txt +++ b/nanovdb/nanovdb/unittest/CMakeLists.txt @@ -41,6 +41,14 @@ add_test(nanovdb_unit_test nanovdb_test_nanovdb) # ----------------------------------------------------------------------------- +if(NANOVDB_USE_CUDA) + add_executable(nanovdb_test_cuda "TestNanoVDB.cu") + target_link_libraries(nanovdb_test_cuda PRIVATE nanovdb GTest::GTest GTest::Main) + add_test(nanovdb_cuda_unit_test nanovdb_test_cuda) +endif() + +# ----------------------------------------------------------------------------- + if(NOT (NANOVDB_USE_TBB AND NANOVDB_USE_OPENVDB)) message(WARNING " - OpenVDB required to build OpenVDB unit tests. Skipping.") return() diff --git a/nanovdb/nanovdb/unittest/TestNanoVDB.cc b/nanovdb/nanovdb/unittest/TestNanoVDB.cc index 22558626c5..aa84b99202 100644 --- a/nanovdb/nanovdb/unittest/TestNanoVDB.cc +++ b/nanovdb/nanovdb/unittest/TestNanoVDB.cc @@ -14,7 +14,7 @@ #include #include -#include +#include #include #include #include @@ -29,8 +29,7 @@ #include #include #include -#include -#include "../examples/ex_util/CpuTimer.h" +#include #if !defined(_MSC_VER) // does not compile in msvc c++ due to zero-sized arrays. #include @@ -44,50 +43,21 @@ #include - -namespace nanovdb {// this namespace is required by gtest - -std::ostream& -operator<<(std::ostream& os, const Coord& ijk) -{ - os << "(" << ijk[0] << "," << ijk[1] << "," << ijk[2] << ")"; - return os; -} - -std::ostream& -operator<<(std::ostream& os, const CoordBBox& b) -{ - os << b[0] << " -> " << b[1]; - return os; -} - -template -std::ostream& -operator<<(std::ostream& os, const Vec3& v) -{ - os << "(" << v[0] << "," << v[1] << "," << v[2] << ")"; - return os; -} -}// namespace nanovdb - namespace { template struct Sphere { - Sphere(const nanovdb::Vec3& center, - ValueT radius, - ValueT voxelSize = 1.0, - ValueT halfWidth = 3.0) + Sphere(const nanovdb::Vec3d& center, + double radius, + double voxelSize = 1.0, + double halfWidth = 3.0) : mCenter(center) , mRadius(radius) , mVoxelSize(voxelSize) , mBackground(voxelSize * halfWidth) { } - ValueT background() const { return mBackground; } - - /// @brief Only method required by GridBuilder ValueT operator()(const nanovdb::Coord& ijk) const { const ValueT dst = this->sdf(ijk); @@ -124,6 +94,60 @@ struct Sphere const nanovdb::Vec3 mCenter; const ValueT mRadius, mVoxelSize, mBackground; }; // Sphere + +class DataBuffer : public std::streambuf +{ +public: + DataBuffer(void* data, size_t size) + { + char* start = static_cast(data); + char* stop = start + size; + this->setg(start, start, stop); + } + + std::iostream::pos_type seekoff(std::iostream::off_type off, std::ios_base::seekdir way, std::ios_base::openmode which) override + { + if (which & std::ios_base::in) + { + if (way == std::ios_base::cur) + { + gbump(off); + } + else if (way == std::ios_base::end) + { + setg(eback(), egptr() + off, egptr()); + } + else if (way == std::ios_base::beg) + { + setg(eback(), eback() + off, egptr()); + } + } + + if (which & std::ios_base::out) + { + if (way == std::ios_base::cur) + { + pbump(off); + } + else if (way == std::ios_base::end) + { + setp(pbase(), epptr()); + pbump(epptr() - pbase() + off); + } + else if (way == std::ios_base::beg) + { + setp(pbase(), epptr()); + pbump(off); + } + } + + return gptr() - eback(); + } + std::iostream::pos_type seekpos(std::iostream::pos_type sp, std::ios_base::openmode which) override + { + return seekoff(sp - std::iostream::pos_type(std::iostream::off_type(0)), std::ios_base::beg, which); + } +}; } // namespace // The fixture for testing class. @@ -161,7 +185,7 @@ class TestNanoVDB : public ::testing::Test const auto n = sizeof(T); std::cerr << "Size of " << s << ": " << n << " bytes which is" << (n % 32 == 0 ? " " : " NOT ") << "32 byte aligned" << std::endl; } - nanovdb::CpuTimer<> mTimer; + nanovdb::CpuTimer mTimer; }; // TestNanoVDB template @@ -195,7 +219,13 @@ using MyTypes = ::testing::Types; @@ -305,10 +335,10 @@ TEST_F(TestNanoVDB, Version) TEST_F(TestNanoVDB, Basic) { - { // CHAR_BIT + { // verify size of CHAR_BIT EXPECT_EQ(8, CHAR_BIT); } - { + {// check that keys in a map are sorted in ascending order std::vector v = {3, 1, 7, 0}; EXPECT_FALSE(std::is_sorted(v.begin(), v.end())); std::map m; @@ -319,17 +349,54 @@ TEST_F(TestNanoVDB, Basic) v.push_back(i.first); EXPECT_TRUE(std::is_sorted(v.begin(), v.end())); } - { + {// check that size of enum is the size of an integer enum tmp { a = 0, b, c, d } t; EXPECT_EQ(sizeof(int), sizeof(t)); } - {// Check size of io::MetaData - EXPECT_EQ(176u, sizeof(nanovdb::io::MetaData)); - //std::cerr << "sizeof(MetaData) = " << sizeof(nanovdb::io::MetaData) << std::endl; + {// Check size of io::FileMetaData + EXPECT_EQ(176u, sizeof(nanovdb::io::FileMetaData)); + //std::cerr << "sizeof(FileMetaData) = " << sizeof(nanovdb::io::FileMetaData) << std::endl; } + {// check that it's safe to case uint64_t to int64_t (as long as its no larger than 2^63 - 1) + const uint64_t i = 9223372036854775807ULL;// = 2^63 - 1 + const int64_t *j = reinterpret_cast(&i); + EXPECT_EQ(i, *j); + //std::cerr << "i="<> magic; EXPECT_EQ(magic, nanovdb::io::reverseEndianness(NANOVDB_MAGIC_NUMBER)); + + {// test all magic numbers + const std::string a_str("NanoVDB0"), b_str("NanoVDB1"), c_str("NanoVDB2"); + const uint64_t a = NANOVDB_MAGIC_NUMBER;// NanoVDB0 + const uint64_t b = NANOVDB_MAGIC_GRID;// NanoVDB1 + const uint64_t c = NANOVDB_MAGIC_FILE;// NanoVDB2 + const uint64_t m = NANOVDB_MAGIC_MASK;// masks out most significant byte + const char *aa= (const char*)&a, *bb = (const char*)&b, *cc = (const char*)&c; + for (int i=0; i<8; ++i) { + EXPECT_EQ(a_str[i], aa[i]); + EXPECT_EQ(b_str[i], bb[i]); + EXPECT_EQ(c_str[i], cc[i]); + } + for (int i=0; i<7; ++i) { + EXPECT_EQ(aa[i], bb[i]); + EXPECT_EQ(aa[i], cc[i]); + } + EXPECT_EQ('0', aa[7]); + EXPECT_EQ('1', bb[7]); + EXPECT_EQ('2', cc[7]); + EXPECT_EQ(m & a, m & b); + EXPECT_EQ(NANOVDB_MAGIC_MASK & NANOVDB_MAGIC_NUMBER, NANOVDB_MAGIC_MASK & NANOVDB_MAGIC_FILE); + } }// Magic TEST_F(TestNanoVDB, FindBits) @@ -406,31 +496,54 @@ TEST_F(TestNanoVDB, CRC32) { // test function that uses iterators const std::string s{"The quick brown fox jumps over the lazy dog"}; std::stringstream ss; - ss << std::hex << std::setw(8) << std::setfill('0') << nanovdb::crc32(s.begin(), s.end()); + ss << std::hex << std::setw(8) << std::setfill('0') << nanovdb::crc32::checksum(s.c_str(), s.size()); EXPECT_EQ("414fa339", ss.str()); } { // test the checksum for a modified string const std::string s{"The quick brown Fox jumps over the lazy dog"}; std::stringstream ss; - ss << std::hex << std::setw(8) << std::setfill('0') << nanovdb::crc32(s.begin(), s.end()); + ss << std::hex << std::setw(8) << std::setfill('0') << nanovdb::crc32::checksum(s.c_str(), s.size()); EXPECT_NE("414fa339", ss.str()); } { // test function that uses void pointer and byte size const std::string s{"The quick brown fox jumps over the lazy dog"}; std::stringstream ss; - ss << std::hex << std::setw(8) << std::setfill('0') << nanovdb::crc32(s.data(), s.size()); + ss << std::hex << std::setw(8) << std::setfill('0') << nanovdb::crc32::checksum(s.c_str(), s.size()); EXPECT_EQ("414fa339", ss.str()); } { // test accumulation - nanovdb::CRC32 crc; const std::string s1{"The quick brown fox jum"}; - crc(s1.begin(), s1.end()); + uint32_t crc = nanovdb::crc32::checksum(s1.c_str(), s1.size()); + const std::string s2{"ps over the lazy dog"}; + crc = nanovdb::crc32::checksum(s2.c_str(), s2.size(), crc); + std::stringstream ss; + ss << std::hex << std::setw(8) << std::setfill('0') << crc; + EXPECT_EQ("414fa339", ss.str()); + } + { // test accumulation with lookup table + auto lut = nanovdb::crc32::createLut(); + const std::string s1{"The quick brown fox jum"}; + uint32_t crc = nanovdb::crc32::checksum(s1.c_str(), s1.size(), lut.get()); const std::string s2{"ps over the lazy dog"}; - crc(s2.begin(), s2.end()); + crc = nanovdb::crc32::checksum(s2.c_str(), s2.size(), lut.get(), crc); std::stringstream ss; - ss << std::hex << std::setw(8) << std::setfill('0') << crc.checksum(); + ss << std::hex << std::setw(8) << std::setfill('0') << crc; EXPECT_EQ("414fa339", ss.str()); } + { + //EXPECT_EQ(~uint64_t(0), nanovdb::GridChecksum::EMPTY); + nanovdb::GridChecksum cs(~uint64_t(0)); + EXPECT_EQ(nanovdb::ChecksumMode::Disable, cs.mode()); + EXPECT_TRUE(cs.isEmpty()); + EXPECT_FALSE(cs.isFull()); + } + { + nanovdb::GridChecksum cs; + EXPECT_EQ(~uint64_t(0), cs.checksum()); + EXPECT_EQ(nanovdb::ChecksumMode::Disable, cs.mode()); + EXPECT_TRUE(cs.isEmpty()); + EXPECT_FALSE(cs.isFull()); + } } TEST_F(TestNanoVDB, Range1D) @@ -642,6 +755,53 @@ TEST_F(TestNanoVDB, reduce) } } +TEST_F(TestNanoVDB, prefixSum) +{ + const uint64_t size = 50000000;// test on fifty million elements + {// multi-threaded inclusive prefix sum + std::vector array(size); + EXPECT_EQ(size, array.size()); + uint64_t sum = 0; + for (uint64_t i=0; i array(size); + EXPECT_EQ(size, array.size()); + uint64_t sum = 0; + for (uint64_t i=0; i::value; + EXPECT_TRUE(test); + test = nanovdb::is_same::value; + EXPECT_FALSE(test); + test = nanovdb::is_same::value; + EXPECT_FALSE(test); + test = nanovdb::is_same::value; + EXPECT_FALSE(test); + } {// float using A = typename nanovdb::BuildToValueMap::Type; bool test = nanovdb::is_same::value; @@ -691,7 +861,7 @@ TEST_F(TestNanoVDB, Traits) test = nanovdb::is_same::value; EXPECT_TRUE(test); } - {// ValueIndex + {// ValueIndex using A = typename nanovdb::BuildToValueMap::Type; bool test = nanovdb::is_same::value; EXPECT_TRUE(test); @@ -702,6 +872,47 @@ TEST_F(TestNanoVDB, Traits) test = nanovdb::is_same::value; EXPECT_TRUE(test); } + {// nanovdb::BuildTraits + bool test = nanovdb::BuildTraits::is_index; + EXPECT_FALSE(test); + test = nanovdb::BuildTraits::is_index; + EXPECT_TRUE(test); + test = nanovdb::BuildTraits::is_index; + EXPECT_TRUE(test); + test = nanovdb::BuildTraits::is_index; + EXPECT_TRUE(test); + test = nanovdb::BuildTraits::is_index; + EXPECT_TRUE(test); + test = nanovdb::BuildTraits::is_Fp; + EXPECT_TRUE(test); + test = nanovdb::BuildTraits::is_Fp; + EXPECT_TRUE(test); + test = nanovdb::BuildTraits::is_Fp; + EXPECT_TRUE(test); + test = nanovdb::BuildTraits::is_Fp; + EXPECT_TRUE(test); + test = nanovdb::BuildTraits::is_FpX; + EXPECT_TRUE(test); + test = nanovdb::BuildTraits::is_FpX; + EXPECT_TRUE(test); + test = nanovdb::BuildTraits::is_FpX; + EXPECT_TRUE(test); + test = nanovdb::BuildTraits::is_FpX; + EXPECT_FALSE(test); + } + {// nanovdb::is_specialization + bool test = nanovdb::is_specialization,nanovdb::Vec3>::value; + EXPECT_TRUE(test); + test = nanovdb::is_specialization::value; + EXPECT_TRUE(test); + test = nanovdb::is_specialization::value; + EXPECT_FALSE(test); + using VecT = std::vector; + test = nanovdb::is_specialization::value; + EXPECT_TRUE(test); + test = nanovdb::is_specialization::value; + EXPECT_FALSE(test); + } } TEST_F(TestNanoVDB, Rgba8) @@ -842,9 +1053,11 @@ TEST_F(TestNanoVDB, BBox) EXPECT_EQ(-std::numeric_limits::max(), bbox[1][1]); EXPECT_EQ(-std::numeric_limits::max(), bbox[1][2]); EXPECT_TRUE(bbox.empty()); + EXPECT_FALSE(bbox); bbox.expand(nanovdb::Vec3f(57.0f, -31.0f, 60.0f)); EXPECT_TRUE(bbox.empty()); + EXPECT_FALSE(bbox); EXPECT_EQ(nanovdb::Vec3f(0.0f), bbox.dim()); EXPECT_EQ(57.0f, bbox[0][0]); EXPECT_EQ(-31.0f, bbox[0][1]); @@ -855,6 +1068,7 @@ TEST_F(TestNanoVDB, BBox) bbox.expand(nanovdb::Vec3f(58.0f, 0.0f, 62.0f)); EXPECT_FALSE(bbox.empty()); + EXPECT_TRUE(bbox); EXPECT_EQ(nanovdb::Vec3f(1.0f, 31.0f, 2.0f), bbox.dim()); EXPECT_EQ(57.0f, bbox[0][0]); EXPECT_EQ(-31.0f, bbox[0][1]); @@ -875,9 +1089,11 @@ TEST_F(TestNanoVDB, CoordBBox) EXPECT_EQ(std::numeric_limits::min(), bbox[1][1]); EXPECT_EQ(std::numeric_limits::min(), bbox[1][2]); EXPECT_TRUE(bbox.empty()); + EXPECT_FALSE(bbox); bbox.expand(nanovdb::Coord(57, -31, 60)); EXPECT_FALSE(bbox.empty()); + EXPECT_TRUE(bbox); EXPECT_EQ(nanovdb::Coord(1), bbox.dim()); EXPECT_EQ(57, bbox[0][0]); EXPECT_EQ(-31, bbox[0][1]); @@ -888,6 +1104,7 @@ TEST_F(TestNanoVDB, CoordBBox) bbox.expand(nanovdb::Coord(58, 0, 62)); EXPECT_FALSE(bbox.empty()); + EXPECT_TRUE(bbox); EXPECT_EQ(nanovdb::Coord(2, 32, 3), bbox.dim()); EXPECT_EQ(57, bbox[0][0]); EXPECT_EQ(-31, bbox[0][1]); @@ -899,6 +1116,7 @@ TEST_F(TestNanoVDB, CoordBBox) { // test convert auto bbox2 = bbox.asReal(); EXPECT_FALSE(bbox2.empty()); + EXPECT_TRUE(bbox2); EXPECT_EQ(nanovdb::Vec3f(57.0f, -31.0f, 60.0f), bbox2.min()); EXPECT_EQ(nanovdb::Vec3f(59.0f, 1.0f, 63.0f), bbox2.max()); } @@ -906,11 +1124,14 @@ TEST_F(TestNanoVDB, CoordBBox) { // test prefix iterator auto iter = bbox.begin(); EXPECT_TRUE(iter); + EXPECT_FALSE(bbox.end()); + EXPECT_NE(iter, bbox.end()); for (int i = bbox.min()[0]; i <= bbox.max()[0]; ++i) { for (int j = bbox.min()[1]; j <= bbox.max()[1]; ++j) { for (int k = bbox.min()[2]; k <= bbox.max()[2]; ++k) { EXPECT_TRUE(bbox.isInside(*iter)); EXPECT_TRUE(iter); + EXPECT_NE(iter, bbox.end()); const auto& ijk = *iter; // note, copy by reference EXPECT_EQ(ijk[0], i); EXPECT_EQ(ijk[1], j); @@ -920,15 +1141,19 @@ TEST_F(TestNanoVDB, CoordBBox) } } EXPECT_FALSE(iter); + EXPECT_EQ(iter, bbox.end()); } { // test postfix iterator auto iter = bbox.begin(); EXPECT_TRUE(iter); + EXPECT_FALSE(bbox.end()); + EXPECT_NE(iter, bbox.end()); for (int i = bbox.min()[0]; i <= bbox.max()[0]; ++i) { for (int j = bbox.min()[1]; j <= bbox.max()[1]; ++j) { for (int k = bbox.min()[2]; k <= bbox.max()[2]; ++k) { EXPECT_TRUE(iter); + EXPECT_NE(iter, bbox.end()); const auto ijk = *iter++; // note, copy by value! EXPECT_EQ(ijk[0], i); EXPECT_EQ(ijk[1], j); @@ -937,6 +1162,25 @@ TEST_F(TestNanoVDB, CoordBBox) } } EXPECT_FALSE(iter); + EXPECT_EQ(iter, bbox.end()); + } + + {// test different approaches to iteration + auto it1 = bbox.begin(), it2 = bbox.begin(), it3 = bbox.begin(), it4 = bbox.begin(); + while(it1 != bbox.end()) ++it1; + while(it2) ++it2; + while(it3 < bbox.end()) ++it3; + while(*it4 <= bbox.max()) ++it4; + EXPECT_EQ(it1, it2); + EXPECT_EQ(it2, it3); + EXPECT_EQ(it3, it4); + } + + {// test CoordBBox::createCube + EXPECT_EQ(nanovdb::Coord(-7,-7,-7), nanovdb::CoordBBox::createCube(nanovdb::Coord(-7), 8).min()); + EXPECT_EQ(nanovdb::Coord( 0, 0, 0), nanovdb::CoordBBox::createCube(nanovdb::Coord(-7), 8).max()); + EXPECT_EQ(nanovdb::Coord(-7,-7,-7), nanovdb::CoordBBox::createCube(-7, 0).min()); + EXPECT_EQ(nanovdb::Coord( 0, 0, 0), nanovdb::CoordBBox::createCube(-7, 0).max()); } } @@ -946,19 +1190,19 @@ TEST_F(TestNanoVDB, Vec3) EXPECT_FALSE(test); test = nanovdb::TensorTraits::IsVector; EXPECT_FALSE(test); - test = nanovdb::is_specialization::value; + test = nanovdb::is_specialization::value; EXPECT_TRUE(test); - test = nanovdb::is_same::value; + test = nanovdb::is_same::value; EXPECT_TRUE(test); - test = nanovdb::TensorTraits::IsVector; + test = nanovdb::TensorTraits::IsVector; EXPECT_TRUE(test); - test = nanovdb::is_same::ElementType>::value; + test = nanovdb::is_same::ElementType>::value; EXPECT_TRUE(test); - test = nanovdb::is_same::FloatType>::value; + test = nanovdb::is_same::FloatType>::value; EXPECT_TRUE(test); - EXPECT_EQ(size_t(3 * 8), sizeof(nanovdb::Vec3R)); + EXPECT_EQ(size_t(3 * 8), sizeof(nanovdb::Vec3d)); - nanovdb::Vec3R xyz(1.0, 2.0, 3.0); + nanovdb::Vec3d xyz(1.0, 2.0, 3.0); EXPECT_EQ(1.0, xyz[0]); EXPECT_EQ(2.0, xyz[1]); EXPECT_EQ(3.0, xyz[2]); @@ -992,7 +1236,7 @@ TEST_F(TestNanoVDB, Vec4) EXPECT_TRUE(test); int rank = nanovdb::TensorTraits::Rank; EXPECT_EQ(0, rank); - rank = nanovdb::TensorTraits::Rank; + rank = nanovdb::TensorTraits::Rank; EXPECT_EQ(1, rank); test = nanovdb::is_same::FloatType>::value; EXPECT_FALSE(test); @@ -1004,11 +1248,11 @@ TEST_F(TestNanoVDB, Vec4) EXPECT_TRUE(test); test = nanovdb::is_specialization::value; EXPECT_TRUE(test); - test = nanovdb::is_specialization::value; + test = nanovdb::is_specialization::value; EXPECT_FALSE(test); test = nanovdb::is_same::value; EXPECT_TRUE(test); - test = nanovdb::TensorTraits::IsVector; + test = nanovdb::TensorTraits::IsVector; EXPECT_TRUE(test); test = nanovdb::is_same::ElementType>::value; EXPECT_TRUE(test); @@ -1041,6 +1285,20 @@ TEST_F(TestNanoVDB, Vec4) EXPECT_NE(nanovdb::Vec4f(1, 2, 3, 4), nanovdb::Vec4f(1, 2, 3, 5)); }// Vec4 +TEST_F(TestNanoVDB, Map) +{ + EXPECT_EQ(264u, sizeof(nanovdb::Map)); + nanovdb::Map map1, map2; + EXPECT_EQ(nanovdb::Vec3d(1.0), map1.getVoxelSize()); + map1.set(1.0, nanovdb::Vec3d(0.0)); + EXPECT_EQ(nanovdb::Vec3d(1.0), map1.getVoxelSize()); + map2.set(2.0, nanovdb::Vec3d(0.0)); + EXPECT_EQ(nanovdb::Vec3d(2.0), map2.getVoxelSize()); + map1 = map2;// default assignment operator + EXPECT_EQ(nanovdb::Vec3d(2.0), map2.getVoxelSize()); + EXPECT_EQ(nanovdb::Vec3d(2.0), map1.getVoxelSize()); +}// Map + TEST_F(TestNanoVDB, Extrema) { { // int @@ -1159,7 +1417,7 @@ TEST_F(TestNanoVDB, Ray) EXPECT_EQ(ray(2.0)[1], 5); //higher y component of intersection ray.reset(eye, dir, t0, t1); - // intersects the lower edge anlong the z-axis of the box + // intersects the lower edge along the z-axis of the box EXPECT_TRUE(ray.clip(BBoxT(Vec3T(1.5, 2.0, 2.0), Vec3T(4.5, 4.0, 6.0)))); //std::cerr << ray(0.5) << ", " << ray(2.0) << std::endl; EXPECT_EQ(0.5, ray.t0()); @@ -1307,23 +1565,29 @@ TEST_F(TestNanoVDB, Mask) EXPECT_EQ(size_t(8 * 8), MaskT::memUsage()); MaskT mask; - EXPECT_EQ(0U, mask.countOn()); + EXPECT_EQ(0u, mask.countOn()); EXPECT_TRUE(mask.isOff()); EXPECT_FALSE(mask.isOn()); EXPECT_FALSE(mask.beginOn()); - for (uint32_t i = 0; i < MaskT::bitCount(); ++i) { + for (uint32_t i=0u; i(i)); + EXPECT_EQ(512u, mask.findPrev(i)); + EXPECT_EQ(i<512u ? i : 512u, mask.findNext(i)); + EXPECT_EQ(i<512u ? i : 512u, mask.findPrev(i)); + } + mask.setOn(256u); EXPECT_FALSE(mask.isOff()); EXPECT_FALSE(mask.isOn()); auto iter = mask.beginOn(); EXPECT_TRUE(iter); - EXPECT_EQ(256U, *iter); + EXPECT_EQ(256u, *iter); EXPECT_FALSE(++iter); - for (uint32_t i = 0; i < MaskT::bitCount(); ++i) { - if (i != 256) { + for (uint32_t i=0u; i(i)); + EXPECT_EQ(i<256u || i>=512u ? 512u : 256u, mask.findPrev(i)); + EXPECT_EQ(i==256u ? 257u : i<512u ? i : 512u, mask.findNext(i)); + EXPECT_EQ(i==256u ? 255u : i<512u ? i : 512u, mask.findPrev(i)); + } - mask.set(256, false); + mask.set(256u, false); EXPECT_TRUE(mask.isOff()); EXPECT_FALSE(mask.isOn()); - EXPECT_FALSE(mask.isOn(256)); + EXPECT_FALSE(mask.isOn(256u)); - mask.set(256, true); + mask.set(256u, true); EXPECT_FALSE(mask.isOff()); EXPECT_FALSE(mask.isOn()); - EXPECT_TRUE(mask.isOn(256)); + EXPECT_TRUE(mask.isOn(256u)); EXPECT_EQ(1u, mask.countOn()); - for (int i=0; i<512; ++i) EXPECT_EQ(i<=256 ? 0u : 1u, mask.countOn(i)); + for (int i=0u; i<512u; ++i) EXPECT_EQ(i<=256u ? 0u : 1u, mask.countOn(i)); mask.setOn(); EXPECT_EQ(512u, mask.countOn()); - for (uint32_t i=0; i<512; ++i) EXPECT_EQ(i, mask.countOn(i)); + for (uint32_t i=0; i<512u; ++i) EXPECT_EQ(i, mask.countOn(i)); + for (uint32_t i=0; i<1000u; ++i) { + EXPECT_EQ(i<512u ? i : 512u, mask.findNext(i)); + EXPECT_EQ(i<512u ? i : 512u, mask.findPrev(i)); + EXPECT_EQ(512u, mask.findNext(i)); + EXPECT_EQ(512u, mask.findPrev(i)); + } mask.setOff(); EXPECT_TRUE(mask.isOff()); - mask.setOn(7); - mask.setOn(123); + mask.setOn(7u); + mask.setOn(123u); EXPECT_FALSE(mask.isOn()); auto it1 = mask.beginOff(); EXPECT_TRUE(it1); - EXPECT_EQ(0, *it1); + EXPECT_EQ(0u, *it1); EXPECT_TRUE(++it1); - EXPECT_EQ(1, *it1); + EXPECT_EQ(1u, *it1); EXPECT_TRUE(++it1); - EXPECT_EQ(2, *it1); + EXPECT_EQ(2u, *it1); auto it2 = mask.beginOn(); EXPECT_TRUE(it2); - EXPECT_EQ(7, *it2); + EXPECT_EQ(7u, *it2); EXPECT_TRUE(++it2); - EXPECT_EQ(123, *it2); + EXPECT_EQ(123u, *it2); EXPECT_FALSE(++it2); } @@ -1424,10 +1700,11 @@ TEST_F(TestNanoVDB, LeafNode) EXPECT_EQ(8u, data.mValueMask.wordCount()); nanovdb::CoordBBox bbox(nanovdb::Coord(-1), nanovdb::Coord(-1)); - uint64_t word = 0u; + uint64_t word = 0u; + const uint64_t *w = data.mValueMask.words(); for (int i = 0; i < 8; ++i) { - if (uint64_t w = data.mValueMask.getWord(i)) { - word |= w; + if (w[i]) { + word |= w[i]; if (bbox[0][0] == -1) bbox[0][0] = i; bbox[1][0] = i; @@ -1467,10 +1744,11 @@ TEST_F(TestNanoVDB, LeafNode) auto localBBox = [](const LeafT* leaf) { // static_assert(8u == LeafT::dim(), "Expected dim = 8"); nanovdb::CoordBBox bbox(nanovdb::Coord(-1, 0, 0), nanovdb::Coord(-1, 7, 7)); - uint64_t word64 = 0u; + uint64_t word64 = 0u; + const uint64_t *w = leaf->valueMask().words(); for (int i = 0; i < 8; ++i) { - if (uint64_t w = leaf->valueMask().getWord(i)) { - word64 |= w; + if (w[i]) { + word64 |= w[i]; if (bbox[0][0] == -1) bbox[0][0] = i; // only set once bbox[1][0] = i; @@ -1503,8 +1781,22 @@ TEST_F(TestNanoVDB, LeafNode) EXPECT_EQ(bbox[1], max); } + { // test LeafNode::updateBBox + leaf->data()->mValueMask.setOff(); + leaf->data()->mBBoxMin = nanovdb::Coord(0); + const nanovdb::Coord min(1, 2, 3); + leaf->setValue(min, 1.0f); + EXPECT_EQ(1.0f, leaf->getValue(min)); + leaf->updateBBox(); + const auto bbox = leaf->bbox(); + //std::cerr << "bbox = " << bbox << std::endl; + EXPECT_EQ(bbox[0], min); + EXPECT_EQ(bbox[1], min); + } + { // test LeafNode::updateBBox leaf->data()->mValueMask.setOff(); + leaf->data()->mBBoxMin = nanovdb::Coord(0); const nanovdb::Coord min(1, 2, 3), max(5, 6, 7); leaf->setValue(min, 1.0f); leaf->setValue(max, 2.0f); @@ -1789,6 +2081,7 @@ TEST_F(TestNanoVDB, RootNode) EXPECT_EQ(0u, root->tileCount()); EXPECT_EQ(nanovdb::AlignUp(sizeof(nanovdb::CoordBBox) + sizeof(uint32_t) + (5 * sizeof(float))), root->memUsage()); // background, min, max, tileCount + bbox EXPECT_EQ(1.234f, root->getValue(CoordT(1, 2, 3))); + EXPECT_EQ(1.234f, root->getValue(1, 2, 3)); {// examine padding of RootNode //std::cerr << "sizeof(Coord) = " << sizeof(nanovdb::Coord) << " bytes\n"; @@ -1835,6 +2128,35 @@ TEST_F(TestNanoVDB, RootNode) TEST_F(TestNanoVDB, Offsets) { + {// check GridBlindMetaData + /* + static const int MaxNameSize = 256;// due to NULL termination the maximum length is one less! + int64_t mDataOffset; // byte offset to the blind data, relative to the GridData. + uint64_t mValueCount; // number of elements, e.g. point count + uint32_t mFlags; // flags + GridBlindDataSemantic mSemantic; // semantic meaning of the data. + GridBlindDataClass mDataClass; // 4 bytes + GridType mDataType; // 4 bytes + char mName[MaxNameSize];// note this includes the NULL termination + */ + int offset = 0; + EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::GridBlindMetaData, mDataOffset), offset); + offset += 8; + EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::GridBlindMetaData, mValueCount), offset); + offset += 8; + EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::GridBlindMetaData, mValueSize), offset); + offset += 4; + EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::GridBlindMetaData, mSemantic), offset); + offset += 4; + EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::GridBlindMetaData, mDataClass), offset); + offset += 4; + EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::GridBlindMetaData, mDataType), offset); + offset += 4; + EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::GridBlindMetaData, mName), offset); + offset += 256; + //std::cerr << "offset = " << offset << " sizeof() = " << sizeof(nanovdb::GridBlindMetaData) << std::endl; + EXPECT_EQ(offset, sizeof(nanovdb::GridBlindMetaData)); + } { // check GridData memory alignment, total 672 bytes /* static const int MaxNameSize = 256;// due to NULL termination the maximum length is one less @@ -1847,8 +2169,8 @@ TEST_F(TestNanoVDB, Offsets) uint64_t mGridSize; // 8B. byte count of this entire grid occupied in the buffer. char mGridName[MaxNameSize]; // 256B Map mMap; // 264B. affine transformation between index and world space in both single and double precision - BBox mWorldBBox; // 48B. floating-point AABB of active values in WORLD SPACE (2 x 3 doubles) - Vec3R mVoxelSize; // 24B. size of a voxel in world units + BBox mWorldBBox; // 48B. floating-point AABB of active values in WORLD SPACE (2 x 3 doubles) + Vec3d mVoxelSize; // 24B. size of a voxel in world units GridClass mGridClass; // 4B. GridType mGridType; // 4B. int64_t mBlindMetadataOffset; // 8B. offset of GridBlindMetaData structures that follow this grid. @@ -1906,18 +2228,18 @@ TEST_F(TestNanoVDB, Offsets) uint64_t mVoxelCount;// 8B, total number of active voxels in the root and all its child nodes. */ int offset = 0; - EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::TreeData<>, mNodeOffset), offset); + EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::TreeData, mNodeOffset), offset); offset += 4*8; - EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::TreeData<>, mNodeCount), offset); + EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::TreeData, mNodeCount), offset); offset += 12; - EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::TreeData<>, mTileCount), offset); + EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::TreeData, mTileCount), offset); offset += 12; - EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::TreeData<>, mVoxelCount), offset); + EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::TreeData, mVoxelCount), offset); offset += 8; //std::cerr << "TreeData padding at end = " << (nanovdb::AlignUp(offset)-offset) << std::endl; offset = nanovdb::AlignUp(offset); //std::cerr << "TreeData: Offset = " << offset << std::endl; - EXPECT_EQ(offset, (int)sizeof(nanovdb::TreeData<>)); + EXPECT_EQ(offset, (int)sizeof(nanovdb::TreeData)); } } @@ -2156,10 +2478,44 @@ template<> void checkLeaf(int &offset) { using DataT = typename nanovdb::LeafNode::DataType; - EXPECT_EQ(NANOVDB_OFFSETOF(DataT, mStatsOff), offset); + EXPECT_EQ(NANOVDB_OFFSETOF(DataT, mOffset), offset); + offset += 8; + EXPECT_EQ(NANOVDB_OFFSETOF(DataT, mPrefixSum), offset); + offset += 8; +} + +template<> +void checkLeaf(int &offset) +{ + using DataT = typename nanovdb::LeafNode::DataType; + EXPECT_EQ(NANOVDB_OFFSETOF(DataT, mOffset), offset); + offset += 8; + EXPECT_EQ(NANOVDB_OFFSETOF(DataT, mPrefixSum), offset); + offset += 8; + EXPECT_EQ(NANOVDB_OFFSETOF(DataT, mMask), offset); + offset += 64; +} + +template<> +void checkLeaf(int &offset) +{ + using DataT = typename nanovdb::LeafNode::DataType; + EXPECT_EQ(NANOVDB_OFFSETOF(DataT, mOffset), offset); + offset += 8; + EXPECT_EQ(NANOVDB_OFFSETOF(DataT, mPrefixSum), offset); offset += 8; - EXPECT_EQ(NANOVDB_OFFSETOF(DataT, mValueOff), offset); +} + +template<> +void checkLeaf(int &offset) +{ + using DataT = typename nanovdb::LeafNode::DataType; + EXPECT_EQ(NANOVDB_OFFSETOF(DataT, mOffset), offset); offset += 8; + EXPECT_EQ(NANOVDB_OFFSETOF(DataT, mPrefixSum), offset); + offset += 8; + EXPECT_EQ(NANOVDB_OFFSETOF(DataT, mMask), offset); + offset += 64; } template<> @@ -2244,6 +2600,19 @@ void checkLeaf(int &offset) offset = nanovdb::AlignUp<32>(offset); } +template<> +void checkLeaf(int &offset) +{ + using DataT = typename nanovdb::LeafNode::DataType; + EXPECT_EQ(NANOVDB_OFFSETOF(DataT, mOffset), offset); + offset += sizeof(uint64_t); + EXPECT_EQ(NANOVDB_OFFSETOF(DataT, mPointCount), offset); + offset += sizeof(uint64_t); + offset = nanovdb::AlignUp<32>(offset); + EXPECT_EQ(NANOVDB_OFFSETOF(DataT, mValues), offset); + offset += (8*8*8)*sizeof(uint16_t); +} + TEST_F(TestNanoVDB, BasicGrid) { using LeafT = nanovdb::LeafNode; @@ -2390,26 +2759,31 @@ TEST_F(TestNanoVDB, BasicGrid) EXPECT_DOUBLE_EQ(expected, sum); } } - - data->setFlagsOff(); +#if 1 + nanovdb::Map map; + map.set(mat, invMat); + data->init({nanovdb::GridFlags::HasMinMax, nanovdb::GridFlags::IsBreadthFirst}, bytes[5], map, nanovdb::GridType::Float); +#else + data-> setFlagsOff(); data->setMinMaxOn(); data->mGridIndex = 0; data->mGridCount = 1; data->mBlindMetadataOffset = 0; data->mBlindMetadataCount = 0; - data->mVoxelSize = nanovdb::Vec3R(dx); + data->mVoxelSize = nanovdb::Vec3d(dx); data->mMap.set(mat, invMat, 1.0); data->mGridClass = nanovdb::GridClass::Unknown; data->mGridType = nanovdb::GridType::Float; data->mMagic = NANOVDB_MAGIC_NUMBER; data->mVersion = nanovdb::Version(); +#endif memcpy(data->mGridName, name.c_str(), name.size() + 1); } EXPECT_EQ(tree, &grid->tree()); - const nanovdb::Vec3R p1(1.0, 2.0, 3.0); + const nanovdb::Vec3d p1(1.0, 2.0, 3.0); const auto p2 = grid->worldToIndex(p1); - EXPECT_EQ(nanovdb::Vec3R(0.5, 1.0, 1.5), p2); + EXPECT_EQ(nanovdb::Vec3d(0.5, 1.0, 1.5), p2); const auto p3 = grid->indexToWorld(p2); EXPECT_EQ(p1, p3); { @@ -2434,7 +2808,7 @@ TEST_F(TestNanoVDB, BasicGrid) EXPECT_DOUBLE_EQ(expected, sum); } } - data->mVoxelSize = nanovdb::Vec3R(dx); + data->mVoxelSize = nanovdb::Vec3d(dx); data->mMap.set(mat, invMat, 1.0); } @@ -2442,7 +2816,7 @@ TEST_F(TestNanoVDB, BasicGrid) // Start actual tests auto const p4 = grid->worldToIndex(p3); - EXPECT_EQ(nanovdb::Vec3R(0.0, 0.0, 0.0), p4); + EXPECT_EQ(nanovdb::Vec3d(0.0, 0.0, 0.0), p4); const auto p5 = grid->indexToWorld(p4); EXPECT_EQ(p1, p5); } @@ -2566,9 +2940,10 @@ TEST_F(TestNanoVDB, BasicGrid) TEST_F(TestNanoVDB, GridBuilderEmpty) { { // empty grid - nanovdb::GridBuilder builder(0.0f); - auto srcAcc = builder.getAccessor(); - auto handle = builder.getHandle<>(1.0, nanovdb::Vec3d(0.0), "test"); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.0f, "test"); + auto srcAcc = srcGrid.getAccessor(); + auto handle = nanovdb::createNanoGrid(srcGrid); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); EXPECT_TRUE(meta); @@ -2579,6 +2954,7 @@ TEST_F(TestNanoVDB, GridBuilderEmpty) EXPECT_EQ(uint32_t(NANOVDB_MAJOR_VERSION_NUMBER), meta->version().getMajor()); EXPECT_EQ(uint32_t(NANOVDB_MINOR_VERSION_NUMBER), meta->version().getMinor()); EXPECT_EQ(uint32_t(NANOVDB_PATCH_VERSION_NUMBER), meta->version().getPatch()); + EXPECT_TRUE(meta->isBreadthFirst()); auto* dstGrid = handle.grid(); EXPECT_TRUE(dstGrid); EXPECT_EQ("test", std::string(dstGrid->gridName())); @@ -2587,22 +2963,80 @@ TEST_F(TestNanoVDB, GridBuilderEmpty) EXPECT_EQ(0.0f, srcAcc.getValue(nanovdb::Coord(1, 2, 3))); EXPECT_FALSE(srcAcc.isActive(nanovdb::Coord(1, 2, 3))); EXPECT_EQ(0.0f, dstAcc.getValue(nanovdb::Coord(1, 2, 3))); + EXPECT_TRUE(dstGrid->isEmpty()); + EXPECT_TRUE(dstGrid->tree().isEmpty()); + EXPECT_TRUE(dstGrid->tree().root().isEmpty()); + EXPECT_EQ(0u, dstGrid->tree().nodeCount(0)); + EXPECT_EQ(0u, dstGrid->tree().nodeCount(1)); + EXPECT_EQ(0u, dstGrid->tree().nodeCount(2)); + EXPECT_EQ(dstGrid->tree().root().minimum(), 0.0f); EXPECT_EQ(dstGrid->tree().root().maximum(), 0.0f); EXPECT_EQ(dstGrid->tree().root().average(), 0.0f); + EXPECT_EQ(dstGrid->tree().root().variance(), 0.0f); EXPECT_EQ(dstGrid->tree().root().stdDeviation(), 0.0f); } } // GridBuilderEmpty -TEST_F(TestNanoVDB, GridBuilderBasic1) +TEST_F(TestNanoVDB, BuilderGridEmpty) +{ + { // empty grid + using SrcGridT = nanovdb::build::Grid; + SrcGridT grid(0.0f, "test"); + auto srcAcc = grid.getAccessor(); + auto handle = nanovdb::createNanoGrid(grid); + EXPECT_TRUE(handle); + auto* meta = handle.gridMetaData(); + EXPECT_TRUE(meta); + EXPECT_TRUE(meta->isEmpty()); + EXPECT_EQ("test", std::string(meta->shortGridName())); + EXPECT_EQ(nanovdb::GridType::Float, meta->gridType()); + EXPECT_EQ(nanovdb::GridClass::Unknown, meta->gridClass()); + EXPECT_EQ(uint32_t(NANOVDB_MAJOR_VERSION_NUMBER), meta->version().getMajor()); + EXPECT_EQ(uint32_t(NANOVDB_MINOR_VERSION_NUMBER), meta->version().getMinor()); + EXPECT_EQ(uint32_t(NANOVDB_PATCH_VERSION_NUMBER), meta->version().getPatch()); + auto* dstGrid = handle.grid(); + EXPECT_TRUE(dstGrid); + EXPECT_EQ("test", std::string(dstGrid->gridName())); + EXPECT_EQ(0u, dstGrid->activeVoxelCount()); + auto dstAcc = dstGrid->getAccessor(); + EXPECT_EQ(0.0f, srcAcc.getValue(nanovdb::Coord(1, 2, 3))); + EXPECT_FALSE(srcAcc.isActive(nanovdb::Coord(1, 2, 3))); + EXPECT_EQ(0.0f, dstAcc.getValue(nanovdb::Coord(1, 2, 3))); + EXPECT_TRUE(dstGrid->isEmpty()); + EXPECT_TRUE(dstGrid->tree().isEmpty()); + EXPECT_TRUE(dstGrid->tree().root().isEmpty()); + EXPECT_EQ(0u, dstGrid->tree().nodeCount(0)); + EXPECT_EQ(0u, dstGrid->tree().nodeCount(1)); + EXPECT_EQ(0u, dstGrid->tree().nodeCount(2)); + + EXPECT_EQ(dstGrid->tree().root().minimum(), 0.0f); + EXPECT_EQ(dstGrid->tree().root().maximum(), 0.0f); + EXPECT_EQ(dstGrid->tree().root().average(), 0.0f); + + EXPECT_EQ(dstGrid->tree().root().variance(), 0.0f); + EXPECT_EQ(dstGrid->tree().root().stdDeviation(), 0.0f); + } +} // BuilderGridEmpty + +// make -j 6 testNanoVDB && ./unittest/testNanoVDB --gtest_filter="*CreateNanoGrid_Basic1" --gtest_break_on_failure +TEST_F(TestNanoVDB, CreateNanoGrid_Basic1) { { // 1 grid point - nanovdb::GridBuilder builder(0.0f); - auto srcAcc = builder.getAccessor(); - srcAcc.setValue(nanovdb::Coord(1, 2, 3), 1.0f); - EXPECT_EQ(1.0f, srcAcc.getValue(nanovdb::Coord(1, 2, 3))); - auto handle = builder.getHandle<>(); + using SrcGridT = nanovdb::build::Grid; + const nanovdb::Coord ijk(1,2,3); + SrcGridT grid(0.0f); + auto srcAcc = grid.getAccessor(); + srcAcc.setValue(ijk, 1.0f); + auto nodeCount = grid.nodeCount(); + EXPECT_EQ(1u, nodeCount[0]); + EXPECT_EQ(1u, nodeCount[1]); + EXPECT_EQ(1u, nodeCount[2]); + EXPECT_EQ(1.0f, srcAcc.getValue(ijk)); + EXPECT_EQ(1.0f, srcAcc.getValue(1,2,3)); + + auto handle = nanovdb::createNanoGrid(grid); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); EXPECT_TRUE(meta); @@ -2616,11 +3050,12 @@ TEST_F(TestNanoVDB, GridBuilderBasic1) auto* dstGrid = handle.grid(); EXPECT_TRUE(dstGrid); EXPECT_EQ("", std::string(dstGrid->gridName())); - EXPECT_EQ(nanovdb::Vec3R(1.0), dstGrid->voxelSize()); - //EXPECT_EQ(1u, dstGrid->activeVoxelCount()); + EXPECT_EQ(nanovdb::Vec3d(1.0), dstGrid->voxelSize()); + EXPECT_EQ(1u, dstGrid->activeVoxelCount()); + EXPECT_EQ(1.0f, dstGrid->tree().getValue(ijk)); auto dstAcc = dstGrid->getAccessor(); - EXPECT_EQ(1.0f, dstAcc.getValue(nanovdb::Coord(1, 2, 3))); - EXPECT_TRUE(srcAcc.isActive(nanovdb::Coord(1, 2, 3))); + EXPECT_EQ(1.0f, dstAcc.getValue(ijk)); + EXPECT_TRUE(srcAcc.isActive(ijk)); EXPECT_EQ(nanovdb::Coord(1, 2, 3), dstGrid->indexBBox()[0]); EXPECT_EQ(nanovdb::Coord(1, 2, 3), dstGrid->indexBBox()[1]); EXPECT_EQ(dstGrid->tree().root().minimum(), 1.0f);// minimum active value @@ -2628,18 +3063,145 @@ TEST_F(TestNanoVDB, GridBuilderBasic1) EXPECT_NEAR(dstGrid->tree().root().average(), 1.0f, 1e-6); EXPECT_NEAR(dstGrid->tree().root().variance(), 0.0f,1e-6); EXPECT_NEAR(dstGrid->tree().root().stdDeviation(), 0.0f, 1e-6); + EXPECT_FALSE(dstGrid->isEmpty()); + EXPECT_FALSE(dstGrid->tree().isEmpty()); + EXPECT_FALSE(dstGrid->tree().root().isEmpty()); + EXPECT_EQ(1u, dstGrid->tree().nodeCount(0)); + EXPECT_EQ(1u, dstGrid->tree().nodeCount(1)); + EXPECT_EQ(1u, dstGrid->tree().nodeCount(2)); } } // GridBuilderBasic1 +TEST_F(TestNanoVDB, CreateNanoGrid_addTile) +{ + { // 1 grid point and 1 tile + using SrcGridT = nanovdb::build::Grid; + const nanovdb::Coord ijk(1,2,3); + SrcGridT grid(0.0f); + auto srcAcc = grid.getAccessor(); + srcAcc.setValue(ijk, 1.0f); + + const nanovdb::Coord ijk2(-1,-2,-3); + grid.tree().root().addTile<1>(ijk2, 2.0f, true); + + auto nodeCount = grid.nodeCount(); + EXPECT_EQ(1u, nodeCount[0]); + EXPECT_EQ(2u, nodeCount[1]); + EXPECT_EQ(2u, nodeCount[2]); + EXPECT_EQ(1.0f, srcAcc.getValue(ijk)); + EXPECT_EQ(1.0f, srcAcc.getValue(1,2,3)); + EXPECT_EQ(2.0f, srcAcc.getValue(ijk2)); + EXPECT_EQ(2.0f, srcAcc.getValue(-1,-2,-3)); + + auto handle = nanovdb::createNanoGrid(grid); + EXPECT_TRUE(handle); + auto* meta = handle.gridMetaData(); + EXPECT_TRUE(meta); + EXPECT_FALSE(meta->isEmpty()); + EXPECT_EQ(uint32_t(NANOVDB_MAJOR_VERSION_NUMBER), meta->version().getMajor()); + EXPECT_EQ(uint32_t(NANOVDB_MINOR_VERSION_NUMBER), meta->version().getMinor()); + EXPECT_EQ(uint32_t(NANOVDB_PATCH_VERSION_NUMBER), meta->version().getPatch()); + EXPECT_EQ("", std::string(meta->shortGridName())); + EXPECT_EQ(nanovdb::GridType::Float, meta->gridType()); + EXPECT_EQ(nanovdb::GridClass::Unknown, meta->gridClass()); + auto* dstGrid = handle.grid(); + EXPECT_TRUE(dstGrid); + EXPECT_EQ("", std::string(dstGrid->gridName())); + EXPECT_EQ(nanovdb::Vec3d(1.0), dstGrid->voxelSize()); + EXPECT_EQ(128u * 128u * 128u + 1u, dstGrid->activeVoxelCount()); + EXPECT_EQ(1.0f, dstGrid->tree().getValue(ijk)); + auto dstAcc = dstGrid->getAccessor(); + EXPECT_EQ(1.0f, dstAcc.getValue(ijk)); + EXPECT_EQ(2.0f, dstAcc.getValue(ijk2)); + EXPECT_TRUE(srcAcc.isActive(ijk)); + EXPECT_EQ(nanovdb::Coord(-128, -128, -128), dstGrid->indexBBox()[0]); + EXPECT_EQ(nanovdb::Coord(1, 2, 3), dstGrid->indexBBox()[1]); + EXPECT_EQ(dstGrid->tree().root().minimum(), 1.0f);// minimum active value + EXPECT_EQ(dstGrid->tree().root().maximum(), 2.0f);// maximum active value + EXPECT_NEAR(dstGrid->tree().root().average(), 1.999999f, 1e-6);// 1 of 1.0 and 128*128*128 of 2.0 + EXPECT_NEAR(dstGrid->tree().root().variance(), 0.0f,1e-6); + EXPECT_NEAR(dstGrid->tree().root().stdDeviation(), 0.00069f, 1e-6); + } +} // CreateNanoGrid_addTile + +TEST_F(TestNanoVDB, GridBuilderValueMask) +{ + { // 1 grid point + using SrcGridT = nanovdb::build::Grid; + const nanovdb::Coord ijk(1,2,3); + SrcGridT grid(false); + auto srcAcc = grid.getAccessor(); + srcAcc.setValue(ijk, true); + auto nodeCount = grid.nodeCount(); + EXPECT_EQ(1u, nodeCount[0]); + EXPECT_EQ(1u, nodeCount[1]); + EXPECT_EQ(1u, nodeCount[2]); + EXPECT_EQ(true, srcAcc.getValue(ijk)); + auto handle = nanovdb::createNanoGrid(grid); + EXPECT_TRUE(handle); + auto* meta = handle.gridMetaData(); + EXPECT_TRUE(meta); + EXPECT_FALSE(meta->isEmpty()); + EXPECT_EQ(uint32_t(NANOVDB_MAJOR_VERSION_NUMBER), meta->version().getMajor()); + EXPECT_EQ(uint32_t(NANOVDB_MINOR_VERSION_NUMBER), meta->version().getMinor()); + EXPECT_EQ(uint32_t(NANOVDB_PATCH_VERSION_NUMBER), meta->version().getPatch()); + EXPECT_EQ("", std::string(meta->shortGridName())); + EXPECT_EQ(nanovdb::GridType::Mask, meta->gridType()); + EXPECT_EQ(nanovdb::GridClass::Topology, meta->gridClass()); + auto* dstGrid = handle.grid(); + EXPECT_TRUE(dstGrid); + EXPECT_EQ("", std::string(dstGrid->gridName())); + EXPECT_EQ(nanovdb::Vec3d(1.0), dstGrid->voxelSize()); + EXPECT_EQ(1u, dstGrid->activeVoxelCount()); + auto dstAcc = dstGrid->getAccessor(); + EXPECT_EQ(false, dstAcc.getValue(nanovdb::Coord(1, 2, 2))); + EXPECT_EQ(true, dstAcc.getValue(ijk)); + EXPECT_EQ(false, dstAcc.getValue(nanovdb::Coord(0, 2, 2))); + EXPECT_TRUE( srcAcc.isActive(ijk)); + EXPECT_FALSE(srcAcc.isActive(nanovdb::Coord(2, 2, 3))); + EXPECT_EQ(ijk, dstGrid->indexBBox()[0]); + EXPECT_EQ(ijk, dstGrid->indexBBox()[1]); + EXPECT_FALSE(dstGrid->isEmpty()); + EXPECT_FALSE(dstGrid->tree().isEmpty()); + EXPECT_FALSE(dstGrid->tree().root().isEmpty()); + EXPECT_EQ(1u, dstGrid->tree().nodeCount(0)); + EXPECT_EQ(1u, dstGrid->tree().nodeCount(1)); + EXPECT_EQ(1u, dstGrid->tree().nodeCount(2)); + //EXPECT_EQ(dstGrid->tree().root().minimum(), false);// minimum active value + //EXPECT_EQ(dstGrid->tree().root().maximum(), true);// maximum active value + //EXPECT_NEAR(dstGrid->tree().root().average(), 1.0f, 1e-6); + //EXPECT_NEAR(dstGrid->tree().root().variance(), 0.0f,1e-6); + //EXPECT_NEAR(dstGrid->tree().root().stdDeviation(), 0.0f, 1e-6); + } +} // GridBuilderValueMask + TEST_F(TestNanoVDB, GridBuilderBasic2) { { // 2 grid points - nanovdb::GridBuilder builder(0.0f); - auto srcAcc = builder.getAccessor(); - srcAcc.setValue(nanovdb::Coord(1, 2, 3), 1.0f); - srcAcc.setValue(nanovdb::Coord(2, -2, 9),-1.0f); - //srcAcc.setValue(nanovdb::Coord(20,-20,90), 0.0f);// same as background - auto handle = builder.getHandle<>(1.0, nanovdb::Vec3d(0.0), "test"); + using SrcGridT = nanovdb::build::Grid; + SrcGridT grid(0.0f, "test"); + auto srcAcc = grid.getAccessor(); + const nanovdb::Coord ijk1(1,2,3), ijk2(2,-2,9); + srcAcc.setValue(ijk1, 1.0f); + srcAcc.setValue(ijk2, -1.0f); + EXPECT_EQ( 1.0f, srcAcc.getValue(ijk1)); + EXPECT_EQ(-1.0f, srcAcc.getValue(ijk2)); + auto nodeCount = grid.nodeCount(); + EXPECT_EQ(2u, nodeCount[0]); + EXPECT_EQ(2u, nodeCount[1]); + EXPECT_EQ(2u, nodeCount[2]); + + nanovdb::build::NodeManager srcMgr(grid); + EXPECT_EQ(2u, srcMgr.nodeCount(0)); + EXPECT_EQ(2u, srcMgr.nodeCount(1)); + EXPECT_EQ(2u, srcMgr.nodeCount(2)); + EXPECT_EQ(-1.0f, srcMgr.node<0>(0).getValue(ijk2)); + EXPECT_EQ( 1.0f, srcMgr.node<0>(1).getValue(ijk1)); + //for (int i=0;igridName())); + EXPECT_FALSE(dstGrid->isEmpty()); + EXPECT_FALSE(dstGrid->tree().isEmpty()); + EXPECT_FALSE(dstGrid->tree().root().isEmpty()); EXPECT_EQ(2u, dstGrid->activeVoxelCount()); + EXPECT_EQ(2u, dstGrid->tree().nodeCount(0)); + EXPECT_EQ(2u, dstGrid->tree().nodeCount(1)); + EXPECT_EQ(2u, dstGrid->tree().nodeCount(2)); + auto *dstLeaf = dstGrid->tree().getFirstNode<0>(); + EXPECT_EQ(1u, (dstLeaf+0)->getValueMask().countOn()); + EXPECT_EQ(1u, (dstLeaf+1)->getValueMask().countOn()); + EXPECT_EQ(-1.0f, (dstLeaf+0)->getValue(ijk2)); + EXPECT_EQ( 1.0f, (dstLeaf+1)->getValue(ijk1)); + auto *dstLower = dstGrid->tree().getFirstNode<1>(); + EXPECT_EQ(1u, (dstLower+0)->getChildMask().countOn()); + EXPECT_EQ(1u, (dstLower+1)->getChildMask().countOn()); + EXPECT_EQ(-1.0f, (dstLower+0)->getValue(ijk2)); + EXPECT_EQ( 1.0f, (dstLower+1)->getValue(ijk1)); + auto *dstUpper = dstGrid->tree().getFirstNode<2>(); + EXPECT_EQ(1u, (dstUpper+0)->getChildMask().countOn()); + EXPECT_EQ(1u, (dstUpper+1)->getChildMask().countOn()); + EXPECT_EQ(-1.0f, (dstUpper+0)->getValue(ijk2)); + EXPECT_EQ( 1.0f, (dstUpper+1)->getValue(ijk1)); + + EXPECT_EQ(-1.0f, dstGrid->tree().getValue(ijk2)); + EXPECT_EQ( 1.0f, dstGrid->tree().getValue(ijk1)); + auto dstAcc = dstGrid->getAccessor(); - EXPECT_EQ( 1.0f, dstAcc.getValue(nanovdb::Coord(1, 2, 3))); - EXPECT_EQ(-1.0f, dstAcc.getValue(nanovdb::Coord(2, -2, 9))); + EXPECT_EQ(-1.0f, dstAcc.getValue(ijk2)); + EXPECT_EQ( 1.0f, dstAcc.getValue(ijk1)); - const nanovdb::BBox indexBBox = dstGrid->indexBBox(); + const nanovdb::BBox indexBBox = dstGrid->indexBBox(); EXPECT_DOUBLE_EQ( 1.0, indexBBox[0][0]); EXPECT_DOUBLE_EQ(-2.0, indexBBox[0][1]); EXPECT_DOUBLE_EQ( 3.0, indexBBox[0][2]); @@ -2668,7 +3255,7 @@ TEST_F(TestNanoVDB, GridBuilderBasic2) EXPECT_DOUBLE_EQ(10.0, indexBBox[1][2]); EXPECT_EQ(nanovdb::Coord(1, -2, 3), dstGrid->indexBBox()[0]); - EXPECT_EQ(nanovdb::Coord(2, 2, 9), dstGrid->indexBBox()[1]); + EXPECT_EQ(nanovdb::Coord(2, 2, 9), dstGrid->indexBBox()[1]); EXPECT_EQ(dstGrid->tree().root().minimum(),-1.0f); EXPECT_EQ(dstGrid->tree().root().maximum(), 1.0f); @@ -2681,18 +3268,18 @@ TEST_F(TestNanoVDB, GridBuilderBasic2) TEST_F(TestNanoVDB, GridBuilderPrune) { { - nanovdb::GridBuilder builder(0.0f); - auto srcAcc = builder.getAccessor(); - const nanovdb::CoordBBox bbox(nanovdb::Coord(0), nanovdb::Coord(8*16-1)); - auto func = [](const nanovdb::Coord&) { return 1.0f; }; - //auto func = [](const nanovdb::Coord&, float &v) { v = 1.0f; return true; }; - builder(func, bbox); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.0f, "test"); + auto srcAcc = srcGrid.getAccessor(); + const nanovdb::CoordBBox bbox(nanovdb::Coord(0), nanovdb::Coord(8*16-1)); + auto func = [](const nanovdb::Coord&) { return 1.0f; }; + srcGrid(func, bbox); + for (auto ijk = bbox.begin(); ijk; ++ijk) { EXPECT_EQ(1.0f, srcAcc.getValue(*ijk)); EXPECT_TRUE(srcAcc.isActive(*ijk)); } - - auto handle = builder.getHandle<>(1.0, nanovdb::Vec3d(0.0), "test"); + auto handle = nanovdb::createNanoGrid(srcGrid); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); EXPECT_TRUE(meta); @@ -2719,7 +3306,7 @@ TEST_F(TestNanoVDB, GridBuilderPrune) } EXPECT_EQ( 0.0f, dstAcc.getValue(nanovdb::Coord(2, -2, 9))); - const nanovdb::BBox indexBBox = dstGrid->indexBBox(); + const nanovdb::BBox indexBBox = dstGrid->indexBBox(); EXPECT_DOUBLE_EQ( 0.0, indexBBox[0][0]); EXPECT_DOUBLE_EQ( 0.0, indexBBox[0][1]); EXPECT_DOUBLE_EQ( 0.0, indexBBox[0][2]); @@ -2730,6 +3317,9 @@ TEST_F(TestNanoVDB, GridBuilderPrune) EXPECT_EQ(nanovdb::Coord(0), dstGrid->indexBBox()[0]); EXPECT_EQ(nanovdb::Coord(8*16-1), dstGrid->indexBBox()[1]); + EXPECT_FALSE(dstGrid->isEmpty()); + EXPECT_FALSE(dstGrid->tree().isEmpty()); + EXPECT_FALSE(dstGrid->tree().root().isEmpty()); EXPECT_EQ(0u, dstGrid->tree().nodeCount(0));// all pruned away EXPECT_EQ(0u, dstGrid->tree().nodeCount(1));// all pruned away EXPECT_EQ(1u, dstGrid->tree().nodeCount(2)); @@ -2747,8 +3337,9 @@ TEST_F(TestNanoVDB, GridBuilder_Vec3f) using VoxelT = nanovdb::Vec3f; EXPECT_EQ(nanovdb::AlignUp(12 + 3 + 1 + 2*4 + 64 + 3*(2*4 + 512*4)), sizeof(nanovdb::NanoLeaf)); { // 3 grid point - nanovdb::GridBuilder builder(nanovdb::Vec3f(0.0f)); - auto srcAcc = builder.getAccessor(); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(VoxelT(0.0f)); + auto srcAcc = srcGrid.getAccessor(); srcAcc.setValue(nanovdb::Coord( 1, 2, 3), nanovdb::Vec3f(1.0f)); srcAcc.setValue(nanovdb::Coord(-10, 20,-50), nanovdb::Vec3f(2.0f)); srcAcc.setValue(nanovdb::Coord( 50,-12, 30), nanovdb::Vec3f(3.0f)); @@ -2758,9 +3349,7 @@ TEST_F(TestNanoVDB, GridBuilder_Vec3f) EXPECT_EQ(nanovdb::Vec3f(2.0f), srcAcc.getValue(nanovdb::Coord(-10, 20,-50))); EXPECT_EQ(nanovdb::Vec3f(3.0f), srcAcc.getValue(nanovdb::Coord( 50,-12, 30))); - builder.setStats(nanovdb::StatsMode::All); - auto handle = builder.getHandle(); - + auto handle = nanovdb::createNanoGrid(srcGrid, nanovdb::StatsMode::All); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); EXPECT_TRUE(meta); @@ -2789,7 +3378,7 @@ TEST_F(TestNanoVDB, GridBuilder_Vec3f) EXPECT_TRUE(dstGrid->isSequential<1>()); EXPECT_TRUE(dstGrid->isSequential<0>()); - EXPECT_EQ(nanovdb::Vec3R(1.0), dstGrid->voxelSize()); + EXPECT_EQ(nanovdb::Vec3d(1.0), dstGrid->voxelSize()); auto *leaf = dstGrid->tree().root().probeLeaf(nanovdb::Coord(1, 2, 3)); EXPECT_TRUE(leaf); //std::cerr << leaf->origin() << ", " << leaf->data()->mBBoxMin << std::endl; @@ -2813,8 +3402,9 @@ TEST_F(TestNanoVDB, GridBuilder_Vec4f) using VoxelT = nanovdb::Vec4f; EXPECT_EQ(nanovdb::AlignUp(12 + 3 + 1 + 2*4 + 64 + 4*(2*4 + 512*4)), sizeof(nanovdb::NanoLeaf)); { // 3 grid point - nanovdb::GridBuilder builder(nanovdb::Vec4f(0.0f)); - auto srcAcc = builder.getAccessor(); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(VoxelT(0.0f)); + auto srcAcc = srcGrid.getAccessor(); srcAcc.setValue(nanovdb::Coord( 1, 2, 3), nanovdb::Vec4f(1.0f)); srcAcc.setValue(nanovdb::Coord(-10, 20,-50), nanovdb::Vec4f(2.0f)); srcAcc.setValue(nanovdb::Coord( 50,-12, 30), nanovdb::Vec4f(3.0f)); @@ -2824,9 +3414,7 @@ TEST_F(TestNanoVDB, GridBuilder_Vec4f) EXPECT_EQ(nanovdb::Vec4f(2.0f), srcAcc.getValue(nanovdb::Coord(-10, 20,-50))); EXPECT_EQ(nanovdb::Vec4f(3.0f), srcAcc.getValue(nanovdb::Coord( 50,-12, 30))); - builder.setStats(nanovdb::StatsMode::All); - auto handle = builder.getHandle(); - + auto handle = nanovdb::createNanoGrid(srcGrid, nanovdb::StatsMode::All); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); EXPECT_TRUE(meta); @@ -2855,7 +3443,7 @@ TEST_F(TestNanoVDB, GridBuilder_Vec4f) EXPECT_TRUE(dstGrid->isSequential<1>()); EXPECT_TRUE(dstGrid->isSequential<0>()); - EXPECT_EQ(nanovdb::Vec3R(1.0), dstGrid->voxelSize()); + EXPECT_EQ(nanovdb::Vec3d(1.0), dstGrid->voxelSize()); auto *leaf = dstGrid->tree().root().probeLeaf(nanovdb::Coord(1, 2, 3)); EXPECT_TRUE(leaf); //std::cerr << leaf->origin() << ", " << leaf->data()->mBBoxMin << std::endl; @@ -2874,14 +3462,14 @@ TEST_F(TestNanoVDB, GridBuilder_Vec4f) } } // GridBuilder_Vec4f - TEST_F(TestNanoVDB, GridBuilder_Fp4) { using VoxelT = nanovdb::Fp4; EXPECT_EQ(96u + 512u/2, sizeof(nanovdb::NanoLeaf)); { // 3 grid point - nanovdb::GridBuilder builder(0.0f); - auto srcAcc = builder.getAccessor(); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.0f); + auto srcAcc = srcGrid.getAccessor(); srcAcc.setValue(nanovdb::Coord( 1, 2, 3), 1.0f); srcAcc.setValue(nanovdb::Coord(-10, 20,-50), 2.0f); srcAcc.setValue(nanovdb::Coord( 50,-12, 30), 3.0f); @@ -2891,9 +3479,7 @@ TEST_F(TestNanoVDB, GridBuilder_Fp4) EXPECT_EQ(2.0f, srcAcc.getValue(nanovdb::Coord(-10, 20,-50))); EXPECT_EQ(3.0f, srcAcc.getValue(nanovdb::Coord( 50,-12, 30))); - builder.setStats(nanovdb::StatsMode::All); - auto handle = builder.getHandle(); - + auto handle = nanovdb::createNanoGrid(srcGrid, nanovdb::StatsMode::All); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); EXPECT_TRUE(meta); @@ -2908,10 +3494,13 @@ TEST_F(TestNanoVDB, GridBuilder_Fp4) EXPECT_TRUE(dstGrid); EXPECT_EQ("", std::string(dstGrid->gridName())); EXPECT_EQ((const char*)handle.data(), (const char*)dstGrid); + EXPECT_TRUE(dstGrid->isBreadthFirst()); + EXPECT_EQ(1.0f, dstGrid->tree().getValue(nanovdb::Coord( 1, 2, 3))); + EXPECT_EQ(2.0f, dstGrid->tree().getValue(nanovdb::Coord(-10, 20,-50))); + EXPECT_EQ(3.0f, dstGrid->tree().getValue(nanovdb::Coord( 50,-12, 30))); EXPECT_EQ(1.0f, dstGrid->tree().root().minimum()); EXPECT_EQ(3.0f, dstGrid->tree().root().maximum()); EXPECT_EQ(2.0f, dstGrid->tree().root().average()); - EXPECT_TRUE(dstGrid->isBreadthFirst()); using GridT = std::remove_pointer::type; EXPECT_TRUE(dstGrid->isSequential()); EXPECT_TRUE(dstGrid->isSequential()); @@ -2920,7 +3509,7 @@ TEST_F(TestNanoVDB, GridBuilder_Fp4) EXPECT_TRUE(dstGrid->isSequential<1>()); EXPECT_TRUE(dstGrid->isSequential<0>()); - EXPECT_EQ(nanovdb::Vec3R(1.0), dstGrid->voxelSize()); + EXPECT_EQ(nanovdb::Vec3d(1.0), dstGrid->voxelSize()); auto *leaf = dstGrid->tree().root().probeLeaf(nanovdb::Coord(1, 2, 3)); EXPECT_TRUE(leaf); //std::cerr << leaf->origin() << ", " << leaf->data()->mBBoxMin << std::endl; @@ -2941,20 +3530,20 @@ TEST_F(TestNanoVDB, GridBuilder_Fp4) EXPECT_EQ(nanovdb::Coord(-10,-12,-50), dstGrid->indexBBox()[0]); EXPECT_EQ(nanovdb::Coord( 50, 20, 30), dstGrid->indexBBox()[1]); - auto mgrHandle = nanovdb::createNodeManager(*dstGrid); + auto mgrHandle = nanovdb::createNodeManager(*dstGrid); auto *nodeMgr = mgrHandle.mgr(); EXPECT_TRUE(nanovdb::isValid(nodeMgr)); EXPECT_TRUE(nodeMgr->isLinear()); uint64_t n[3]={0}; - for (auto it2 = dstGrid->tree().root().beginChild(); it2; ++it2) { + for (auto it2 = dstGrid->tree().root().cbeginChild(); it2; ++it2) { auto *node2 = &nodeMgr->upper(n[0]++); EXPECT_TRUE(nanovdb::isValid(node2)); EXPECT_EQ(&*it2, node2); - for (auto it1 = it2->beginChild(); it1; ++it1) { + for (auto it1 = it2->cbeginChild(); it1; ++it1) { auto *node1 = &nodeMgr->lower(n[1]++); EXPECT_TRUE(nanovdb::isValid(node1)); EXPECT_EQ(&*it1, node1); - for (auto it0 = it1->beginChild(); it0; ++it0) { + for (auto it0 = it1->cbeginChild(); it0; ++it0) { auto *node0 = &nodeMgr->leaf(n[2]++); EXPECT_TRUE(nanovdb::isValid(node0)); EXPECT_EQ(&*it0, node0); @@ -2966,22 +3555,18 @@ TEST_F(TestNanoVDB, GridBuilder_Fp4) EXPECT_EQ(dstGrid->tree().nodeCount(2), n[0]); } {// Sphere - const double voxelSize = 0.1, halfWidth = 3.0; - const float radius = 10.0f; - const nanovdb::Vec3f center(0); - const nanovdb::Vec3d origin(0); + const double voxelSize = 0.1, halfWidth = 3.0, radius = 10.0f; + const nanovdb::Vec3d center(0), origin(0); const float tolerance = 0.5f * voxelSize; - auto handle = nanovdb::createLevelSetSphere(radius, center, - voxelSize, halfWidth, - origin, "sphere", - nanovdb::StatsMode::Default, - nanovdb::ChecksumMode::Default, - tolerance, - false); + auto handle = nanovdb::createLevelSetSphere(radius, center, + voxelSize, halfWidth, + origin, "sphere", + nanovdb::StatsMode::Default, + nanovdb::ChecksumMode::Default); auto* nanoGrid = handle.grid(); EXPECT_TRUE(nanoGrid); - Sphere sphere(center, radius, float(voxelSize), float(halfWidth)); + Sphere sphere(center, radius, voxelSize, halfWidth); auto kernel = [&](const nanovdb::CoordBBox& bbox) { auto nanoAcc = nanoGrid->getAccessor(); for (auto it = bbox.begin(); it; ++it) { @@ -2992,10 +3577,24 @@ TEST_F(TestNanoVDB, GridBuilder_Fp4) nanovdb::forEach(nanoGrid->indexBBox(), kernel); nanovdb::io::writeGrid("data/sphere_fp4.nvdb", handle); - handle = nanovdb::io::readGrid("data/sphere_fp4.nvdb"); + ASSERT_THROW(nanovdb::io::readGrid("data/sphere_fp4.nvdb", 1), std::runtime_error); + //nanovdb::CpuTimer timer; + //timer.start("read all grids"); + //handle = nanovdb::io::readGrid("data/sphere_fp4.nvdb"); + //timer.start("read first grid"); + handle = nanovdb::io::readGrid("data/sphere_fp4.nvdb", 0); + //timer.stop(); nanoGrid = handle.grid(); EXPECT_TRUE(nanoGrid); + nanovdb::forEach(nanoGrid->indexBBox(), kernel); + //timer.start("read first grid"); + //handle = nanovdb::io::readGrid("data/sphere_fp4.nvdb", 0); + //timer.start("read all grids"); + handle = nanovdb::io::readGrid("data/sphere_fp4.nvdb"); + //timer.stop(); + nanoGrid = handle.grid(); + EXPECT_TRUE(nanoGrid); nanovdb::forEach(nanoGrid->indexBBox(), kernel); } } // GridBuilder_Fp4 @@ -3005,8 +3604,10 @@ TEST_F(TestNanoVDB, GridBuilder_Fp8) using VoxelT = nanovdb::Fp8; EXPECT_EQ(96u + 512u, sizeof(nanovdb::NanoLeaf)); { // 3 grid point - nanovdb::GridBuilder builder(0.0f); - auto srcAcc = builder.getAccessor(); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.0f); + auto srcAcc = srcGrid.getAccessor(); + srcAcc.setValue(nanovdb::Coord( 1, 2, 3), 1.0f); srcAcc.setValue(nanovdb::Coord(-10, 20,-50), 2.0f); srcAcc.setValue(nanovdb::Coord( 50,-12, 30), 3.0f); @@ -3016,9 +3617,7 @@ TEST_F(TestNanoVDB, GridBuilder_Fp8) EXPECT_EQ(2.0f, srcAcc.getValue(nanovdb::Coord(-10, 20,-50))); EXPECT_EQ(3.0f, srcAcc.getValue(nanovdb::Coord( 50,-12, 30))); - builder.setStats(nanovdb::StatsMode::All); - auto handle = builder.getHandle(); - + auto handle = nanovdb::createNanoGrid(srcGrid, nanovdb::StatsMode::All); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); EXPECT_TRUE(meta); @@ -3045,7 +3644,7 @@ TEST_F(TestNanoVDB, GridBuilder_Fp8) EXPECT_TRUE(dstGrid->isSequential<1>()); EXPECT_TRUE(dstGrid->isSequential<0>()); - EXPECT_EQ(nanovdb::Vec3R(1.0), dstGrid->voxelSize()); + EXPECT_EQ(nanovdb::Vec3d(1.0), dstGrid->voxelSize()); auto *leaf = dstGrid->tree().root().probeLeaf(nanovdb::Coord(1, 2, 3)); EXPECT_TRUE(leaf); //std::cerr << leaf->origin() << ", " << leaf->data()->mBBoxMin << std::endl; @@ -3071,15 +3670,15 @@ TEST_F(TestNanoVDB, GridBuilder_Fp8) EXPECT_TRUE(nanovdb::isValid(nodeMgr)); EXPECT_TRUE(nodeMgr->isLinear()); uint64_t n[3]={0}; - for (auto it2 = dstGrid->tree().root().beginChild(); it2; ++it2) { + for (auto it2 = dstGrid->tree().root().cbeginChild(); it2; ++it2) { auto *node2 = &nodeMgr->upper(n[0]++); EXPECT_TRUE(nanovdb::isValid(node2)); EXPECT_EQ(&*it2, node2); - for (auto it1 = it2->beginChild(); it1; ++it1) { + for (auto it1 = it2->cbeginChild(); it1; ++it1) { auto *node1 = &nodeMgr->lower(n[1]++); EXPECT_TRUE(nanovdb::isValid(node1)); EXPECT_EQ(&*it1, node1); - for (auto it0 = it1->beginChild(); it0; ++it0) { + for (auto it0 = it1->cbeginChild(); it0; ++it0) { auto *node0 = &nodeMgr->leaf(n[2]++); EXPECT_TRUE(nanovdb::isValid(node0)); EXPECT_EQ(&*it0, node0); @@ -3091,19 +3690,15 @@ TEST_F(TestNanoVDB, GridBuilder_Fp8) EXPECT_EQ(dstGrid->tree().nodeCount(2), n[0]); } {// Sphere - const double voxelSize = 0.1, halfWidth = 3.0; - const float radius = 10.0f; - const nanovdb::Vec3f center(0); - const nanovdb::Vec3d origin(0); + const double voxelSize = 0.1, halfWidth = 3.0, radius = 10.0f; + const nanovdb::Vec3d center(0), origin(0); const float tolerance = 0.05f * voxelSize; - auto handle = nanovdb::createLevelSetSphere(radius, center, - voxelSize, halfWidth, - origin, "sphere", - nanovdb::StatsMode::Default, - nanovdb::ChecksumMode::Default, - tolerance, - false); + auto handle = nanovdb::createLevelSetSphere(radius, center, + voxelSize, halfWidth, + origin, "sphere", + nanovdb::StatsMode::Default, + nanovdb::ChecksumMode::Default); auto* nanoGrid = handle.grid(); EXPECT_TRUE(nanoGrid); Sphere sphere(center, radius, float(voxelSize), float(halfWidth)); @@ -3130,8 +3725,9 @@ TEST_F(TestNanoVDB, GridBuilder_Fp16) using VoxelT = nanovdb::Fp16; EXPECT_EQ(96u + 512u*2, sizeof(nanovdb::NanoLeaf)); { // 3 grid point - nanovdb::GridBuilder builder(0.0f); - auto srcAcc = builder.getAccessor(); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.0f); + auto srcAcc = srcGrid.getAccessor(); srcAcc.setValue(nanovdb::Coord( 1, 2, 3), 1.0f); srcAcc.setValue(nanovdb::Coord(-10, 20,-50), 2.0f); srcAcc.setValue(nanovdb::Coord( 50,-12, 30), 3.0f); @@ -3141,9 +3737,7 @@ TEST_F(TestNanoVDB, GridBuilder_Fp16) EXPECT_EQ(2.0f, srcAcc.getValue(nanovdb::Coord(-10, 20,-50))); EXPECT_EQ(3.0f, srcAcc.getValue(nanovdb::Coord( 50,-12, 30))); - builder.setStats(nanovdb::StatsMode::All); - auto handle = builder.getHandle(); - + auto handle = nanovdb::createNanoGrid(srcGrid, nanovdb::StatsMode::All); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); EXPECT_TRUE(meta); @@ -3170,7 +3764,7 @@ TEST_F(TestNanoVDB, GridBuilder_Fp16) EXPECT_TRUE(dstGrid->isSequential<1>()); EXPECT_TRUE(dstGrid->isSequential<0>()); - EXPECT_EQ(nanovdb::Vec3R(1.0), dstGrid->voxelSize()); + EXPECT_EQ(nanovdb::Vec3d(1.0), dstGrid->voxelSize()); auto *leaf = dstGrid->tree().root().probeLeaf(nanovdb::Coord(1, 2, 3)); EXPECT_TRUE(leaf); //std::cerr << leaf->origin() << ", " << leaf->data()->mBBoxMin << std::endl; @@ -3196,15 +3790,15 @@ TEST_F(TestNanoVDB, GridBuilder_Fp16) EXPECT_TRUE(nanovdb::isValid(nodeMgr)); EXPECT_TRUE(nodeMgr->isLinear()); uint64_t n[3]={0}; - for (auto it2 = dstGrid->tree().root().beginChild(); it2; ++it2) { + for (auto it2 = dstGrid->tree().root().cbeginChild(); it2; ++it2) { auto *node2 = &nodeMgr->upper(n[0]++); EXPECT_TRUE(nanovdb::isValid(node2)); EXPECT_EQ(&*it2, node2); - for (auto it1 = it2->beginChild(); it1; ++it1) { + for (auto it1 = it2->cbeginChild(); it1; ++it1) { auto *node1 = &nodeMgr->lower(n[1]++); EXPECT_TRUE(nanovdb::isValid(node1)); EXPECT_EQ(&*it1, node1); - for (auto it0 = it1->beginChild(); it0; ++it0) { + for (auto it0 = it1->cbeginChild(); it0; ++it0) { auto *node0 = &nodeMgr->leaf(n[2]++); EXPECT_TRUE(nanovdb::isValid(node0)); EXPECT_EQ(&*it0, node0); @@ -3216,19 +3810,15 @@ TEST_F(TestNanoVDB, GridBuilder_Fp16) EXPECT_EQ(dstGrid->tree().nodeCount(2), n[0]); } {// Sphere - const double voxelSize = 0.1, halfWidth = 3.0; - const float radius = 10.0f; - const nanovdb::Vec3f center(0); - const nanovdb::Vec3d origin(0); + const double voxelSize = 0.1, halfWidth = 3.0, radius = 10.0f; + const nanovdb::Vec3d center(0), origin(0); const float tolerance = 0.005f * voxelSize; - auto handle = nanovdb::createLevelSetSphere(radius, center, - voxelSize, halfWidth, - origin, "sphere", - nanovdb::StatsMode::Default, - nanovdb::ChecksumMode::Default, - tolerance, - false); + auto handle = nanovdb::createLevelSetSphere(radius, center, + voxelSize, halfWidth, + origin, "sphere", + nanovdb::StatsMode::Default, + nanovdb::ChecksumMode::Default); auto* nanoGrid = handle.grid(); EXPECT_TRUE(nanoGrid); Sphere sphere(center, radius, float(voxelSize), float(halfWidth)); @@ -3255,17 +3845,15 @@ TEST_F(TestNanoVDB, GridBuilder_FpN_Basic1) using VoxelT = nanovdb::FpN; EXPECT_EQ(96u, sizeof(nanovdb::NanoLeaf)); { // 1 grid point - nanovdb::GridBuilder builder(0.0f); - auto srcAcc = builder.getAccessor(); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.0f); + auto srcAcc = srcGrid.getAccessor(); srcAcc.setValue(nanovdb::Coord( 0, 0, 0), 1.0f); EXPECT_TRUE(srcAcc.isActive(nanovdb::Coord(0, 0, 0))); EXPECT_TRUE(srcAcc.isValueOn(nanovdb::Coord(0, 0, 0))); EXPECT_EQ(1.0f, srcAcc.getValue(nanovdb::Coord( 0, 0, 0))); - builder.setStats(nanovdb::StatsMode::All); - //builder.setVerbose(true); - auto handle = builder.getHandle(); - + auto handle = nanovdb::createNanoGrid(srcGrid, nanovdb::StatsMode::All); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); EXPECT_TRUE(meta); @@ -3293,7 +3881,7 @@ TEST_F(TestNanoVDB, GridBuilder_FpN_Basic1) EXPECT_TRUE(dstGrid->isSequential<1>()); EXPECT_FALSE(dstGrid->isSequential<0>()); - EXPECT_EQ(nanovdb::Vec3R(1.0), dstGrid->voxelSize()); + EXPECT_EQ(nanovdb::Vec3d(1.0), dstGrid->voxelSize()); auto *leaf = dstGrid->tree().root().probeLeaf(nanovdb::Coord(0, 0, 0)); EXPECT_TRUE(leaf); //std::cerr << leaf->origin() << ", " << leaf->data()->mBBoxMin << std::endl; @@ -3317,8 +3905,9 @@ TEST_F(TestNanoVDB, GridBuilder_FpN_Basic3) using VoxelT = nanovdb::FpN; EXPECT_EQ(96u, sizeof(nanovdb::NanoLeaf)); { // 3 grid point - nanovdb::GridBuilder builder(0.0f); - auto srcAcc = builder.getAccessor(); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.0f); + auto srcAcc = srcGrid.getAccessor(); srcAcc.setValue(nanovdb::Coord( 1, 2, 3), 1.0f); srcAcc.setValue(nanovdb::Coord(-10, 20,-50), 2.0f); srcAcc.setValue(nanovdb::Coord( 50,-12, 30), 3.0f); @@ -3328,10 +3917,7 @@ TEST_F(TestNanoVDB, GridBuilder_FpN_Basic3) EXPECT_EQ(2.0f, srcAcc.getValue(nanovdb::Coord(-10, 20,-50))); EXPECT_EQ(3.0f, srcAcc.getValue(nanovdb::Coord( 50,-12, 30))); - builder.setStats(nanovdb::StatsMode::All); - //builder.setVerbose(true); - auto handle = builder.getHandle(); - + auto handle = nanovdb::createNanoGrid(srcGrid, nanovdb::StatsMode::All); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); EXPECT_TRUE(meta); @@ -3358,7 +3944,7 @@ TEST_F(TestNanoVDB, GridBuilder_FpN_Basic3) EXPECT_TRUE(dstGrid->isSequential<1>()); EXPECT_FALSE(dstGrid->isSequential<0>()); - EXPECT_EQ(nanovdb::Vec3R(1.0), dstGrid->voxelSize()); + EXPECT_EQ(nanovdb::Vec3d(1.0), dstGrid->voxelSize()); auto *leaf = dstGrid->tree().root().probeLeaf(nanovdb::Coord(1, 2, 3)); EXPECT_TRUE(leaf); //std::cerr << leaf->origin() << ", " << leaf->data()->mBBoxMin << std::endl; @@ -3384,15 +3970,15 @@ TEST_F(TestNanoVDB, GridBuilder_FpN_Basic3) EXPECT_TRUE(nanovdb::isValid(nodeMgr)); EXPECT_FALSE(nodeMgr->isLinear()); uint64_t n[3]={0}; - for (auto it2 = dstGrid->tree().root().beginChild(); it2; ++it2) { + for (auto it2 = dstGrid->tree().root().cbeginChild(); it2; ++it2) { auto *node2 = &nodeMgr->upper(n[0]++); EXPECT_TRUE(nanovdb::isValid(node2)); EXPECT_EQ(&*it2, node2); - for (auto it1 = it2->beginChild(); it1; ++it1) { + for (auto it1 = it2->cbeginChild(); it1; ++it1) { auto *node1 = &nodeMgr->lower(n[1]++); EXPECT_TRUE(nanovdb::isValid(node1)); EXPECT_EQ(&*it1, node1); - for (auto it0 = it1->beginChild(); it0; ++it0) { + for (auto it0 = it1->cbeginChild(); it0; ++it0) { auto *node0 = &nodeMgr->leaf(n[2]++); EXPECT_TRUE(nanovdb::isValid(node0)); EXPECT_EQ(&*it0, node0); @@ -3410,19 +3996,17 @@ TEST_F(TestNanoVDB, GridBuilder_FpN_Sphere) using VoxelT = nanovdb::FpN; EXPECT_EQ(96u, sizeof(nanovdb::NanoLeaf)); {// Sphere - const double voxelSize = 0.1, halfWidth = 3.0; - const float radius = 10.0f; - const nanovdb::Vec3f center(0); - const nanovdb::Vec3d origin(0); + const double voxelSize = 0.1, halfWidth = 3.0, radius = 10.0f; + const nanovdb::Vec3d center(0), origin(0); const float tolerance = 0.5f * voxelSize; - auto handle = nanovdb::createLevelSetSphere(radius, center, - voxelSize, halfWidth, - origin, "sphere", - nanovdb::StatsMode::Default, - nanovdb::ChecksumMode::Default, - tolerance, - false); + auto handle = nanovdb::createLevelSetSphere(radius, center, + voxelSize, halfWidth, + origin, "sphere", + nanovdb::StatsMode::Default, + nanovdb::ChecksumMode::Default, + tolerance, + false); auto* nanoGrid = handle.grid(); EXPECT_TRUE(nanoGrid); Sphere sphere(center, radius, float(voxelSize), float(halfWidth)); @@ -3447,12 +4031,12 @@ TEST_F(TestNanoVDB, GridBuilder_FpN_Sphere) TEST_F(TestNanoVDB, NodeManager) { { // 1 active voxel - nanovdb::GridBuilder builder(0.0f); - auto srcAcc = builder.getAccessor(); - builder.setGridClass(nanovdb::GridClass::LevelSet); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.0f, "test", nanovdb::GridClass::LevelSet); + auto srcAcc = srcGrid.getAccessor(); const nanovdb::Coord x0(1, 2, 3), x1(1, 2, 4); srcAcc.setValue(x1, 1.0f); - auto handle = builder.getHandle<>(1.0, nanovdb::Vec3d(0.0), "test"); + auto handle = nanovdb::createNanoGrid(srcGrid); EXPECT_TRUE(handle); auto* dstGrid = handle.grid(); EXPECT_TRUE(dstGrid); @@ -3499,15 +4083,15 @@ TEST_F(TestNanoVDB, NodeManager) EXPECT_EQ(&nodeMgr->upper(0), dstGrid->tree().getFirstNode< 2 >()); uint64_t n[3]={0}; - for (auto it2 = dstGrid->tree().root().beginChild(); it2; ++it2) { + for (auto it2 = dstGrid->tree().root().cbeginChild(); it2; ++it2) { auto *node2 = &nodeMgr->upper(n[0]++); EXPECT_TRUE(nanovdb::isValid(node2)); EXPECT_EQ(&*it2, node2); - for (auto it1 = it2->beginChild(); it1; ++it1) { + for (auto it1 = it2->cbeginChild(); it1; ++it1) { auto *node1 = &nodeMgr->lower(n[1]++); EXPECT_TRUE(nanovdb::isValid(node1)); EXPECT_EQ(&*it1, node1); - for (auto it0 = it1->beginChild(); it0; ++it0) { + for (auto it0 = it1->cbeginChild(); it0; ++it0) { auto *node0 = &nodeMgr->leaf(n[2]++); EXPECT_TRUE(nanovdb::isValid(node0)); EXPECT_EQ(&*it0, node0); @@ -3519,12 +4103,13 @@ TEST_F(TestNanoVDB, NodeManager) EXPECT_EQ(dstGrid->tree().nodeCount(2), n[0]); } { // 2 active voxels - nanovdb::GridBuilder builder(0.0f, nanovdb::GridClass::LevelSet); - auto srcAcc = builder.getAccessor(); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.0f, "test", nanovdb::GridClass::LevelSet); + auto srcAcc = srcGrid.getAccessor(); const nanovdb::Coord x0(1, 2, 3), x1(2,-2, 9), x2(1, 2, 4); srcAcc.setValue(x1, 1.0f); srcAcc.setValue(x2, 2.0f); - auto handle = builder.getHandle<>(1.0, nanovdb::Vec3d(0.0), "test"); + auto handle = nanovdb::createNanoGrid(srcGrid); EXPECT_TRUE(handle); auto* dstGrid = handle.grid(); EXPECT_TRUE(dstGrid); @@ -3566,15 +4151,15 @@ TEST_F(TestNanoVDB, NodeManager) EXPECT_EQ(2.0f, nodeMgr->leaf(1).getValue(x2)); uint64_t n[3]={0}; - for (auto it2 = dstGrid->tree().root().beginChild(); it2; ++it2) { + for (auto it2 = dstGrid->tree().root().cbeginChild(); it2; ++it2) { auto *node2 = &nodeMgr->upper(n[0]++); EXPECT_TRUE(nanovdb::isValid(node2)); EXPECT_EQ(&*it2, node2); - for (auto it1 = it2->beginChild(); it1; ++it1) { + for (auto it1 = it2->cbeginChild(); it1; ++it1) { auto *node1 = &nodeMgr->lower(n[1]++); EXPECT_TRUE(nanovdb::isValid(node1)); EXPECT_EQ(&*it1, node1); - for (auto it0 = it1->beginChild(); it0; ++it0) { + for (auto it0 = it1->cbeginChild(); it0; ++it0) { auto *node0 = &nodeMgr->leaf(n[2]++); EXPECT_TRUE(nanovdb::isValid(node0)); EXPECT_EQ(&*it0, node0); @@ -3599,14 +4184,15 @@ TEST_F(TestNanoVDB, NodeManager) } } EXPECT_EQ(voxelCount, voxels.size()); - nanovdb::GridBuilder builder(-1.0f, nanovdb::GridClass::LevelSet); - auto srcAcc = builder.getAccessor(); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(-1.0f, "test", nanovdb::GridClass::LevelSet); + auto srcAcc = srcGrid.getAccessor(); for (size_t i=0; i(1.0, nanovdb::Vec3d(0.0), "test"); + auto handle = nanovdb::createNanoGrid(srcGrid); EXPECT_TRUE(handle); - auto* dstGrid = handle.grid(); + const auto* dstGrid = handle.grid(); EXPECT_TRUE(dstGrid); EXPECT_TRUE(dstGrid->isBreadthFirst()); using GridT = std::remove_pointer::type; @@ -3624,15 +4210,15 @@ TEST_F(TestNanoVDB, NodeManager) } uint64_t n[3]={0}; - for (auto it2 = dstGrid->tree().root().beginChild(); it2; ++it2) { + for (auto it2 = dstGrid->tree().root().cbeginChild(); it2; ++it2) { auto *node2 = &nodeMgr->upper(n[0]++); EXPECT_TRUE(nanovdb::isValid(node2)); EXPECT_EQ(&*it2, node2); - for (auto it1 = it2->beginChild(); it1; ++it1) { + for (auto it1 = it2->cbeginChild(); it1; ++it1) { auto *node1 = &nodeMgr->lower(n[1]++); EXPECT_TRUE(nanovdb::isValid(node1)); EXPECT_EQ(&*it1, node1); - for (auto it0 = it1->beginChild(); it0; ++it0) { + for (auto it0 = it1->cbeginChild(); it0; ++it0) { auto *node0 = &nodeMgr->leaf(n[2]++); EXPECT_TRUE(nanovdb::isValid(node0)); EXPECT_EQ(&*it0, node0); @@ -3648,17 +4234,17 @@ TEST_F(TestNanoVDB, NodeManager) TEST_F(TestNanoVDB, GridBuilderBasicDense) { { // dense functor - nanovdb::GridBuilder builder(0.0f, nanovdb::GridClass::LevelSet); - auto srcAcc = builder.getAccessor(); - const nanovdb::CoordBBox bbox(nanovdb::Coord(0), nanovdb::Coord(100)); - auto func = [](const nanovdb::Coord&) { return 1.0f; }; - //auto func = [](const nanovdb::Coord&, float &v) { v = 1.0f; return true; }; - builder(func, bbox); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.0f, "test", nanovdb::GridClass::LevelSet); + const nanovdb::CoordBBox bbox(nanovdb::Coord(0), nanovdb::Coord(100)); + auto func = [](const nanovdb::Coord&) { return 1.0f; }; + srcGrid(func, bbox); + auto srcAcc = srcGrid.getAccessor(); for (auto ijk = bbox.begin(); ijk; ++ijk) { EXPECT_EQ(1.0f, srcAcc.getValue(*ijk)); EXPECT_TRUE(srcAcc.isActive(*ijk)); } - auto handle = builder.getHandle<>(1.0, nanovdb::Vec3d(0.0), "test"); + auto handle = nanovdb::createNanoGrid(srcGrid); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); EXPECT_TRUE(meta); @@ -3698,8 +4284,9 @@ TEST_F(TestNanoVDB, GridBuilderBasicDense) TEST_F(TestNanoVDB, GridBuilderBackground) { { - nanovdb::GridBuilder builder(0.5f); - auto acc = builder.getAccessor(); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.5f); + auto acc = srcGrid.getAccessor(); acc.setValue(nanovdb::Coord(1), 1); acc.setValue(nanovdb::Coord(2), 0); @@ -3710,8 +4297,7 @@ TEST_F(TestNanoVDB, GridBuilderBackground) EXPECT_TRUE(acc.isActive(nanovdb::Coord(1))); EXPECT_EQ(0, acc.getValue(nanovdb::Coord(2))); EXPECT_TRUE(acc.isActive(nanovdb::Coord(1))); - - auto gridHdl = builder.getHandle<>(); + auto gridHdl = nanovdb::createNanoGrid(srcGrid); auto grid = gridHdl.grid(); EXPECT_TRUE(grid); EXPECT_FALSE(grid->isEmpty()); @@ -3723,7 +4309,8 @@ TEST_F(TestNanoVDB, GridBuilderBackground) TEST_F(TestNanoVDB, GridBuilderSphere) { - Sphere sphere(nanovdb::Vec3(50), 20.0f); + using SrcGridT = nanovdb::build::Grid; + Sphere sphere(nanovdb::Vec3d(50), 20.0f); EXPECT_EQ(3.0f, sphere.background()); EXPECT_EQ(3.0f, sphere(nanovdb::Coord(100))); EXPECT_EQ(-3.0f, sphere(nanovdb::Coord(50))); @@ -3731,16 +4318,12 @@ TEST_F(TestNanoVDB, GridBuilderSphere) EXPECT_EQ(-1.0f, sphere(nanovdb::Coord(50, 50, 69))); EXPECT_EQ(2.0f, sphere(nanovdb::Coord(50, 50, 72))); - nanovdb::GridBuilder builder(sphere.background(), nanovdb::GridClass::LevelSet); - auto srcAcc = builder.getAccessor(); - + SrcGridT srcGrid(sphere.background(), "test", nanovdb::GridClass::LevelSet); const nanovdb::CoordBBox bbox(nanovdb::Coord(-100), nanovdb::Coord(100)); //mTimer.start("GridBulder Sphere"); - builder(sphere, bbox); + srcGrid(sphere, bbox); //mTimer.stop(); - - - auto handle = builder.getHandle<>(1.0, nanovdb::Vec3d(0.0), "test"); + auto handle = nanovdb::createNanoGrid(srcGrid); EXPECT_TRUE(handle); EXPECT_EQ(1u, handle.gridCount()); auto* meta = handle.gridMetaData(); @@ -3770,6 +4353,7 @@ TEST_F(TestNanoVDB, GridBuilderSphere) uint64_t count = 0; auto dstAcc = dstGrid->getAccessor(); + auto srcAcc = srcGrid.getAccessor(); for (nanovdb::Coord ijk = bbox[0]; ijk[0] <= bbox[1][0]; ++ijk[0]) { for (ijk[1] = bbox[0][1]; ijk[1] <= bbox[1][1]; ++ijk[1]) { for (ijk[2] = bbox[0][2]; ijk[2] <= bbox[1][2]; ++ijk[2]) { @@ -3787,29 +4371,32 @@ TEST_F(TestNanoVDB, GridBuilderSphere) TEST_F(TestNanoVDB, createLevelSetSphere) { - Sphere sphere(nanovdb::Vec3(50), 20.0f); - EXPECT_EQ(3.0f, sphere.background()); - EXPECT_EQ(3.0f, sphere(nanovdb::Coord(100))); - EXPECT_EQ(-3.0f, sphere(nanovdb::Coord(50))); - EXPECT_EQ(0.0f, sphere(nanovdb::Coord(50, 50, 70))); - EXPECT_EQ(-1.0f, sphere(nanovdb::Coord(50, 50, 69))); - EXPECT_EQ(2.0f, sphere(nanovdb::Coord(50, 50, 72))); - - auto handle = nanovdb::createLevelSetSphere(20.0f, nanovdb::Vec3f(50), - 1.0, 3.0, nanovdb::Vec3d(0), "sphere_20"); - - const nanovdb::CoordBBox bbox(nanovdb::Coord(0), nanovdb::Coord(100)); + const int radius = 100, center = 50, width = 3, voxelSize = 1; + const std::string gridName("sphere_" + std::to_string(radius)); + Sphere sphere(nanovdb::Vec3d(center), radius); + EXPECT_EQ( 3.0f, sphere.background()); + EXPECT_EQ( 3.0f, sphere(nanovdb::Coord(center+2*radius))); + EXPECT_EQ(-3.0f, sphere(nanovdb::Coord(center))); + EXPECT_EQ( 0.0f, sphere(nanovdb::Coord(center, center, center+radius))); + EXPECT_EQ(-1.0f, sphere(nanovdb::Coord(center, center, center+radius-1))); + EXPECT_EQ( 2.0f, sphere(nanovdb::Coord(center, center, center+radius+2))); + //mTimer.start("createLevelSetSphere"); + auto handle = nanovdb::createLevelSetSphere(radius, nanovdb::Vec3d(center), + voxelSize, width, nanovdb::Vec3d(0), gridName); + //mTimer.stop(); + const nanovdb::CoordBBox bbox(nanovdb::Coord(center-radius-width-1), + nanovdb::Coord(center+radius+width+1)); EXPECT_TRUE(handle); EXPECT_EQ(1u, handle.gridCount()); auto* meta = handle.gridMetaData(); EXPECT_TRUE(meta); - EXPECT_EQ("sphere_20", std::string(meta->shortGridName())); + EXPECT_EQ(gridName, std::string(meta->shortGridName())); EXPECT_EQ(nanovdb::GridType::Float, meta->gridType()); EXPECT_EQ(nanovdb::GridClass::LevelSet, meta->gridClass()); auto* dstGrid = handle.grid(); EXPECT_TRUE(dstGrid); - EXPECT_EQ("sphere_20", std::string(dstGrid->gridName())); + EXPECT_EQ(gridName, std::string(dstGrid->gridName())); EXPECT_TRUE(dstGrid->hasBBox()); EXPECT_TRUE(dstGrid->hasMinMax()); @@ -3818,38 +4405,34 @@ TEST_F(TestNanoVDB, createLevelSetSphere) EXPECT_NEAR( -3.0f, dstGrid->tree().root().minimum(), 0.04f); EXPECT_NEAR( 3.0f, dstGrid->tree().root().maximum(), 0.04f); - EXPECT_NEAR( 0.0f, dstGrid->tree().root().average(), 0.3f); + EXPECT_NEAR( 0.0f, dstGrid->tree().root().average(), 0.30f); //std::cerr << dstGrid->tree().root().minimum() << std::endl; //std::cerr << dstGrid->tree().root().maximum() << std::endl; //std::cerr << dstGrid->tree().root().average() << std::endl; //std::cerr << dstGrid->tree().root().stdDeviation() << std::endl; - - EXPECT_EQ(nanovdb::Coord(50 - 20 - 2), dstGrid->indexBBox()[0]); - EXPECT_EQ(nanovdb::Coord(50 + 20 + 2), dstGrid->indexBBox()[1]); + EXPECT_EQ(nanovdb::Coord(center - radius - 2), dstGrid->indexBBox()[0]); + EXPECT_EQ(nanovdb::Coord(center + radius + 2), dstGrid->indexBBox()[1]); //std::cerr << "bbox.min = (" << dstGrid->indexBBox()[0][0] << ", " << dstGrid->indexBBox()[0][1] << ", " << dstGrid->indexBBox()[0][2] << ")" << std::endl; //std::cerr << "bbox.max = (" << dstGrid->indexBBox()[1][0] << ", " << dstGrid->indexBBox()[1][1] << ", " << dstGrid->indexBBox()[1][2] << ")" << std::endl; - uint64_t count = 0; - auto dstAcc = dstGrid->getAccessor(); - for (nanovdb::Coord ijk = bbox[0]; ijk[0] <= bbox[1][0]; ++ijk[0]) { - for (ijk[1] = bbox[0][1]; ijk[1] <= bbox[1][1]; ++ijk[1]) { - for (ijk[2] = bbox[0][2]; ijk[2] <= bbox[1][2]; ++ijk[2]) { - if (sphere.inNarrowBand(ijk)) - ++count; - EXPECT_EQ(sphere(ijk), dstAcc.getValue(ijk)); - EXPECT_EQ(sphere.inNarrowBand(ijk), dstAcc.isActive(ijk)); - } + std::atomic count{0}; + nanovdb::forEach(bbox, [&](const nanovdb::CoordBBox &b){ + auto dstAcc = dstGrid->getAccessor(); + for (auto it = b.begin(); it; ++it) { + const nanovdb::Coord ijk = *it; + if (sphere.inNarrowBand(ijk)) ++count; + EXPECT_EQ(sphere(ijk), dstAcc.getValue(ijk)); + EXPECT_EQ(sphere.inNarrowBand(ijk), dstAcc.isActive(ijk)); } - } - + }); EXPECT_EQ(count, dstGrid->activeVoxelCount()); } // createLevelSetSphere TEST_F(TestNanoVDB, createFogVolumeSphere) { - auto handle = nanovdb::createFogVolumeSphere(20.0f, nanovdb::Vec3f(50), + auto handle = nanovdb::createFogVolumeSphere(20.0f, nanovdb::Vec3d(50), 1.0, 3.0, nanovdb::Vec3d(0), "sphere_20"); const nanovdb::CoordBBox bbox(nanovdb::Coord(0), nanovdb::Coord(100)); @@ -3883,7 +4466,7 @@ TEST_F(TestNanoVDB, createFogVolumeSphere) EXPECT_EQ(nanovdb::Coord(50 - 20), dstGrid->indexBBox()[0]); EXPECT_EQ(nanovdb::Coord(50 + 20), dstGrid->indexBBox()[1]); - Sphere sphere(nanovdb::Vec3(50), 20.0f); + Sphere sphere(nanovdb::Vec3d(50), 20.0f); uint64_t count = 0; auto dstAcc = dstGrid->getAccessor(); for (nanovdb::Coord ijk = bbox[0]; ijk[0] <= bbox[1][0]; ++ijk[0]) { @@ -3908,7 +4491,7 @@ TEST_F(TestNanoVDB, createFogVolumeSphere) TEST_F(TestNanoVDB, createPointSphere) { - Sphere sphere(nanovdb::Vec3(0), 100.0f, 1.0f, 1.0f); + Sphere sphere(nanovdb::Vec3d(0), 100.0, 1.0, 1.0); EXPECT_EQ(1.0f, sphere.background()); EXPECT_EQ(1.0f, sphere(nanovdb::Coord(101, 0, 0))); EXPECT_EQ(-1.0f, sphere(nanovdb::Coord(0))); @@ -3916,12 +4499,12 @@ TEST_F(TestNanoVDB, createPointSphere) EXPECT_EQ(-1.0f, sphere(nanovdb::Coord(0, 0, 99))); EXPECT_EQ(1.0f, sphere(nanovdb::Coord(0, 0, 101))); - auto handle = nanovdb::createPointSphere(1,// pointer per voxel - 100.0f,// radius of sphere - nanovdb::Vec3f(0),// center sphere - 1.0,// voxel size - nanovdb::Vec3d(0),// origin of grid - "point_sphere"); + auto handle = nanovdb::createPointSphere(1,// pointer per voxel + 100.0,// radius of sphere + nanovdb::Vec3d(0),// center sphere + 1.0,// voxel size + nanovdb::Vec3d(0),// origin of grid + "point_sphere"); const nanovdb::CoordBBox bbox(nanovdb::Coord(-100), nanovdb::Coord(100)); @@ -3939,16 +4522,16 @@ TEST_F(TestNanoVDB, createPointSphere) EXPECT_TRUE(dstGrid->hasMinMax()); EXPECT_FALSE(dstGrid->hasAverage()); EXPECT_FALSE(dstGrid->hasStdDeviation()); + EXPECT_EQ(dstGrid->voxelSize()[0], 1.0); + //std::cerr << "BBox = " << dstGrid->indexBBox() << std::endl; EXPECT_EQ(bbox[0], dstGrid->indexBBox()[0]); EXPECT_EQ(bbox[1], dstGrid->indexBBox()[1]); - //std::cerr << "bbox.min = (" << dstGrid->indexBBox()[0][0] << ", " << dstGrid->indexBBox()[0][1] << ", " << dstGrid->indexBBox()[0][2] << ")" << std::endl; - //std::cerr << "bbox.max = (" << dstGrid->indexBBox()[1][0] << ", " << dstGrid->indexBBox()[1][1] << ", " << dstGrid->indexBBox()[1][2] << ")" << std::endl; - - uint64_t count = 0; + uint64_t count = 0; nanovdb::PointAccessor acc(*dstGrid); - const nanovdb::Vec3f * begin = nullptr, *end = nullptr; + EXPECT_TRUE(acc); + const nanovdb::Vec3f *begin = nullptr, *end = nullptr; for (nanovdb::Coord ijk = bbox[0]; ijk[0] <= bbox[1][0]; ++ijk[0]) { for (ijk[1] = bbox[0][1]; ijk[1] <= bbox[1][1]; ++ijk[1]) { for (ijk[2] = bbox[0][2]; ijk[2] <= bbox[1][2]; ++ijk[2]) { @@ -3956,12 +4539,19 @@ TEST_F(TestNanoVDB, createPointSphere) ++count; EXPECT_TRUE(acc.isActive(ijk)); EXPECT_TRUE(acc.getValue(ijk) != std::numeric_limits::max()); - EXPECT_EQ(1u, acc.voxelPoints(ijk, begin, end)); // exactly one point per voxel - const nanovdb::Vec3f p = *begin + ijk.asVec3s();// local voxel coordinate + global index coordinates + const auto n = acc.voxelPoints(ijk, begin, end); + EXPECT_TRUE(begin); + EXPECT_TRUE(end); + EXPECT_LT(begin, end); + EXPECT_EQ(1u, n); // exactly one point per voxel + const nanovdb::Vec3f p = *begin;// + ijk.asVec3s();// local voxel coordinate + global index coordinates EXPECT_TRUE(nanovdb::Abs(sphere(p)) <= 1.0f); } else { EXPECT_FALSE(acc.isActive(ijk)); EXPECT_TRUE(acc.getValue(ijk) < 512 || acc.getValue(ijk) == std::numeric_limits::max()); + EXPECT_EQ(0u, acc.voxelPoints(ijk, begin, end)); + EXPECT_FALSE(begin); + EXPECT_FALSE(end); } } } @@ -3971,7 +4561,7 @@ TEST_F(TestNanoVDB, createPointSphere) TEST_F(TestNanoVDB, createLevelSetTorus) { - auto handle = nanovdb::createLevelSetTorus(100.0f, 50.0f, nanovdb::Vec3f(50), + auto handle = nanovdb::createLevelSetTorus(100.0f, 50.0f, nanovdb::Vec3d(50), 1.0, 3.0, nanovdb::Vec3d(0), "torus_100"); EXPECT_TRUE(handle); @@ -4008,7 +4598,7 @@ TEST_F(TestNanoVDB, createLevelSetTorus) TEST_F(TestNanoVDB, createFogVolumeTorus) { - auto handle = nanovdb::createFogVolumeTorus(100.0f, 50.0f, nanovdb::Vec3f(50), + auto handle = nanovdb::createFogVolumeTorus(100.0f, 50.0f, nanovdb::Vec3d(50), 1.0, 3.0, nanovdb::Vec3d(0), "torus_100"); EXPECT_TRUE(handle); @@ -4049,7 +4639,7 @@ TEST_F(TestNanoVDB, createFogVolumeTorus) TEST_F(TestNanoVDB, createLevelSetBox) { - auto handle = nanovdb::createLevelSetBox(40.0f, 60.0f, 80.0f, nanovdb::Vec3f(50), + auto handle = nanovdb::createLevelSetBox(40.0f, 60.0f, 80.0f, nanovdb::Vec3d(50), 1.0, 3.0, nanovdb::Vec3d(0), "box"); EXPECT_TRUE(handle); EXPECT_EQ(1u, handle.gridCount()); @@ -4085,7 +4675,7 @@ TEST_F(TestNanoVDB, createLevelSetBox) TEST_F(TestNanoVDB, createFogVolumeBox) { - auto handle = nanovdb::createFogVolumeBox(40.0f, 60.0f, 80.0f, nanovdb::Vec3f(50), + auto handle = nanovdb::createFogVolumeBox(40.0f, 60.0f, 80.0f, nanovdb::Vec3d(50), 1.0, 3.0, nanovdb::Vec3d(0), "box"); EXPECT_TRUE(handle); EXPECT_EQ(1u, handle.gridCount()); @@ -4121,7 +4711,7 @@ TEST_F(TestNanoVDB, createFogVolumeBox) TEST_F(TestNanoVDB, createLevelSetOctahedron) { - auto handle = nanovdb::createLevelSetOctahedron(100.0f, nanovdb::Vec3f(50), + auto handle = nanovdb::createLevelSetOctahedron(100.0f, nanovdb::Vec3d(50), 1.0f, 3.0f, nanovdb::Vec3d(0), "octahedron"); EXPECT_TRUE(handle); EXPECT_EQ(1u, handle.gridCount()); @@ -4162,7 +4752,7 @@ TEST_F(TestNanoVDB, CNanoVDBSize) EXPECT_EQ(sizeof(cnanovdb_mask3), sizeof(nanovdb::Mask<3>)); EXPECT_EQ(sizeof(cnanovdb_mask4), sizeof(nanovdb::Mask<4>)); EXPECT_EQ(sizeof(cnanovdb_mask5), sizeof(nanovdb::Mask<5>)); - EXPECT_EQ(sizeof(cnanovdb_map), sizeof(nanovdb::Map)); + EXPECT_EQ(sizeof(cnanovdb_map), sizeof(nanovdb::Map)); EXPECT_EQ(sizeof(cnanovdb_coord), sizeof(nanovdb::Coord)); EXPECT_EQ(sizeof(cnanovdb_Vec3F), sizeof(nanovdb::Vec3f)); @@ -4210,6 +4800,10 @@ TEST_F(TestNanoVDB, PNanoVDB_Basic) EXPECT_EQ((int)nanovdb::GridType::Vec4f, PNANOVDB_GRID_TYPE_VEC4F); EXPECT_EQ((int)nanovdb::GridType::Vec4d, PNANOVDB_GRID_TYPE_VEC4D); EXPECT_EQ((int)nanovdb::GridType::Index, PNANOVDB_GRID_TYPE_INDEX); + EXPECT_EQ((int)nanovdb::GridType::OnIndex, PNANOVDB_GRID_TYPE_ONINDEX); + EXPECT_EQ((int)nanovdb::GridType::IndexMask, PNANOVDB_GRID_TYPE_INDEXMASK); + EXPECT_EQ((int)nanovdb::GridType::OnIndexMask, PNANOVDB_GRID_TYPE_ONINDEXMASK); + EXPECT_EQ((int)nanovdb::GridType::PointIndex, PNANOVDB_GRID_TYPE_POINTINDEX); EXPECT_EQ((int)nanovdb::GridType::End, PNANOVDB_GRID_TYPE_END); EXPECT_EQ((int)nanovdb::GridClass::Unknown, PNANOVDB_GRID_CLASS_UNKNOWN); @@ -4250,96 +4844,129 @@ TEST_F(TestNanoVDB, PNanoVDB_Basic) EXPECT_EQ(NANOVDB_OFFSETOF(pnanovdb_map_t, taperd), PNANOVDB_MAP_OFF_TAPERD); EXPECT_TRUE(validate_strides(printf));// checks strides and prints out new ones if they have changed -} +}// PNanoVDB_Basic template void validateLeaf(pnanovdb_grid_type_t grid_type) { - using nodedata0_t = typename nanovdb::LeafData; - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mMinimum), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_min); - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mMaximum), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_max); - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mAverage), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_ave); - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mStdDevi), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_stddev); - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mValues), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_table); + using leaf_t = typename nanovdb::LeafNode; + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mMinimum), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_min); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mMaximum), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_max); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mAverage), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_ave); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mStdDevi), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_stddev); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mValues), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_table); } template <> void validateLeaf(pnanovdb_grid_type_t grid_type) { - using nodedata0_t = typename nanovdb::LeafData; - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mMin), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_min); - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mMax), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_max); - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mAvg), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_ave); - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mDev), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_stddev); + using leaf_t = typename nanovdb::LeafNode; + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mMin), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_min); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mMax), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_max); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mAvg), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_ave); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mDev), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_stddev); } template <> void validateLeaf(pnanovdb_grid_type_t grid_type) { - using nodedata0_t = typename nanovdb::LeafData; - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mMin), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_min); - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mMax), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_max); - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mAvg), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_ave); - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mDev), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_stddev); + using leaf_t = typename nanovdb::LeafNode; + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mMin), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_min); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mMax), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_max); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mAvg), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_ave); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mDev), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_stddev); } template <> void validateLeaf(pnanovdb_grid_type_t grid_type) { - using nodedata0_t = typename nanovdb::LeafData; - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mMin), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_min); - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mMax), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_max); - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mAvg), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_ave); - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mDev), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_stddev); + using leaf_t = typename nanovdb::LeafNode; + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mMin), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_min); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mMax), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_max); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mAvg), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_ave); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mDev), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_stddev); } template <> void validateLeaf(pnanovdb_grid_type_t grid_type) { - using nodedata0_t = typename nanovdb::LeafData; - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mMin), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_min); - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mMax), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_max); - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mAvg), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_ave); - EXPECT_EQ(NANOVDB_OFFSETOF(nodedata0_t, mDev), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_stddev); + using leaf_t = typename nanovdb::LeafNode; + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mMin), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_min); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mMax), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_max); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mAvg), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_ave); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mDev), (int)pnanovdb_grid_type_constants[grid_type].leaf_off_stddev); } // template specializations for bool types template <> void validateLeaf(pnanovdb_grid_type_t grid_type) { - using nodeLeaf_t = typename nanovdb::LeafData; using leaf_t = typename nanovdb::LeafNode; - EXPECT_EQ(sizeof(leaf_t), (pnanovdb_grid_type_constants[grid_type].leaf_size)); - EXPECT_EQ(NANOVDB_OFFSETOF(nodeLeaf_t, mBBoxMin), PNANOVDB_LEAF_OFF_BBOX_MIN); - EXPECT_EQ(NANOVDB_OFFSETOF(nodeLeaf_t, mBBoxDif), PNANOVDB_LEAF_OFF_BBOX_DIF_AND_FLAGS); - EXPECT_EQ(NANOVDB_OFFSETOF(nodeLeaf_t, mValueMask), PNANOVDB_LEAF_OFF_VALUE_MASK); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mValues), PNANOVDB_LEAF_OFF_VALUE_MASK + 64); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mPadding), PNANOVDB_LEAF_OFF_VALUE_MASK + 2*64); } // template specializations for nanovdb::ValueMask types template <> void validateLeaf(pnanovdb_grid_type_t grid_type) { - using nodeLeaf_t = typename nanovdb::LeafData; using leaf_t = typename nanovdb::LeafNode; - EXPECT_EQ(sizeof(leaf_t), (pnanovdb_grid_type_constants[grid_type].leaf_size)); - EXPECT_EQ(NANOVDB_OFFSETOF(nodeLeaf_t, mBBoxMin), PNANOVDB_LEAF_OFF_BBOX_MIN); - EXPECT_EQ(NANOVDB_OFFSETOF(nodeLeaf_t, mBBoxDif), PNANOVDB_LEAF_OFF_BBOX_DIF_AND_FLAGS); - EXPECT_EQ(NANOVDB_OFFSETOF(nodeLeaf_t, mValueMask), PNANOVDB_LEAF_OFF_VALUE_MASK); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mPadding), PNANOVDB_LEAF_OFF_VALUE_MASK + 64); } // template specializations for nanovdb::ValueIndex types template <> void validateLeaf(pnanovdb_grid_type_t grid_type) { - using nodeLeaf_t = typename nanovdb::LeafData; using leaf_t = typename nanovdb::LeafNode; + EXPECT_EQ(sizeof(leaf_t), (pnanovdb_grid_type_constants[grid_type].leaf_size)); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mOffset), PNANOVDB_LEAF_OFF_VALUE_MASK + 64); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mPrefixSum), PNANOVDB_LEAF_OFF_VALUE_MASK + 64 + 8); +} + +// template specializations for nanovdb::ValueIndexMask types +template <> +void validateLeaf(pnanovdb_grid_type_t grid_type) +{ + using leaf_t = typename nanovdb::LeafNode; + EXPECT_EQ(sizeof(leaf_t), (pnanovdb_grid_type_constants[grid_type].leaf_size)); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mOffset), PNANOVDB_LEAF_OFF_VALUE_MASK + 64); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mPrefixSum), PNANOVDB_LEAF_OFF_VALUE_MASK + 64 + 8); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mMask), PNANOVDB_LEAF_OFF_VALUE_MASK + 64 + 8 + 8); +} +// template specializations for nanovdb::ValueOnIndex types +template <> +void validateLeaf(pnanovdb_grid_type_t grid_type) +{ + using leaf_t = typename nanovdb::LeafNode; + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mOffset), PNANOVDB_LEAF_OFF_VALUE_MASK + 64); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mPrefixSum), PNANOVDB_LEAF_OFF_VALUE_MASK + 64 + 8); EXPECT_EQ(sizeof(leaf_t), (pnanovdb_grid_type_constants[grid_type].leaf_size)); - EXPECT_EQ(NANOVDB_OFFSETOF(nodeLeaf_t, mBBoxMin), PNANOVDB_LEAF_OFF_BBOX_MIN); - EXPECT_EQ(NANOVDB_OFFSETOF(nodeLeaf_t, mBBoxDif), PNANOVDB_LEAF_OFF_BBOX_DIF_AND_FLAGS); - EXPECT_EQ(NANOVDB_OFFSETOF(nodeLeaf_t, mValueMask), PNANOVDB_LEAF_OFF_VALUE_MASK); +} + +// template specializations for nanovdb::ValueOnIndexMask types +template <> +void validateLeaf(pnanovdb_grid_type_t grid_type) +{ + using leaf_t = typename nanovdb::LeafNode; + EXPECT_EQ(sizeof(leaf_t), (pnanovdb_grid_type_constants[grid_type].leaf_size)); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mOffset), PNANOVDB_LEAF_OFF_VALUE_MASK + 64); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mPrefixSum), PNANOVDB_LEAF_OFF_VALUE_MASK + 64 + 8); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mMask), PNANOVDB_LEAF_OFF_VALUE_MASK + 64 + 8 + 8); +} + +// template specializations for nanovdb::Point types +template <> +void validateLeaf(pnanovdb_grid_type_t grid_type) +{ + using leaf_t = typename nanovdb::LeafNode; + EXPECT_EQ(sizeof(leaf_t), (pnanovdb_grid_type_constants[grid_type].leaf_size)); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mOffset), PNANOVDB_LEAF_OFF_VALUE_MASK + 64); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mPointCount), PNANOVDB_LEAF_OFF_VALUE_MASK + 64 + 8); + EXPECT_EQ(NANOVDB_OFFSETOF(leaf_t, mValues), PNANOVDB_LEAF_OFF_VALUE_MASK + 64 + 8 + 8); } TYPED_TEST(TestOffsets, PNanoVDB) @@ -4366,6 +4993,12 @@ TYPED_TEST(TestOffsets, PNanoVDB) grid_type = PNANOVDB_GRID_TYPE_MASK; } else if (std::is_same::value) { grid_type = PNANOVDB_GRID_TYPE_INDEX; + } else if (std::is_same::value) { + grid_type = PNANOVDB_GRID_TYPE_ONINDEX; + } else if (std::is_same::value) { + grid_type = PNANOVDB_GRID_TYPE_INDEXMASK; + } else if (std::is_same::value) { + grid_type = PNANOVDB_GRID_TYPE_ONINDEXMASK; } else if (std::is_same::value) { grid_type = PNANOVDB_GRID_TYPE_BOOLEAN; } else if (std::is_same::value) { @@ -4376,10 +5009,15 @@ TYPED_TEST(TestOffsets, PNanoVDB) grid_type = PNANOVDB_GRID_TYPE_FP16; } else if (std::is_same::value) { grid_type = PNANOVDB_GRID_TYPE_FPN; + } else if (std::is_same::value) { + grid_type = PNANOVDB_GRID_TYPE_POINTINDEX; + } else if (std::is_same::value) { + grid_type = PNANOVDB_GRID_TYPE_VEC3U8; + } else if (std::is_same::value) { + grid_type = PNANOVDB_GRID_TYPE_VEC3U16; } else { - EXPECT_TRUE(false); + EXPECT_TRUE(!"your forgot to add a grid_type to TestOffsets::PNanoVDB!"); } - static const uint32_t rootLevel = 3u; using nodeLeaf_t = typename nanovdb::LeafData; using leaf_t = typename nanovdb::LeafNode; using nodeLower_t = typename nanovdb::InternalData; @@ -4390,7 +5028,7 @@ TYPED_TEST(TestOffsets, PNanoVDB) using root_t = typename nanovdb::RootNode; using rootdata_tile_t = typename nanovdb::RootData::Tile; using root_tile_t = typename nanovdb::RootNode::Tile; - using treedata_t = typename nanovdb::TreeData; + using treedata_t = nanovdb::TreeData; using tree_t = typename nanovdb::Tree; // grid @@ -4426,9 +5064,9 @@ TYPED_TEST(TestOffsets, PNanoVDB) // test GridBlindMetaData EXPECT_EQ((int)sizeof(nanovdb::GridBlindMetaData), PNANOVDB_GRIDBLINDMETADATA_SIZE); - EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::GridBlindMetaData, mByteOffset), PNANOVDB_GRIDBLINDMETADATA_OFF_BYTE_OFFSET); - EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::GridBlindMetaData, mElementCount), PNANOVDB_GRIDBLINDMETADATA_OFF_ELEMENT_COUNT); - EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::GridBlindMetaData, mFlags), PNANOVDB_GRIDBLINDMETADATA_OFF_FLAGS); + EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::GridBlindMetaData, mDataOffset), PNANOVDB_GRIDBLINDMETADATA_OFF_BYTE_OFFSET); + EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::GridBlindMetaData, mValueCount), PNANOVDB_GRIDBLINDMETADATA_OFF_ELEMENT_COUNT); + EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::GridBlindMetaData, mValueSize), PNANOVDB_GRIDBLINDMETADATA_OFF_FLAGS); EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::GridBlindMetaData, mSemantic), PNANOVDB_GRIDBLINDMETADATA_OFF_SEMANTIC); EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::GridBlindMetaData, mDataClass), PNANOVDB_GRIDBLINDMETADATA_OFF_DATA_CLASS); EXPECT_EQ(NANOVDB_OFFSETOF(nanovdb::GridBlindMetaData, mDataType), PNANOVDB_GRIDBLINDMETADATA_OFF_DATA_TYPE); @@ -4543,25 +5181,26 @@ TYPED_TEST(TestOffsets, PNanoVDB) EXPECT_EQ(8u*sizeof(nodeLower_t::mStdDevi), pnanovdb_grid_type_stat_strides_bits[grid_type]); // leaf nodes - EXPECT_EQ(sizeof(leaf_t), (pnanovdb_grid_type_constants[grid_type].leaf_size)); - EXPECT_EQ(NANOVDB_OFFSETOF(nodeLeaf_t, mBBoxMin), PNANOVDB_LEAF_OFF_BBOX_MIN); - EXPECT_EQ(NANOVDB_OFFSETOF(nodeLeaf_t, mBBoxDif), PNANOVDB_LEAF_OFF_BBOX_DIF_AND_FLAGS); + // The following data members exist in all flavors of the leaf nodes so we test them first + EXPECT_EQ(NANOVDB_OFFSETOF(nodeLeaf_t, mBBoxMin), PNANOVDB_LEAF_OFF_BBOX_MIN); + EXPECT_EQ(NANOVDB_OFFSETOF(nodeLeaf_t, mBBoxDif), PNANOVDB_LEAF_OFF_BBOX_DIF_AND_FLAGS); EXPECT_EQ(NANOVDB_OFFSETOF(nodeLeaf_t, mValueMask), PNANOVDB_LEAF_OFF_VALUE_MASK); validateLeaf(grid_type); }// PNanoVDB -#endif +#endif // DISABLE_PNANOVDB TEST_F(TestNanoVDB, GridStats) { using GridT = nanovdb::NanoGrid; - Sphere sphere(nanovdb::Vec3(50), 50.0f); - nanovdb::GridBuilder builder(sphere.background(), nanovdb::GridClass::LevelSet); + Sphere sphere(nanovdb::Vec3d(50), 50.0f); + nanovdb::build::Grid grid(sphere.background(), "test", nanovdb::GridClass::LevelSet); const nanovdb::CoordBBox bbox(nanovdb::Coord(-100), nanovdb::Coord(100)); //mTimer.start("GridBuilder"); - builder(sphere, bbox); + grid(sphere, bbox); //mTimer.stop(); - auto handle1 = builder.getHandle<>(1.0, nanovdb::Vec3d(0.0), "test"); - auto handle2 = builder.getHandle<>(1.0, nanovdb::Vec3d(0.0), "test"); + nanovdb::CreateNanoGrid> converter(grid); + auto handle1 = converter.getHandle(); + auto handle2 = converter.getHandle(); EXPECT_TRUE(handle1); EXPECT_TRUE(handle2); GridT* grid1 = handle1.grid(); @@ -4579,12 +5218,10 @@ TEST_F(TestNanoVDB, GridStats) auto nodeMgrHandle2 = nanovdb::createNodeManager(*grid2); auto *mgr2 = nodeMgrHandle2.mgr(); EXPECT_TRUE(mgr2); - //nanovdb::NodeManager mgr1(*grid1); - //nanovdb::NodeManager mgr2(*grid2); { // reset stats in grid2 //grid2->tree().data()->mVoxelCount = uint64_t(0); - grid2->data()->mWorldBBox = nanovdb::BBox(); + grid2->data()->mWorldBBox = nanovdb::BBox(); grid2->tree().root().data()->mBBox = nanovdb::BBox(); for (uint32_t i = 0; i < grid2->tree().nodeCount(0); ++i) { auto& leaf = mgr2->leaf(i); @@ -4691,11 +5328,12 @@ TEST_F(TestNanoVDB, ScalarSampleFromVoxels) auto trilinearIndex = [&](const nanovdb::Coord& ijk) -> float { return 0.34f + 1.6f * dx * ijk[0] + 6.7f * dx * ijk[1] - 3.5f * dx * ijk[2]; // index coordinates }; - - nanovdb::GridBuilder builder(1.0f); - const nanovdb::CoordBBox bbox(nanovdb::Coord(0), nanovdb::Coord(128)); - builder(trilinearIndex, bbox); - auto handle = builder.getHandle<>(dx); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(1.0f); + srcGrid.setTransform(dx); + const nanovdb::CoordBBox bbox(nanovdb::Coord(0), nanovdb::Coord(128)); + srcGrid(trilinearIndex, bbox); + auto handle = nanovdb::createNanoGrid(srcGrid); EXPECT_TRUE(handle); EXPECT_EQ(1u, handle.gridCount()); auto* grid = handle.grid(); @@ -4748,11 +5386,12 @@ TEST_F(TestNanoVDB, VectorSampleFromVoxels) auto trilinearIndex = [&](const nanovdb::Coord& ijk) -> nanovdb::Vec3f { return nanovdb::Vec3f(0.34f, 1.6f * dx * ijk[0] + 6.7f * dx * ijk[1], -3.5f * dx * ijk[2]); // index coordinates }; - - nanovdb::GridBuilder builder(nanovdb::Vec3f(1.0f)); - const nanovdb::CoordBBox bbox(nanovdb::Coord(0), nanovdb::Coord(128)); - builder(trilinearIndex, bbox); - auto handle = builder.getHandle<>(dx); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(nanovdb::Vec3f(1.0f)); + const nanovdb::CoordBBox bbox(nanovdb::Coord(0), nanovdb::Coord(128)); + srcGrid(trilinearIndex, bbox); + srcGrid.setTransform(dx); + auto handle = nanovdb::createNanoGrid(srcGrid); EXPECT_TRUE(handle); EXPECT_EQ(1u, handle.gridCount()); auto* grid = handle.grid(); @@ -4796,16 +5435,16 @@ TEST_F(TestNanoVDB, GridChecksum) EXPECT_EQ(nanovdb::ChecksumMode::Default, nanovdb::ChecksumMode::Partial); EXPECT_NE(nanovdb::ChecksumMode::Default, nanovdb::ChecksumMode::Full); - nanovdb::CpuTimer<> timer; + nanovdb::CpuTimer timer; //timer.start("nanovdb::createLevelSetSphere"); - auto handle = nanovdb::createLevelSetSphere(100.0f, - nanovdb::Vec3f(50), - 1.0, - 3.0, - nanovdb::Vec3d(0), - "sphere_20", - nanovdb::StatsMode::Disable, - nanovdb::ChecksumMode::Disable); + auto handle = nanovdb::createLevelSetSphere(100.0f, + nanovdb::Vec3d(50), + 1.0, + 3.0, + nanovdb::Vec3d(0), + "sphere_20", + nanovdb::StatsMode::Disable, + nanovdb::ChecksumMode::Disable); //timer.stop(); EXPECT_TRUE(handle); EXPECT_EQ(1u, handle.gridCount()); @@ -4856,15 +5495,15 @@ TEST_F(TestNanoVDB, GridChecksum) TEST_F(TestNanoVDB, GridValidator) { - nanovdb::CpuTimer<> timer; + nanovdb::CpuTimer timer; //timer.start("nanovdb::createLevelSetSphere"); - auto handle = nanovdb::createLevelSetSphere(100.0f, - nanovdb::Vec3f(50), - 1.0, 3.0, - nanovdb::Vec3d(0), - "sphere_20", - nanovdb::StatsMode::All, - nanovdb::ChecksumMode::Full); + auto handle = nanovdb::createLevelSetSphere(100.0f, + nanovdb::Vec3d(50), + 1.0, 3.0, + nanovdb::Vec3d(0), + "sphere_20", + nanovdb::StatsMode::All, + nanovdb::ChecksumMode::Full); //timer.stop(); EXPECT_TRUE(handle); EXPECT_EQ(1u, handle.gridCount()); @@ -4909,10 +5548,10 @@ TEST_F(TestNanoVDB, RandomReadAccessor) const int voxelCount = 512, min = -10000, max = 10000; std::srand(98765); auto op = [&](){return rand() % (max - min) + min;}; - + using SrcGridT = nanovdb::build::Grid; for (int i=0; i<10; ++i) { - nanovdb::GridBuilder builder(background); - auto acc = builder.getAccessor(); + SrcGridT srcGrid(background); + auto acc = srcGrid.getAccessor(); std::vector voxels(voxelCount); for (int j=0; j(); + auto gridHdl = nanovdb::createNanoGrid(srcGrid); EXPECT_TRUE(gridHdl); EXPECT_EQ(1u, gridHdl.gridCount()); auto grid = gridHdl.grid(); @@ -4970,17 +5609,18 @@ TEST_F(TestNanoVDB, RandomReadAccessor) TEST_F(TestNanoVDB, StandardDeviation) { - nanovdb::GridBuilder builder(0.5f); + using OpT = nanovdb::GetNodeInfo; + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.5f); { - auto acc = builder.getAccessor(); + auto acc = srcGrid.getAccessor(); acc.setValue(nanovdb::Coord(-1), 1.0f); acc.setValue(nanovdb::Coord(0), 2.0f); acc.setValue(nanovdb::Coord(1), 3.0f); acc.setValue(nanovdb::Coord(2), 0.0f); } - - auto gridHdl = builder.getHandle<>(); + auto gridHdl = nanovdb::createNanoGrid(srcGrid); EXPECT_TRUE(gridHdl); auto grid = gridHdl.grid(); EXPECT_TRUE(grid); @@ -4992,26 +5632,49 @@ TEST_F(TestNanoVDB, StandardDeviation) EXPECT_EQ( 2.0f, acc.getValue(nanovdb::Coord( 0)) ); EXPECT_EQ( 3.0f, acc.getValue(nanovdb::Coord( 1)) ); EXPECT_EQ( 0.0f, acc.getValue(nanovdb::Coord( 2)) ); +#if 0 auto nodeInfo = acc.getNodeInfo(nanovdb::Coord(-1)); EXPECT_EQ(nodeInfo.mAverage, 1.f); EXPECT_EQ(nodeInfo.mLevel, 0u); EXPECT_EQ(nodeInfo.mDim, 8u); + { + auto nodeInfo = acc.getNodeInfo(nanovdb::Coord(1)); + EXPECT_EQ(nodeInfo.mAverage, (2.0f + 3.0f) / 3.0f); + auto getStdDev = [&](int n, float a, float b, float c) { + float m = (a + b + c) / n; + float sd = sqrtf(((a - m) * (a - m) + + (b - m) * (b - m) + + (c - m) * (c - m)) / + n); + return sd; + }; + EXPECT_NEAR(nodeInfo.mStdDevi, getStdDev(3.0f, 2.0f, 3.0f, 0), 1e-5); + EXPECT_EQ(nodeInfo.mLevel, 0u); + EXPECT_EQ(nodeInfo.mDim, 8u); + } +#else + auto nodeInfo = acc.get(nanovdb::Coord(-1)); + EXPECT_EQ(nodeInfo.average, 1.f); + EXPECT_EQ(nodeInfo.level, 0u); + EXPECT_EQ(nodeInfo.dim, 8u); + { + auto nodeInfo = acc.get(nanovdb::Coord(1)); + EXPECT_EQ(nodeInfo.average, (2.0f + 3.0f) / 3.0f); + auto getStdDev = [&](int n, float a, float b, float c) { + float m = (a + b + c) / n; + float sd = sqrtf(((a - m) * (a - m) + + (b - m) * (b - m) + + (c - m) * (c - m)) / + n); + return sd; + }; + EXPECT_NEAR(nodeInfo.stdDevi, getStdDev(3.0f, 2.0f, 3.0f, 0), 1e-5); + EXPECT_EQ(nodeInfo.level, 0u); + EXPECT_EQ(nodeInfo.dim, 8u); + } +#endif } - { - auto nodeInfo = acc.getNodeInfo(nanovdb::Coord(1)); - EXPECT_EQ(nodeInfo.mAverage, (2.0f + 3.0f) / 3.0f); - auto getStdDev = [&](int n, float a, float b, float c) { - float m = (a + b + c) / n; - float sd = sqrtf(((a - m) * (a - m) + - (b - m) * (b - m) + - (c - m) * (c - m)) / - n); - return sd; - }; - EXPECT_NEAR(nodeInfo.mStdDevi, getStdDev(3.0f, 2.0f, 3.0f, 0), 1e-5); - EXPECT_EQ(nodeInfo.mLevel, 0u); - EXPECT_EQ(nodeInfo.mDim, 8u); - } + } // ReadAccessor TEST_F(TestNanoVDB, BoxStencil) @@ -5019,12 +5682,13 @@ TEST_F(TestNanoVDB, BoxStencil) const float a = 0.54f, b[3]={0.12f, 0.78f,-0.34f}; const nanovdb::Coord min(-17, -10, -8), max(10, 21, 13); const nanovdb::CoordBBox bbox(min, max), bbox2(min, max.offsetBy(-1)); - nanovdb::GridBuilder builder(0.0f); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.0f); auto func = [&](const nanovdb::Coord &ijk) { return a + b[0]*ijk[0] + b[1]*ijk[1] + b[2]*ijk[2]; }; - builder(func, bbox); - auto handle = builder.getHandle(); + srcGrid(func, bbox); + auto handle = nanovdb::createNanoGrid(srcGrid); EXPECT_TRUE(handle); EXPECT_EQ(1u, handle.gridCount()); auto* grid = handle.grid(); @@ -5053,8 +5717,8 @@ TEST_F(TestNanoVDB, CurvatureStencil) { {// test of level set to sphere at (6,8,10) with R=10 and dx=0.5 const float radius = 10.0f; - const nanovdb::Vec3f center(6.0, 8.0, 10.0);//i.e. (12,16,20) in index space - auto handle = nanovdb::createLevelSetSphere(radius, + const nanovdb::Vec3d center(6.0, 8.0, 10.0);//i.e. (12,16,20) in index space + auto handle = nanovdb::createLevelSetSphere(radius, center, 0.5, // dx 20.0); // half-width so dense inside @@ -5117,9 +5781,9 @@ TEST_F(TestNanoVDB, CurvatureStencil) const int dim = 256; // sparse level set sphere - nanovdb::Vec3f C(0.35f, 0.35f, 0.35f); + nanovdb::Vec3d C(0.35f, 0.35f, 0.35f); double r = 0.15, voxelSize = 1.0/(dim-1); - auto handle = nanovdb::createLevelSetSphere(float(r), C, voxelSize); + auto handle = nanovdb::createLevelSetSphere(r, C, voxelSize); EXPECT_TRUE(handle); EXPECT_EQ(1u, handle.gridCount()); auto* sphere = handle.grid(); @@ -5193,7 +5857,7 @@ TEST_F(TestNanoVDB, GradStencil) { {// test of level set to sphere at (6,8,10) with R=10 and dx=0.5 const float radius = 10.0f;// 20 voxels - const nanovdb::Vec3f center(6.0, 8.0, 10.0);//i.e. (12,16,20) in index space + const nanovdb::Vec3d center(6.0, 8.0, 10.0);//i.e. (12,16,20) in index space auto handle = nanovdb::createLevelSetSphere(radius, center, 0.5, // dx @@ -5241,7 +5905,7 @@ TEST_F(TestNanoVDB, WenoStencil) { {// test of level set to sphere at (6,8,10) with R=10 and dx=0.5 const float radius = 10.0f;// 20 voxels - const nanovdb::Vec3f center(6.0, 8.0, 10.0);//i.e. (12,16,20) in index space + const nanovdb::Vec3d center(6.0, 8.0, 10.0);//i.e. (12,16,20) in index space auto handle = nanovdb::createLevelSetSphere(radius, center, 0.5, // dx @@ -5287,11 +5951,13 @@ TEST_F(TestNanoVDB, WenoStencil) TEST_F(TestNanoVDB, StencilIntersection) { + using SrcGridT = nanovdb::build::Grid; const nanovdb::Coord ijk(1,4,-9); - nanovdb::GridBuilder builder(0.0f); - auto acc = builder.getAccessor(); + SrcGridT srcGrid(0.0f); + auto acc = srcGrid.getAccessor(); acc.setValue(ijk,-1.0f); int cases = 0; + for (int mx=0; mx<2; ++mx) { acc.setValue(ijk.offsetBy(-1,0,0), mx ? 1.0f : -1.0f); for (int px=0; px<2; ++px) { @@ -5305,7 +5971,7 @@ TEST_F(TestNanoVDB, StencilIntersection) for (int pz=0; pz<2; ++pz) { acc.setValue(ijk.offsetBy(0,0,1), pz ? 1.0f : -1.0f); ++cases; - auto handle = builder.getHandle<>(); + auto handle = nanovdb::createNanoGrid(srcGrid); EXPECT_TRUE(handle); auto grid = handle.grid(); EXPECT_TRUE(grid); @@ -5318,12 +5984,12 @@ TEST_F(TestNanoVDB, StencilIntersection) EXPECT_TRUE(mask.none() == (count == 0)); EXPECT_TRUE(mask.any() == (count > 0)); EXPECT_EQ(count, mask.count()); - EXPECT_TRUE(mask.test(0) == mx); - EXPECT_TRUE(mask.test(1) == px); - EXPECT_TRUE(mask.test(2) == my); - EXPECT_TRUE(mask.test(3) == py); - EXPECT_TRUE(mask.test(4) == mz); - EXPECT_TRUE(mask.test(5) == pz); + EXPECT_TRUE(mask.test(0) == (mx > 0)); + EXPECT_TRUE(mask.test(1) == (px > 0)); + EXPECT_TRUE(mask.test(2) == (my > 0)); + EXPECT_TRUE(mask.test(3) == (py > 0)); + EXPECT_TRUE(mask.test(4) == (mz > 0)); + EXPECT_TRUE(mask.test(5) == (pz > 0)); }//pz }//mz }//py @@ -5335,43 +6001,47 @@ TEST_F(TestNanoVDB, StencilIntersection) TEST_F(TestNanoVDB, MultiFile) { + { // check nanovdb::io::stringHash + EXPECT_EQ(nanovdb::io::stringHash("generated_id_0"), nanovdb::io::stringHash("generated_id_0")); + EXPECT_NE(nanovdb::io::stringHash("generated_id_0"), nanovdb::io::stringHash("generated_id_1")); + EXPECT_EQ(0u, nanovdb::io::stringHash("\0")); + EXPECT_EQ(0u, nanovdb::io::stringHash(nullptr)); + } std::vector> handles; { // add an int32_t grid - nanovdb::GridBuilder builder(-1); - auto acc = builder.getAccessor(); + nanovdb::build::Grid grid(-1, "Int32 grid"); + auto acc = grid.getAccessor(); acc.setValue(nanovdb::Coord(-256), 10); - handles.push_back(builder.getHandle(1.0, nanovdb::Vec3d(0), "Int32 grid")); + handles.push_back(nanovdb::createNanoGrid(grid)); } { // add an empty int32_t grid - nanovdb::GridBuilder builder(-4); - handles.push_back(builder.getHandle(1.0, nanovdb::Vec3d(0), "Int32 grid, empty")); + nanovdb::build::Grid grid(-4, "Int32 grid, empty"); + handles.push_back(nanovdb::createNanoGrid(grid)); } { // add a Vec3f grid - nanovdb::GridBuilder builder(nanovdb::Vec3f(0.0f, 0.0f, -1.0f)); - builder.setGridClass(nanovdb::GridClass::Staggered); - auto acc = builder.getAccessor(); + nanovdb::build::Grid grid(nanovdb::Vec3f(0.0f, 0.0f, -1.0f),"Float vector grid",nanovdb::GridClass::Staggered); + auto acc = grid.getAccessor(); acc.setValue(nanovdb::Coord(-256), nanovdb::Vec3f(1.0f, 0.0f, 0.0f)); - handles.push_back(builder.getHandle(1.0, nanovdb::Vec3d(0), "Float vector grid")); + handles.push_back(nanovdb::createNanoGrid(grid)); } { // add an int64_t grid - nanovdb::GridBuilder builder(0); - auto acc = builder.getAccessor(); + nanovdb::build::Grid grid(0, "Int64 grid"); + auto acc = grid.getAccessor(); acc.setValue(nanovdb::Coord(0), 10); - handles.push_back(builder.getHandle(1.0, nanovdb::Vec3d(0), "Int64 grid")); + handles.push_back(nanovdb::createNanoGrid(grid)); } for (int i = 0; i < 10; ++i) { const float radius = 100.0f; const float voxelSize = 1.0f, width = 3.0f; - const nanovdb::Vec3f center(i * 10.0f, 0.0f, 0.0f); + const nanovdb::Vec3d center(i * 10.0f, 0.0f, 0.0f); handles.push_back(nanovdb::createLevelSetSphere(radius, center, voxelSize, width, nanovdb::Vec3d(0), "Level set sphere at (" + std::to_string(i * 10) + ",0,0)")); } { // add a double grid - nanovdb::GridBuilder builder(0.0); - builder.setGridClass(nanovdb::GridClass::FogVolume); - auto acc = builder.getAccessor(); + nanovdb::build::Grid grid(0.0, "Double grid", nanovdb::GridClass::FogVolume); + auto acc = grid.getAccessor(); acc.setValue(nanovdb::Coord(6000), 1.0); - handles.push_back(builder.getHandle(1.0, nanovdb::Vec3d(0), "Double grid")); + handles.push_back(nanovdb::createNanoGrid(grid)); } #if defined(NANOVDB_USE_BLOSC) nanovdb::io::writeGrids("data/multi1.nvdb", handles, nanovdb::io::Codec::BLOSC); @@ -5547,8 +6217,8 @@ TEST_F(TestNanoVDB, HostBuffer) std::vector > gridHdls; // create two grids... - gridHdls.push_back(nanovdb::createLevelSetSphere(100.0f, nanovdb::Vec3f(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3R(0), "spheref")); - gridHdls.push_back(nanovdb::createLevelSetSphere(100.0, nanovdb::Vec3d( 20, 0, 0), 1.0, 3.0, nanovdb::Vec3R(0), "sphered")); + gridHdls.push_back(nanovdb::createLevelSetSphere(100.0, nanovdb::Vec3d(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "spheref")); + gridHdls.push_back(nanovdb::createLevelSetSphere(100.0, nanovdb::Vec3d( 20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphered")); EXPECT_TRUE(gridHdls[0]); auto* meta0 = gridHdls[0].gridMetaData(); @@ -5586,8 +6256,8 @@ TEST_F(TestNanoVDB, HostBuffer) std::vector > gridHdls; // create two grids... - gridHdls.push_back(nanovdb::createLevelSetSphere(100.0f, nanovdb::Vec3f(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3R(0), "spheref", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, -1.0f, false, pool)); - gridHdls.push_back(nanovdb::createLevelSetSphere(100.0, nanovdb::Vec3d( 20, 0, 0), 1.0, 3.0, nanovdb::Vec3R(0), "sphered", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, -1.0f, false, pool)); + gridHdls.push_back(nanovdb::createLevelSetSphere(100.0, nanovdb::Vec3d(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "spheref", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, pool)); + gridHdls.push_back(nanovdb::createLevelSetSphere(100.0, nanovdb::Vec3d( 20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphered", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, pool)); EXPECT_TRUE(gridHdls[0]); auto* meta0 = gridHdls[0].gridMetaData(); @@ -5667,8 +6337,8 @@ TEST_F(TestNanoVDB, HostBuffer) std::vector > gridHdls; // create two grids... - ASSERT_THROW(gridHdls.push_back(nanovdb::createLevelSetSphere( 100.0f, nanovdb::Vec3f(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "spheref", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, -1.0f, false, pool)), std::runtime_error); - ASSERT_THROW(gridHdls.push_back(nanovdb::createLevelSetSphere( 100.0, nanovdb::Vec3d( 20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphered", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, -1.0f, false, pool)), std::runtime_error); + ASSERT_THROW(gridHdls.push_back(nanovdb::createLevelSetSphere( 100.0f, nanovdb::Vec3d(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "spheref", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, pool)), std::runtime_error); + ASSERT_THROW(gridHdls.push_back(nanovdb::createLevelSetSphere( 100.0, nanovdb::Vec3d( 20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphered", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, pool)), std::runtime_error); } {// zero internal memory size ASSERT_THROW(nanovdb::HostBuffer::createPool(0), std::runtime_error); @@ -5689,8 +6359,8 @@ TEST_F(TestNanoVDB, HostBuffer) std::vector > gridHdls; // create two grids... - gridHdls.push_back(nanovdb::createLevelSetSphere( 100.0f, nanovdb::Vec3f(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "spheref", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, -1.0f, false, pool)); - gridHdls.push_back(nanovdb::createLevelSetSphere( 100.0, nanovdb::Vec3d( 20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphered", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, -1.0f, false, pool)); + gridHdls.push_back(nanovdb::createLevelSetSphere( 100.0f, nanovdb::Vec3d(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "spheref", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, pool)); + gridHdls.push_back(nanovdb::createLevelSetSphere( 100.0, nanovdb::Vec3d( 20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphered", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, pool)); EXPECT_TRUE(gridHdls[0]); auto* meta0 = gridHdls[0].gridMetaData(); @@ -5745,8 +6415,8 @@ TEST_F(TestNanoVDB, HostBuffer) std::vector > gridHdls; // create two grids... - ASSERT_THROW(gridHdls.push_back(nanovdb::createLevelSetSphere( 100.0f, nanovdb::Vec3f(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "spheref", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, -1.0f, false, pool)), std::runtime_error); - ASSERT_THROW(gridHdls.push_back(nanovdb::createLevelSetSphere( 100.0, nanovdb::Vec3d( 20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphered", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, -1.0f, false, pool)), std::runtime_error); + ASSERT_THROW(gridHdls.push_back(nanovdb::createLevelSetSphere( 100.0, nanovdb::Vec3d(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "spheref", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, pool)), std::runtime_error); + ASSERT_THROW(gridHdls.push_back(nanovdb::createLevelSetSphere( 100.0, nanovdb::Vec3d( 20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphered", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, pool)), std::runtime_error); EXPECT_FALSE(pool.isManaged()); pool.resizePool(1<<26);// resize to 64 MB @@ -5767,8 +6437,8 @@ TEST_F(TestNanoVDB, HostBuffer) EXPECT_FALSE(buffer.isFull()); EXPECT_TRUE(buffer.isManaged()); - gridHdls.push_back(nanovdb::createLevelSetSphere( 100.0f, nanovdb::Vec3f(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "spheref", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, -1.0f, false, pool)); - gridHdls.push_back(nanovdb::createLevelSetSphere( 100.0, nanovdb::Vec3d( 20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphered", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, -1.0f, false, pool)); + gridHdls.push_back(nanovdb::createLevelSetSphere( 100.0, nanovdb::Vec3d(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "spheref", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, pool)); + gridHdls.push_back(nanovdb::createLevelSetSphere( 100.0, nanovdb::Vec3d( 20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphered", nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Partial, pool)); EXPECT_TRUE(gridHdls[0]); auto* meta0 = gridHdls[0].gridMetaData(); @@ -5841,9 +6511,9 @@ TEST_F(TestNanoVDB, NodeIterators) const double voxelSize = 0.1; const float radius = 10.0f; const float halfWidth = 3.0f; - const nanovdb::Vec3f center(0); + const nanovdb::Vec3d center(0); //mTimer.start("Create level set sphere"); - auto handle1 = nanovdb::createLevelSetSphere(radius, center, voxelSize, halfWidth); + auto handle1 = nanovdb::createLevelSetSphere(radius, center, voxelSize, halfWidth); //mTimer.stop(); auto *fltGrid = handle1.grid(); EXPECT_TRUE(fltGrid); @@ -5936,15 +6606,33 @@ TEST_F(TestNanoVDB, NodeIterators) } } -// make testNanoVDB && ./unittest/testNanoVDB --gtest_filter="*IndexGridBuilder*" --gtest_break_on_failure --gtest_repeat=5 -TEST_F(TestNanoVDB, IndexGridBuilder1) +// make testNanoVDB && ./unittest/testNanoVDB --gtest_filter="*BasicValueIndexStats*" --gtest_break_on_failure --gtest_repeat=5 +TEST_F(TestNanoVDB, BasicValueIndexStats) { + { + using ValueIndexT = typename nanovdb::NanoLeaf::DataType; + using ValueIndexMaskT = typename nanovdb::NanoLeaf::DataType; + using ValueOnIndexT = typename nanovdb::NanoLeaf::DataType; + using ValueOnIndexMaskT = typename nanovdb::NanoLeaf::DataType; + const size_t size1 = sizeof(ValueOnIndexT), + size2 = sizeof(ValueOnIndexMaskT), + size3 = sizeof(ValueIndexT), + size4 = sizeof(ValueIndexMaskT); + EXPECT_EQ(size1, ValueOnIndexT::memUsage()); + EXPECT_EQ(size2, ValueOnIndexMaskT::memUsage()); + EXPECT_EQ(size3, ValueIndexT::memUsage()); + EXPECT_EQ(size4, ValueIndexMaskT::memUsage()); + EXPECT_EQ(64u, size2 - size1);// 512 bits = 64 bytes + EXPECT_EQ(64u, size4 - size3);// 512 bits = 64 bytes + } EXPECT_TRUE(nanovdb::Version() >= nanovdb::Version(32,3,4)); - nanovdb::GridBuilder builder1(0.0f); - auto acc = builder1.getAccessor(); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.0f); + auto acc = srcGrid.getAccessor(); const nanovdb::Coord ijk(0,0,1); acc.setValue(ijk, 1.0f); - auto handle1 = builder1.getHandle(); + + auto handle1 = nanovdb::createNanoGrid(srcGrid); auto *fltGrid = handle1.grid(); EXPECT_TRUE(fltGrid); @@ -5960,11 +6648,16 @@ TEST_F(TestNanoVDB, IndexGridBuilder1) EXPECT_EQ(1.0f, fltGrid->tree().getValue(ijk)); EXPECT_EQ(0.0f, fltGrid->tree().getValue(nanovdb::Coord(0,0,0))); - nanovdb::IndexGridBuilder builder2(*fltGrid); - auto handle2 = builder2.getHandle(); + auto handle2 = nanovdb::createNanoGrid(*fltGrid, 1u, true, true); auto *idxGrid = handle2.grid(); EXPECT_TRUE(idxGrid); - + EXPECT_EQ(1u, idxGrid->blindDataCount()); + //std::cerr << "meta name = " << idxGrid->blindMetaData(0).mName << std::endl; + EXPECT_EQ(-1, idxGrid->findBlindData("channel_")); + EXPECT_EQ(-1, idxGrid->findBlindData("channel_0 ")); + EXPECT_EQ(-1, idxGrid->findBlindData(" channel_0")); + EXPECT_EQ( 0, idxGrid->findBlindData("channel_0")); + EXPECT_EQ(std::string("channel_0"), std::string(idxGrid->blindMetaData(0).mName)); EXPECT_EQ(1u, idxGrid->tree().nodeCount(2)); EXPECT_EQ(1u, idxGrid->tree().nodeCount(1)); EXPECT_EQ(1u, idxGrid->tree().nodeCount(0)); @@ -5974,21 +6667,21 @@ TEST_F(TestNanoVDB, IndexGridBuilder1) EXPECT_EQ(nanovdb::Vec3d(1.0,1.0,1.0), idxGrid->voxelSize()); EXPECT_EQ(1u, idxGrid->tree().root().tileCount()); EXPECT_EQ(1u, idxGrid->activeVoxelCount()); - EXPECT_EQ(5u+4u+32*32*32u-1 + 4u+16*16*16u-1 + 4u+8*8*8u, idxGrid->valueCount()); + EXPECT_EQ(5u + 4u+32*32*32u-1u + 4u+16*16*16u-1u + 4u+8*8*8u, idxGrid->valueCount()); EXPECT_EQ(0u, idxGrid->tree().root().background()); EXPECT_EQ(1u, idxGrid->tree().root().minimum()); EXPECT_EQ(2u, idxGrid->tree().root().maximum()); EXPECT_EQ(3u, idxGrid->tree().root().average()); EXPECT_EQ(4u, idxGrid->tree().root().stdDeviation()); - EXPECT_EQ(idxGrid->valueCount(), builder2.getValueCount()); + //EXPECT_EQ(idxGrid->valueCount(), converter2.valueCount()); EXPECT_FALSE(idxGrid->tree().isActive(nanovdb::Coord(0,0,0))); EXPECT_TRUE(idxGrid->tree().isActive(ijk)); - EXPECT_EQ(5u+4u+32*32*32-1+4u+16*16*16-1+4u, idxGrid->tree().getValue(nanovdb::Coord(0,0,0))); - EXPECT_EQ(5u+4u+32*32*32-1+4u+16*16*16-1+4u+1u, idxGrid->tree().getValue(nanovdb::Coord(0,0,1))); - EXPECT_EQ(5u+4u+32*32*32-1+4u+16*16*16-1+4u+7u, idxGrid->tree().getValue(nanovdb::Coord(0,0,7))); - EXPECT_EQ(5u+4u+32*32*32-1+4u+16*16*16-1+4u+8*8*8-1u, idxGrid->tree().getValue(nanovdb::Coord(7,7,7))); + EXPECT_EQ(5u + 4u+32*32*32-1 + 4u+16*16*16-1 + 0u, idxGrid->tree().getValue(nanovdb::Coord(0,0,0))); + EXPECT_EQ(5u + 4u+32*32*32-1 + 4u+16*16*16-1 + 1u, idxGrid->tree().getValue(nanovdb::Coord(0,0,1))); + EXPECT_EQ(5u + 4u+32*32*32-1 + 4u+16*16*16-1 + 7u, idxGrid->tree().getValue(nanovdb::Coord(0,0,7))); + EXPECT_EQ(5u + 4u+32*32*32-1 + 4u+16*16*16-1 + 8*8*8-1u, idxGrid->tree().getValue(nanovdb::Coord(7,7,7))); EXPECT_EQ(0u, idxGrid->tree().getValue(nanovdb::Coord(-1,0,0))); EXPECT_EQ(0u, idxGrid->tree().getValue(nanovdb::Coord(-1,0,0))); @@ -6001,32 +6694,150 @@ TEST_F(TestNanoVDB, IndexGridBuilder1) EXPECT_EQ(nanovdb::Coord(0), fltAcc.getNode<2>()->origin()); auto idxAcc = idxGrid->getAccessor(); - EXPECT_EQ(5u+4u+32*32*32-1+4u+16*16*16-1+4u, idxAcc.getValue(nanovdb::Coord(0,0,0))); - EXPECT_EQ(5u+4u+32*32*32-1+4u+16*16*16-1+4u+1u, idxAcc.getValue(nanovdb::Coord(0,0,1))); - EXPECT_EQ(5u+4u+32*32*32-1+4u+16*16*16-1+4u+7u, idxAcc.getValue(nanovdb::Coord(0,0,7))); + EXPECT_EQ(5u + 4u+32*32*32-1 + 4u+16*16*16-1 + 0u, idxAcc.getValue(nanovdb::Coord(0,0,0))); + EXPECT_EQ(5u + 4u+32*32*32-1 + 4u+16*16*16-1 + 1u, idxAcc.getValue(nanovdb::Coord(0,0,1))); + EXPECT_EQ(5u + 4u+32*32*32-1 + 4u+16*16*16-1 + 7u, idxAcc.getValue(nanovdb::Coord(0,0,7))); EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<0>()->origin()); EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<1>()->origin()); EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<2>()->origin()); - auto buffer = builder2.getValues(1);// only allocate one channel - //std::cerr << "Value buffer count: " << (buffer.size()>>10) << "KB" << std::endl; - float *values = reinterpret_cast(buffer.data()); - // compare the values of the functor with the original fltGrid + const float *values = idxGrid->getBlindData(0); + EXPECT_TRUE(values); + EXPECT_EQ(values[0], srcGrid.tree().root().background()); for (auto iter = idxGrid->indexBBox().begin(); iter; ++iter) { - //std::cerr << "Grid" << *iter << " = " << getValue(*iter) << std::endl; + //std::cerr << "Grid" << *iter << " = " << fltGrid->tree().getValue(*iter) << std::endl; EXPECT_EQ(values[idxAcc.getValue(*iter)], fltGrid->tree().getValue(*iter)); } +}// BasicValueIndexStats -}// IndexGridBuilder1 +// make testNanoVDB && ./unittest/testNanoVDB --gtest_filter="*BasicValueIndexStats*" --gtest_break_on_failure --gtest_repeat=5 +TEST_F(TestNanoVDB, BasicValueIndexStats2) +{ + EXPECT_TRUE(nanovdb::Version() >= nanovdb::Version(32,3,4)); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.0f); + auto acc = srcGrid.getAccessor(); + const nanovdb::Coord ijk(0,0,1); + acc.setValue(ijk, 1.0f); -TEST_F(TestNanoVDB, SparseIndexGridBuilder1) + auto handle2 = nanovdb::createNanoGrid(srcGrid, 1u, true, true); + auto *idxGrid = handle2.grid(); + EXPECT_TRUE(idxGrid); + + EXPECT_EQ(1u, idxGrid->tree().nodeCount(2)); + EXPECT_EQ(1u, idxGrid->tree().nodeCount(1)); + EXPECT_EQ(1u, idxGrid->tree().nodeCount(0)); + EXPECT_EQ(1u, idxGrid->gridCount()); + EXPECT_EQ(nanovdb::Vec3d(1.0,1.0,1.0), idxGrid->voxelSize()); + EXPECT_EQ(1u, idxGrid->tree().root().tileCount()); + EXPECT_EQ(1u, idxGrid->activeVoxelCount()); + EXPECT_EQ(5u + 4u+32*32*32u-1u + 4u+16*16*16u-1u + 4u+8*8*8u, idxGrid->valueCount()); + EXPECT_EQ(0u, idxGrid->tree().root().background()); + EXPECT_EQ(1u, idxGrid->tree().root().minimum()); + EXPECT_EQ(2u, idxGrid->tree().root().maximum()); + EXPECT_EQ(3u, idxGrid->tree().root().average()); + EXPECT_EQ(4u, idxGrid->tree().root().stdDeviation()); + + EXPECT_FALSE(idxGrid->tree().isActive(nanovdb::Coord(0,0,0))); + EXPECT_TRUE(idxGrid->tree().isActive(ijk)); + + EXPECT_EQ(5u + 4u+32*32*32-1 + 4u+16*16*16-1 + 0u, idxGrid->tree().getValue(nanovdb::Coord(0,0,0))); + EXPECT_EQ(5u + 4u+32*32*32-1 + 4u+16*16*16-1 + 1u, idxGrid->tree().getValue(nanovdb::Coord(0,0,1))); + EXPECT_EQ(5u + 4u+32*32*32-1 + 4u+16*16*16-1 + 7u, idxGrid->tree().getValue(nanovdb::Coord(0,0,7))); + EXPECT_EQ(5u + 4u+32*32*32-1 + 4u+16*16*16-1 + 8*8*8-1u, idxGrid->tree().getValue(nanovdb::Coord(7,7,7))); + EXPECT_EQ(0u, idxGrid->tree().getValue(nanovdb::Coord(-1,0,0))); + EXPECT_EQ(0u, idxGrid->tree().getValue(nanovdb::Coord(-1,0,0))); + + //auto fltAcc = fltGrid->getAccessor(); + EXPECT_EQ(0.0f, acc.getValue(nanovdb::Coord(0,0,0))); + EXPECT_EQ(1.0f, acc.getValue(nanovdb::Coord(0,0,1))); + EXPECT_EQ(0.0f, acc.getValue(nanovdb::Coord(0,0,7))); + //EXPECT_EQ(nanovdb::Coord(0), fltAcc.getNode<0>()->origin()); + //EXPECT_EQ(nanovdb::Coord(0), fltAcc.getNode<1>()->origin()); + //EXPECT_EQ(nanovdb::Coord(0), fltAcc.getNode<2>()->origin()); + + auto idxAcc = idxGrid->getAccessor(); + const uint64_t count = 5u + 4u+32*32*32-1 + 4u+16*16*16-1; + EXPECT_EQ(count + 0u, idxAcc.getValue(nanovdb::Coord(0,0,0))); + EXPECT_EQ(count + 1u, idxAcc.getValue(nanovdb::Coord(0,0,1))); + EXPECT_EQ(count + 7u, idxAcc.getValue(nanovdb::Coord(0,0,7))); + EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<0>()->origin()); + EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<1>()->origin()); + EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<2>()->origin()); + + auto *leaf = idxAcc.probeLeaf(nanovdb::Coord(0,0,-1)); + EXPECT_FALSE(leaf); + leaf = idxAcc.probeLeaf(nanovdb::Coord(0,0,1)); + EXPECT_TRUE(leaf); + EXPECT_EQ(count + 512u, leaf->minimum()); + EXPECT_EQ(count + 513u, leaf->maximum()); + EXPECT_EQ(count + 514u, leaf->average()); + EXPECT_EQ(count + 515u, leaf->stdDeviation()); + + const float *values = idxGrid->getBlindData(0); + EXPECT_TRUE(values); + for (auto iter = idxGrid->indexBBox().begin(); iter; ++iter) { + EXPECT_EQ(values[idxAcc.getValue(*iter)], acc.getValue(*iter)); + } + +}// BasicValueIndexStats2 + +TEST_F(TestNanoVDB, ValueMask2ValueIndex) +{ + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(true); + auto acc = srcGrid.getAccessor(); + const nanovdb::Coord ijk(0,0,1); + acc.setValue(ijk, true); + auto handle = nanovdb::createNanoGrid(srcGrid, 0u, false, false);// no stats or tiles + auto *idxGrid = handle.grid(); + EXPECT_TRUE(idxGrid); + EXPECT_EQ(1u, idxGrid->activeVoxelCount()); + EXPECT_EQ(1u + 512u, idxGrid->valueCount());// background and 512 leaf values +}// ValueMask2ValueIndex + +TEST_F(TestNanoVDB, ValueMask2ValueOnIndex) +{ + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(true); + auto acc = srcGrid.getAccessor(); + const nanovdb::Coord ijk(0,0,1); + acc.setValue(ijk, true); + auto handle = nanovdb::createNanoGrid(srcGrid, 0u, true, false);// stats but no tiles + auto *idxGrid = handle.grid(); + EXPECT_TRUE(idxGrid); + EXPECT_EQ(1u, idxGrid->activeVoxelCount()); + EXPECT_EQ(1u + 4u + 1u, idxGrid->valueCount());// background, stats, and one active value + + auto idxAcc = idxGrid->getAccessor(); + const uint64_t count = 1u;// background + EXPECT_EQ(0u, idxAcc.getValue(nanovdb::Coord(0,0,0))); + EXPECT_EQ(1u, idxAcc.getValue(nanovdb::Coord(0,0,1))); + EXPECT_EQ(0u, idxAcc.getValue(nanovdb::Coord(0,0,7))); + EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<0>()->origin()); + EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<1>()->origin()); + EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<2>()->origin()); + + auto *leaf = idxAcc.probeLeaf(nanovdb::Coord(0,0,-1)); + EXPECT_FALSE(leaf); + leaf = idxAcc.probeLeaf(nanovdb::Coord(0,0,1)); + EXPECT_TRUE(leaf); + EXPECT_EQ(count + 1u, leaf->minimum()); + EXPECT_EQ(count + 2u, leaf->maximum()); + EXPECT_EQ(count + 3u, leaf->average()); + EXPECT_EQ(count + 4u, leaf->stdDeviation()); +}// ValueMask2ValueOnIndex + +TEST_F(TestNanoVDB, BasicValueIndexNoStats) { EXPECT_TRUE(nanovdb::Version() >= nanovdb::Version(32,3,4)); - nanovdb::GridBuilder builder1(0.0f); - auto acc = builder1.getAccessor(); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.0f); + auto acc = srcGrid.getAccessor(); const nanovdb::Coord ijk(0,0,1); acc.setValue(ijk, 1.0f); - auto handle1 = builder1.getHandle(); + nanovdb::CreateNanoGrid converter(srcGrid); + auto handle1 = converter.getHandle(); auto *fltGrid = handle1.grid(); EXPECT_TRUE(fltGrid); @@ -6042,11 +6853,176 @@ TEST_F(TestNanoVDB, SparseIndexGridBuilder1) EXPECT_EQ(1.0f, fltGrid->tree().getValue(ijk)); EXPECT_EQ(0.0f, fltGrid->tree().getValue(nanovdb::Coord(0,0,0))); - nanovdb::IndexGridBuilder builder2(*fltGrid, false, false);// no stats and no inactive values - auto handle2 = builder2.getHandle(); + auto handle2 = converter.getHandle(1, false, true); auto *idxGrid = handle2.grid(); EXPECT_TRUE(idxGrid); + EXPECT_EQ(1u, idxGrid->tree().nodeCount(2)); + EXPECT_EQ(1u, idxGrid->tree().nodeCount(1)); + EXPECT_EQ(1u, idxGrid->tree().nodeCount(0)); + EXPECT_EQ(1u, idxGrid->gridCount()); + EXPECT_EQ(fltGrid->worldBBox(), idxGrid->worldBBox()); + EXPECT_EQ(fltGrid->indexBBox(), idxGrid->indexBBox()); + EXPECT_EQ(nanovdb::Vec3d(1.0,1.0,1.0), idxGrid->voxelSize()); + EXPECT_EQ(1u, idxGrid->tree().root().tileCount()); + EXPECT_EQ(1u, idxGrid->activeVoxelCount()); + EXPECT_EQ(1u + 32*32*32u-1u + 16*16*16u-1u + 8*8*8u, idxGrid->valueCount()); + EXPECT_EQ(0u, idxGrid->tree().root().background()); + EXPECT_EQ(0u, idxGrid->tree().root().minimum()); + EXPECT_EQ(0u, idxGrid->tree().root().maximum()); + EXPECT_EQ(0u, idxGrid->tree().root().average()); + EXPECT_EQ(0u, idxGrid->tree().root().stdDeviation()); + + EXPECT_FALSE(idxGrid->tree().isActive(nanovdb::Coord(0,0,0))); + EXPECT_TRUE(idxGrid->tree().isActive(ijk)); + + EXPECT_EQ(1u + 32*32*32-1 + 16*16*16-1 + 0u, idxGrid->tree().getValue(nanovdb::Coord(0,0,0))); + EXPECT_EQ(1u + 32*32*32-1 + 16*16*16-1 + 1u, idxGrid->tree().getValue(nanovdb::Coord(0,0,1))); + EXPECT_EQ(1u + 32*32*32-1 + 16*16*16-1 + 7u, idxGrid->tree().getValue(nanovdb::Coord(0,0,7))); + EXPECT_EQ(1u + 32*32*32-1 + 16*16*16-1 + 8*8*8-1u, idxGrid->tree().getValue(nanovdb::Coord(7,7,7))); + EXPECT_EQ(0u, idxGrid->tree().getValue(nanovdb::Coord(-1,0,0))); + EXPECT_EQ(0u, idxGrid->tree().getValue(nanovdb::Coord(-1,0,0))); + + auto fltAcc = fltGrid->getAccessor(); + EXPECT_EQ(0.0f, fltAcc.getValue(nanovdb::Coord(0,0,0))); + EXPECT_EQ(1.0f, fltAcc.getValue(nanovdb::Coord(0,0,1))); + EXPECT_EQ(0.0f, fltAcc.getValue(nanovdb::Coord(0,0,7))); + EXPECT_EQ(nanovdb::Coord(0), fltAcc.getNode<0>()->origin()); + EXPECT_EQ(nanovdb::Coord(0), fltAcc.getNode<1>()->origin()); + EXPECT_EQ(nanovdb::Coord(0), fltAcc.getNode<2>()->origin()); + + auto idxAcc = idxGrid->getAccessor(); + EXPECT_EQ(1u + 32*32*32-1 + 16*16*16-1 + 0u, idxAcc.getValue(nanovdb::Coord(0,0,0))); + EXPECT_EQ(1u + 32*32*32-1 + 16*16*16-1 + 1u, idxAcc.getValue(nanovdb::Coord(0,0,1))); + EXPECT_EQ(1u + 32*32*32-1 + 16*16*16-1 + 7u, idxAcc.getValue(nanovdb::Coord(0,0,7))); + EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<0>()->origin()); + EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<1>()->origin()); + EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<2>()->origin()); + + const float *values = idxGrid->getBlindData(0); + EXPECT_TRUE(values); + EXPECT_EQ(values[0], srcGrid.tree().root().background()); + for (auto iter = idxGrid->indexBBox().begin(); iter; ++iter) { + //std::cerr << "Grid" << *iter << " = " << fltGrid->tree().getValue(*iter) << std::endl; + EXPECT_EQ(values[idxAcc.getValue(*iter)], fltGrid->tree().getValue(*iter)); + } +}// BasicValueIndexNoStats + +TEST_F(TestNanoVDB, BasicValueIndexNoStatsNoTiles) +{ + EXPECT_TRUE(nanovdb::Version() >= nanovdb::Version(32,3,4)); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.0f); + auto acc = srcGrid.getAccessor(); + const nanovdb::Coord ijk(0,0,1); + acc.setValue(ijk, 1.0f); + nanovdb::CreateNanoGrid converter(srcGrid); + + auto handle1 = converter.getHandle(); + auto *fltGrid = handle1.grid(); + EXPECT_TRUE(fltGrid); + + EXPECT_EQ(1u, fltGrid->tree().nodeCount(2)); + EXPECT_EQ(1u, fltGrid->tree().nodeCount(1)); + EXPECT_EQ(1u, fltGrid->tree().nodeCount(0)); + EXPECT_EQ(1u, fltGrid->gridCount()); + EXPECT_EQ(nanovdb::Vec3d(1.0,1.0,1.0), fltGrid->voxelSize()); + EXPECT_EQ(1u, fltGrid->tree().root().tileCount()); + EXPECT_EQ(1u, fltGrid->activeVoxelCount()); + EXPECT_FALSE(fltGrid->tree().isActive(nanovdb::Coord(0,0,0))); + EXPECT_TRUE(fltGrid->tree().isActive(ijk)); + EXPECT_EQ(1.0f, fltGrid->tree().getValue(ijk)); + EXPECT_EQ(0.0f, fltGrid->tree().getValue(nanovdb::Coord(0,0,0))); + + auto handle2 = converter.getHandle(1u, false, false); + auto *idxGrid = handle2.grid(); + EXPECT_TRUE(idxGrid); + + EXPECT_EQ(1u, idxGrid->tree().nodeCount(2)); + EXPECT_EQ(1u, idxGrid->tree().nodeCount(1)); + EXPECT_EQ(1u, idxGrid->tree().nodeCount(0)); + EXPECT_EQ(1u, idxGrid->gridCount()); + EXPECT_EQ(fltGrid->worldBBox(), idxGrid->worldBBox()); + EXPECT_EQ(fltGrid->indexBBox(), idxGrid->indexBBox()); + EXPECT_EQ(nanovdb::Vec3d(1.0,1.0,1.0), idxGrid->voxelSize()); + EXPECT_EQ(1u, idxGrid->tree().root().tileCount()); + EXPECT_EQ(1u, idxGrid->activeVoxelCount()); + EXPECT_EQ(1u + 8*8*8u, idxGrid->valueCount()); + EXPECT_EQ(0u, idxGrid->tree().root().background()); + EXPECT_EQ(0u, idxGrid->tree().root().minimum()); + EXPECT_EQ(0u, idxGrid->tree().root().maximum()); + EXPECT_EQ(0u, idxGrid->tree().root().average()); + EXPECT_EQ(0u, idxGrid->tree().root().stdDeviation()); + + EXPECT_FALSE(idxGrid->tree().isActive(nanovdb::Coord(0,0,0))); + EXPECT_TRUE(idxGrid->tree().isActive(ijk)); + + EXPECT_EQ(1u + 0u, idxGrid->tree().getValue(nanovdb::Coord(0,0,0))); + EXPECT_EQ(1u + 1u, idxGrid->tree().getValue(nanovdb::Coord(0,0,1))); + EXPECT_EQ(1u + 7u, idxGrid->tree().getValue(nanovdb::Coord(0,0,7))); + EXPECT_EQ(1u + 8*8*8-1u, idxGrid->tree().getValue(nanovdb::Coord(7,7,7))); + EXPECT_EQ(0u, idxGrid->tree().getValue(nanovdb::Coord(-1,0,0))); + EXPECT_EQ(0u, idxGrid->tree().getValue(nanovdb::Coord(-1,0,0))); + + auto fltAcc = fltGrid->getAccessor(); + EXPECT_EQ(0.0f, fltAcc.getValue(nanovdb::Coord(0,0,0))); + EXPECT_EQ(1.0f, fltAcc.getValue(nanovdb::Coord(0,0,1))); + EXPECT_EQ(0.0f, fltAcc.getValue(nanovdb::Coord(0,0,7))); + EXPECT_EQ(nanovdb::Coord(0), fltAcc.getNode<0>()->origin()); + EXPECT_EQ(nanovdb::Coord(0), fltAcc.getNode<1>()->origin()); + EXPECT_EQ(nanovdb::Coord(0), fltAcc.getNode<2>()->origin()); + + auto idxAcc = idxGrid->getAccessor(); + EXPECT_EQ(1u + 0u, idxAcc.getValue(nanovdb::Coord(0,0,0))); + EXPECT_EQ(1u + 1u, idxAcc.getValue(nanovdb::Coord(0,0,1))); + EXPECT_EQ(1u + 7u, idxAcc.getValue(nanovdb::Coord(0,0,7))); + EXPECT_EQ(1u + 8*8*8-1u, idxAcc.getValue(nanovdb::Coord(7,7,7))); + EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<0>()->origin()); + EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<1>()->origin()); + EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<2>()->origin()); + + const float *values = idxGrid->getBlindData(0); + EXPECT_TRUE(values); + EXPECT_EQ(values[0], srcGrid.tree().root().background()); + for (auto iter = idxGrid->indexBBox().begin(); iter; ++iter) { + //std::cerr << "Grid" << *iter << " = " << fltGrid->tree().getValue(*iter) << std::endl; + if (auto *leaf = idxAcc.probeLeaf(*iter)) { + EXPECT_FALSE(leaf->data()->hasStats()); + EXPECT_EQ(512u, leaf->data()->valueCount());// ValueIndex produces dense leaf nodes + EXPECT_EQ(values[idxAcc.getValue(*iter)], fltGrid->tree().getValue(*iter)); + } + } +}// BasicValueIndexNoStatsNoTiles + +TEST_F(TestNanoVDB, SparseIndexGridBuilder1) +{ + EXPECT_TRUE(nanovdb::Version() >= nanovdb::Version(32,3,4)); + using SrcGridT = nanovdb::build::Grid; + SrcGridT srcGrid(0.0f); + auto acc = srcGrid.getAccessor(); + const nanovdb::Coord ijk(0,0,1); + acc.setValue(ijk, 1.0f); + nanovdb::CreateNanoGrid converter(srcGrid); + auto handle1 = converter.getHandle(); + auto *fltGrid = handle1.grid(); + EXPECT_TRUE(fltGrid); + + EXPECT_EQ(1u, fltGrid->tree().nodeCount(2)); + EXPECT_EQ(1u, fltGrid->tree().nodeCount(1)); + EXPECT_EQ(1u, fltGrid->tree().nodeCount(0)); + EXPECT_EQ(1u, fltGrid->gridCount()); + EXPECT_EQ(nanovdb::Vec3d(1.0,1.0,1.0), fltGrid->voxelSize()); + EXPECT_EQ(1u, fltGrid->tree().root().tileCount()); + EXPECT_EQ(1u, fltGrid->activeVoxelCount()); + EXPECT_FALSE(fltGrid->tree().isActive(nanovdb::Coord(0,0,0))); + EXPECT_TRUE(fltGrid->tree().isActive(ijk)); + EXPECT_EQ(1.0f, fltGrid->tree().getValue(ijk)); + EXPECT_EQ(0.0f, fltGrid->tree().getValue(nanovdb::Coord(0,0,0))); + + auto handle2 = converter.getHandle(1u, false, true);// no stats and include active tile values + auto *idxGrid = handle2.grid(); + EXPECT_TRUE(idxGrid); + EXPECT_EQ(1u, idxGrid->tree().nodeCount(2)); EXPECT_EQ(1u, idxGrid->tree().nodeCount(1)); EXPECT_EQ(1u, idxGrid->tree().nodeCount(0)); @@ -6062,7 +7038,6 @@ TEST_F(TestNanoVDB, SparseIndexGridBuilder1) EXPECT_EQ(0u, idxGrid->tree().root().average()); EXPECT_EQ(0u, idxGrid->tree().root().stdDeviation()); EXPECT_EQ(2u, idxGrid->valueCount());// background + ijk(0,0,1) - EXPECT_EQ(idxGrid->valueCount(), builder2.getValueCount()); EXPECT_FALSE(idxGrid->tree().isActive(nanovdb::Coord(0,0,0))); EXPECT_TRUE(idxGrid->tree().isActive(ijk)); @@ -6090,12 +7065,10 @@ TEST_F(TestNanoVDB, SparseIndexGridBuilder1) EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<1>()->origin()); EXPECT_EQ(nanovdb::Coord(0), idxAcc.getNode<2>()->origin()); - auto buffer = builder2.getValues(1);// only allocate one channel - //std::cerr << "Value buffer size: " << buffer.size() << "bytes" << std::endl; - float *values = reinterpret_cast(buffer.data()); - // compare the values of the functor with the original fltGrid + const float *values = idxGrid->getBlindData(0); + EXPECT_TRUE(values); + EXPECT_EQ(values[0], srcGrid.tree().root().background()); for (auto iter = idxGrid->indexBBox().begin(); iter; ++iter) { - //std::cerr << "Grid" << *iter << " = " << getValue(*iter) << std::endl; EXPECT_EQ(values[idxAcc.getValue(*iter)], fltGrid->tree().getValue(*iter)); } @@ -6108,9 +7081,9 @@ TEST_F(TestNanoVDB, IndexGridBuilder2) const double voxelSize = 0.1; const float radius = 10.0f; const float halfWidth = 3.0f; - const nanovdb::Vec3f center(0); + const nanovdb::Vec3d center(0); //mTimer.start("Create level set sphere"); - auto handle1 = nanovdb::createLevelSetSphere(radius, center, voxelSize, halfWidth); + auto handle1 = nanovdb::createLevelSetSphere(radius, center, voxelSize, halfWidth); //mTimer.stop(); auto *fltGrid = handle1.grid(); EXPECT_TRUE(fltGrid); @@ -6119,9 +7092,9 @@ TEST_F(TestNanoVDB, IndexGridBuilder2) //std::cerr << "FloatGrid footprint: " << (fltGrid->gridSize()>>20) << "MB" << std::endl; // create an IndexGrid for the FloatGrid - nanovdb::IndexGridBuilder builder2(*fltGrid); + nanovdb::CreateNanoGrid builder2(*fltGrid); //mTimer.start("Create IndexGrid"); - auto handle2 = builder2.getHandle(); + auto handle2 = builder2.getHandle(1u); //mTimer.stop(); auto *idxGrid = handle2.grid(); EXPECT_TRUE(idxGrid); @@ -6146,9 +7119,8 @@ TEST_F(TestNanoVDB, IndexGridBuilder2) EXPECT_EQ(3u, idxGrid->tree().root().average()); EXPECT_EQ(4u, idxGrid->tree().root().stdDeviation()); - EXPECT_EQ(idxGrid->valueCount(), builder2.getValueCount()); - EXPECT_EQ(idxGrid->valueCount(), builder2.getValueCount()); - //EXPECT_EQ(idxAcc.valueCount(), builder2.getValueCount()); + EXPECT_EQ(idxGrid->valueCount(), builder2.valueCount()); + //EXPECT_EQ(idxAcc.valueCount(), builder2.valueCount()); EXPECT_TRUE(idxGrid->valueCount()>0);// this is the number of values pointed to by the indexGrid for (auto iter = fltGrid->indexBBox().begin(); iter; ++iter) { @@ -6157,9 +7129,9 @@ TEST_F(TestNanoVDB, IndexGridBuilder2) } {// allocate an external buffer and manually populate it with the floatGrid values - float *buffer = new float[builder2.getValueCount()];// this is the number of values pointed to by the indexGrid + float *buffer = new float[idxGrid->valueCount()];// this is the number of values pointed to by the indexGrid EXPECT_TRUE(buffer); - //std::cerr << "Buffer footprint: " << ((4*builder2.getValueCount())>>20) << "MB" << std::endl; + //std::cerr << "Buffer footprint: " << ((4*builder2.valueCount())>>20) << "MB" << std::endl; buffer[0] = fltTree.background(); for (auto iter = idxGrid->indexBBox().begin(); iter; ++iter) { const uint64_t idx = idxAcc.getValue(*iter); @@ -6168,6 +7140,7 @@ TEST_F(TestNanoVDB, IndexGridBuilder2) } // compare the values of the functor with the original fltGrid nanovdb::ChannelAccessor acc(*idxGrid, buffer); + EXPECT_TRUE(acc); for (auto iter = idxGrid->indexBBox().begin(); iter; ++iter) { EXPECT_EQ(buffer[idxAcc.getValue(*iter)], fltTree.getValue(*iter)); EXPECT_EQ(acc.getValue(*iter), fltTree.getValue(*iter)); @@ -6176,10 +7149,11 @@ TEST_F(TestNanoVDB, IndexGridBuilder2) } {// allocate an external buffer and populate it with the floatGrid values - float *buffer = new float[builder2.getValueCount()];// this is the number of values pointed to by the indexGrid + float *buffer = new float[builder2.valueCount()];// this is the number of values pointed to by the indexGrid EXPECT_TRUE(buffer); //std::cerr << "Buffer footprint: " << ((4*idxGrid->valueCount())>>20) << "MB" << std::endl; - EXPECT_TRUE(builder2.copyValues(buffer, builder2.getValueCount())); + builder2.copyValues(buffer); + //EXPECT_TRUE(builder2.copyValues(buffer, builder2.valueCount())); EXPECT_EQ(buffer[idxRoot.minimum()], fltRoot.minimum()); EXPECT_EQ(buffer[idxRoot.maximum()], fltRoot.maximum()); @@ -6188,6 +7162,7 @@ TEST_F(TestNanoVDB, IndexGridBuilder2) // compare the values of the functor with the original fltGrid nanovdb::ChannelAccessor acc(*idxGrid, buffer); + EXPECT_TRUE(acc); for (auto iter = idxGrid->indexBBox().begin(); iter; ++iter) { EXPECT_EQ(buffer[idxAcc.getValue(*iter)], fltTree.getValue(*iter)); EXPECT_EQ(acc.getValue(*iter), fltTree.getValue(*iter)); @@ -6196,12 +7171,13 @@ TEST_F(TestNanoVDB, IndexGridBuilder2) delete [] buffer; }// IndexGridBuilder2 - {// test the value buffer in IndexGridBuilder - //mTimer.start("Create value buffer"); - auto buffer = builder2.getValues(1);// only allocate one channel - //mTimer.stop(); - //std::cerr << "Value buffer footprint: " << (buffer.size()>>20) << "MB" << std::endl; - float *values = reinterpret_cast(buffer.data()); + {// test the value buffer in IndexGrid + const float *values = idxGrid->getBlindData(0); + EXPECT_TRUE(values); + EXPECT_EQ(values[idxRoot.minimum()], fltRoot.minimum()); + EXPECT_EQ(values[idxRoot.maximum()], fltRoot.maximum()); + EXPECT_EQ(values[idxRoot.average()], fltRoot.average()); + EXPECT_EQ(values[idxRoot.stdDeviation()], fltRoot.stdDeviation()); //mTimer.start("Sequential test of value buffer"); // compare the values of the functor with the original fltGrid for (auto iter = idxGrid->indexBBox().begin(); iter; ++iter) { @@ -6216,10 +7192,10 @@ TEST_F(TestNanoVDB, IndexGridBuilder2) }); auto fltAcc = fltTree.getAccessor();// NOT thread-safe! //mTimer.start("Dense IndexGrid: Sequential node iterator test of active voxels"); - for (auto it2 = idxRoot.beginChild(); it2; ++it2) { - for (auto it1 = it2->beginChild(); it1; ++it1) { - for (auto it0 = it1->beginChild(); it0; ++it0) { - for (auto vox = it0->beginValueOn(); vox; ++vox) { + for (auto it2 = idxRoot.cbeginChild(); it2; ++it2) { + for (auto it1 = it2->cbeginChild(); it1; ++it1) { + for (auto it0 = it1->cbeginChild(); it0; ++it0) { + for (auto vox = it0->cbeginValueOn(); vox; ++vox) { EXPECT_EQ(values[*vox], fltAcc.getValue(vox.getCoord())); }// loop over active voxels in the leaf node }// loop over child nodes of the lower internal nodes @@ -6270,9 +7246,9 @@ TEST_F(TestNanoVDB, SparseIndexGridBuilder2) const double voxelSize = 0.1; const float radius = 10.0f; const float halfWidth = 3.0f; - const nanovdb::Vec3f center(0); + const nanovdb::Vec3d center(0); //mTimer.start("Create level set sphere"); - auto handle1 = nanovdb::createLevelSetSphere(radius, center, voxelSize, halfWidth); + auto handle1 = nanovdb::createLevelSetSphere(radius, center, voxelSize, halfWidth); //mTimer.stop(); auto *fltGrid = handle1.grid(); EXPECT_TRUE(fltGrid); @@ -6281,11 +7257,11 @@ TEST_F(TestNanoVDB, SparseIndexGridBuilder2) //std::cerr << "FloatGrid footprint: " << (fltGrid->gridSize()>>20) << "MB" << std::endl; // create an IndexGrid for the FloatGrid - nanovdb::IndexGridBuilder builder2(*fltGrid, false, false); + nanovdb::CreateNanoGrid builder2(*fltGrid); //mTimer.start("Create IndexGrid"); - auto handle2 = builder2.getHandle(); + auto handle2 = builder2.getHandle(1u, false, true); //mTimer.stop(); - auto *idxGrid = handle2.grid(); + auto *idxGrid = handle2.grid(); EXPECT_TRUE(idxGrid); auto &idxTree = idxGrid->tree(); auto &idxRoot = idxTree.root(); @@ -6306,15 +7282,16 @@ TEST_F(TestNanoVDB, SparseIndexGridBuilder2) EXPECT_EQ(0u, idxRoot.maximum()); EXPECT_EQ(0u, idxRoot.average()); EXPECT_EQ(0u, idxRoot.stdDeviation()); - EXPECT_EQ(idxGrid->valueCount(), builder2.getValueCount()); - //(idxAcc.valueCount(), builder2.getValueCount()); + EXPECT_EQ(idxGrid->valueCount(), builder2.valueCount()); + //(idxAcc.valueCount(), builder2.valueCount()); EXPECT_TRUE(idxGrid->valueCount()>0);// this is the number of values pointed to by the indexGrid for (auto it = fltGrid->indexBBox().begin(); it; ++it) EXPECT_EQ(fltTree.isActive(*it), idxTree.isActive(*it)); {// allocate an external buffer and manually populate it with the floatGrid values - float *buffer = new float[builder2.getValueCount()];// this is the number of values pointed to by the indexGrid + float *buffer = new float[builder2.valueCount()];// this is the number of values pointed to by the indexGrid EXPECT_TRUE(buffer); + buffer[0] = fltTree.background();// not required since we only check active values //std::cerr << "Value buffer footprint: " << ((4*idxRoot.maximum())>>20) << "MB" << std::endl; for (auto iter = idxGrid->indexBBox().begin(); iter; ++iter) { const uint64_t idx = idxAcc.getValue(*iter); @@ -6323,7 +7300,10 @@ TEST_F(TestNanoVDB, SparseIndexGridBuilder2) } // compare the values of the functor with the original fltGrid for (auto it = idxGrid->indexBBox().begin(); it; ++it) { - if (fltTree.isActive(*it)) EXPECT_EQ(buffer[idxAcc.getValue(*it)], fltTree.getValue(*it)); + EXPECT_LT(idxAcc.getValue(*it), idxGrid->valueCount()); + if (fltTree.isActive(*it)) { + EXPECT_EQ(buffer[idxAcc.getValue(*it)], fltTree.getValue(*it)); + } } delete [] buffer; } @@ -6332,7 +7312,7 @@ TEST_F(TestNanoVDB, SparseIndexGridBuilder2) float *buffer = new float[idxGrid->valueCount()];// this is the number of values pointed to by the indexGrid EXPECT_TRUE(buffer); //std::cerr << "Buffer footprint: " << ((4*idxGrid->valueCount())>>20) << "MB" << std::endl; - EXPECT_TRUE(builder2.copyValues(buffer, idxGrid->valueCount())); + builder2.copyValues(buffer); // compare the values of the functor with the original fltGrid for (auto it = idxGrid->indexBBox().begin(); it; ++it) { @@ -6341,13 +7321,9 @@ TEST_F(TestNanoVDB, SparseIndexGridBuilder2) delete [] buffer; } - {// test the value buffer in IndexGridBuilder - //mTimer.start("Create value buffer"); - auto buffer = builder2.getValues(1);// only allocate one channel - //mTimer.stop(); - EXPECT_EQ(sizeof(float)*builder2.getValueCount(), buffer.size()); - //std::cerr << "Value buffer footprint: " << (buffer.size()>>20) << "MB" << std::endl; - float *values = reinterpret_cast(buffer.data()); + {// test the value buffer in IndexGrid + const float *values = idxGrid->getBlindData(0); + EXPECT_TRUE(values); //mTimer.start("Sequential test of value buffer"); // compare the values of the functor with the original fltGrid for (auto it = idxGrid->indexBBox().begin(); it; ++it) { @@ -6369,10 +7345,10 @@ TEST_F(TestNanoVDB, SparseIndexGridBuilder2) }); auto fltAcc = fltTree.getAccessor();// NOT thread-safe! //mTimer.start("Sparse IndexGrid: Sequential node iterator test of active voxels"); - for (auto it2 = idxRoot.beginChild(); it2; ++it2) { - for (auto it1 = it2->beginChild(); it1; ++it1) { - for (auto it0 = it1->beginChild(); it0; ++it0) { - for (auto v = it0->beginValueOn(); v; ++v) { + for (auto it2 = idxRoot.cbeginChild(); it2; ++it2) { + for (auto it1 = it2->cbeginChild(); it1; ++it1) { + for (auto it0 = it1->cbeginChild(); it0; ++it0) { + for (auto v = it0->cbeginValueOn(); v; ++v) { EXPECT_EQ(values[*v], fltAcc.getValue(v.getCoord())); }// loop over active voxels in the leaf node }// loop over child nodes of the lower internal nodes @@ -6408,9 +7384,9 @@ TEST_F(TestNanoVDB, ChannelIndexGridBuilder) const double voxelSize = 0.1; const float radius = 10.0f; const float halfWidth = 3.0f; - const nanovdb::Vec3f center(0); + const nanovdb::Vec3d center(0); //mTimer.start("Create level set sphere"); - auto handle1 = nanovdb::createLevelSetSphere(radius, center, voxelSize, halfWidth); + auto handle1 = nanovdb::createLevelSetSphere(radius, center, voxelSize, halfWidth); //mTimer.stop(); auto *fltGrid = handle1.grid(); EXPECT_TRUE(fltGrid); @@ -6419,9 +7395,9 @@ TEST_F(TestNanoVDB, ChannelIndexGridBuilder) //std::cerr << "FloatGrid footprint: " << (fltGrid->gridSize()>>20) << "MB" << std::endl; // create an IndexGrid for the FloatGrid - nanovdb::IndexGridBuilder builder2(*fltGrid, false, false); + nanovdb::CreateNanoGrid builder2(*fltGrid); //mTimer.start("Create IndexGrid"); - auto handle2 = builder2.getHandle("IndexGrid_test", channels); + auto handle2 = builder2.getHandle(channels, false); //mTimer.stop(); auto *idxGrid = handle2.grid(); EXPECT_TRUE(idxGrid); @@ -6439,20 +7415,21 @@ TEST_F(TestNanoVDB, ChannelIndexGridBuilder) EXPECT_EQ(fltGrid->activeVoxelCount(), idxGrid->activeVoxelCount()); EXPECT_EQ(fltGrid->worldBBox(), idxGrid->worldBBox()); EXPECT_EQ(fltGrid->indexBBox(), idxGrid->indexBBox()); - EXPECT_EQ(idxGrid->valueCount(), builder2.getValueCount()); + EXPECT_EQ(idxGrid->valueCount(), builder2.valueCount()); EXPECT_EQ(channels, idxGrid->blindDataCount()); EXPECT_TRUE(idxGrid->valueCount()>0);// this is the number of values pointed to by the indexGrid auto *leaf = idxTree.getFirstNode<0>(); for (uint32_t i=0; ivalueCount(), idxGrid->blindMetaData(i).mElementCount); + EXPECT_EQ(idxGrid->valueCount(), idxGrid->blindMetaData(i).mValueCount); EXPECT_EQ(nanovdb::GridType::Float, idxGrid->blindMetaData(i).mDataType); EXPECT_EQ(nanovdb::GridBlindDataClass::ChannelArray, idxGrid->blindMetaData(i).mDataClass); EXPECT_EQ(nanovdb::GridBlindDataSemantic::Unknown, idxGrid->blindMetaData(i).mSemantic); - const std::string name = std::string("float_channel_") + std::to_string(i); + const std::string name = std::string("channel_") + std::to_string(i); EXPECT_EQ(0, std::strcmp(idxGrid->blindMetaData(i).mName, name.c_str() )); //mTimer.start("Parallel leaf iterator test of active voxels in channel"); - auto *values = reinterpret_cast(idxGrid->blindData(i)); + const float *values = idxGrid->getBlindData(i); + EXPECT_TRUE(values); nanovdb::forEach(0,idxTree.nodeCount(0),8,[&](const nanovdb::Range1D &r){ auto fltAcc = fltTree.getAccessor();// NOT thread-safe! for (auto i=r.begin(); i!=r.end(); ++i){ @@ -6465,16 +7442,18 @@ TEST_F(TestNanoVDB, ChannelIndexGridBuilder) }; for (uint32_t i=0; ivalueCount(), idxGrid->blindMetaData(i).mElementCount); + EXPECT_EQ(idxGrid->valueCount(), idxGrid->blindMetaData(i).mValueCount); EXPECT_EQ(nanovdb::GridType::Float, idxGrid->blindMetaData(i).mDataType); EXPECT_EQ(nanovdb::GridBlindDataClass::ChannelArray, idxGrid->blindMetaData(i).mDataClass); EXPECT_EQ(nanovdb::GridBlindDataSemantic::Unknown, idxGrid->blindMetaData(i).mSemantic); - const std::string name = std::string("float_channel_") + std::to_string(i); + const std::string name = std::string("channel_") + std::to_string(i); EXPECT_EQ(0, std::strcmp(idxGrid->blindMetaData(i).mName, name.c_str() )); //mTimer.start("Parallel leaf iterator test of active voxels in channel"); - auto *values = reinterpret_cast(idxGrid->blindData(i)); + const float *values = idxGrid->getBlindData(i); + EXPECT_TRUE(values); nanovdb::forEach(0,idxTree.nodeCount(0),8,[&](const nanovdb::Range1D &r){ nanovdb::ChannelAccessor acc(*idxGrid, i);// NOT thread-safe + EXPECT_TRUE(acc); auto fltAcc = fltTree.getAccessor();// NOT thread-safe! float val; for (auto i=r.begin(); i!=r.end(); ++i){ @@ -6502,9 +7481,7 @@ TEST_F(TestNanoVDB, HelloWorld_IndexGrid_Dense) EXPECT_EQ(1.0f, fltGrid->tree().getValue(ijk)); {// create an IndexGrid with an internal channel and write it to file - nanovdb::IndexGridBuilder builder(*fltGrid, true, true);// include stats and inactive values - auto tmp = builder.getHandle("IndexGrid_test", 1u);// 1 channel - nanovdb::io::writeGrid("data/index_grid.nvdb", tmp); + nanovdb::io::writeGrid("data/index_grid.nvdb", nanovdb::createNanoGrid(*fltGrid,1u, true, true));// 1 channel, include stats and tile values } {// read and test IndexGrid auto tmp = nanovdb::io::readGrid("data/index_grid.nvdb"); @@ -6513,6 +7490,7 @@ TEST_F(TestNanoVDB, HelloWorld_IndexGrid_Dense) //std::cerr << "Dense IndexGrid size: " << (idxGrid->gridSize() >> 20) << " MB\n"; EXPECT_GT(idxGrid->gridSize(), fltGrid->gridSize()); nanovdb::ChannelAccessor acc(*idxGrid, 0u);// channel ID = 0 + EXPECT_TRUE(acc); EXPECT_EQ(1.0f, acc(ijk)); // compute the gradient from channel ID 0 @@ -6539,21 +7517,20 @@ TEST_F(TestNanoVDB, HelloWorld_IndexGrid_Sparse) EXPECT_EQ(1.0f, fltGrid->tree().getValue(ijk)); {// create an IndexGrid with an internal channel and write it to file - nanovdb::IndexGridBuilder builder(*fltGrid, false, false);// no stats, no inactive values - auto tmp = builder.getHandle("IndexGrid_test", 1u);// 1 channel - nanovdb::io::writeGrid("data/index_grid.nvdb", tmp); + nanovdb::io::writeGrid("data/index_grid.nvdb", nanovdb::createNanoGrid(*fltGrid, 1u, false, true));// 1 channel, no stats and include tile values } {// read and test IndexGrid auto tmp = nanovdb::io::readGrid("data/index_grid.nvdb"); - auto *idxGrid = tmp.grid(); + auto *idxGrid = tmp.grid(); EXPECT_TRUE(idxGrid); //std::cerr << "Sparse IndexGrid size: " << (idxGrid->gridSize() >> 20) << " MB\n"; EXPECT_LT(idxGrid->gridSize(), fltGrid->gridSize()); - nanovdb::ChannelAccessor acc(*idxGrid, 0u);// channel ID = 0 + nanovdb::ChannelAccessor acc(*idxGrid, 0u);// channel ID = 0 + EXPECT_TRUE(acc); EXPECT_EQ(1.0f, acc(ijk)); // compute the gradient from channel ID 0 - nanovdb::GradStencil> stencil(acc); + nanovdb::GradStencil> stencil(acc); stencil.moveTo(ijk); EXPECT_EQ(nanovdb::Vec3f(1.0f,0.0f,0.0f), stencil.gradient()); @@ -6566,6 +7543,42 @@ TEST_F(TestNanoVDB, HelloWorld_IndexGrid_Sparse) } }// HelloWorld_IndexGrid_Sparse +TEST_F(TestNanoVDB, HelloWorld_IndexGrid_Sparse2) +{ + const nanovdb::Coord ijk(101,0,0); + auto handle1 = nanovdb::createLevelSetSphere(); + auto *fltGrid = handle1.grid(); + EXPECT_TRUE(fltGrid); + //std::cerr << "Grid size: " << (fltGrid->gridSize() >> 20) << " MB\n"; + EXPECT_EQ(1.0f, fltGrid->tree().getValue(ijk)); + + {// create an IndexGrid with an internal channel and write it to file + nanovdb::io::writeGrid("data/index_grid2.nvdb", nanovdb::createNanoGrid(*fltGrid, 1u, false, false));// 1 channel, no stats and no tile values + } + {// read and test IndexGrid + auto tmp = nanovdb::io::readGrid("data/index_grid2.nvdb"); + auto *idxGrid = tmp.grid(); + EXPECT_TRUE(idxGrid); + //std::cerr << "Sparse IndexGrid size: " << (idxGrid->gridSize() >> 20) << " MB\n"; + EXPECT_LT(idxGrid->gridSize(), fltGrid->gridSize()); + nanovdb::ChannelAccessor acc(*idxGrid, 0u);// channel ID = 0 + EXPECT_TRUE(acc); + EXPECT_EQ(1.0f, acc(ijk)); + + // compute the gradient from channel ID 0 + nanovdb::GradStencil> stencil(acc); + stencil.moveTo(ijk); + EXPECT_EQ(nanovdb::Vec3f(1.0f,0.0f,0.0f), stencil.gradient()); + + EXPECT_EQ(0.0f, acc(100,0,0)); + acc(100,0,0) = 1.0f;// legal since acc was template on "float" and not "const float" + EXPECT_EQ(1.0f, acc(100,0,0)); + EXPECT_EQ(nanovdb::Vec3f(1.0f,0.0f,0.0f), stencil.gradient());// since stencil caches + stencil.moveTo(ijk);// re-populates the stencil cache + EXPECT_EQ(nanovdb::Vec3f(0.5f,0.0f,0.0f), stencil.gradient()); + } +}// HelloWorld_IndexGrid_Sparse2 + TEST_F(TestNanoVDB, writeReadUncompressedGrid) { using GridHandleT = nanovdb::GridHandle; @@ -6585,8 +7598,581 @@ TEST_F(TestNanoVDB, writeReadUncompressedGrid) auto *fltGrid2 = handles2[0].grid(); EXPECT_TRUE(fltGrid2); EXPECT_EQ(1.0f, fltGrid2->tree().getValue(ijk)); +}// writeReadUncompressedGrid + +TEST_F(TestNanoVDB, writeReadUncompressedGridRaw) +{ + using GridHandleT = nanovdb::GridHandle; + const nanovdb::Coord ijk(101,0,0); + std::vector handles1; + handles1.emplace_back(nanovdb::createLevelSetSphere()); + EXPECT_EQ(1u, handles1.size()); + auto *fltGrid1 = handles1[0].grid(); + EXPECT_TRUE(fltGrid1); + EXPECT_EQ(1.0f, fltGrid1->tree().getValue(ijk)); + + nanovdb::io::writeUncompressedGrids("data/test1_raw.nvdb", handles1, true); + + auto handles2 = nanovdb::io::readUncompressedGrids("data/test1_raw.nvdb"); + EXPECT_EQ(1u, handles2.size()); + + auto *fltGrid2 = handles2[0].grid(); + EXPECT_TRUE(fltGrid2); + EXPECT_EQ(1.0f, fltGrid2->tree().getValue(ijk)); +}// writeReadUncompressedGridRaw + +TEST_F(TestNanoVDB, GridMetaData) +{ + auto handle = nanovdb::createLevelSetSphere(); + auto *grid = handle.grid(); + EXPECT_TRUE(grid); + EXPECT_TRUE(grid->isRootConnected()); + nanovdb::GridMetaData meta(*grid);// deep copy + EXPECT_EQ(672 + 64 + 24 + 8, sizeof(meta)); + EXPECT_TRUE(nanovdb::GridMetaData::safeCast(*grid)); + auto *metaPtr = reinterpret_cast(grid); + EXPECT_EQ(meta.indexBBox(), metaPtr->indexBBox()); + EXPECT_EQ(meta.rootTableSize(), metaPtr->rootTableSize()); +} + +TEST_F(TestNanoVDB, BuildTree) +{ + nanovdb::CoordBBox bbox(nanovdb::Coord(0), nanovdb::Coord(511)); + nanovdb::build::Grid grid1(false), grid2(false); + { + //mTimer.start("Serial build::Tree"); + auto kernel = [&](const nanovdb::CoordBBox& bbox) { + auto acc = grid1.getAccessor(); + for (auto it = bbox.begin(); it; ++it) acc.setValueOn(*it); + }; + kernel(bbox); + //mTimer.stop(); + } + { + //mTimer.start("Parallel build::Tree"); + auto kernel = [&](const nanovdb::CoordBBox& bbox) { + auto acc = grid2.getWriteAccessor(); + for (auto it = bbox.begin(); it; ++it) acc.setValueOn(*it); + }; + nanovdb::forEach(bbox, kernel); + //mTimer.stop(); + } + { + auto acc1 = grid1.getAccessor(), acc2 = grid2.getAccessor(); + for (auto it = bbox.begin(); it; ++it) { + EXPECT_EQ(acc1.getValue(*it), acc2.getValue(*it)); + } + } +}// BuildTree + +TEST_F(TestNanoVDB, CreateNanoGridFromFloat) +{ + using SrcGridT = nanovdb::FloatGrid; + const float tolerance = 0.001f; + const nanovdb::Coord ijk(101,0,0); + auto srcHandle = nanovdb::createLevelSetSphere(); + SrcGridT *srcGrid = srcHandle.grid(); + EXPECT_TRUE(srcGrid); + //std::cerr << "Grid size: " << (srcGrid->gridSize() >> 20) << " MB\n"; + EXPECT_EQ(1.0f, srcGrid->tree().getValue(ijk)); + + nanovdb::CreateNanoGrid converter(*srcGrid); + + {// create nanovdb::FloatGrid from nanovdb::FloatGrid + using DstBuildT = float; + auto dstHandle = converter.getHandle(); + auto *dstGrid = dstHandle.grid(); + EXPECT_TRUE(dstGrid); + //std::cerr << "Grid<"<())<<"> size: " << (dstGrid->gridSize() >> 20) << " MB\n"; + EXPECT_EQ(1.0f, dstGrid->tree().getValue(ijk)); + } + {// create nanovdb::DoubleGrid from nanovdb::FloatGrid + using DstBuildT = double; + auto dstHandle = converter.getHandle(); + auto *dstGrid = dstHandle.grid(); + EXPECT_TRUE(dstGrid); + //std::cerr << "Grid<"<())<<"> size: " << (dstGrid->gridSize() >> 20) << " MB\n"; + EXPECT_EQ(1.0, dstGrid->tree().getValue(ijk)); + } + {// create nanovdb::Fp4Grid from nanovdb::FloatGrid + using DstBuildT = nanovdb::Fp4; + auto dstHandle = converter.getHandle(); + auto *dstGrid = dstHandle.grid(); + EXPECT_TRUE(dstGrid); + //std::cerr << "Grid<"<())<<"> size: " << (dstGrid->gridSize() >> 20) << " MB\n"; + EXPECT_NEAR(1.0f, dstGrid->tree().getValue(ijk), tolerance); + //EXPECT_EQ(1.0f, dstGrid->tree().getValue(ijk)); + } + {// create nanovdb::Fp8Grid from nanovdb::FloatGrid + using DstBuildT = nanovdb::Fp8; + auto dstHandle = converter.getHandle(); + auto *dstGrid = dstHandle.grid(); + EXPECT_TRUE(dstGrid); + //std::cerr << "Grid<"<())<<"> size: " << (dstGrid->gridSize() >> 20) << " MB\n"; + EXPECT_NEAR(1.0f, dstGrid->tree().getValue(ijk), tolerance); + //EXPECT_EQ(1.0f, dstGrid->tree().getValue(ijk)); + } + {// create nanovdb::Fp16Grid from nanovdb::FloatGrid + using DstBuildT = nanovdb::Fp16; + auto dstHandle = converter.getHandle(); + auto *dstGrid = dstHandle.grid(); + EXPECT_TRUE(dstGrid); + //std::cerr << "Grid<"<())<<"> size: " << (dstGrid->gridSize() >> 20) << " MB\n"; + EXPECT_NEAR(1.0f, dstGrid->tree().getValue(ijk), tolerance); + //EXPECT_EQ(1.0f, dstGrid->tree().getValue(ijk)); + } + {// create nanovdb::FpNGrid from nanovdb::FloatGrid + using DstBuildT = nanovdb::FpN; + auto dstHandle = converter.getHandle(); + auto *dstGrid = dstHandle.grid(); + EXPECT_TRUE(dstGrid); + //std::cerr << "Grid<"<())<<"> size: " << (dstGrid->gridSize() >> 20) << " MB\n"; + EXPECT_NEAR(1.0f, dstGrid->tree().getValue(ijk), tolerance); + //EXPECT_EQ(1.0f, dstGrid->tree().getValue(ijk)); + } + {// create nanovdb::MaskGrid from nanovdb::FloatGrid + using DstBuildT = nanovdb::ValueMask; + auto dstHandle = converter.getHandle(); + auto *dstGrid = dstHandle.grid(); + EXPECT_TRUE(dstGrid); + //std::cerr << "Grid<"<())<<"> size: " << (dstGrid->gridSize() >> 20) << " MB\n"; + EXPECT_EQ(true, dstGrid->tree().getValue(ijk)); + } +}// CreateNanoGridFromFloat + +TEST_F(TestNanoVDB, CreateNanoGridFromVec3f) +{ + using SrcBuildT = nanovdb::Vec3f; + using SrcGridT = nanovdb::build::Grid; + + // + const SrcBuildT a(1.5f,0.0f,-9.1f), b(0.0f,0.0f,0.0f); + SrcGridT grid(b); + const nanovdb::Coord p(0,0,7), q(0,0,0); + grid.setValue(p, a); + EXPECT_EQ(a, grid.tree().getValue(p)); + EXPECT_EQ(b, grid.tree().getValue(q)); + // + auto srcHandle = nanovdb::createNanoGrid(grid); + auto *srcGrid = srcHandle.grid(); + EXPECT_TRUE(srcGrid); + EXPECT_EQ(a, srcGrid->tree().getValue(p)); + EXPECT_EQ(b, srcGrid->tree().getValue(q)); + + {// create nanovdb::ValueIndexGrid from nanovdb::build::Grid + using DstBuildT = nanovdb::ValueIndex; + auto handle = nanovdb::createNanoGrid(grid, 0u, false, false);// no channels, stats or tiles + auto *idxGrid = handle.grid(); + EXPECT_TRUE(idxGrid); + EXPECT_EQ(1u, idxGrid->activeVoxelCount()); + EXPECT_EQ(1u + 512u, idxGrid->valueCount());// background and 512 leaf values + EXPECT_EQ(1, idxGrid->tree().getValue(q)); + EXPECT_EQ(8, idxGrid->tree().getValue(p)); + } + {// create nanovdb::ValueOnIndexGrid from nanovdb::build::Grid + using DstBuildT = nanovdb::ValueOnIndex; + auto handle = nanovdb::createNanoGrid(grid, 0u, false, false);// no channels, stats or tiles + auto *idxGrid = handle.grid(); + EXPECT_TRUE(idxGrid); + EXPECT_EQ(1u, idxGrid->activeVoxelCount()); + EXPECT_EQ(1u + 1u, idxGrid->valueCount());// background and one leaf value + EXPECT_EQ(0, idxGrid->tree().getValue(q)); + EXPECT_EQ(1, idxGrid->tree().getValue(p)); + } + {// create nanovdb::ValueIndexGrid from nanovdb::Grid + using DstBuildT = nanovdb::ValueIndex; + using SrcGridT = nanovdb::Vec3fGrid; + auto handle = nanovdb::createNanoGrid(*srcGrid, 0u, false, false);// no channels, stats or tiles + auto *idxGrid = handle.grid(); + EXPECT_TRUE(idxGrid); + EXPECT_EQ(1u, idxGrid->activeVoxelCount()); + EXPECT_EQ(1u + 512u, idxGrid->valueCount());// background and 512 leaf values + EXPECT_EQ(1, idxGrid->tree().getValue(q)); + EXPECT_EQ(8, idxGrid->tree().getValue(p)); + } + {// create nanovdb::ValueOnIndexGrid from nanovdb::Grid + using DstBuildT = nanovdb::ValueOnIndex; + using SrcGridT = nanovdb::Vec3fGrid; + auto handle = nanovdb::createNanoGrid(*srcGrid, 0u, false, false);// no channels, stats or tiles + auto *idxGrid = handle.grid(); + EXPECT_TRUE(idxGrid); + EXPECT_EQ(1u, idxGrid->activeVoxelCount()); + EXPECT_EQ(1u + 1u, idxGrid->valueCount());// background and 512 leaf values + EXPECT_EQ(0, idxGrid->tree().getValue(q)); + EXPECT_EQ(1, idxGrid->tree().getValue(p)); + } +}// CreateNanoGridFromVec3f + +TEST_F(TestNanoVDB, LongGridName) +{ + using SrcGridT = nanovdb::build::Grid; + nanovdb::GridData tmp; + tmp.init(); + EXPECT_EQ('\0', tmp.mGridName[0]); + for (int n = -10; n <= 10; ++n) { + SrcGridT srcGrid(0.0f); + const int limit = nanovdb::GridData::MaxNameSize - 1, length = limit + n; + char buffer[limit + 10 + 1] = {'\0'}; + srand (time(NULL)); + for (int i = 0; i < length; ++i) buffer[i] = 'a' + (rand() % 26);// a-z + buffer[length] = '\0'; + const std::string gridName(buffer); + //std::cout << "Long random grid name: " << gridName << std::endl; + EXPECT_EQ(gridName.length(), size_t(length)); + srcGrid.setName(gridName); + EXPECT_EQ(gridName, srcGrid.getName()); + srcGrid.tree().setValue(nanovdb::Coord(-256), 10.0f); + const bool isLong = length > limit; + auto handle = nanovdb::createNanoGrid(srcGrid); + auto* dstGrid = handle.grid(); + EXPECT_TRUE(dstGrid); + EXPECT_EQ(1u, dstGrid->activeVoxelCount()); + EXPECT_EQ(isLong ? 1u : 0u, dstGrid->blindDataCount()); + EXPECT_EQ(isLong, dstGrid->hasLongGridName()); + //std::cerr << "\nHas long grid name: " << (isLong?"yes":"no") << std::endl; + //std::cerr << "length = " << length << ", limit = " << limit << std::endl; + EXPECT_EQ(gridName, std::string(dstGrid->gridName())); + EXPECT_EQ( !isLong, std::string(dstGrid->shortGridName()) == std::string(dstGrid->gridName()) ); + EXPECT_EQ( 0.0, dstGrid->tree().getValue(nanovdb::Coord(-255))); + EXPECT_EQ(10.0, dstGrid->tree().getValue(nanovdb::Coord(-256))); + EXPECT_EQ(!isLong, tmp.setGridName(gridName.c_str())); + const char *ptr = dstGrid->getBlindData(0);// might be NULL + if (isLong) { + EXPECT_TRUE(ptr); + EXPECT_STREQ(buffer, dstGrid->gridName()); + EXPECT_STREQ(buffer, ptr); + EXPECT_EQ(ptr, dstGrid->gridName());// should point to the same memory + const nanovdb::GridBlindMetaData &blindMeta = dstGrid->blindMetaData(0); + //const nanovdb::GridBlindMetaData test = dstGrid->blindMetaData(0);// fails since + EXPECT_EQ(nanovdb::GridBlindDataClass::GridName, blindMeta.mDataClass); + EXPECT_EQ(nanovdb::GridBlindDataSemantic::Unknown, blindMeta.mSemantic); + EXPECT_EQ(nanovdb::GridType::Unknown, blindMeta.mDataType); + EXPECT_EQ(length + 1, blindMeta.mValueCount);// number of characters + terminating 0 + EXPECT_EQ(1u, blindMeta.mValueSize);// byte size of a character + EXPECT_TRUE(blindMeta.isValid()); + const char *str = blindMeta.getBlindData(); + EXPECT_TRUE(str); + //printf("ptr at address: %p\n", (const void*)ptr); + //printf("str at address: %p\n", (const void*)str); + EXPECT_EQ(str, ptr); + EXPECT_STREQ(buffer, ptr); + EXPECT_STREQ(buffer, str); + } else { + EXPECT_FALSE(ptr); + EXPECT_EQ(gridName, std::string(tmp.mGridName)); + for (int i = length; i<=limit; ++i) EXPECT_EQ('\0', tmp.mGridName[i]); + } + } +}// LongGridName + +TEST_F(TestNanoVDB, mergeSplitGrids) +{ + size_t size1 = 0, size2 = 0; + std::vector> handles1, handles2; + std::vector gridNames; + //nanovdb::CpuTimer timer("create 5 host grids"); + for (int radius = 100; radius<150; radius += 10) { + gridNames.emplace_back("sphere_" + std::to_string(radius)); + handles1.emplace_back(nanovdb::createLevelSetSphere(radius,nanovdb::Vec3d(0),1,3, + nanovdb::Vec3d(0), gridNames.back())); + EXPECT_FALSE(handles1.back().isPadded()); + size1 += handles1.back().size(); + } + EXPECT_EQ(5u, gridNames.size()); + EXPECT_EQ(5u, handles1.size()); + //timer.restart("create 5 host grids"); + for (int radius = 150; radius<200; radius += 10) { + gridNames.emplace_back("sphere_" + std::to_string(radius)); + handles2.emplace_back(nanovdb::createLevelSetSphere(radius,nanovdb::Vec3d(0),1,3, + nanovdb::Vec3d(0), gridNames.back())); + size2 += handles2.back().size(); + } + EXPECT_EQ(10u, gridNames.size()); + EXPECT_EQ( 5u, handles2.size()); + //timer.restart("merging 5 host grids"); + auto mergedHandle = nanovdb::mergeGrids(handles2);// merge last 5 grid handles + EXPECT_EQ(size2, mergedHandle.size()); + EXPECT_FALSE(mergedHandle.isPadded()); + EXPECT_TRUE(mergedHandle.data()); + auto *gridData = mergedHandle.gridData();// first grid + EXPECT_TRUE(gridData); + EXPECT_EQ(5u, gridData->mGridCount); + EXPECT_EQ(0u, gridData->mGridIndex); + EXPECT_EQ(handles2[0].size(), gridData->mGridSize); + //timer.restart("unit-test host grids"); + for (int i=0; i<5; ++i){ + gridData = mergedHandle.gridData(i); + EXPECT_TRUE(gridData); + EXPECT_EQ(i, gridData->mGridIndex); + EXPECT_EQ(handles2[i].size(), gridData->mGridSize); + EXPECT_EQ(strcmp(gridNames[i+5].c_str(), gridData->mGridName),0); + } + + EXPECT_FALSE(mergedHandle.empty()); + handles1.push_back(std::move(mergedHandle));// append one handle with 5 merged grids + EXPECT_TRUE(mergedHandle.empty()); + EXPECT_EQ(6u, handles1.size()); + +#if defined(NANOVDB_USE_BLOSC) + nanovdb::io::writeGrids("data/merge1.nvdb", handles1, nanovdb::io::Codec::BLOSC); +#elif defined(NANOVDB_USE_ZIP) + nanovdb::io::writeGrids("data/merge1.nvdb", handles1, nanovdb::io::Codec::ZIP); +#else + nanovdb::io::writeGrids("data/merge1.nvdb", handles1, nanovdb::io::Codec::NONE); +#endif + auto meta = nanovdb::io::readGridMetaData("data/merge1.nvdb"); + EXPECT_EQ(10u, meta.size()); + EXPECT_EQ(std::string("sphere_190"), meta.back().gridName); + auto handles3 = nanovdb::io::readGrids("data/merge1.nvdb"); + EXPECT_EQ(6u, handles3.size()); + auto& handle = handles3[5]; + EXPECT_EQ(5u, handle.gridCount()); + + //timer.restart("merging 10 host grids"); + mergedHandle = nanovdb::mergeGrids(handles1); + EXPECT_EQ(size1 + size2, mergedHandle.size()); + EXPECT_TRUE(mergedHandle.data()); + gridData = mergedHandle.gridData();// first grid + EXPECT_TRUE(gridData); + EXPECT_EQ(10u, gridData->mGridCount); + EXPECT_EQ( 0u, gridData->mGridIndex); + EXPECT_EQ(handles1[0].size(), gridData->mGridSize); + + //timer.restart("splitting host grids"); + auto splitHandles = nanovdb::splitGrids(mergedHandle); + //timer.restart("unit-test split grids"); + EXPECT_EQ(10u, splitHandles.size()); + for (int i=0; i<5; ++i){ + EXPECT_EQ(handles1[i].size(), splitHandles[i].size()); + gridData = splitHandles[i].gridData(); + EXPECT_EQ(0u, gridData->mGridIndex); + EXPECT_EQ(1u, gridData->mGridCount); + EXPECT_EQ(strcmp(gridNames[i].c_str(), gridData->mGridName),0); + } + for (int i=5; i<10; ++i){ + EXPECT_EQ(handles2[i-5].size(), splitHandles[i].size()); + gridData = splitHandles[i].gridData(); + EXPECT_EQ(0u, gridData->mGridIndex); + EXPECT_EQ(1u, gridData->mGridCount); + EXPECT_EQ(strcmp(gridNames[i].c_str(), gridData->mGridName),0); + } + //timer.stop(); +}// mergeSplitGrids + +TEST_F(TestNanoVDB, writeReadRadGrid) +{ + const nanovdb::Coord ijk(101,0,0); + auto handle1 = nanovdb::createLevelSetSphere(); + auto *fltGrid = handle1.grid(); + EXPECT_TRUE(fltGrid); + //std::cerr << "Grid size: " << (fltGrid->gridSize() >> 20) << " MB\n"; + EXPECT_EQ(1.0f, fltGrid->tree().getValue(ijk)); + + {// create an IndexGrid with an internal channel and write it to file + auto handle = nanovdb::createNanoGrid(*fltGrid,1u, true, true);// 1 channel, include stats and tile values + handle.write("data/raw_grid.nvdb"); + } + {// read and test IndexGrid + nanovdb::GridHandle<> handle; + ASSERT_THROW(handle.read("data/merge1.nvdb"), std::logic_error); + } + {// read and test IndexGrid + nanovdb::GridHandle<> tmp; + tmp.read("data/raw_grid.nvdb"); + auto *idxGrid = tmp.grid(); + EXPECT_TRUE(idxGrid); + //std::cerr << "Dense IndexGrid size: " << (idxGrid->gridSize() >> 20) << " MB\n"; + EXPECT_GT(idxGrid->gridSize(), fltGrid->gridSize()); + nanovdb::ChannelAccessor acc(*idxGrid, 0u);// channel ID = 0 + EXPECT_TRUE(acc); + EXPECT_EQ(1.0f, acc(ijk)); + + // compute the gradient from channel ID 0 + nanovdb::GradStencil> stencil(acc); + stencil.moveTo(ijk); + EXPECT_EQ(nanovdb::Vec3f(1.0f,0.0f,0.0f), stencil.gradient()); + + EXPECT_EQ(0.0f, acc(100,0,0)); + acc(100,0,0) = 1.0f;// legal since acc was template on "float" and not "const float" + EXPECT_EQ(1.0f, acc(100,0,0)); + EXPECT_EQ(nanovdb::Vec3f(1.0f,0.0f,0.0f), stencil.gradient());// since stencil caches + stencil.moveTo(ijk);// re-populates the stencil cache + EXPECT_EQ(nanovdb::Vec3f(0.5f,0.0f,0.0f), stencil.gradient()); + } +}// writeReadRadGrid + +TEST_F(TestNanoVDB, GridHandleIO) +{ + auto handle = nanovdb::createLevelSetSphere(); + EXPECT_TRUE(handle.grid()); + handle.write("data/sphere_raw.nvdb"); + ASSERT_THROW(handle.read("data/dummy_raw.nvdb"), std::ios_base::failure); + ASSERT_THROW(handle.read("data/dummy_raw.nvdb"), std::exception); + handle.read("data/sphere_raw.nvdb"); + auto *grid = handle.grid(); + EXPECT_TRUE(handle.grid()); + handle.read("data/raw_grid.nvdb"); + EXPECT_FALSE(handle.grid()); + EXPECT_TRUE(handle.grid()); + ASSERT_THROW(handle.read("data/merge1.nvdb"), std::logic_error); + ASSERT_THROW(handle.read("data/merge1.nvdb"), std::exception); } +TEST_F(TestNanoVDB, GridCountAndIndex) +{ + {// create multiple grids and write them to file + std::vector> handles; + handles.emplace_back(nanovdb::createLevelSetSphere()); + handles.emplace_back(nanovdb::createLevelSetSphere()); + handles.emplace_back(nanovdb::createLevelSetSphere()); + EXPECT_EQ(3u, handles.size()); + for (auto &h : handles) EXPECT_EQ(1u, h.gridCount()); + nanovdb::io::writeGrids("data/3_spheres.nvdb", handles); + } + {// default readGrid + auto handle = nanovdb::io::readGrid("data/3_spheres.nvdb"); + EXPECT_EQ(1u, handle.gridCount()); + auto *grid = handle.grid(); + EXPECT_TRUE(grid); + EXPECT_EQ(0u, grid->gridIndex()); + EXPECT_EQ(1u, grid->gridCount()); + EXPECT_TRUE(nanovdb::validateChecksum(*grid)); + EXPECT_TRUE(nanovdb::validateChecksum(*grid, nanovdb::ChecksumMode::Full)); + } + {// readGrid one by one + for (uint32_t i=0; i<3u; ++i) { + auto handle = nanovdb::io::readGrid("data/3_spheres.nvdb", i); + EXPECT_EQ(1u, handle.gridCount()); + auto *grid = handle.grid(); + EXPECT_TRUE(grid); + EXPECT_EQ(0u, grid->gridIndex()); + EXPECT_EQ(1u, grid->gridCount()); + EXPECT_TRUE(nanovdb::validateChecksum(*grid)); + EXPECT_TRUE(nanovdb::validateChecksum(*grid, nanovdb::ChecksumMode::Full)); + } + } + {// read all grids + auto handle = nanovdb::io::readGrid("data/3_spheres.nvdb", -1); + handle.write("data/3_spheres_raw.nvdb"); + EXPECT_EQ(3u, handle.gridCount()); + for (uint32_t i=0; i(i); + EXPECT_TRUE(grid); + EXPECT_EQ(i, grid->gridIndex()); + EXPECT_EQ(3u, grid->gridCount()); + EXPECT_TRUE(nanovdb::validateChecksum(*grid)); + EXPECT_TRUE(nanovdb::validateChecksum(*grid, nanovdb::ChecksumMode::Full)); + } + } + {// read all raw grids + auto handle = nanovdb::io::readGrid("data/3_spheres_raw.nvdb", -1); + handle.write("data/3_spheres_raw.nvdb"); + EXPECT_EQ(3u, handle.gridCount()); + for (uint32_t i=0; i(i); + EXPECT_TRUE(grid); + EXPECT_EQ(i, grid->gridIndex()); + EXPECT_EQ(3u, grid->gridCount()); + EXPECT_TRUE(nanovdb::validateChecksum(*grid)); + EXPECT_TRUE(nanovdb::validateChecksum(*grid, nanovdb::ChecksumMode::Full)); + } + } + {// read all raw grids + nanovdb::GridHandle<> handle; + handle.read("data/3_spheres_raw.nvdb"); + EXPECT_EQ(3u, handle.gridCount()); + for (uint32_t i=0; i(i); + EXPECT_TRUE(grid); + EXPECT_EQ(i, grid->gridIndex()); + EXPECT_EQ(3u, grid->gridCount()); + EXPECT_TRUE(nanovdb::validateChecksum(*grid)); + EXPECT_TRUE(nanovdb::validateChecksum(*grid, nanovdb::ChecksumMode::Full)); + } + } + {// read single raw grid + nanovdb::GridHandle<> handle; + for (uint32_t i=0; i<3u; ++i) { + handle.read("data/3_spheres_raw.nvdb", i); + EXPECT_EQ(1u, handle.gridCount()); + auto *grid = handle.grid(0u); + EXPECT_TRUE(grid); + EXPECT_EQ(0u, grid->gridIndex()); + EXPECT_EQ(1u, grid->gridCount()); + EXPECT_TRUE(nanovdb::validateChecksum(*grid)); + EXPECT_TRUE(nanovdb::validateChecksum(*grid, nanovdb::ChecksumMode::Full)); + } + ASSERT_THROW(handle.read("data/3_spheres_raw.nvdb", 4), std::runtime_error); + ASSERT_THROW(handle.read("data/3_spheres_raw.nvdb",-1), std::runtime_error); + } + {// read raw grids one by one + for (uint32_t i=0; i<3u; ++i) { + auto handle = nanovdb::io::readGrid("data/3_spheres_raw.nvdb", i); + EXPECT_EQ(1u, handle.gridCount()); + auto *grid = handle.grid(); + EXPECT_TRUE(grid); + EXPECT_EQ(0u, grid->gridIndex()); + EXPECT_EQ(1u, grid->gridCount()); + EXPECT_TRUE(nanovdb::validateChecksum(*grid)); + EXPECT_TRUE(nanovdb::validateChecksum(*grid, nanovdb::ChecksumMode::Full)); + } + ASSERT_THROW(nanovdb::io::readGrid("data/3_spheres_raw.nvdb", 4), std::runtime_error); + } +}// GridCountAndIndex + +TEST_F(TestNanoVDB, CustomStreamIO) +{ + std::ostringstream outputStream(std::ios_base::out | std::ios_base::binary); + { + std::vector> handles; + handles.emplace_back(nanovdb::createLevelSetSphere()); + EXPECT_EQ(1u, handles.size()); + nanovdb::io::writeGrids(outputStream, handles, nanovdb::io::Codec::NONE); + } + + std::string payload = outputStream.str(); + std::unique_ptr pool(new uint8_t[payload.length()+NANOVDB_DATA_ALIGNMENT]); + uint8_t *buffer = nanovdb::alignPtr(pool.get()); + std::memcpy(buffer, payload.data(), payload.length()); + DataBuffer dataBuffer(buffer, payload.length()); + std::istream dataStream(&dataBuffer); + { + std::vector> handles = nanovdb::io::readGrids(dataStream); + EXPECT_EQ(1u, handles.size()); + auto *grid = handles[0].grid(0u); + EXPECT_TRUE(grid); + EXPECT_EQ(0u, grid->gridIndex()); + EXPECT_EQ(1u, grid->gridCount()); + EXPECT_TRUE(nanovdb::validateChecksum(*grid)); + EXPECT_TRUE(nanovdb::validateChecksum(*grid, nanovdb::ChecksumMode::Full)); + } +}// CustomStreamIO + +TEST_F(TestNanoVDB, CustomStreamGridHandleIO) +{ + std::ostringstream outputStream(std::ios_base::out | std::ios_base::binary); + { + nanovdb::createLevelSetSphere().write(outputStream); + } + + std::string payload = outputStream.str(); + std::unique_ptr pool(new uint8_t[payload.length()+NANOVDB_DATA_ALIGNMENT]); + uint8_t *buffer = nanovdb::alignPtr(pool.get()); + std::memcpy(buffer, payload.data(), payload.length()); + DataBuffer dataBuffer(buffer, payload.length()); + std::istream dataStream(&dataBuffer); + { + nanovdb::GridHandle handle; + handle.read(dataStream); + auto *grid = handle.grid(0u); + EXPECT_TRUE(grid); + EXPECT_EQ(0u, grid->gridIndex()); + EXPECT_EQ(1u, grid->gridCount()); + EXPECT_TRUE(nanovdb::validateChecksum(*grid)); + EXPECT_TRUE(nanovdb::validateChecksum(*grid, nanovdb::ChecksumMode::Full)); + } +}// CustomStreamGridHandleIO + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); diff --git a/nanovdb/nanovdb/unittest/TestNanoVDB.cu b/nanovdb/nanovdb/unittest/TestNanoVDB.cu new file mode 100644 index 0000000000..fc88e95d99 --- /dev/null +++ b/nanovdb/nanovdb/unittest/TestNanoVDB.cu @@ -0,0 +1,2693 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include // for std::sort + +namespace nanovdb {// this namespace is required by gtest + +namespace test { +// used for testing CudaDeviceBuffer +void device2host(size_t count) +{ + const size_t size = count * sizeof(float); + auto buffer = nanovdb::CudaDeviceBuffer::create(size, nullptr, false);// on device only + EXPECT_EQ(size, buffer.size()); + EXPECT_FALSE(buffer.data()); + EXPECT_TRUE(buffer.deviceData()); + float *d_array = reinterpret_cast(buffer.deviceData()); + constexpr unsigned int num_threads = 256; + unsigned int num_blocks = num_blocks = (static_cast(count) + num_threads - 1) / num_threads; + cudaLambdaKernel<<>>(count, [=] __device__ (size_t i) {d_array[i] = float(i);}); + buffer.deviceDownload();// copy device -> host + EXPECT_EQ(size, buffer.size()); + EXPECT_TRUE(buffer.data()); + EXPECT_TRUE(buffer.deviceData()); + float *array = reinterpret_cast(buffer.data()); + for (size_t i=0; i(buffer.data()); + for (size_t i=0; i device + EXPECT_EQ(size, buffer.size()); + EXPECT_TRUE(buffer.data()); + EXPECT_TRUE(buffer.deviceData()); + float *d_array = reinterpret_cast(buffer.deviceData()); + constexpr unsigned int num_threads = 256; + unsigned int num_blocks = num_blocks = (static_cast(count) + num_threads - 1) / num_threads; + cudaLambdaKernel<<>>(count, [=] __device__ (size_t i) { + if (d_array[i] != float(i)) *d_test = false; + d_array[i] = float(i) + 1.0f; + }); + cudaCheck(cudaMemcpy(test, d_test, sizeof(bool), cudaMemcpyDeviceToHost)); + EXPECT_TRUE(*test); + cudaCheck(cudaFreeHost(test)); + cudaCheck(cudaFree(d_test)); + buffer.deviceDownload();// copy device -> host + EXPECT_EQ(size, buffer.size()); + EXPECT_TRUE(buffer.data()); + EXPECT_TRUE(buffer.deviceData()); + for (size_t i=0; i>>(1, [=] __device__ (size_t) { + cudaStrcpy(d_str, "this is a test"); + }); + cudaCheck(cudaMemcpy(str, d_str, size, cudaMemcpyDeviceToHost)); + EXPECT_STREQ(str, "this is a test"); + cudaLambdaKernel<<<1, 1>>>(1, [=] __device__ (size_t) { + cudaStrcat(d_str, " #2"); + }); + cudaCheck(cudaMemcpy(str, d_str, size, cudaMemcpyDeviceToHost)); + EXPECT_STREQ(str, "this is a test #2"); + + cudaLambdaKernel<<<1, 1>>>(1, [=] __device__ (size_t) { + *d_n = cudaStrcmp(d_str, "this is a test"); + }); + cudaCheck(cudaMemcpy(&n, d_n, sizeof(int), cudaMemcpyDeviceToHost)); + //std::cerr << "n = " << n << std::endl; + EXPECT_EQ(signum(std::strcmp(str, "this is a test")), signum(n)); + cudaLambdaKernel<<<1, 1>>>(1, [=] __device__ (size_t) { + *d_n = cudaStrcmp(d_str, "this is a test #2"); + }); + cudaCheck(cudaMemcpy(&n, d_n, sizeof(int), cudaMemcpyDeviceToHost)); + EXPECT_EQ(std::strcmp(str, "this is a test #2"), n); + EXPECT_EQ(0, n); + + cudaCheck(cudaFreeHost(str)); + cudaCheck(cudaFree(d_n)); + cudaCheck(cudaFree(d_str)); +}// cudaStr +}// namespace test +}// namespace nanovdb + +TEST(TestNanoVDBCUDA, CudaDeviceBuffer) +{ + nanovdb::test::device2host(1000); + nanovdb::test::host2device2host(1000); +} + +TEST(TestNanoVDBCUDA, CudaStr) +{ + nanovdb::test::cudaStr(); +} + +TEST(TestNanoVDBCUDA, Basic_CudaPointsToGrid_float) +{ + using BuildT = float; + using GridT = nanovdb::NanoGrid; + const size_t num_points = 1; + nanovdb::Coord coords[num_points] = {nanovdb::Coord(1, 2, 3)}, *d_coords = nullptr; + cudaCheck(cudaMalloc(&d_coords, num_points * sizeof(nanovdb::Coord))); + cudaCheck(cudaMemcpy(d_coords, coords, num_points * sizeof(nanovdb::Coord), cudaMemcpyHostToDevice));// CPU -> GPU + + auto handle = nanovdb::cudaVoxelsToGrid(d_coords, num_points); + cudaCheck(cudaFree(d_coords)); + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + + const uint64_t size = sizeof(GridT) + + sizeof(GridT::TreeType) + + GridT::RootType::memUsage(1) + + sizeof(GridT::UpperNodeType) + + sizeof(GridT::LowerNodeType) + + sizeof(GridT::LeafNodeType); + EXPECT_EQ(handle.size(), size); + + GridT *grid = handle.grid();// no grid on the CPU + EXPECT_FALSE(grid); + handle.deviceDownload();// creates a copy up the CPU + EXPECT_TRUE(handle.deviceData()); + EXPECT_TRUE(handle.data()); + auto *data = handle.gridData(); + EXPECT_TRUE(data); + grid = handle.grid(); + EXPECT_TRUE(grid); + + auto acc = grid->getAccessor(); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(0,2,3))); + EXPECT_TRUE( acc.isActive(nanovdb::Coord(1,2,3))); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(1,2,4))); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(2,2,3))); + auto *leaf = acc.probeLeaf(nanovdb::Coord(1,2,3)); + EXPECT_TRUE(leaf); + EXPECT_EQ(nanovdb::Coord(0), leaf->origin()); + EXPECT_EQ(1u, leaf->valueMask().countOn()); + EXPECT_EQ(nanovdb::Coord(1,2,3), leaf->bbox()[0]); + EXPECT_EQ(nanovdb::Coord(1,2,3), leaf->bbox()[1]); +}// Basic_CudaPointsToGrid_float + +namespace nanovdb { +namespace test { + +/// @brief Implements Tree::probeValue(Coord) +/// @tparam BuildT Build type of the grid being called +template +struct ProbeValueNew { + using ValueT = typename BuildToValueMap::Type; + struct Probe { + bool state; + ValueT value; + operator bool() const { return state; } + }; + __hostdev__ static Probe get(const NanoRoot &root) { + return Probe{false, root.mBackground}; + } + __hostdev__ static Probe get(const typename NanoRoot::Tile &tile) { + return Probe{tile.state>0, tile.value}; + } + __hostdev__ static Probe get(const NanoUpper &node, uint32_t n) { + return Probe{node.mValueMask.isOn(n), node.mTable[n].value}; + } + __hostdev__ static Probe get(const NanoLower &node, uint32_t n) { + return Probe{node.mValueMask.isOn(n), node.mTable[n].value}; + } + __hostdev__ static Probe get(const NanoLeaf &leaf, uint32_t n) { + return Probe{leaf.isActive(n), leaf.getValue(n)}; + } +};// ProbeValueNew + +template +struct AccessLeafMask; + +// template specialization of AccessLeafMask wrt ValueOnIndexMask +template <> +struct AccessLeafMask{ + __hostdev__ static bool get(const NanoRoot&) {return false;} + __hostdev__ static bool get(const typename NanoRoot::Tile&) {return false;} + __hostdev__ static bool get(const NanoUpper&, uint32_t) {return false;} + __hostdev__ static bool get(const NanoLower&, uint32_t) {return false;} + __hostdev__ static bool get(const NanoLeaf &leaf, uint32_t n) {return leaf.mMask.isOn(n);} + __hostdev__ static void set(NanoRoot&) {} + __hostdev__ static void set(typename NanoRoot::Tile&) {} + __hostdev__ static void set(NanoUpper&, uint32_t) {} + __hostdev__ static void set(NanoLower&, uint32_t) {} + __hostdev__ static void set(NanoLeaf &leaf, uint32_t n) {leaf.mMask.setOn(n);} +};// AccessLeafMask + +}// end of test namespace +}// end of nanovdb namespace + +TEST(TestNanoVDBCUDA, Basic_CudaPointsToGrid_ValueIndex) +{ + using BuildT = nanovdb::ValueIndex; + using GridT = nanovdb::NanoGrid; + const size_t num_points = 3; + nanovdb::Coord coords[num_points] = {nanovdb::Coord(1, 2, 3), + nanovdb::Coord(1, 2, 4), + nanovdb::Coord(8, 2, 3)}, *d_coords = nullptr; + cudaCheck(cudaMalloc(&d_coords, num_points * sizeof(nanovdb::Coord))); + cudaCheck(cudaMemcpy(d_coords, coords, num_points * sizeof(nanovdb::Coord), cudaMemcpyHostToDevice));// CPU -> GPU +#if 0 + nanovdb::CudaPointsToGrid converter; + auto handle = converter.getHandle(d_coords, num_points); +#else + auto handle = nanovdb::cudaVoxelsToGrid(d_coords, num_points); +#endif + cudaCheck(cudaFree(d_coords)); + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + + const uint64_t size = sizeof(GridT) + + sizeof(GridT::TreeType) + + GridT::RootType::memUsage(1) + + sizeof(GridT::UpperNodeType) + + sizeof(GridT::LowerNodeType) + + 2*sizeof(GridT::LeafNodeType); + EXPECT_EQ(handle.size(), size); + + GridT *grid = handle.grid();// no grid on the CPU + EXPECT_FALSE(grid); + handle.deviceDownload();// creates a copy up the CPU + EXPECT_TRUE(handle.deviceData()); + EXPECT_TRUE(handle.data()); + auto *data = handle.gridData(); + EXPECT_TRUE(data); + grid = handle.grid(); + EXPECT_TRUE(grid); + EXPECT_EQ(1u + 2*512u, grid->valueCount()); + + auto acc = grid->getAccessor(); + EXPECT_FALSE( acc.isActive(nanovdb::Coord(0,2,3))); + EXPECT_TRUE( acc.isActive(nanovdb::Coord(1,2,3))); + EXPECT_TRUE( acc.isActive(nanovdb::Coord(1,2,4))); + EXPECT_TRUE( acc.isActive(nanovdb::Coord(8,2,3))); + EXPECT_EQ(1u + nanovdb::NanoLeaf::CoordToOffset(nanovdb::Coord(0,2,3)), acc.getValue(nanovdb::Coord(0,2,3))); + EXPECT_EQ(1u + nanovdb::NanoLeaf::CoordToOffset(nanovdb::Coord(1,2,3)), acc.getValue(nanovdb::Coord(1,2,3))); + EXPECT_EQ(1u + nanovdb::NanoLeaf::CoordToOffset(nanovdb::Coord(2,2,3)), acc.getValue(nanovdb::Coord(2,2,3))); + EXPECT_EQ(1u + 512u + nanovdb::NanoLeaf::CoordToOffset(nanovdb::Coord(8,2,3)), acc.getValue(nanovdb::Coord(8,2,3))); + + using OpT = nanovdb::GetValue; + EXPECT_EQ(1u + nanovdb::NanoLeaf::CoordToOffset(nanovdb::Coord(0,2,3)), acc.get(nanovdb::Coord(0,2,3))); + EXPECT_EQ(1u + nanovdb::NanoLeaf::CoordToOffset(nanovdb::Coord(1,2,3)), acc.get(nanovdb::Coord(1,2,3))); + EXPECT_EQ(1u + nanovdb::NanoLeaf::CoordToOffset(nanovdb::Coord(2,2,3)), acc.get(nanovdb::Coord(2,2,3))); + EXPECT_EQ(1u + 512u + nanovdb::NanoLeaf::CoordToOffset(nanovdb::Coord(8,2,3)), acc.get(nanovdb::Coord(8,2,3))); + + for (size_t i=0; i>(ijk); + EXPECT_TRUE(leaf); + const auto offset = leaf->CoordToOffset(ijk); + EXPECT_EQ(ijk, leaf->offsetToGlobalCoord(offset)); + } +}// Basic_CudaPointsToGrid_ValueIndex + +TEST(TestNanoVDBCUDA, Basic_CudaPointsToGrid_ValueOnIndex) +{ + using BuildT = nanovdb::ValueOnIndex; + using GridT = nanovdb::NanoGrid; + EXPECT_TRUE(nanovdb::BuildTraits::is_index); + EXPECT_FALSE(nanovdb::BuildTraits::is_indexmask); + EXPECT_TRUE(nanovdb::BuildTraits::is_onindex); + EXPECT_FALSE(nanovdb::BuildTraits::is_offindex); + const size_t num_points = 3; + nanovdb::Coord coords[num_points] = {nanovdb::Coord(1, 2, 3), + nanovdb::Coord(1, 2, 4), + nanovdb::Coord(8, 2, 3)}, *d_coords = nullptr; + cudaCheck(cudaMalloc(&d_coords, num_points * sizeof(nanovdb::Coord))); + cudaCheck(cudaMemcpy(d_coords, coords, num_points * sizeof(nanovdb::Coord), cudaMemcpyHostToDevice));// CPU -> GPU + +#if 0 + nanovdb::CudaPointsToGrid converter; + auto handle = converter.getHandle(d_coords, num_points); +#else + auto handle = nanovdb::cudaVoxelsToGrid(d_coords, num_points); +#endif + + cudaCheck(cudaFree(d_coords)); + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + + const uint64_t size = sizeof(GridT) + + sizeof(GridT::TreeType) + + GridT::RootType::memUsage(1) + + sizeof(GridT::UpperNodeType) + + sizeof(GridT::LowerNodeType) + + 2*sizeof(GridT::LeafNodeType); + EXPECT_EQ(handle.size(), size); + + GridT *grid = handle.grid();// no grid on the CPU + EXPECT_FALSE(grid); + handle.deviceDownload();// creates a copy up the CPU + EXPECT_TRUE(handle.deviceData()); + EXPECT_TRUE(handle.data()); + auto *data = handle.gridData(); + EXPECT_TRUE(data); + grid = handle.grid(); + EXPECT_TRUE(grid); + EXPECT_EQ(1u + num_points, grid->valueCount()); + + auto acc = grid->getAccessor(); + EXPECT_FALSE( acc.isActive(nanovdb::Coord(0,2,3))); + EXPECT_TRUE( acc.isActive(nanovdb::Coord(1,2,3))); + EXPECT_TRUE( acc.isActive(nanovdb::Coord(1,2,4))); + EXPECT_TRUE( acc.isActive(nanovdb::Coord(8,2,3))); + EXPECT_EQ(0u, acc.getValue(nanovdb::Coord(0,2,3))); + EXPECT_EQ(1u, acc.getValue(nanovdb::Coord(1,2,3))); + EXPECT_EQ(2u, acc.getValue(nanovdb::Coord(1,2,4))); + EXPECT_EQ(3u, acc.getValue(nanovdb::Coord(8,2,3))); + + using GetT = nanovdb::GetValue; + EXPECT_EQ(0u, acc.get(nanovdb::Coord(0,2,3))); + EXPECT_EQ(1u, acc.get(nanovdb::Coord(1,2,3))); + EXPECT_EQ(2u, acc.get(nanovdb::Coord(1,2,4))); + EXPECT_EQ(3u, acc.get(nanovdb::Coord(8,2,3))); + + { + using T = nanovdb::test::ProbeValueNew; + auto tmp = acc.get(nanovdb::Coord(0,2,3)); + EXPECT_EQ(false, tmp.state); + EXPECT_EQ(0u, tmp.value); + tmp = acc.get(nanovdb::Coord(1,2,3)); + EXPECT_EQ(true, tmp.state); + EXPECT_EQ(1u, tmp.value); + tmp = acc.get(nanovdb::Coord(1,2,4)); + EXPECT_EQ(true, tmp.state); + EXPECT_EQ(2u, tmp.value); + tmp = acc.get(nanovdb::Coord(8,2,3)); + EXPECT_EQ(true, tmp.state); + EXPECT_EQ(3u, tmp.value); + } + { + using T = nanovdb::ProbeValue; + uint64_t value = 0; + EXPECT_EQ(false, acc.get(nanovdb::Coord(0,2,3), value) ); + EXPECT_EQ(0u, value); + EXPECT_EQ(true, acc.get(nanovdb::Coord(1,2,3), value) ); + EXPECT_EQ(1u, value); + EXPECT_EQ(true, acc.get(nanovdb::Coord(1,2,4), value) ); + EXPECT_EQ(2u, value); + EXPECT_EQ(true, acc.get(nanovdb::Coord(8,2,3), value) ); + EXPECT_EQ(3u, value); + } + + for (size_t i=0; i>(ijk); + EXPECT_TRUE(leaf); + const auto offset = leaf->CoordToOffset(ijk); + EXPECT_EQ(ijk, leaf->offsetToGlobalCoord(offset)); + } +}// Basic_CudaPointsToGrid_ValueOnIndex + +TEST(TestNanoVDBCUDA, Basic_CudaPointsToGrid_ValueOnIndexMask) +{ + using BuildT = nanovdb::ValueOnIndexMask; + using GridT = nanovdb::NanoGrid; + EXPECT_TRUE(nanovdb::BuildTraits::is_index); + EXPECT_TRUE(nanovdb::BuildTraits::is_indexmask); + EXPECT_TRUE(nanovdb::BuildTraits::is_onindex); + EXPECT_FALSE(nanovdb::BuildTraits::is_offindex); + const size_t num_points = 3; + nanovdb::Coord coords[num_points] = {nanovdb::Coord(1, 2, 3), + nanovdb::Coord(1, 2, 4), + nanovdb::Coord(8, 2, 3)}, *d_coords = nullptr; + cudaCheck(cudaMalloc(&d_coords, num_points * sizeof(nanovdb::Coord))); + cudaCheck(cudaMemcpy(d_coords, coords, num_points * sizeof(nanovdb::Coord), cudaMemcpyHostToDevice));// CPU -> GPU + +#if 0 + nanovdb::CudaPointsToGrid converter; + auto handle = converter.getHandle(d_coords, num_points); +#else + auto handle = nanovdb::cudaVoxelsToGrid(d_coords, num_points); +#endif + + cudaCheck(cudaFree(d_coords)); + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + + const uint64_t size = sizeof(GridT) + + sizeof(GridT::TreeType) + + GridT::RootType::memUsage(1) + + sizeof(GridT::UpperNodeType) + + sizeof(GridT::LowerNodeType) + + 2*sizeof(GridT::LeafNodeType); + EXPECT_EQ(handle.size(), size); + + GridT *grid = handle.grid();// no grid on the CPU + EXPECT_FALSE(grid); + handle.deviceDownload();// creates a copy up the CPU + EXPECT_TRUE(handle.deviceData()); + EXPECT_TRUE(handle.data()); + auto *data = handle.gridData(); + EXPECT_TRUE(data); + grid = handle.grid(); + EXPECT_TRUE(grid); + EXPECT_EQ(4u, grid->valueCount()); + + auto acc = grid->getAccessor(); + EXPECT_FALSE( acc.isActive(nanovdb::Coord(0,2,3))); + EXPECT_TRUE( acc.isActive(nanovdb::Coord(1,2,3))); + EXPECT_TRUE( acc.isActive(nanovdb::Coord(1,2,4))); + EXPECT_TRUE( acc.isActive(nanovdb::Coord(8,2,3))); + EXPECT_EQ(0u, acc.getValue(nanovdb::Coord(0,2,3))); + EXPECT_EQ(1u, acc.getValue(nanovdb::Coord(1,2,3))); + EXPECT_EQ(2u, acc.getValue(nanovdb::Coord(1,2,4))); + EXPECT_EQ(3u, acc.getValue(nanovdb::Coord(8,2,3))); + + using GetT = nanovdb::GetValue; + EXPECT_EQ(0u, acc.get(nanovdb::Coord(0,2,3))); + EXPECT_EQ(1u, acc.get(nanovdb::Coord(1,2,3))); + EXPECT_EQ(2u, acc.get(nanovdb::Coord(1,2,4))); + EXPECT_EQ(3u, acc.get(nanovdb::Coord(8,2,3))); + + using OpT = nanovdb::test::AccessLeafMask; + EXPECT_EQ(false, acc.get(nanovdb::Coord(0,2,3))); + EXPECT_EQ(true, acc.get(nanovdb::Coord(1,2,3))); + EXPECT_EQ(true, acc.get(nanovdb::Coord(1,2,4))); + EXPECT_EQ(true, acc.get(nanovdb::Coord(8,2,3))); + + acc.set(nanovdb::Coord(1,2,3)); + acc.set(nanovdb::Coord(8,2,3)); + + EXPECT_EQ(false, acc.get(nanovdb::Coord(0,2,3))); + EXPECT_EQ(true , acc.get(nanovdb::Coord(1,2,3))); + EXPECT_EQ(true, acc.get(nanovdb::Coord(1,2,4))); + EXPECT_EQ(true, acc.get(nanovdb::Coord(8,2,3))); + + { + using T = nanovdb::ProbeValue; + uint64_t value = 0; + EXPECT_EQ(false, acc.get(nanovdb::Coord(0,2,3), value) ); + EXPECT_EQ(0u, value); + EXPECT_EQ(true, acc.get(nanovdb::Coord(1,2,3), value) ); + EXPECT_EQ(1u, value); + EXPECT_EQ(true, acc.get(nanovdb::Coord(1,2,4), value) ); + EXPECT_EQ(2u, value); + EXPECT_EQ(true, acc.get(nanovdb::Coord(8,2,3), value) ); + EXPECT_EQ(3u, value); + EXPECT_EQ(false, acc.get(nanovdb::Coord(-18,2,3), value) ); + EXPECT_EQ(0u, value); + + EXPECT_EQ(false, grid->tree().get(nanovdb::Coord(0,2,3), value) ); + EXPECT_EQ(0u, value); + EXPECT_EQ(true, grid->tree().get(nanovdb::Coord(1,2,3), value) ); + EXPECT_EQ(1u, value); + EXPECT_EQ(true, grid->tree().get(nanovdb::Coord(1,2,4), value) ); + EXPECT_EQ(2u, value); + EXPECT_EQ(true, grid->tree().get(nanovdb::Coord(8,2,3), value) ); + EXPECT_EQ(3u, value); + EXPECT_EQ(false, grid->tree().get(nanovdb::Coord(-18,2,3), value) ); + EXPECT_EQ(0u, value); + } + + for (size_t i=0; i>(ijk); + EXPECT_TRUE(leaf); + const auto offset = leaf->CoordToOffset(ijk); + EXPECT_EQ(ijk, leaf->offsetToGlobalCoord(offset)); + EXPECT_EQ(leaf->mValueMask, leaf->mMask); + } +}// Basic_CudaPointsToGrid_ValueOnIndexMask + +TEST(TestNanoVDBCUDA, Large_CudaPointsToGrid_old) +{ + using BuildT = nanovdb::ValueOnIndex; + //nanovdb::CpuTimer timer; + const size_t voxelCount = 1 << 20;// 1048576 + std::vector voxels; + {//generate random voxels + voxels.reserve(voxelCount); + std::srand(98765); + const int max = 512, min = -max; + auto op = [&](){return rand() % (max - min) + min;}; + //timer.start("Creating "+std::to_string(voxelCount)+" random voxels on the CPU"); + while (voxels.size() < voxelCount) voxels.push_back(nanovdb::Coord(op(), op(), op())); + //timer.stop(); + EXPECT_EQ(voxelCount, voxels.size()); + } +#if 0 + {// Build grid on CPU + nanovdb::build::Grid buildGrid(0.0f); + //timer.start("Building grid on CPU from "+std::to_string(voxels.size())+" points"); + nanovdb::forEach(0, voxelCount, voxelCount >> 6, [&](const nanovdb::Range1D &r){ + auto acc = buildGrid.getWriteAccessor(); + for (size_t i=r.begin(); i!=r.end(); ++i) acc.setValueOn(voxels[i]); + }); + //timer.restart("Converting CPU build::Grid to nanovdb"); + auto handle = nanovdb::createNanoGrid(buildGrid); + //timer.stop(); + } +#endif + nanovdb::Coord* d_coords; + const size_t voxelSize = voxels.size() * sizeof(nanovdb::Coord); + //timer.start("Allocating "+std::to_string(voxelSize >> 20)+" MB on the GPU"); + cudaCheck(cudaMalloc(&d_coords, voxelSize)); + //timer.restart("Copying voxels from CPU to GPU"); + cudaCheck(cudaMemcpy(d_coords, voxels.data(), voxelSize, cudaMemcpyHostToDevice)); + //timer.stop(); + + //timer.start("Building grid on GPU from "+std::to_string(voxels.size())+" points"); + auto handle = nanovdb::cudaVoxelsToGrid(d_coords, voxelCount, 1.0); + //timer.stop(); + + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_TRUE(handle.deviceGrid()); + EXPECT_FALSE(handle.deviceGrid(0)); + EXPECT_TRUE(handle.deviceGrid(0)); + EXPECT_FALSE(handle.deviceGrid(1)); + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + + //timer.start("Allocating and copying grid from GPU to CPU"); + auto *grid = handle.grid();// no grid on the CPU + EXPECT_FALSE(grid); + handle.deviceDownload();// creates a copy on the CPU + EXPECT_TRUE(handle.deviceData()); + EXPECT_TRUE(handle.data()); + auto *data = handle.gridData(); + EXPECT_TRUE(data); + grid = handle.grid(); + EXPECT_TRUE(grid); + EXPECT_TRUE(grid->valueCount()>0); + EXPECT_EQ(nanovdb::Vec3d(1.0), grid->voxelSize()); + + //timer.restart("Parallel unit-testing on CPU"); + nanovdb::forEach(voxels,[&](const nanovdb::Range1D &r){ + auto acc = grid->getAccessor(); + for (size_t i=r.begin(); i!=r.end(); ++i) { + const nanovdb::Coord &ijk = voxels[i]; + EXPECT_TRUE(acc.probeLeaf(ijk)!=nullptr); + EXPECT_TRUE(acc.isActive(ijk)); + EXPECT_TRUE(acc.getValue(ijk) > 0u); + const auto *leaf = acc.get>(ijk); + EXPECT_TRUE(leaf); + const auto offset = leaf->CoordToOffset(ijk); + EXPECT_EQ(ijk, leaf->offsetToGlobalCoord(offset)); + } + }); + + //timer.stop(); +}// Large_CudaPointsToGrid_old + +TEST(TestNanoVDBCUDA, mergeSplitGrids) +{ + size_t size1 = 0, size2 = 0; + std::vector> handles1, handles2; + std::vector gridNames; + //nanovdb::CpuTimer timer("create 5 host grids"); + for (int radius = 100; radius<150; radius += 10) { + gridNames.emplace_back("sphere_" + std::to_string(radius)); + handles1.emplace_back(nanovdb::createLevelSetSphere(radius,nanovdb::Vec3d(0),1,3, + nanovdb::Vec3d(0), gridNames.back())); + EXPECT_FALSE(handles1.back().isPadded()); + size1 += handles1.back().size(); + } + EXPECT_EQ(5u, gridNames.size()); + EXPECT_EQ(5u, handles1.size()); + //timer.restart("create 5 host grids"); + for (int radius = 150; radius<200; radius += 10) { + gridNames.emplace_back("sphere_" + std::to_string(radius)); + handles2.emplace_back(nanovdb::createLevelSetSphere(radius,nanovdb::Vec3d(0),1,3, + nanovdb::Vec3d(0), gridNames.back())); + size2 += handles2.back().size(); + } + EXPECT_EQ(10u, gridNames.size()); + EXPECT_EQ( 5u, handles2.size()); + //timer.restart("merging 5 host grids"); + auto mergedHandle = nanovdb::mergeGrids(handles2);// merge last 5 grid handles + EXPECT_EQ(size2, mergedHandle.size()); + EXPECT_FALSE(mergedHandle.isPadded()); + EXPECT_TRUE(mergedHandle.data()); + auto *gridData = mergedHandle.gridData();// first grid + EXPECT_TRUE(gridData); + EXPECT_EQ(5u, gridData->mGridCount); + EXPECT_EQ(0u, gridData->mGridIndex); + EXPECT_EQ(handles2[0].size(), gridData->mGridSize); + //timer.restart("unit-test host grids"); + for (int i=0; i<5; ++i){ + gridData = mergedHandle.gridData(i); + EXPECT_TRUE(gridData); + EXPECT_EQ(i, gridData->mGridIndex); + EXPECT_EQ(handles2[i].size(), gridData->mGridSize); + EXPECT_EQ(strcmp(gridNames[i+5].c_str(), gridData->mGridName),0); + } + + EXPECT_FALSE(mergedHandle.empty()); + handles1.push_back(std::move(mergedHandle));// append one handle with 5 merged grids + EXPECT_TRUE(mergedHandle.empty()); + EXPECT_EQ(6u, handles1.size()); + //timer.restart("merging 10 host grids"); + mergedHandle = nanovdb::mergeGrids(handles1); + EXPECT_EQ(size1 + size2, mergedHandle.size()); + EXPECT_TRUE(mergedHandle.data()); + gridData = mergedHandle.gridData();// first grid + EXPECT_TRUE(gridData); + EXPECT_EQ(10u, gridData->mGridCount); + EXPECT_EQ( 0u, gridData->mGridIndex); + EXPECT_EQ(handles1[0].size(), gridData->mGridSize); + + //timer.restart("splitting host grids"); + auto splitHandles = nanovdb::splitGrids(mergedHandle); + //timer.restart("unit-test split grids"); + EXPECT_EQ(10u, splitHandles.size()); + for (int i=0; i<5; ++i){ + EXPECT_EQ(handles1[i].size(), splitHandles[i].size()); + gridData = splitHandles[i].gridData(); + EXPECT_EQ(0u, gridData->mGridIndex); + EXPECT_EQ(1u, gridData->mGridCount); + EXPECT_EQ(strcmp(gridNames[i].c_str(), gridData->mGridName),0); + } + for (int i=5; i<10; ++i){ + EXPECT_EQ(handles2[i-5].size(), splitHandles[i].size()); + gridData = splitHandles[i].gridData(); + EXPECT_EQ(0u, gridData->mGridIndex); + EXPECT_EQ(1u, gridData->mGridCount); + EXPECT_EQ(strcmp(gridNames[i].c_str(), gridData->mGridName),0); + } + //timer.stop(); +}// mergeSplitGrids + +TEST(TestNanoVDBCUDA, mergeSplitDeviceGrids) +{ + using BufferT = nanovdb::CudaDeviceBuffer; + using HandleT = nanovdb::GridHandle; + size_t size = 0; + std::vector handles; + std::vector gridNames; + //nanovdb::CpuTimer timer("create 10 host grids"); + for (int radius = 100; radius<200; radius += 10) { + gridNames.emplace_back("sphere_" + std::to_string(radius)); + handles.emplace_back(nanovdb::createLevelSetSphere(radius,nanovdb::Vec3d(0),1,3, + nanovdb::Vec3d(0), gridNames.back())); + EXPECT_FALSE(handles.back().isPadded()); + size += handles.back().size(); + } + //timer.restart("copy grids to device"); + for (auto &h : handles) h.deviceUpload(); + EXPECT_EQ(10u, handles.size()); + //timer.restart("merging device grids"); + auto mergedHandle = nanovdb::mergeDeviceGrids(handles); + EXPECT_EQ(size, mergedHandle.size()); + EXPECT_FALSE(mergedHandle.data()); + EXPECT_TRUE(mergedHandle.deviceData()); + EXPECT_FALSE(mergedHandle.isPadded()); + //timer.restart("copy grids to host"); + mergedHandle.deviceDownload(); + EXPECT_TRUE(mergedHandle.data()); + EXPECT_TRUE(mergedHandle.deviceData()); + EXPECT_FALSE(mergedHandle.isPadded()); + auto *gridData = mergedHandle.gridData();// first grid + EXPECT_TRUE(gridData); + EXPECT_EQ(10u, gridData->mGridCount); + EXPECT_EQ(0u, gridData->mGridIndex); + //timer.restart("unit-test host grids"); + for (uint32_t i=0; i<10; ++i) { + gridData = mergedHandle.gridData(i); + EXPECT_TRUE(gridData); + EXPECT_EQ(i, gridData->mGridIndex); + EXPECT_EQ(strcmp(gridNames[i].c_str(), gridData->mGridName),0); + } + //timer.restart("splitting device grids"); + auto splitHandles = nanovdb::splitDeviceGrids(mergedHandle); + //timer.restart("unit-test split grids"); + EXPECT_EQ(10u, splitHandles.size()); + for (uint32_t i=0u; i<10u; ++i) { + EXPECT_EQ(handles[i].size(), splitHandles[i].size()); + EXPECT_FALSE(splitHandles[i].isPadded()); + EXPECT_FALSE(splitHandles[i].gridData()); + splitHandles[i].deviceDownload(); + gridData = splitHandles[i].gridData(); + EXPECT_TRUE(gridData); + EXPECT_EQ(0u, gridData->mGridIndex); + EXPECT_EQ(1u, gridData->mGridCount); + EXPECT_EQ(strcmp(gridNames[i].c_str(), gridData->mGridName),0); + } + //timer.stop(); +}// mergeSplitDeviceGrids + +// make -j 4 testNanoVDB && ./unittest/testNanoVDB --gtest_filter="*Cuda*" --gtest_break_on_failure +TEST(TestNanoVDBCUDA, CudaIndexGridToGrid_basic) +{ + using BufferT = nanovdb::CudaDeviceBuffer; + const float value = 1.23456f, backgroud = 1.0f; + const nanovdb::Coord ijk(1,2,3); + nanovdb::GridHandle floatHdl; + nanovdb::FloatGrid *floatGrid = nullptr; + //nanovdb::CpuTimer timer; + {// create float grid with one active voxel + nanovdb::build::Grid grid(backgroud); + auto srcAcc = grid.getAccessor(); + srcAcc.setValue(ijk, value); + auto nodeCount = grid.nodeCount(); + EXPECT_EQ(1u, nodeCount[0]); + EXPECT_EQ(1u, nodeCount[1]); + EXPECT_EQ(1u, nodeCount[2]); + EXPECT_EQ(value, srcAcc.getValue(ijk)); + EXPECT_EQ(value, srcAcc.getValue(1,2,3)); + //timer.start("Create FloatGrid on CPU"); + floatHdl = nanovdb::createNanoGrid, float, BufferT>(grid); + EXPECT_TRUE(floatHdl); + floatGrid = floatHdl.grid(); + EXPECT_TRUE(floatGrid); + EXPECT_EQ(ijk, floatGrid->indexBBox()[0]); + EXPECT_EQ(ijk, floatGrid->indexBBox()[1]); + auto acc = floatGrid->getAccessor(); + EXPECT_EQ(backgroud, acc.getValue(nanovdb::Coord(-1))); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(-1))); + EXPECT_EQ(backgroud, acc.getValue(nanovdb::Coord(8))); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(8))); + EXPECT_EQ(backgroud, acc.getValue(nanovdb::Coord(0))); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(0))); + EXPECT_EQ(value, acc.getValue(ijk)); + EXPECT_TRUE(acc.isActive(ijk)); + } + //timer.restart("Create IndexGrid on CPU"); + using BufferT = nanovdb::CudaDeviceBuffer; + auto idxHdl = nanovdb::createNanoGrid(*floatGrid, 0u, false, false, 1); + //timer.restart("Copy IndexGrid from CPU to GPU"); + EXPECT_FALSE(idxHdl.deviceGrid()); + idxHdl.deviceUpload(); + EXPECT_TRUE(idxHdl.deviceGrid()); + auto *idxGrid = idxHdl.grid(); + EXPECT_TRUE(idxGrid); + //timer.restart("Create value list on CPU"); + EXPECT_EQ(1u + 512u, idxGrid->valueCount());// background + 512 values in one leaf node + float *values = new float[idxGrid->valueCount()], *d_values = nullptr; + values[0] = backgroud; + const float *q = floatGrid->tree().getFirstLeaf()->data()->mValues; + for (float *p=values+1, *e=p+512;p!=e; ++p) *p = *q++; + //timer.restart("Allocate and copy values from CPU to GPU"); + cudaCheck(cudaMalloc((void**)&d_values, idxGrid->valueCount()*sizeof(float))); + EXPECT_TRUE(d_values); + cudaCheck(cudaMemcpy(d_values, values, idxGrid->valueCount()*sizeof(float), cudaMemcpyHostToDevice)); + EXPECT_FALSE(idxHdl.deviceGrid()); + auto *d_idxGrid = idxHdl.deviceGrid(); + EXPECT_TRUE(d_idxGrid); + //timer.restart("Call CudaIndexToGrid"); + auto hdl = nanovdb::cudaIndexToGrid(d_idxGrid, d_values); + //timer.restart("unit-test"); + EXPECT_FALSE(hdl.grid());// no host grid + EXPECT_TRUE(hdl.deviceGrid()); + hdl.deviceDownload(); + auto *floatGrid2 = hdl.grid(); + EXPECT_TRUE(floatGrid2); + auto *leaf2 = floatGrid2->tree().getFirstLeaf(); + EXPECT_TRUE(leaf2); + auto acc = floatGrid->getAccessor(); + auto acc2 = floatGrid2->getAccessor(); + EXPECT_EQ(floatGrid->indexBBox(), floatGrid2->indexBBox()); + EXPECT_EQ(floatGrid->worldBBox(), floatGrid2->worldBBox()); + // probe background in root node + EXPECT_EQ(backgroud, acc.getValue(nanovdb::Coord(-1))); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(-1))); + EXPECT_EQ(backgroud, acc2.getValue(nanovdb::Coord(-1))); + EXPECT_FALSE(acc2.isActive(nanovdb::Coord(-1))); + // probe background in upper node + EXPECT_EQ(backgroud, acc.getValue(nanovdb::Coord(128))); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(128))); + EXPECT_EQ(backgroud, floatGrid2->tree().getValue(nanovdb::Coord(128))); + EXPECT_EQ(backgroud, acc2.getValue(nanovdb::Coord(128))); + EXPECT_FALSE(acc2.isActive(nanovdb::Coord(128))); + // probe background in leaf node + EXPECT_EQ(backgroud, acc.getValue(nanovdb::Coord(0))); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(0))); + EXPECT_EQ(backgroud, leaf2->getValue(nanovdb::Coord(0))); + EXPECT_FALSE(leaf2->isActive(nanovdb::Coord(0))); + EXPECT_EQ(backgroud, floatGrid2->tree().getValue(nanovdb::Coord(0))); + EXPECT_EQ(backgroud, acc2.getValue(nanovdb::Coord(0))); + EXPECT_FALSE(acc2.isActive(nanovdb::Coord(0))); + + EXPECT_EQ(value, acc2.getValue(ijk)); + EXPECT_TRUE(acc2.isActive(ijk)); + //timer.stop(); + cudaFree(d_values); +}// CudaIndexGridToGrid_basic + +TEST(TestNanoVDBCUDA, CudaIndexGridToGrid_ValueIndex) +{ + using BuildT = nanovdb::ValueIndex; + using BufferT = nanovdb::CudaDeviceBuffer; + //nanovdb::CpuTimer timer("Create FloatGrid on CPU"); + auto floatHdl = nanovdb::createLevelSetSphere(100,nanovdb::Vec3d(0),1,3, nanovdb::Vec3d(0), "test"); + auto *floatGrid = floatHdl.grid(); + EXPECT_TRUE(floatGrid); + auto acc = floatGrid->getAccessor(); + //timer.restart("Create IndexGrid on CPU"); + auto idxHdl = nanovdb::createNanoGrid(*floatGrid); + //timer.restart("Copy IndexGrid from CPU to GPU"); + idxHdl.deviceUpload(); + auto *idxGrid = idxHdl.grid(); + EXPECT_TRUE(idxGrid); + //timer.restart("Create value list on CPU"); + float *values = new float[idxGrid->valueCount()], *d_values = nullptr; + values[0] = floatGrid->tree().root().background(); + for (auto it = floatGrid->indexBBox().begin(); it; ++it) { + EXPECT_EQ(acc.isActive(*it), idxGrid->tree().isActive(*it)); + const uint64_t idx = idxGrid->tree().getValue(*it); + EXPECT_TRUE(idx < idxGrid->valueCount()); + values[idx] = acc.getValue(*it); + } + //timer.restart("Allocate and copy values from CPU to GPU"); + cudaCheck(cudaMalloc((void**)&d_values, idxGrid->valueCount()*sizeof(float))); + cudaCheck(cudaMemcpy(d_values, values, idxGrid->valueCount()*sizeof(float), cudaMemcpyHostToDevice)); + EXPECT_FALSE(idxHdl.deviceGrid()); + auto *d_idxGrid = idxHdl.deviceGrid(); + EXPECT_TRUE(d_idxGrid); + //timer.restart("Call CudaIndexToGrid"); + auto hdl = nanovdb::cudaIndexToGrid(d_idxGrid, d_values); + //timer.restart("unit-test"); + EXPECT_FALSE(hdl.grid());// no host grid + EXPECT_TRUE(hdl.deviceGrid()); + hdl.deviceDownload(); + auto *floatGrid2 = hdl.grid(); + EXPECT_TRUE(floatGrid2); + auto acc2 = floatGrid2->getAccessor(); + EXPECT_EQ(floatGrid->indexBBox(), floatGrid2->indexBBox()); + EXPECT_EQ(floatGrid->worldBBox(), floatGrid2->worldBBox()); + EXPECT_EQ(floatGrid->tree().root().background(), floatGrid2->tree().root().background()); + for (auto it = floatGrid->indexBBox().begin(); it; ++it) { + EXPECT_EQ(acc.isActive(*it), acc2.isActive(*it)); + EXPECT_EQ(acc.getValue(*it), acc2.getValue(*it)); + } + //timer.stop(); + cudaFree(d_values); +}// CudaPointToGrid_ValueIndex + +TEST(TestNanoVDBCUDA, CudaIndexGridToGrid_ValueOnIndex) +{ + using BuildT = nanovdb::ValueOnIndex; + using BufferT = nanovdb::CudaDeviceBuffer; + //nanovdb::CpuTimer timer("Create FloatGrid on CPU"); + auto floatHdl = nanovdb::createLevelSetSphere(100,nanovdb::Vec3d(0),1,3, nanovdb::Vec3d(0), "test"); + auto *floatGrid = floatHdl.grid(); + EXPECT_TRUE(floatGrid); + auto acc = floatGrid->getAccessor(); + //timer.restart("Create IndexGrid on CPU"); + auto idxHdl = nanovdb::createNanoGrid(*floatGrid); + //timer.restart("Copy IndexGrid from CPU to GPU"); + idxHdl.deviceUpload(); + auto *idxGrid = idxHdl.grid(); + EXPECT_TRUE(idxGrid); + //timer.restart("Create value list on CPU"); + float *values = new float[idxGrid->valueCount()], *d_values = nullptr; + values[0] = floatGrid->tree().root().background(); + for (auto it = floatGrid->indexBBox().begin(); it; ++it) { + EXPECT_EQ(acc.isActive(*it), idxGrid->tree().isActive(*it)); + if (acc.isActive(*it)) { + const uint64_t idx = idxGrid->tree().getValue(*it); + EXPECT_TRUE(idx < idxGrid->valueCount()); + values[idx] = acc.getValue(*it); + } + } + //timer.restart("Allocate and copy values from CPU to GPU"); + cudaCheck(cudaMalloc((void**)&d_values, idxGrid->valueCount()*sizeof(float))); + cudaCheck(cudaMemcpy(d_values, values, idxGrid->valueCount()*sizeof(float), cudaMemcpyHostToDevice)); + EXPECT_FALSE(idxHdl.deviceGrid()); + auto *d_idxGrid = idxHdl.deviceGrid(); + EXPECT_TRUE(d_idxGrid); + //timer.restart("Call CudaIndexToGrid"); + auto hdl = nanovdb::cudaIndexToGrid(d_idxGrid, d_values); + //timer.restart("unit-test"); + EXPECT_FALSE(hdl.grid());// no host grid + EXPECT_TRUE(hdl.deviceGrid()); + hdl.deviceDownload(); + auto *floatGrid2 = hdl.grid(); + EXPECT_TRUE(floatGrid2); + auto acc2 = floatGrid2->getAccessor(); + EXPECT_EQ(floatGrid->indexBBox(), floatGrid2->indexBBox()); + EXPECT_EQ(floatGrid->worldBBox(), floatGrid2->worldBBox()); + EXPECT_EQ(floatGrid->tree().root().background(), floatGrid2->tree().root().background()); + for (auto it = floatGrid->indexBBox().begin(); it; ++it) { + EXPECT_EQ(acc.isActive(*it), acc2.isActive(*it)); + if (acc.isActive(*it)) EXPECT_EQ(acc.getValue(*it), acc2.getValue(*it)); + } + //timer.stop(); + cudaFree(d_values); +}// CudaPointToGrid_ValueOnIndex + +TEST(TestNanoVDBCUDA, CudaSignedFloodFill) +{ + using BufferT = nanovdb::CudaDeviceBuffer; + //nanovdb::CpuTimer timer("Create FloatGrid on CPU"); + auto floatHdl = nanovdb::createLevelSetSphere(100); + auto *floatGrid = floatHdl.grid(); + EXPECT_TRUE(floatGrid); + auto acc = floatGrid->getAccessor(); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(103,0,0))); + EXPECT_TRUE( acc.isActive(nanovdb::Coord(100,0,0))); + EXPECT_FALSE(acc.isActive(nanovdb::Coord( 97,0,0))); + EXPECT_EQ( 3.0f, acc(103,0,0)); + EXPECT_EQ( 0.0f, acc(100,0,0)); + EXPECT_EQ(-3.0f, acc( 97,0,0)); + using OpT = nanovdb::SetVoxel;// only set the voxel value + acc.set(nanovdb::Coord(103,0,0),-1.0f);// flip sign and value of inactive voxel + acc.set(nanovdb::Coord( 97,0,0), 1.0f);// flip sign and value of inactive voxel + EXPECT_EQ(-1.0f, acc(103,0,0)); + EXPECT_EQ( 0.0f, acc(100,0,0)); + EXPECT_EQ( 1.0f, acc( 97,0,0)); + //timer.restart("Copy FloatGrid from CPU to GPU"); + floatHdl.deviceUpload();// CPU -> GPU + auto *d_floatGrid = floatHdl.deviceGrid(); + EXPECT_TRUE(d_floatGrid); + //timer.restart("Signed flood-fill on the GPU"); + //nanovdb::cudaSignedFloodFill(d_floatGrid, true); + nanovdb::cudaSignedFloodFill(d_floatGrid); + //timer.restart("Copy FloatGrid from GPU to CPU"); + floatHdl.deviceDownload();// GPU -> CPU + //timer.stop(); + floatGrid = floatHdl.grid(); + EXPECT_TRUE(floatGrid); + acc = floatGrid->getAccessor(); + EXPECT_EQ( 3.0f, acc(103,0,0)); + EXPECT_EQ( 0.0f, acc(100,0,0)); + EXPECT_EQ(-3.0f, acc( 97,0,0)); + //EXPECT_FALSE(floatGrid->isLexicographic()); + EXPECT_TRUE(floatGrid->isBreadthFirst()); +}// CudaSignedFloodFill + +TEST(TestNanoVDBCUDA, OneVoxelToGrid) +{ + using BuildT = float; + using GridT = nanovdb::NanoGrid; + const size_t num_points = 1; + nanovdb::Coord coords[num_points] = {nanovdb::Coord(1, 2, 3)}, *d_coords = nullptr; + cudaCheck(cudaMalloc(&d_coords, num_points * sizeof(nanovdb::Coord))); + cudaCheck(cudaMemcpy(d_coords, coords, num_points * sizeof(nanovdb::Coord), cudaMemcpyHostToDevice));// CPU -> GPU + + //nanovdb::GpuTimer timer("Create FloatGrid on GPU"); + nanovdb::CudaPointsToGrid converter; + auto handle = converter.getHandle(d_coords, num_points); + cudaCheck(cudaFree(d_coords)); + //timer.stop(); + + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + + const uint64_t size = sizeof(GridT) + + sizeof(GridT::TreeType) + + GridT::RootType::memUsage(1) + + sizeof(GridT::UpperNodeType) + + sizeof(GridT::LowerNodeType) + + sizeof(GridT::LeafNodeType); + EXPECT_EQ(handle.size(), size); + + GridT *grid = handle.grid();// no grid on the CPU + EXPECT_FALSE(grid); + //timer.start("Copy data from GPU to CPU"); + handle.deviceDownload();// creates a copy up the CPU + //timer.stop(); + EXPECT_TRUE(handle.deviceData()); + EXPECT_TRUE(handle.data()); + auto *data = handle.gridData(); + EXPECT_TRUE(data); + grid = handle.grid(); + EXPECT_TRUE(grid); + + //timer.start("Unit-testing grid on the CPU"); + auto acc = grid->getAccessor(); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(0,2,3))); + EXPECT_TRUE( acc.isActive(nanovdb::Coord(1,2,3))); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(1,2,4))); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(2,2,3))); + auto *leaf = acc.probeLeaf(nanovdb::Coord(1,2,3)); + EXPECT_TRUE(leaf); + EXPECT_EQ(nanovdb::Coord(0), leaf->origin()); + EXPECT_EQ(1u, leaf->valueMask().countOn()); + EXPECT_EQ(nanovdb::Coord(1,2,3), leaf->bbox()[0]); + EXPECT_EQ(nanovdb::Coord(1,2,3), leaf->bbox()[1]); + auto *lower = acc.getNode<1>(); + EXPECT_TRUE(lower); + EXPECT_EQ(nanovdb::Coord(1,2,3), lower->bbox()[0]); + EXPECT_EQ(nanovdb::Coord(1,2,3), lower->bbox()[1]); + auto *upper = acc.getNode<2>(); + EXPECT_TRUE(upper); + EXPECT_EQ(nanovdb::Coord(1,2,3), upper->bbox()[0]); + EXPECT_EQ(nanovdb::Coord(1,2,3), upper->bbox()[1]); + EXPECT_EQ(nanovdb::Coord(1,2,3), acc.root().bbox()[0]); + EXPECT_EQ(nanovdb::Coord(1,2,3), acc.root().bbox()[1]); + //timer.stop(); +}// OneVoxelToGrid + +TEST(TestNanoVDBCUDA, ThreePointsToGrid) +{ + using BuildT = nanovdb::Point; + using Vec3T = nanovdb::Vec3f; + using GridT = nanovdb::NanoGrid; + const size_t num_points = 3; + Vec3T points[num_points] = {Vec3T(1, 0, 0),Vec3T(1, 2, 3),Vec3T(1, 2, 3)}, *d_points = nullptr; + cudaCheck(cudaMalloc(&d_points, num_points * sizeof(Vec3T))); + cudaCheck(cudaMemcpy(d_points, points, num_points * sizeof(Vec3T), cudaMemcpyHostToDevice));// CPU -> GPU + + //nanovdb::GpuTimer timer("Create FloatGrid on GPU"); + nanovdb::CudaPointsToGrid converter; + auto handle = converter.getHandle(d_points, num_points); + cudaCheck(cudaFree(d_points)); + //timer.stop(); + + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + + const uint64_t size = sizeof(GridT) + + sizeof(GridT::TreeType) + + GridT::RootType::memUsage(1) + + sizeof(GridT::UpperNodeType) + + sizeof(GridT::LowerNodeType) + + sizeof(GridT::LeafNodeType) + + sizeof(nanovdb::GridBlindMetaData) + + num_points*sizeof(Vec3T); + EXPECT_EQ(handle.size(), size); + + GridT *grid = handle.grid();// no grid on the CPU + EXPECT_FALSE(grid); + //timer.start("Copy data from GPU to CPU"); + handle.deviceDownload();// creates a copy on the CPU + //timer.stop(); + EXPECT_TRUE(handle.deviceData()); + EXPECT_TRUE(handle.data()); + auto *data = handle.gridData(); + EXPECT_TRUE(data); + grid = handle.grid(); + EXPECT_TRUE(grid); + //EXPECT_TRUE(grid->isLexicographic()); + EXPECT_TRUE(grid->isBreadthFirst()); + EXPECT_EQ(1u, grid->blindDataCount()); + const Vec3T *blindData = grid->getBlindData(0); + EXPECT_TRUE(blindData); + for (const Vec3T *p = blindData, *q=p+num_points, *ptr=points; p!=q; ++p) { + EXPECT_EQ(*ptr++, *p); + } + //timer.start("Unit-testing grid on the CPU"); + nanovdb::PointAccessor acc(*grid); + EXPECT_TRUE(acc); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(0,2,3))); + EXPECT_TRUE( acc.isActive(nanovdb::Coord(1,0,0))); + EXPECT_TRUE( acc.isActive(nanovdb::Coord(1,2,3))); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(1,2,4))); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(2,2,3))); + auto *leaf = acc.probeLeaf(nanovdb::Coord(1,2,3)); + EXPECT_TRUE(leaf); + EXPECT_EQ(nanovdb::Coord(0), leaf->origin()); + EXPECT_EQ(2u, leaf->valueMask().countOn()); + EXPECT_EQ(nanovdb::Coord(1,0,0), leaf->bbox()[0]); + EXPECT_EQ(nanovdb::Coord(1,2,3), leaf->bbox()[1]); + nanovdb::CoordBBox bbox(nanovdb::Coord(0), nanovdb::Coord(7)); + for (auto it = bbox.begin(); it; ++it) { + //std::cerr << *it << " offset = " << leaf->CoordToOffset(*it) << " value = " << leaf->getValue(*it) << std::endl; + if (*it < nanovdb::Coord(1,0,0)) { + EXPECT_EQ(0u, leaf->getValue(*it)); + } else if (*it < nanovdb::Coord(1,2,3)) { + EXPECT_EQ(1u, leaf->getValue(*it)); + } else { + EXPECT_EQ(3u, leaf->getValue(*it)); + } + } + const Vec3T *start=nullptr, *stop=nullptr; + + EXPECT_EQ(0u, acc.voxelPoints(nanovdb::Coord(0,0,0), start, stop)); + EXPECT_FALSE(start); + EXPECT_FALSE(stop); + + EXPECT_EQ(1u, acc.voxelPoints(nanovdb::Coord(1,0,0), start, stop)); + EXPECT_TRUE(start); + EXPECT_TRUE(stop); + EXPECT_LT(start, stop); + EXPECT_EQ(Vec3T(1, 0, 0), start[0]); + + EXPECT_EQ(2u, acc.voxelPoints(nanovdb::Coord(1,2,3), start, stop)); + EXPECT_TRUE(start); + EXPECT_TRUE(stop); + EXPECT_LT(start, stop); + EXPECT_EQ(Vec3T(1, 2, 3), start[0]); + EXPECT_EQ(Vec3T(1, 2, 3), start[1]); + + auto *lower = acc.getNode<1>(); + EXPECT_TRUE(lower); + EXPECT_EQ(nanovdb::Coord(1,0,0), lower->bbox()[0]); + EXPECT_EQ(nanovdb::Coord(1,2,3), lower->bbox()[1]); + auto *upper = acc.getNode<2>(); + EXPECT_TRUE(upper); + EXPECT_EQ(nanovdb::Coord(1,0,0), upper->bbox()[0]); + EXPECT_EQ(nanovdb::Coord(1,2,3), upper->bbox()[1]); + EXPECT_EQ(nanovdb::Coord(1,0,0), acc.root().bbox()[0]); + EXPECT_EQ(nanovdb::Coord(1,2,3), acc.root().bbox()[1]); + //timer.stop(); +}// ThreePointsToGrid + +TEST(TestNanoVDBCUDA, EightVoxelsToFloatGrid) +{ + using BuildT = float; + using GridT = nanovdb::NanoGrid; + const size_t num_points = 8; + //std::cerr << nanovdb::NanoLeaf::CoordToOffset(nanovdb::Coord( 1, 1, 1)) << std::endl; + //std::cerr << nanovdb::NanoLeaf::CoordToOffset(nanovdb::Coord(-7, 1, 1)) << std::endl; + //std::cerr << nanovdb::NanoLeaf::CoordToOffset(nanovdb::Coord( 1,-7, 1)) << std::endl; + //std::cerr << nanovdb::NanoLeaf::CoordToOffset(nanovdb::Coord( 1,-7, 1)) << std::endl; + nanovdb::Coord coords[num_points] = {nanovdb::Coord( 1, 1, 1), + nanovdb::Coord(-7, 1, 1), + nanovdb::Coord( 1,-7, 1), + nanovdb::Coord( 1, 1,-7), + nanovdb::Coord(-7,-7, 1), + nanovdb::Coord(-7, 1,-7), + nanovdb::Coord( 1,-7,-7), + nanovdb::Coord(-7,-7,-7)}, *d_coords = nullptr; + for (int i=0; i<8; ++i) EXPECT_EQ(73u, nanovdb::NanoLeaf::CoordToOffset(coords[i])); + cudaCheck(cudaMalloc(&d_coords, num_points * sizeof(nanovdb::Coord))); + cudaCheck(cudaMemcpy(d_coords, coords, num_points * sizeof(nanovdb::Coord), cudaMemcpyHostToDevice));// CPU -> GPU + + //nanovdb::GpuTimer timer("Create FloatGrid on GPU"); + nanovdb::CudaPointsToGrid converter; + auto handle = converter.getHandle(d_coords, num_points); + //timer.stop(); + cudaCheck(cudaFree(d_coords)); + + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + + const uint64_t size = sizeof(GridT) + + sizeof(GridT::TreeType) + + GridT::RootType::memUsage(8) + + 8*sizeof(GridT::UpperNodeType) + + 8*sizeof(GridT::LowerNodeType) + + 8*sizeof(GridT::LeafNodeType); + EXPECT_EQ(handle.size(), size); + + GridT *grid = handle.grid();// no grid on the CPU + EXPECT_FALSE(grid); + //timer.start("Copy data from GPU to CPU"); + handle.deviceDownload();// creates a copy up the CPU + //timer.stop(); + EXPECT_TRUE(handle.deviceData()); + EXPECT_TRUE(handle.data()); + auto *data = handle.gridData(); + EXPECT_TRUE(data); + grid = handle.grid(); + EXPECT_TRUE(grid); + //EXPECT_TRUE(grid->isLexicographic()); + EXPECT_TRUE(grid->isBreadthFirst()); + + //timer.start("Unit-testing grid on the CPU"); + auto acc = grid->getAccessor(); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(0,2,3))); + EXPECT_TRUE( acc.isActive(nanovdb::Coord(1,1,1))); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(1,2,4))); + EXPECT_FALSE(acc.isActive(nanovdb::Coord(2,2,3))); + auto *leaf = acc.probeLeaf(nanovdb::Coord(1,0,0)); + EXPECT_TRUE(leaf); + EXPECT_EQ(nanovdb::Coord(0), leaf->origin()); + EXPECT_EQ(1u, leaf->valueMask().countOn()); + EXPECT_EQ(nanovdb::Coord( 1, 1, 1), leaf->bbox()[0]); + EXPECT_EQ(nanovdb::Coord( 1, 1, 1), leaf->bbox()[1]); + auto *lower = acc.getNode<1>(); + EXPECT_TRUE(lower); + EXPECT_EQ(nanovdb::Coord(1,1,1), lower->bbox()[0]); + EXPECT_EQ(nanovdb::Coord(1,1,1), lower->bbox()[1]); + auto *upper = acc.getNode<2>(); + EXPECT_TRUE(upper); + EXPECT_EQ(nanovdb::Coord(1,1,1), upper->bbox()[0]); + EXPECT_EQ(nanovdb::Coord(1,1,1), upper->bbox()[1]); + EXPECT_EQ(nanovdb::Coord(-7,-7,-7), acc.root().bbox()[0]); + EXPECT_EQ(nanovdb::Coord( 1, 1, 1), acc.root().bbox()[1]); + //timer.stop(); +}// EightVoxelsToFloatGrid + +TEST(TestNanoVDBCUDA, Random_CudaPointsToGrid_World64) +{ + using BuildT = nanovdb::Point;//uint32_t; + using Vec3T = nanovdb::Vec3d; + //nanovdb::CpuTimer timer; + const size_t pointCount = 1 << 20;// 1048576 + std::vector points; + //generate random points + points.reserve(pointCount); + std::srand(98765); + const int max = 512, min = -max; + auto op = [&](){return rand() % (max - min) + min;}; + //timer.start("Creating "+std::to_string(pointCount)+" random points on the CPU"); + while (points.size() < pointCount) points.emplace_back(op(), op(), op()); + //timer.stop(); + EXPECT_EQ(pointCount, points.size()); + Vec3T* d_points; + const size_t pointSize = points.size() * sizeof(Vec3T); + //std::cerr << "Point footprint: " << (pointSize >> 20) << " MB" << std::endl; + //timer.start("Allocating "+std::to_string(pointSize >> 20)+" MB on the GPU"); + cudaCheck(cudaMalloc(&d_points, pointSize)); + //timer.restart("Copying points from CPU to GPU"); + cudaCheck(cudaMemcpy(d_points, points.data(), pointSize, cudaMemcpyHostToDevice)); + //timer.stop(); + + const double voxelSize = 8.0; + //timer.start("Building grid on GPU from "+std::to_string(points.size())+" points"); + nanovdb::CudaPointsToGrid converter(voxelSize);// unit map + //converter.setVerbose(); + auto handle = converter.getHandle(d_points, pointCount); + //timer.stop(); + cudaCheck(cudaFree(d_points)); + //std::cerr << "Grid size: " << (handle.size() >> 20) << " MB" << std::endl; + + const uint32_t maxPointsPerVoxel = converter.maxPointsPerVoxel(); + const uint32_t maxPointsPerLeaf = converter.maxPointsPerLeaf(); + EXPECT_GT(maxPointsPerVoxel, 0u); + EXPECT_LT(maxPointsPerLeaf, 1024u); + EXPECT_LE(maxPointsPerVoxel, maxPointsPerLeaf); + //std::cerr << "maxPointsPerLeaf = " << maxPointsPerLeaf << " maxPointsPerVoxel = " << maxPointsPerVoxel << std::endl; + + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_TRUE(handle.deviceGrid()); + EXPECT_FALSE(handle.deviceGrid(0)); + EXPECT_TRUE(handle.deviceGrid(0)); + EXPECT_FALSE(handle.deviceGrid(1)); + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + + //timer.start("Allocating and copying grid from GPU to CPU"); + auto *grid = handle.grid();// no grid on the CPU + EXPECT_FALSE(grid); + handle.deviceDownload();// creates a copy on the CPU + EXPECT_TRUE(handle.deviceData()); + EXPECT_TRUE(handle.data()); + auto *data = handle.gridData(); + EXPECT_TRUE(data); + grid = handle.grid(); + EXPECT_TRUE(grid); + //EXPECT_TRUE(grid->isLexicographic()); + EXPECT_TRUE(grid->isBreadthFirst()); + EXPECT_EQ(nanovdb::Vec3d(voxelSize), grid->voxelSize()); + EXPECT_TRUE(nanovdb::CoordBBox::createCube(min, max-1).isInside(grid->indexBBox())); + //std::cerr << grid->indexBBox() << std::endl; + EXPECT_STREQ("World64: Vec3 point coordinates in world space", grid->blindMetaData(0).mName); + { + auto mgrHdl = nanovdb::createNodeManager(*grid); + auto *mgr = mgrHdl.mgr(); + EXPECT_TRUE(mgr); + for (uint32_t i=0; ileafCount(); ++i) { + const auto &leaf = mgr->leaf(i); + for (int j=0; j<512; ++j) { + EXPECT_LE(leaf.getValue(j), maxPointsPerLeaf); + if (leaf.isActive(j)) { + if (j>0) { + EXPECT_LE(leaf.getValue(j) - leaf.getValue(j-1), maxPointsPerVoxel); + } else { + EXPECT_LE(leaf.getValue(0), maxPointsPerVoxel); + } + } else if (j>0) { + EXPECT_EQ(leaf.getValue(j), leaf.getValue(j-1)); + } else { + EXPECT_EQ(leaf.getValue(0), 0u); + } + }// loop over voxels + }// loop over leaf nodes + } + + //timer.restart("Parallel unit-testing on CPU"); + nanovdb::forEach(points,[&](const nanovdb::Range1D &r){ + nanovdb::PointAccessor acc(*grid); + EXPECT_TRUE(acc); + const Vec3T *start = nullptr, *stop = nullptr; + for (size_t i=r.begin(); i!=r.end(); ++i) { + const nanovdb::Coord ijk = grid->worldToIndex(points[i]).round(); + EXPECT_TRUE(acc.probeLeaf(ijk)!=nullptr); + EXPECT_TRUE(acc.isActive(ijk)); + EXPECT_LE(acc.getValue(ijk), pointCount); + const auto *leaf = acc.get>(ijk); + EXPECT_TRUE(leaf); + const auto offset = leaf->CoordToOffset(ijk); + EXPECT_EQ(ijk, leaf->offsetToGlobalCoord(offset)); + const uint64_t count = acc.voxelPoints(ijk, start, stop); + EXPECT_TRUE(start); + EXPECT_TRUE(stop); + EXPECT_LT(start, stop); + EXPECT_LE(count, maxPointsPerVoxel); + bool test = false; + for (uint64_t j=0; test == false && j points; + //generate random points + points.reserve(pointCount); + std::srand(98765); + const int max = 512, min = -max; + auto op = [&](){return rand() % (max - min) + min;}; + //timer.start("Creating "+std::to_string(pointCount)+" random points on the CPU"); + while (points.size() < pointCount) points.emplace_back(op(), op(), op()); + //timer.stop(); + EXPECT_EQ(pointCount, points.size()); + Vec3T* d_points; + const size_t pointSize = points.size() * sizeof(Vec3T); + //std::cerr << "Point footprint: " << (pointSize >> 20) << " MB" << std::endl; + //timer.start("Allocating "+std::to_string(pointSize >> 20)+" MB on the GPU"); + cudaCheck(cudaMalloc(&d_points, pointSize)); + //timer.restart("Copying points from CPU to GPU"); + cudaCheck(cudaMemcpy(d_points, points.data(), pointSize, cudaMemcpyHostToDevice)); + //timer.stop(); + + const double voxelSize = 8.0; + //timer.start("Building grid on GPU from "+std::to_string(points.size())+" points"); + nanovdb::CudaPointsToGrid converter(voxelSize);// unit map + //converter.setVerbose(); + auto handle = converter.getHandle(d_points, pointCount); + //timer.stop(); + cudaCheck(cudaFree(d_points)); + //std::cerr << "Grid size: " << (handle.size() >> 20) << " MB" << std::endl; + + const uint32_t maxPointsPerVoxel = converter.maxPointsPerVoxel(); + const uint32_t maxPointsPerLeaf = converter.maxPointsPerLeaf(); + EXPECT_GT(maxPointsPerVoxel, 0u); + EXPECT_LT(maxPointsPerLeaf, 1024u); + EXPECT_LE(maxPointsPerVoxel, maxPointsPerLeaf); + //std::cerr << "maxPointsPerLeaf = " << maxPointsPerLeaf << " maxPointsPerVoxel = " << maxPointsPerVoxel << std::endl; + + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_TRUE(handle.deviceGrid()); + EXPECT_FALSE(handle.deviceGrid(0)); + EXPECT_TRUE(handle.deviceGrid(0)); + EXPECT_FALSE(handle.deviceGrid(1)); + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + + //timer.start("Allocating and copying grid from GPU to CPU"); + auto *grid = handle.grid();// no grid on the CPU + EXPECT_FALSE(grid); + handle.deviceDownload();// creates a copy on the CPU + EXPECT_TRUE(handle.deviceData()); + EXPECT_TRUE(handle.data()); + auto *data = handle.gridData(); + EXPECT_TRUE(data); + grid = handle.grid(); + EXPECT_TRUE(grid); + //EXPECT_TRUE(grid->isLexicographic()); + EXPECT_TRUE(grid->isBreadthFirst()); + EXPECT_EQ(nanovdb::Vec3d(voxelSize), grid->voxelSize()); + EXPECT_EQ(pointCount, grid->pointCount()); + EXPECT_TRUE(nanovdb::CoordBBox::createCube(min, max-1).isInside(grid->indexBBox())); + //std::cerr << grid->indexBBox() << std::endl; + + EXPECT_STREQ("World64: Vec3 point coordinates in world space", grid->blindMetaData(0).mName); + { + auto mgrHdl = nanovdb::createNodeManager(*grid); + auto *mgr = mgrHdl.mgr(); + EXPECT_TRUE(mgr); + for (uint32_t i=0; ileafCount(); ++i) { + const auto &leaf = mgr->leaf(i); + for (int j=0; j<512; ++j) { + EXPECT_LE(leaf.getValue(j), maxPointsPerLeaf); + if (leaf.isActive(j)) { + if (j>0) { + EXPECT_LE(leaf.getValue(j) - leaf.getValue(j-1), maxPointsPerVoxel); + } else { + EXPECT_LE(leaf.getValue(0), maxPointsPerVoxel); + } + } else if (j>0) { + EXPECT_EQ(leaf.getValue(j), leaf.getValue(j-1)); + } else { + EXPECT_EQ(leaf.getValue(0), 0u); + } + }// loop over voxels + }// loop over leaf nodes + } + + //timer.restart("Parallel unit-testing on CPU"); + nanovdb::forEach(points,[&](const nanovdb::Range1D &r){ + nanovdb::PointAccessor acc(*grid); + EXPECT_TRUE(acc); + const Vec3T *start = nullptr, *stop = nullptr; + for (size_t i=r.begin(); i!=r.end(); ++i) { + const nanovdb::Coord ijk = grid->worldToIndex(points[i]).round(); + EXPECT_TRUE(acc.probeLeaf(ijk)!=nullptr); + EXPECT_TRUE(acc.isActive(ijk)); + EXPECT_LE(acc.getValue(ijk), pointCount); + const auto *leaf = acc.get>(ijk); + EXPECT_TRUE(leaf); + const auto offset = leaf->CoordToOffset(ijk); + EXPECT_EQ(ijk, leaf->offsetToGlobalCoord(offset)); + const uint64_t count = acc.voxelPoints(ijk, start, stop); + EXPECT_TRUE(start); + EXPECT_TRUE(stop); + EXPECT_LT(start, stop); + EXPECT_LE(count, maxPointsPerVoxel); + bool test = false; + for (uint64_t j=0; test == false && j( (points[i] - xyz).lengthSqr() ); + } + EXPECT_TRUE(test); + } + }); + + //timer.stop(); +}// Large_CudaPointsToGrid_World64 + +TEST(TestNanoVDBCUDA, Sphere_CudaPointsToGrid_World32) +{ + using BuildT = nanovdb::Point; + using Vec3T = nanovdb::Vec3f; + + //nanovdb::CpuTimer timer("Generate sphere with points"); + auto pointsHandle = nanovdb::createPointSphere(8, 100.0, nanovdb::Vec3d(0.0), 0.5); + //timer.stop(); + + auto *pointGrid = pointsHandle.grid(); + EXPECT_TRUE(pointGrid); + nanovdb::PointAccessor acc2(*pointGrid); + EXPECT_TRUE(acc2); + const Vec3T *begin, *end; + const size_t pointCount = acc2.gridPoints(begin, end); + EXPECT_TRUE(begin); + EXPECT_TRUE(end); + EXPECT_LT(begin, end); + + const size_t pointSize = pointCount * sizeof(Vec3T); + //std::cerr << "Point count = " << pointCount << ", point footprint: " << (pointSize >> 20) << " MB" << std::endl; + //std::cerr << "Upper count: " << pointGrid->tree().nodeCount(2) << ", lower count: " << pointGrid->tree().nodeCount(1) + // << ", leaf count: " << pointGrid->tree().nodeCount(0) << ", voxelSize = " << pointGrid->voxelSize()[0] << std::endl; + + //timer.start("Allocating "+std::to_string(pointSize >> 20)+" MB on the GPU"); + Vec3T* d_points; + cudaCheck(cudaMalloc(&d_points, pointSize)); + //timer.restart("Copying points from CPU to GPU"); + cudaCheck(cudaMemcpy(d_points, begin, pointSize, cudaMemcpyHostToDevice)); + //timer.stop(); + + //timer.start("Building grid on GPU from "+std::to_string(pointCount)+" points"); + nanovdb::CudaPointsToGrid converter(pointGrid->map()); + //converter.setVerbose(); + auto handle = converter.getHandle(d_points, pointCount); + //timer.stop(); + cudaCheck(cudaFree(d_points)); + //std::cerr << "Grid size: " << (handle.size() >> 20) << " MB" << std::endl; + + const uint32_t maxPointsPerVoxel = converter.maxPointsPerVoxel(); + const uint32_t maxPointsPerLeaf = converter.maxPointsPerLeaf(); + EXPECT_GT(maxPointsPerVoxel, 0u); + EXPECT_LT(maxPointsPerLeaf, 1024u); + EXPECT_LE(maxPointsPerVoxel, maxPointsPerLeaf); + //std::cerr << "maxPointsPerLeaf = " << maxPointsPerLeaf << " maxPointsPerVoxel = " << maxPointsPerVoxel << std::endl; + + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_TRUE(handle.deviceGrid()); + EXPECT_FALSE(handle.deviceGrid(0)); + EXPECT_TRUE(handle.deviceGrid(0)); + EXPECT_FALSE(handle.deviceGrid(1)); + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + + //timer.start("Allocating and copying grid from GPU to CPU"); + auto *grid = handle.grid();// no grid on the CPU + EXPECT_FALSE(grid); + handle.deviceDownload();// creates a copy on the CPU + EXPECT_TRUE(handle.deviceData()); + EXPECT_TRUE(handle.data()); + auto *data = handle.gridData(); + EXPECT_TRUE(data); + grid = handle.grid(); + EXPECT_TRUE(grid); + EXPECT_EQ(pointGrid->voxelSize(), grid->voxelSize()); + //EXPECT_TRUE(grid->isLexicographic()); + EXPECT_TRUE(grid->isBreadthFirst()); + //std::cerr << grid->indexBBox() << std::endl; + + EXPECT_STREQ("World32: Vec3 point coordinates in world space", grid->blindMetaData(0).mName); + + { + auto mgrHdl = nanovdb::createNodeManager(*grid); + auto *mgr = mgrHdl.mgr(); + EXPECT_TRUE(mgr); + for (uint32_t i=0; ileafCount(); ++i) { + const auto &leaf = mgr->leaf(i); + for (int j=0; j<512; ++j) { + EXPECT_LE(leaf.getValue(j), maxPointsPerLeaf); + if (leaf.isActive(j)) { + if (j>0) { + EXPECT_LE(leaf.getValue(j) - leaf.getValue(j-1), maxPointsPerVoxel); + } else { + EXPECT_LE(leaf.getValue(0), maxPointsPerVoxel); + } + } else if (j>0) { + EXPECT_EQ(leaf.getValue(j), leaf.getValue(j-1)); + } else { + EXPECT_EQ(leaf.getValue(0), 0u); + } + }// loop over voxels + }// loop over leaf nodes + } + + //timer.restart("Parallel unit-testing on CPU"); + nanovdb::forEach(0u, pointCount, 1u,[&](const nanovdb::Range1D &r){ + nanovdb::PointAccessor acc(*grid); + EXPECT_TRUE(acc); + const Vec3T *start = nullptr, *stop = nullptr; + for (size_t i=r.begin(); i!=r.end(); ++i) { + const nanovdb::Coord ijk = grid->worldToIndex(begin[i]).round(); + EXPECT_TRUE(acc.probeLeaf(ijk)!=nullptr); + EXPECT_TRUE(acc.isActive(ijk)); + EXPECT_LE(acc.getValue(ijk), pointCount); + const auto *leaf = acc.get>(ijk); + EXPECT_TRUE(leaf); + const auto offset = leaf->CoordToOffset(ijk); + EXPECT_EQ(ijk, leaf->offsetToGlobalCoord(offset)); + const uint64_t count = acc.voxelPoints(ijk, start, stop); + EXPECT_TRUE(start); + EXPECT_TRUE(stop); + EXPECT_LT(start, stop); + EXPECT_LE(count, maxPointsPerVoxel); + bool test = false; + for (uint64_t j=0; test == false && j(); + EXPECT_TRUE(pointGrid); + nanovdb::PointAccessor acc2(*pointGrid); + EXPECT_TRUE(acc2); + const Vec3T *begin, *end; + const size_t pointCount = acc2.gridPoints(begin, end); + EXPECT_TRUE(begin); + EXPECT_TRUE(end); + EXPECT_LT(begin, end); + + const size_t pointSize = pointCount * sizeof(Vec3T); + //std::cerr << "Point count = " << pointCount << ", point footprint: " << (pointSize >> 20) << " MB" << std::endl; + //std::cerr << "Upper count: " << pointGrid->tree().nodeCount(2) << ", lower count: " << pointGrid->tree().nodeCount(1) + // << ", leaf count: " << pointGrid->tree().nodeCount(0) << ", voxelSize = " << pointGrid->voxelSize()[0] << std::endl; + + //timer.start("Allocating "+std::to_string(pointSize >> 20)+" MB on the GPU"); + Vec3T* d_points; + cudaCheck(cudaMalloc(&d_points, pointSize)); + //timer.restart("Copying points from CPU to GPU"); + cudaCheck(cudaMemcpy(d_points, begin, pointSize, cudaMemcpyHostToDevice)); + //timer.stop(); + + //timer.start("Building grid on GPU from "+std::to_string(pointCount)+" points"); + ///////////////////////////////////////////////////////////////////////// + nanovdb::CudaPointsToGrid converter(pointGrid->map()); + //converter.setVerbose(); + converter.setPointType(nanovdb::PointType::Voxel32); + auto handle = converter.getHandle(d_points, pointCount); + ///////////////////////////////////////////////////////////////////////// + //timer.stop(); + cudaCheck(cudaFree(d_points)); + //std::cerr << "Grid size: " << (handle.size() >> 20) << " MB" << std::endl; + + const uint32_t maxPointsPerVoxel = converter.maxPointsPerVoxel(); + const uint32_t maxPointsPerLeaf = converter.maxPointsPerLeaf(); + EXPECT_GT(maxPointsPerVoxel, 0u); + EXPECT_LT(maxPointsPerLeaf, 1024u); + EXPECT_LE(maxPointsPerVoxel, maxPointsPerLeaf); + //std::cerr << "maxPointsPerLeaf = " << maxPointsPerLeaf << " maxPointsPerVoxel = " << maxPointsPerVoxel << std::endl; + + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_TRUE(handle.deviceGrid()); + EXPECT_FALSE(handle.deviceGrid(0)); + EXPECT_TRUE(handle.deviceGrid(0)); + EXPECT_FALSE(handle.deviceGrid(1)); + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + + //timer.start("Allocating and copying grid from GPU to CPU"); + auto *grid = handle.grid();// no grid on the CPU + EXPECT_FALSE(grid); + handle.deviceDownload();// creates a copy on the CPU + EXPECT_TRUE(handle.deviceData()); + EXPECT_TRUE(handle.data()); + auto *data = handle.gridData(); + EXPECT_TRUE(data); + grid = handle.grid(); + EXPECT_TRUE(grid); + EXPECT_EQ(pointGrid->voxelSize(), grid->voxelSize()); + //EXPECT_TRUE(grid->isLexicographic()); + EXPECT_TRUE(grid->isBreadthFirst()); + //std::cerr << grid->indexBBox() << std::endl; + + EXPECT_STREQ("Voxel32: Vec3 point coordinates in voxel space", grid->blindMetaData(0).mName); + + { + auto mgrHdl = nanovdb::createNodeManager(*grid); + auto *mgr = mgrHdl.mgr(); + EXPECT_TRUE(mgr); + for (uint32_t i=0; ileafCount(); ++i) { + const auto &leaf = mgr->leaf(i); + for (int j=0; j<512; ++j) { + EXPECT_LE(leaf.getValue(j), maxPointsPerLeaf); + if (leaf.isActive(j)) { + if (j>0) { + EXPECT_LE(leaf.getValue(j) - leaf.getValue(j-1), maxPointsPerVoxel); + } else { + EXPECT_LE(leaf.getValue(0), maxPointsPerVoxel); + } + } else if (j>0) { + EXPECT_EQ(leaf.getValue(j), leaf.getValue(j-1)); + } else { + EXPECT_EQ(leaf.getValue(0), 0u); + } + }// loop over voxels + }// loop over leaf nodes + } + + //timer.restart("Parallel unit-testing on CPU"); + nanovdb::forEach(0u, pointCount, 1u,[&](const nanovdb::Range1D &r){ + nanovdb::PointAccessor acc(*grid); + EXPECT_TRUE(acc); + const Vec3T *start = nullptr, *stop = nullptr; + for (size_t i=r.begin(); i!=r.end(); ++i) { + const nanovdb::Coord ijk = grid->worldToIndex(begin[i]).round(); + EXPECT_TRUE(acc.probeLeaf(ijk)!=nullptr); + EXPECT_TRUE(acc.isActive(ijk)); + EXPECT_LE(acc.getValue(ijk), pointCount); + const auto *leaf = acc.get>(ijk); + EXPECT_TRUE(leaf); + const auto offset = leaf->CoordToOffset(ijk); + EXPECT_EQ(ijk, leaf->offsetToGlobalCoord(offset)); + const uint64_t count = acc.voxelPoints(ijk, start, stop); + EXPECT_TRUE(start); + EXPECT_TRUE(stop); + EXPECT_LT(start, stop); + EXPECT_LE(count, maxPointsPerVoxel); + bool test = false; + for (uint64_t j=0; test == false && jmap())).length() < 1e-9; + } + EXPECT_TRUE(test); + } + }); + + //timer.stop(); +}// Sphere_CudaPointsToGrid_Voxel32 + +TEST(TestNanoVDBCUDA, Sphere_CudaPointsToGrid_Voxel16) +{ + EXPECT_EQ(6u, sizeof(nanovdb::Vec3u16)); + using BuildT = nanovdb::Point; + using Vec3T = nanovdb::Vec3f; + + //nanovdb::CpuTimer timer("Generate sphere with points"); + auto pointsHandle = nanovdb::createPointSphere(8, 100.0, nanovdb::Vec3d(0.0), 0.5); + //timer.stop(); + + auto *pointGrid = pointsHandle.grid(); + EXPECT_TRUE(pointGrid); + nanovdb::PointAccessor acc2(*pointGrid); + EXPECT_TRUE(acc2); + const Vec3T *begin, *end; + const size_t pointCount = acc2.gridPoints(begin, end); + EXPECT_TRUE(begin); + EXPECT_TRUE(end); + EXPECT_LT(begin, end); + + const size_t pointSize = pointCount * sizeof(Vec3T); + //std::cerr << "Point count = " << pointCount << ", point footprint: " << (pointSize >> 20) << " MB" << std::endl; + //std::cerr << "Upper count: " << pointGrid->tree().nodeCount(2) << ", lower count: " << pointGrid->tree().nodeCount(1) + // << ", leaf count: " << pointGrid->tree().nodeCount(0) << ", voxelSize = " << pointGrid->voxelSize()[0] << std::endl; + + //timer.start("Allocating "+std::to_string(pointSize >> 20)+" MB on the GPU"); + Vec3T* d_points; + cudaCheck(cudaMalloc(&d_points, pointSize)); + //timer.restart("Copying points from CPU to GPU"); + cudaCheck(cudaMemcpy(d_points, begin, pointSize, cudaMemcpyHostToDevice)); + //timer.stop(); + + //timer.start("Building grid on GPU from "+std::to_string(pointCount)+" points"); + ///////////////////////////////////////////////////////////////////////// + nanovdb::CudaPointsToGrid converter(pointGrid->map()); + //converter.setVerbose(); + converter.setPointType(nanovdb::PointType::Voxel16); + auto handle = converter.getHandle(d_points, pointCount); + ///////////////////////////////////////////////////////////////////////// + //timer.stop(); + cudaCheck(cudaFree(d_points)); + //std::cerr << "Grid size: " << (handle.size() >> 20) << " MB" << std::endl; + + const uint32_t maxPointsPerVoxel = converter.maxPointsPerVoxel(); + const uint32_t maxPointsPerLeaf = converter.maxPointsPerLeaf(); + EXPECT_GT(maxPointsPerVoxel, 0u); + EXPECT_LT(maxPointsPerLeaf, 1024u); + EXPECT_LE(maxPointsPerVoxel, maxPointsPerLeaf); + //std::cerr << "maxPointsPerLeaf = " << maxPointsPerLeaf << " maxPointsPerVoxel = " << maxPointsPerVoxel << std::endl; + + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_TRUE(handle.deviceGrid()); + EXPECT_FALSE(handle.deviceGrid(0)); + EXPECT_TRUE(handle.deviceGrid(0)); + EXPECT_FALSE(handle.deviceGrid(1)); + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + + //timer.start("Allocating and copying grid from GPU to CPU"); + auto *grid = handle.grid();// no grid on the CPU + EXPECT_FALSE(grid); + handle.deviceDownload();// creates a copy on the CPU + EXPECT_TRUE(handle.deviceData()); + EXPECT_TRUE(handle.data()); + auto *data = handle.gridData(); + EXPECT_TRUE(data); + grid = handle.grid(); + EXPECT_TRUE(grid); + EXPECT_EQ(pointGrid->voxelSize(), grid->voxelSize()); + //EXPECT_TRUE(grid->isLexicographic()); + EXPECT_TRUE(grid->isBreadthFirst()); + //std::cerr << grid->indexBBox() << std::endl; + + EXPECT_STREQ("Voxel16: Vec3 point coordinates in voxel space", grid->blindMetaData(0).mName); + + { + auto mgrHdl = nanovdb::createNodeManager(*grid); + auto *mgr = mgrHdl.mgr(); + EXPECT_TRUE(mgr); + for (uint32_t i=0; ileafCount(); ++i) { + const auto &leaf = mgr->leaf(i); + for (int j=0; j<512; ++j) { + EXPECT_LE(leaf.getValue(j), maxPointsPerLeaf); + if (leaf.isActive(j)) { + if (j>0) { + EXPECT_LE(leaf.getValue(j) - leaf.getValue(j-1), maxPointsPerVoxel); + } else { + EXPECT_LE(leaf.getValue(0), maxPointsPerVoxel); + } + } else if (j>0) { + EXPECT_EQ(leaf.getValue(j), leaf.getValue(j-1)); + } else { + EXPECT_EQ(leaf.getValue(0), 0u); + } + }// loop over voxels + }// loop over leaf nodes + } + + //timer.restart("Parallel unit-testing on CPU"); + nanovdb::forEach(0u, pointCount, 1u,[&](const nanovdb::Range1D &r){ + nanovdb::PointAccessor acc(*grid); + EXPECT_TRUE(acc); + const nanovdb::Vec3u16 *start = nullptr, *stop = nullptr; + for (size_t i=r.begin(); i!=r.end(); ++i) { + const nanovdb::Coord ijk = grid->worldToIndex(begin[i]).round(); + EXPECT_TRUE(acc.probeLeaf(ijk)!=nullptr); + EXPECT_TRUE(acc.isActive(ijk)); + EXPECT_LE(acc.getValue(ijk), pointCount); + const auto *leaf = acc.get>(ijk); + EXPECT_TRUE(leaf); + const auto offset = leaf->CoordToOffset(ijk); + EXPECT_EQ(ijk, leaf->offsetToGlobalCoord(offset)); + const uint64_t count = acc.voxelPoints(ijk, start, stop); + EXPECT_TRUE(start); + EXPECT_TRUE(stop); + EXPECT_LT(start, stop); + EXPECT_LE(count, maxPointsPerVoxel); + bool test = false; + for (uint64_t j=0; test == false && jmap())).length() < 1e-6; + } + } + }); + + //timer.stop(); +}// Sphere_CudaPointsToGrid_Voxel16 + +TEST(TestNanoVDBCUDA, Sphere_CudaPointsToGrid_Voxel8) +{ + EXPECT_EQ(3u, sizeof(nanovdb::Vec3u8)); + + using BuildT = nanovdb::Point; + using Vec3T = nanovdb::Vec3f; + + //nanovdb::CpuTimer timer("Generate sphere with points"); + auto pointsHandle = nanovdb::createPointSphere(8, 100.0, nanovdb::Vec3d(0.0), 0.5); + //timer.stop(); + + auto *pointGrid = pointsHandle.grid(); + EXPECT_TRUE(pointGrid); + //std::cerr << "nanovdb::bbox = " << pointGrid->indexBBox() << " voxel count = " << pointGrid->activeVoxelCount() << std::endl; + nanovdb::PointAccessor acc2(*pointGrid); + EXPECT_TRUE(acc2); + const Vec3T *begin, *end; + const size_t pointCount = acc2.gridPoints(begin, end); + EXPECT_TRUE(begin); + EXPECT_TRUE(end); + EXPECT_LT(begin, end); + + const size_t pointSize = pointCount * sizeof(Vec3T); + //std::cerr << "Point count = " << pointCount << ", point footprint: " << (pointSize >> 20) << " MB" << std::endl; + //std::cerr << "Upper count: " << pointGrid->tree().nodeCount(2) << ", lower count: " << pointGrid->tree().nodeCount(1) + // << ", leaf count: " << pointGrid->tree().nodeCount(0) << ", voxelSize = " << pointGrid->voxelSize()[0] << std::endl; + + //timer.start("Allocating "+std::to_string(pointSize >> 20)+" MB on the GPU"); + Vec3T* d_points; + cudaCheck(cudaMalloc(&d_points, pointSize)); + //timer.restart("Copying points from CPU to GPU"); + cudaCheck(cudaMemcpy(d_points, begin, pointSize, cudaMemcpyHostToDevice)); + //timer.stop(); + + //timer.start("Building grid on GPU from "+std::to_string(pointCount)+" points"); + ///////////////////////////////////////////////////////////////////////// + //auto handle = nanovdb::cudaPointsToGrid(d_points, pointCount, nanovdb::PointType::Voxel8); + nanovdb::CudaPointsToGrid converter(pointGrid->map()); + //converter.setVerbose(); + converter.setPointType(nanovdb::PointType::Voxel8); + auto handle = converter.getHandle(d_points, pointCount); + ///////////////////////////////////////////////////////////////////////// + //timer.stop(); + cudaCheck(cudaFree(d_points)); + //std::cerr << "Grid size: " << (handle.size() >> 20) << " MB" << std::endl; + + const uint32_t maxPointsPerVoxel = converter.maxPointsPerVoxel(); + const uint32_t maxPointsPerLeaf = converter.maxPointsPerLeaf(); + EXPECT_GT(maxPointsPerVoxel, 0u); + EXPECT_LT(maxPointsPerLeaf, 1024u); + EXPECT_LE(maxPointsPerVoxel, maxPointsPerLeaf); + //std::cerr << "maxPointsPerLeaf = " << maxPointsPerLeaf << " maxPointsPerVoxel = " << maxPointsPerVoxel << std::endl; + + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_TRUE(handle.deviceGrid()); + EXPECT_FALSE(handle.deviceGrid(0)); + EXPECT_TRUE(handle.deviceGrid(0)); + EXPECT_FALSE(handle.deviceGrid(1)); + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + + //timer.start("Allocating and copying grid from GPU to CPU"); + auto *grid = handle.grid();// no grid on the CPU + EXPECT_FALSE(grid); + handle.deviceDownload();// creates a copy on the CPU + EXPECT_TRUE(handle.deviceData()); + EXPECT_TRUE(handle.data()); + auto *data = handle.gridData(); + EXPECT_TRUE(data); + grid = handle.grid(); + EXPECT_TRUE(grid); + EXPECT_EQ(pointGrid->voxelSize(), grid->voxelSize()); + //EXPECT_TRUE(grid->isLexicographic()); + EXPECT_TRUE(grid->isBreadthFirst()); + //std::cerr << grid->indexBBox() << std::endl; + + EXPECT_STREQ("Voxel8: Vec3 point coordinates in voxel space", grid->blindMetaData(0).mName); + + { + auto mgrHdl = nanovdb::createNodeManager(*grid); + auto *mgr = mgrHdl.mgr(); + EXPECT_TRUE(mgr); + for (uint32_t i=0; ileafCount(); ++i) { + const auto &leaf = mgr->leaf(i); + for (int j=0; j<512; ++j) { + EXPECT_LE(leaf.getValue(j), maxPointsPerLeaf); + if (leaf.isActive(j)) { + if (j>0) { + EXPECT_LE(leaf.getValue(j) - leaf.getValue(j-1), maxPointsPerVoxel); + } else { + EXPECT_LE(leaf.getValue(0), maxPointsPerVoxel); + } + } else if (j>0) { + EXPECT_EQ(leaf.getValue(j), leaf.getValue(j-1)); + } else { + EXPECT_EQ(leaf.getValue(0), 0u); + } + }// loop over voxels + }// loop over leaf nodes + } + + //timer.restart("Parallel unit-testing on CPU"); + nanovdb::forEach(0u, pointCount, 1u,[&](const nanovdb::Range1D &r){ + nanovdb::PointAccessor acc(*grid); + EXPECT_TRUE(acc); + const nanovdb::Vec3u8 *start = nullptr, *stop = nullptr; + for (size_t i=r.begin(); i!=r.end(); ++i) { + const nanovdb::Coord ijk = grid->worldToIndex(begin[i]).round(); + EXPECT_TRUE(acc.probeLeaf(ijk)!=nullptr); + EXPECT_TRUE(acc.isActive(ijk)); + EXPECT_LE(acc.getValue(ijk), pointCount); + const auto *leaf = acc.get>(ijk); + EXPECT_TRUE(leaf); + const auto offset = leaf->CoordToOffset(ijk); + EXPECT_EQ(ijk, leaf->offsetToGlobalCoord(offset)); + const uint64_t count = acc.voxelPoints(ijk, start, stop); + EXPECT_TRUE(start); + EXPECT_TRUE(stop); + EXPECT_LT(start, stop); + EXPECT_LE(count, maxPointsPerVoxel); + bool test = false; + for (uint64_t j=0; test == false && jmap())).length() < 1e-2; + } + EXPECT_TRUE(test); + } + }); + //timer.stop(); +}// Sphere_CudaPointsToGrid_Voxel8 + +TEST(TestNanoVDBCUDA, Sphere_CudaPointsToGrid_PointID) +{ + EXPECT_EQ(3u, sizeof(nanovdb::Vec3u8)); + + using BuildT = nanovdb::Point; + using Vec3T = nanovdb::Vec3f; + + //nanovdb::CpuTimer timer("Generate sphere with points"); + auto pointsHandle = nanovdb::createPointSphere(8, 100.0, nanovdb::Vec3d(0.0), 0.5); + //timer.stop(); + + auto *pointGrid = pointsHandle.grid(); + EXPECT_TRUE(pointGrid); + //std::cerr << "nanovdb::bbox = " << pointGrid->indexBBox() << " voxel count = " << pointGrid->activeVoxelCount() << std::endl; + nanovdb::PointAccessor acc2(*pointGrid); + EXPECT_TRUE(acc2); + const Vec3T *begin, *end; + const size_t pointCount = acc2.gridPoints(begin, end); + EXPECT_TRUE(begin); + EXPECT_TRUE(end); + EXPECT_LT(begin, end); + + const size_t pointSize = pointCount * sizeof(Vec3T); + //std::cerr << "Point count = " << pointCount << ", point footprint: " << (pointSize >> 20) << " MB" << std::endl; + //std::cerr << "Upper count: " << pointGrid->tree().nodeCount(2) << ", lower count: " << pointGrid->tree().nodeCount(1) + // << ", leaf count: " << pointGrid->tree().nodeCount(0) << ", voxelSize = " << pointGrid->voxelSize()[0] << std::endl; + + //timer.start("Allocating "+std::to_string(pointSize >> 20)+" MB on the GPU"); + Vec3T* d_points; + cudaCheck(cudaMalloc(&d_points, pointSize)); + //timer.restart("Copying points from CPU to GPU"); + cudaCheck(cudaMemcpy(d_points, begin, pointSize, cudaMemcpyHostToDevice)); + //timer.stop(); + + //timer.start("Building grid on GPU from "+std::to_string(pointCount)+" points"); + ///////////////////////////////////////////////////////////////////////// + //auto handle = nanovdb::cudaPointsToGrid(d_points, pointCount, nanovdb::PointType::Voxel8); + nanovdb::CudaPointsToGrid converter(pointGrid->map()); + //converter.setVerbose(2); + converter.setPointType(nanovdb::PointType::PointID); + auto handle = converter.getHandle(d_points, pointCount); + ///////////////////////////////////////////////////////////////////////// + //timer.stop(); + cudaCheck(cudaFree(d_points)); + //std::cerr << "Grid size: " << (handle.size() >> 20) << " MB" << std::endl; + + const uint32_t maxPointsPerVoxel = converter.maxPointsPerVoxel(); + const uint32_t maxPointsPerLeaf = converter.maxPointsPerLeaf(); + EXPECT_GT(maxPointsPerVoxel, 0u); + EXPECT_LT(maxPointsPerLeaf, 1024u); + EXPECT_LE(maxPointsPerVoxel, maxPointsPerLeaf); + //std::cerr << "maxPointsPerLeaf = " << maxPointsPerLeaf << " maxPointsPerVoxel = " << maxPointsPerVoxel << std::endl; + + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_TRUE(handle.deviceGrid()); + EXPECT_FALSE(handle.deviceGrid(0)); + EXPECT_TRUE(handle.deviceGrid(0)); + EXPECT_FALSE(handle.deviceGrid(1)); + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + + //timer.start("Allocating and copying grid from GPU to CPU"); + auto *grid = handle.grid();// no grid on the CPU + EXPECT_FALSE(grid); + handle.deviceDownload();// creates a copy on the CPU + EXPECT_TRUE(handle.deviceData()); + EXPECT_TRUE(handle.data()); + auto *data = handle.gridData(); + EXPECT_TRUE(data); + grid = handle.grid(); + EXPECT_TRUE(grid); + EXPECT_EQ(pointGrid->voxelSize(), grid->voxelSize()); + //EXPECT_TRUE(grid->isLexicographic()); + EXPECT_TRUE(grid->isBreadthFirst()); + //std::cerr << grid->indexBBox() << std::endl; + + EXPECT_STREQ("PointID: uint32_t indices to points", grid->blindMetaData(0).mName); + + { + auto mgrHdl = nanovdb::createNodeManager(*grid); + auto *mgr = mgrHdl.mgr(); + EXPECT_TRUE(mgr); + for (uint32_t i=0; ileafCount(); ++i) { + const auto &leaf = mgr->leaf(i); + for (int j=0; j<512; ++j) { + EXPECT_LE(leaf.getValue(j), maxPointsPerLeaf); + if (leaf.isActive(j)) { + if (j>0) { + EXPECT_LE(leaf.getValue(j) - leaf.getValue(j-1), maxPointsPerVoxel); + } else { + EXPECT_LE(leaf.getValue(0), maxPointsPerVoxel); + } + } else if (j>0) { + EXPECT_EQ(leaf.getValue(j), leaf.getValue(j-1)); + } else { + EXPECT_EQ(leaf.getValue(0), 0u); + } + }// loop over voxels + }// loop over leaf nodes + } + + //timer.restart("Parallel unit-testing on CPU"); + nanovdb::forEach(0u, pointCount, 1u,[&](const nanovdb::Range1D &r){ + nanovdb::PointAccessor acc(*grid); + EXPECT_TRUE(acc); + const uint32_t *start = nullptr, *stop = nullptr; + for (size_t i=r.begin(); i!=r.end(); ++i) { + const nanovdb::Coord ijk = grid->worldToIndex(begin[i]).round(); + EXPECT_TRUE(acc.probeLeaf(ijk)!=nullptr); + EXPECT_TRUE(acc.isActive(ijk)); + EXPECT_LE(acc.getValue(ijk), pointCount); + const auto *leaf = acc.get>(ijk); + EXPECT_TRUE(leaf); + const auto offset = leaf->CoordToOffset(ijk); + EXPECT_EQ(ijk, leaf->offsetToGlobalCoord(offset)); + const uint64_t count = acc.voxelPoints(ijk, start, stop); + EXPECT_TRUE(start); + EXPECT_TRUE(stop); + EXPECT_LT(start, stop); + EXPECT_LE(count, maxPointsPerVoxel); + } + }); + + //timer.stop(); +}// Sphere_CudaPointsToGrid_PointID + +TEST(TestNanoVDBCUDA, NanoGrid_Rgba8) +{ + using BuildT = nanovdb::Rgba8; + using GridT = nanovdb::NanoGrid; + const size_t num_points = 1; + nanovdb::Coord coords[num_points] = {nanovdb::Coord(1, 2, 3)}, *d_coords = nullptr; + cudaCheck(cudaMalloc(&d_coords, num_points * sizeof(nanovdb::Coord))); + cudaCheck(cudaMemcpy(d_coords, coords, num_points * sizeof(nanovdb::Coord), cudaMemcpyHostToDevice));// CPU -> GPU + + nanovdb::CudaPointsToGrid converter; + auto handle = converter.getHandle(d_coords, num_points); + cudaCheck(cudaFree(d_coords)); + + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + + const uint64_t size = sizeof(GridT) + + sizeof(GridT::TreeType) + + GridT::RootType::memUsage(1) + + sizeof(GridT::UpperNodeType) + + sizeof(GridT::LowerNodeType) + + sizeof(GridT::LeafNodeType); + EXPECT_EQ(handle.size(), size); + + GridT *grid = handle.grid();// no grid on the CPU + EXPECT_FALSE(grid); + handle.deviceDownload();// creates a copy up the CPU + EXPECT_TRUE(handle.deviceData()); + EXPECT_TRUE(handle.data()); + auto *data = handle.gridData(); + EXPECT_TRUE(data); + grid = handle.grid(); + EXPECT_TRUE(grid); +}// NanoGrid_Rgba8 + +TEST(TestNanoVDBCUDA, cudaAddBlindData) +{ + using BuildT = float; + using GridT = nanovdb::NanoGrid; + const size_t num_points = 2; + nanovdb::Coord coords[num_points] = {nanovdb::Coord(1, 2, 3), nanovdb::Coord(10,20,8)}, *d_coords = nullptr; + cudaCheck(cudaMalloc(&d_coords, num_points * sizeof(nanovdb::Coord))); + cudaCheck(cudaMemcpy(d_coords, coords, num_points * sizeof(nanovdb::Coord), cudaMemcpyHostToDevice));// CPU -> GPU + auto handle = nanovdb::cudaVoxelsToGrid(d_coords, num_points); + cudaCheck(cudaFree(d_coords)); + EXPECT_TRUE(handle.deviceData());// grid only exists on the GPU + EXPECT_FALSE(handle.data());// no grid was yet allocated on the CPU + const uint64_t size = sizeof(GridT) + + sizeof(GridT::TreeType) + + GridT::RootType::memUsage(1) + + sizeof(GridT::UpperNodeType) + + sizeof(GridT::LowerNodeType) + + 2*sizeof(GridT::LeafNodeType); + EXPECT_EQ(handle.size(), size); + GridT *d_grid = handle.deviceGrid();// no grid on the CPU + EXPECT_TRUE(d_grid); + float *d_blind = nullptr, blind[num_points] = {1.2f, 3.0f}; + cudaCheck(cudaMalloc(&d_blind, num_points * sizeof(float))); + cudaCheck(cudaMemcpy(d_blind, blind, num_points * sizeof(float), cudaMemcpyHostToDevice));// CPU -> GPU + + //nanovdb::GpuTimer timer("cudaAddBlindData"); + auto handle2 = nanovdb::cudaAddBlindData(d_grid, d_blind, num_points); + cudaCheck(cudaFree(d_blind)); + //timer.stop(); + EXPECT_TRUE(handle2.deviceData());// grid only exists on the GPU + EXPECT_FALSE(handle2.data());// no grid was yet allocated on the CPU + EXPECT_EQ(handle2.size(), handle.size() + sizeof(nanovdb::GridBlindMetaData) + nanovdb::AlignUp(num_points*sizeof(float))); + + auto *grid2 = handle2.grid();// no grid on the CPU + EXPECT_FALSE(grid2); + handle2.deviceDownload();// creates a copy on the CPU + EXPECT_TRUE(handle2.deviceData()); + EXPECT_TRUE(handle2.data()); + auto *data = handle2.gridData(); + EXPECT_TRUE(data); + grid2 = handle2.grid(); + EXPECT_TRUE(grid2); + EXPECT_EQ(nanovdb::Vec3d(1.0), grid2->voxelSize()); + EXPECT_EQ(1u, grid2->blindDataCount()); + const auto &bd2 = grid2->blindMetaData(0); + EXPECT_EQ(num_points, bd2.mValueCount); + EXPECT_EQ(nanovdb::GridBlindDataSemantic::Unknown, bd2.mSemantic); + EXPECT_EQ(nanovdb::GridBlindDataClass::Unknown, bd2.mDataClass); + EXPECT_EQ(nanovdb::GridType::Float, bd2.mDataType); + EXPECT_STREQ("", bd2.mName); + const float *dataPtr = bd2.getBlindData(); + EXPECT_TRUE(dataPtr); + for (size_t i=0; i();// no grid on the CPU + EXPECT_TRUE(d_grid2); + + nanovdb::Vec3f *d_blind2 = nullptr, blind2[num_points] = {nanovdb::Vec3f(1.2f), nanovdb::Vec3f(3.0f)}; + cudaCheck(cudaMalloc(&d_blind2, num_points * sizeof(nanovdb::Vec3f))); + cudaCheck(cudaMemcpy(d_blind2, blind2, num_points * sizeof(nanovdb::Vec3f), cudaMemcpyHostToDevice));// CPU -> GPU + + auto handle3 = nanovdb::cudaAddBlindData(d_grid2, d_blind2, num_points, + nanovdb::GridBlindDataClass::AttributeArray, + nanovdb::GridBlindDataSemantic::PointPosition, + "this is a test"); + cudaCheck(cudaFree(d_blind2)); + handle3.deviceDownload();// creates a copy on the CPU + GridT *grid3 = handle3.grid();// no grid on the CPU + EXPECT_TRUE(grid3); + EXPECT_EQ(2, grid3->blindDataCount()); + + const auto &bd3 = grid3->blindMetaData(0); + EXPECT_EQ(num_points, bd3.mValueCount); + EXPECT_EQ(nanovdb::GridBlindDataSemantic::Unknown, bd3.mSemantic); + EXPECT_EQ(nanovdb::GridBlindDataClass::Unknown, bd3.mDataClass); + EXPECT_EQ(nanovdb::GridType::Float, bd3.mDataType); + EXPECT_STREQ("", bd3.mName); + dataPtr = grid3->getBlindData(0); + EXPECT_TRUE(dataPtr); + for (size_t i=0; iblindMetaData(1); + EXPECT_EQ(num_points, bd4.mValueCount); + EXPECT_EQ(nanovdb::GridBlindDataSemantic::PointPosition, bd4.mSemantic); + EXPECT_EQ(nanovdb::GridBlindDataClass::AttributeArray, bd4.mDataClass); + EXPECT_EQ(nanovdb::GridType::Vec3f, bd4.mDataType); + EXPECT_STREQ("this is a test", bd4.mName); + auto *dataPtr2 = grid3->getBlindData(1); + EXPECT_TRUE(dataPtr2); + for (size_t i=0; i(100); + { + auto *floatGrid = cudaHandle.grid(); + EXPECT_TRUE(floatGrid); + auto acc = floatGrid->getAccessor(); + EXPECT_EQ( 3.0f, acc(103,0,0)); + EXPECT_EQ( 0.0f, acc(100,0,0)); + EXPECT_EQ(-3.0f, acc( 97,0,0)); + } + auto hostHandle = cudaHandle.copy(); + EXPECT_TRUE(cudaHandle.grid());// should be unchanged + { + auto *floatGrid = hostHandle.grid(); + EXPECT_TRUE(floatGrid); + auto acc = floatGrid->getAccessor(); + EXPECT_EQ( 3.0f, acc(103,0,0)); + EXPECT_EQ( 0.0f, acc(100,0,0)); + EXPECT_EQ(-3.0f, acc( 97,0,0)); + } +}// testGridHandleCopy + +// make -j testNanoVDB && ./unittest/testNanoVDB --gtest_break_on_failure --gtest_filter="*compareNodeOrdering" +TEST(TestNanoVDBCUDA, compareNodeOrdering) +{ + using namespace nanovdb; +#if 0 + const int voxelCount = 2; + Coord coords[voxelCount]={Coord(-1,0,0), Coord(0,0,0)}; +#else + const int voxelCount = 5; + Coord coords[voxelCount]={Coord(0,0,0), Coord(256,0,0), Coord(0,0,8), Coord(0,-256,0), Coord(0,2,4)}; +#endif + + {// check coordToKey and keyToCoord used in CudaPointsToGrid + auto coordToKey = [](const nanovdb::Coord &ijk)->uint64_t{ + static constexpr int32_t offset = 1 << 30; + return (uint64_t(uint32_t(ijk[2] + offset) >> 12)) | // z is the lower 21 bits + (uint64_t(uint32_t(ijk[1] + offset) >> 12) << 21) | // y is the middle 21 bits + (uint64_t(uint32_t(ijk[0] + offset) >> 12) << 42); // x is the upper 21 bits + }; + auto keyToCoord = [](uint64_t key)->nanovdb::Coord{ + static constexpr int32_t offset = 1 << 30; + static constexpr uint64_t MASK = (1u << 21) - 1; // used to mask out 21 lower bits + return nanovdb::Coord((((key >> 42) & MASK) << 12) - offset, // x are the upper 21 bits + (((key >> 21) & MASK) << 12) - offset, // y are the middle 21 bits + ((key & MASK) << 12) - offset); // z are the lower 21 bits + }; + using KeyT = std::pair; + KeyT keys[voxelCount]; + for (int i=0; i>>((const uint8_t*)d_str, d_checksum, 1, s.size()); + cudaCheck(cudaMemcpy(&checksum, d_checksum, 4, cudaMemcpyDeviceToHost)); + cudaCheck(cudaFree(d_str)); + cudaCheck(cudaFree(d_checksum)); + std::stringstream ss; + ss << std::hex << std::setw(8) << std::setfill('0') << checksum; + EXPECT_EQ("414fa339", ss.str());// 414FA339 from https://rosettagit.org/drafts/crc-32/#c-1 + } + auto handle = nanovdb::createLevelSetSphere(100); + EXPECT_TRUE(handle.data()); + auto *grid = handle.grid(); + EXPECT_TRUE(grid); + handle.deviceUpload(); + EXPECT_TRUE(handle.deviceData()); +#if 0// entire grid or just GridData+TreeData+RootData + const size_t size = handle.size(); +#else + const uint64_t size = grid->memUsage() + grid->tree().memUsage() + grid->tree().root().memUsage() - 16; +#endif + //std::cerr << "Grid + tree + root data is " << size << " bytes\n"; + nanovdb::CpuTimer cpuTimer; + nanovdb::GpuTimer gpuTimer; + {//benchmark CPU version that uses a table + //cpuTimer.start("CPU Tabled CRC of level set sphere"); + auto lut = nanovdb::crc32::createLut(); + checksum = nanovdb::crc32::checksum(handle.data()+16, size, lut.get()); + //cpuTimer.stop(); + //std::cerr << checksum << std::endl; + } + {//benchmark CPU version that uses no table + //cpuTimer.start("CPU Untabled CRC of level set sphere"); + auto checksum2 = nanovdb::crc32::checksum(handle.data()+16, size); + //cpuTimer.stop(); + //std::cerr << checksum2 << std::endl; + EXPECT_EQ(checksum, checksum2); + } + {//benchmark CPU version that uses table + //cpuTimer.start("CPU tabled crc32::CRC of level set sphere"); + auto lut = nanovdb::crc32::createLut(); + auto checksum2 = nanovdb::crc32::checksum(handle.data()+16, size, lut.get()); + //cpuTimer.stop(); + //std::cerr << checksum2 << std::endl; + EXPECT_EQ(checksum, checksum2); + } + uint32_t checksum2, *d_checksum; + cudaCheck(cudaMalloc((void**)&d_checksum, 4)); + {//benchmark GPU version that uses no table + //gpuTimer.start("GPU Untabled CRC of level set sphere"); + nanovdb::crc32::checksumKernel<<<1, 1>>>(handle.deviceData()+16, d_checksum, 1, size); + //gpuTimer.stop(); + cudaCheck(cudaMemcpy(&checksum2, d_checksum, 4, cudaMemcpyDeviceToHost)); + //std::cerr << checksum2 << std::endl; + EXPECT_EQ(checksum, checksum2); + } + {//benchmark GPU version that uses no table + //gpuTimer.start("GPU tabled CRC of level set sphere"); + uint32_t *d_lut = nanovdb::crc32::cudaCreateLut(); + nanovdb::crc32::checksumKernel<<<1, 1>>>(handle.deviceData()+16, d_checksum, 1, size, d_lut); + //gpuTimer.stop(); + cudaCheck(cudaMemcpy(&checksum2, d_checksum, 4, cudaMemcpyDeviceToHost)); + cudaCheck(cudaFree(d_lut)); + //std::cerr << checksum2 << std::endl; + EXPECT_EQ(checksum, checksum2); + } + { + //cpuTimer.start("CPU GridChecksum of level set sphere"); + nanovdb::GridChecksum cs; + cs(*grid); + checksum2 = cs.checksum(0);// only check the checksum of grid, tree and root data + //cpuTimer.stop(); + //std::cerr << checksum2 << std::endl; + EXPECT_EQ(checksum, checksum2); + } + uint64_t fullChecksum; + { + //cpuTimer.start("CPU FULL cudaGridChecksum tabled CRC of level set sphere"); + nanovdb::updateChecksum(*handle.grid(), nanovdb::ChecksumMode::Full); + //cpuTimer.stop(); + fullChecksum = handle.grid()->checksum(); + EXPECT_EQ(checksum, fullChecksum & 0xFFFFFFFF); + } + { + //gpuTimer.start("GPU FULL cudaGridChecksum tabled CRC of level set sphere"); + nanovdb::cudaGridChecksum(handle.deviceGrid(), nanovdb::ChecksumMode::Full); + //gpuTimer.stop(); + uint64_t fullChecksum2; + cudaCheck(cudaMemcpy(&fullChecksum2, (const uint8_t*)handle.deviceGrid() + 8, 8, cudaMemcpyDeviceToHost)); + EXPECT_EQ(checksum, fullChecksum2 & 0xFFFFFFFF); + EXPECT_EQ(fullChecksum, fullChecksum2); + } + cudaCheck(cudaFree(d_checksum)); +}// CudaGridChecksum + +template +size_t countActiveVoxels(const nanovdb::NodeManager *d_mgr) +{ + size_t count[2], *d_count; + cudaCheck(cudaMalloc((void**)&d_count, 2*sizeof(size_t))); + cudaLambdaKernel<<<1,1>>>(1, [=] __device__ (size_t){ + d_count[0] = 0; + for (int i=0; ileafCount(); ++i) d_count[0] += d_mgr->leaf(i).valueMask().countOn(); + for (int i=0; ilowerCount(); ++i) d_count[0] += d_mgr->lower(i).valueMask().countOn(); + for (int i=0; iupperCount(); ++i) d_count[0] += d_mgr->upper(i).valueMask().countOn(); + d_count[1] = d_mgr->tree().activeVoxelCount(); + //printf("active count = %lu %lu\n", d_count[0], d_count[1]); + }); + cudaCheck(cudaMemcpy(count, d_count, 2*sizeof(size_t), cudaMemcpyDeviceToHost)); + cudaCheck(cudaFree(d_count)); + EXPECT_EQ(count[0], count[1]); + return count[0]; +}// countActiveVoxels + +TEST(TestNanoVDBCUDA, NodeManager) +{ + auto handle = nanovdb::createLevelSetSphere(100); + EXPECT_TRUE(handle.data()); + auto *grid = handle.grid(); + EXPECT_TRUE(grid); + handle.deviceUpload(); + auto *d_grid = handle.deviceGrid(); + EXPECT_TRUE(d_grid); + size_t count = 0; + nanovdb::CpuTimer cpuTimer; + { + //cpuTimer.start("CPU NodeManager"); + auto handle2 = nanovdb::createNodeManager<>(*grid); + //cpuTimer.stop(); + auto *mgr = handle2.mgr(); + EXPECT_TRUE(mgr); + count = mgr->grid().tree().activeVoxelCount(); + } + + nanovdb::GpuTimer gpuTimer; + { + //gpuTimer.start("GPU NodeManager"); + auto handle2 = nanovdb::cudaCreateNodeManager(d_grid); + //gpuTimer.stop(); + auto *d_mgr = handle2.deviceMgr(); + EXPECT_TRUE(d_mgr); + EXPECT_EQ(count, countActiveVoxels(d_mgr)); + } +}// NodeManager + +TEST(TestNanoVDBCUDA, GridStats) +{ + using GridT = nanovdb::NanoGrid; + auto handle = nanovdb::createLevelSetSphere(100, + nanovdb::Vec3d(0), + 1.0, + 3.0, + nanovdb::Vec3d(0), + "test", + nanovdb::StatsMode::Disable); + EXPECT_TRUE(handle.data()); + GridT *grid = handle.grid(); + EXPECT_TRUE(grid); + handle.deviceUpload(); + GridT *d_grid = handle.deviceGrid(); + EXPECT_TRUE(d_grid); + + {// check min/max using const iterators + float min = std::numeric_limits::max(), max = -min; + int n2=0, n1=0, n0=0;// check that nodes are arranged breath-first in memory + for (auto it2 = grid->tree().root().cbeginChild(); it2; ++it2) { + EXPECT_EQ(grid->tree().getFirstUpper() + n2++, &(*it2)); + for (auto it1 = it2->cbeginChild(); it1; ++it1) { + EXPECT_EQ(grid->tree().getFirstLower() + n1++, &(*it1)); + for (auto it0 = it1->cbeginChild(); it0; ++it0) { + EXPECT_EQ(grid->tree().getFirstLeaf() + n0++, &(*it0)); + for (auto it = it0->cbeginValueOn(); it; ++it) { + if (*it < min) min = *it; + if (*it > max) max = *it; + } + }// loop over child nodes of the lower internal node + }// loop over child nodes of the upper internal node + }// loop over child nodes of the root node + EXPECT_NE(min, grid->tree().root().minimum()); + EXPECT_NE(max, grid->tree().root().maximum()); + EXPECT_EQ(n2, grid->tree().nodeCount(2)); + EXPECT_EQ(n1, grid->tree().nodeCount(1)); + EXPECT_EQ(n0, grid->tree().nodeCount(0)); + } + { + //nanovdb::CpuTimer cpuTimer("CPU gridStats: Default = Full"); + nanovdb::gridStats(*grid); + //cpuTimer.stop(); + } + {// check min/max using const iterators + float min = std::numeric_limits::max(), max = -min; + int n2=0, n1=0, n0=0;// check that nodes are arranged breath-first in memory + for (auto it2 = grid->tree().root().cbeginChild(); it2; ++it2) { + EXPECT_EQ(grid->tree().getFirstUpper() + n2++, &(*it2)); + for (auto it1 = it2->cbeginChild(); it1; ++it1) { + EXPECT_EQ(grid->tree().getFirstLower() + n1++, &(*it1)); + for (auto it0 = it1->cbeginChild(); it0; ++it0) { + EXPECT_EQ(grid->tree().getFirstLeaf() + n0++, &(*it0)); + for (auto it = it0->cbeginValueOn(); it; ++it) { + if (*it < min) min = *it; + if (*it > max) max = *it; + } + }// loop over child nodes of the lower internal node + }// loop over child nodes of the upper internal node + }// loop over child nodes of the root node + EXPECT_EQ(min, grid->tree().root().minimum()); + EXPECT_EQ(max, grid->tree().root().maximum()); + EXPECT_EQ(n2, grid->tree().nodeCount(2)); + EXPECT_EQ(n1, grid->tree().nodeCount(1)); + EXPECT_EQ(n0, grid->tree().nodeCount(0)); + } + {// check min/max using non-const iterators + float min = std::numeric_limits::max(), max = -min; + int n2=0, n1=0, n0=0;// check that nodes are arranged breath-first in memory + for (auto it2 = grid->tree().root().beginChild(); it2; ++it2) { + EXPECT_EQ(grid->tree().getFirstUpper() + n2++, &(*it2)); + for (auto it1 = it2->beginChild(); it1; ++it1) { + EXPECT_EQ(grid->tree().getFirstLower() + n1++, &(*it1)); + for (auto it0 = it1->beginChild(); it0; ++it0) { + EXPECT_EQ(grid->tree().getFirstLeaf() + n0++, &(*it0)); + for (auto it = it0->beginValueOn(); it; ++it) { + if (*it < min) min = *it; + if (*it > max) max = *it; + } + }// loop over child nodes of the lower internal node + }// loop over child nodes of the upper internal node + }// loop over child nodes of the root node + EXPECT_EQ(min, grid->tree().root().minimum()); + EXPECT_EQ(max, grid->tree().root().maximum()); + EXPECT_EQ(n2, grid->tree().nodeCount(2)); + EXPECT_EQ(n1, grid->tree().nodeCount(1)); + EXPECT_EQ(n0, grid->tree().nodeCount(0)); + } + + { + //nanovdb::GpuTimer gpuTimer("GPU gridStats: Default = Full"); + nanovdb::cudaGridStats(d_grid); + //gpuTimer.stop(); + } + {// check bbox and stats of device grid + using DataT = nanovdb::NanoRoot::DataType; + std::unique_ptr buffer(new char[sizeof(DataT)]); + cudaMemcpy(buffer.get(), (char*)d_grid + sizeof(nanovdb::GridData) + sizeof(nanovdb::TreeData), sizeof(DataT), cudaMemcpyDeviceToHost); + auto *data = (const DataT*)buffer.get(); + EXPECT_EQ(grid->indexBBox(), data->mBBox); + EXPECT_EQ(grid->tree().root().background(), data->mBackground); + EXPECT_EQ(grid->tree().root().minimum(), data->mMinimum); + EXPECT_EQ(grid->tree().root().maximum(), data->mMaximum); + EXPECT_EQ(grid->tree().root().average(), data->mAverage); + EXPECT_EQ(grid->tree().root().stdDeviation(), data->mStdDevi); + } +}// GridStats diff --git a/nanovdb/nanovdb/unittest/TestOpenVDB.cc b/nanovdb/nanovdb/unittest/TestOpenVDB.cc index e99121bc88..e14792cb81 100644 --- a/nanovdb/nanovdb/unittest/TestOpenVDB.cc +++ b/nanovdb/nanovdb/unittest/TestOpenVDB.cc @@ -8,16 +8,17 @@ #include #include -#include +#include #include #include #include +#include #include -#include -#include #include #include #include +#include +#include #if !defined(_MSC_VER) // does not compile in msvc c++ due to zero-sized arrays. #include @@ -39,31 +40,6 @@ #include -namespace nanovdb {// this namespace is required by gtest -inline std::ostream& -operator<<(std::ostream& os, const CoordBBox& b) -{ - os << "(" << b[0][0] << "," << b[0][1] << "," << b[0][2] << ") -> " - << "(" << b[1][0] << "," << b[1][1] << "," << b[1][2] << ")"; - return os; -} - -inline std::ostream& -operator<<(std::ostream& os, const Coord& ijk) -{ - os << "(" << ijk[0] << "," << ijk[1] << "," << ijk[2] << ")"; - return os; -} - -template -inline std::ostream& -operator<<(std::ostream& os, const Vec3& v) -{ - os << "(" << v[0] << "," << v[1] << "," << v[2] << ")"; - return os; -} -}// namespace nanovdb - // define the environment variable VDB_DATA_PATH to use models from the web // e.g. setenv VDB_DATA_PATH /home/kmu/dev/data/vdb // or export VDB_DATA_PATH=/Users/ken/dev/data/vdb @@ -214,7 +190,7 @@ TEST_F(TestOpenVDB, getExtrema) { using wBBoxT = openvdb::math::BBox; auto srcGrid = this->getSrcGrid(false, 0, 3);// level set of a bunny if available, else an octahedron - auto handle = nanovdb::openToNanoVDB(*srcGrid, nanovdb::StatsMode::All); + auto handle = nanovdb::createNanoGrid(*srcGrid, nanovdb::StatsMode::All); EXPECT_TRUE(handle); auto* dstGrid = handle.grid(); EXPECT_TRUE(dstGrid); @@ -256,11 +232,9 @@ TEST_F(TestOpenVDB, Basic) } } -TEST_F(TestOpenVDB, OpenToNanoType) +TEST_F(TestOpenVDB, MapToNano) { {// Coord - constexpr bool test = std::is_same::Type>::value; - EXPECT_TRUE(test); const openvdb::Coord ijk1(1, 2, -4); nanovdb::Coord ijk2(-2, 7, 9); EXPECT_NE(ijk2, nanovdb::Coord(1, 2, -4)); @@ -268,9 +242,9 @@ TEST_F(TestOpenVDB, OpenToNanoType) EXPECT_EQ(ijk2, nanovdb::Coord(1, 2, -4)); } {// Vec3f - constexpr bool test1 = std::is_same::Type>::value; + constexpr bool test1 = nanovdb::is_same::type>::value; EXPECT_TRUE(test1); - constexpr bool test2 = std::is_same::Type>::value; + constexpr bool test2 = nanovdb::is_same::type>::value; EXPECT_FALSE(test2); const openvdb::Vec3f xyz1(1, 2, -4); nanovdb::Vec3f xyz2(-2, 7, 9); @@ -279,9 +253,9 @@ TEST_F(TestOpenVDB, OpenToNanoType) EXPECT_EQ(xyz2, nanovdb::Vec3f(1, 2, -4)); } {// Vec4d - constexpr bool test1 = std::is_same::Type>::value; + constexpr bool test1 = nanovdb::is_same::type>::value; EXPECT_TRUE(test1); - constexpr bool test2 = std::is_same::Type>::value; + constexpr bool test2 = nanovdb::is_same::type>::value; EXPECT_FALSE(test2); const openvdb::Vec4d xyz1(1, 2, -4, 7); nanovdb::Vec4d xyz2(-2, 7, 9, -4); @@ -290,9 +264,9 @@ TEST_F(TestOpenVDB, OpenToNanoType) EXPECT_EQ(xyz2, nanovdb::Vec4d(1, 2, -4, 7)); } {// MaskValue - constexpr bool test1 = std::is_same::Type>::value; + constexpr bool test1 = nanovdb::is_same::type>::value; EXPECT_TRUE(test1); - constexpr bool test2 = std::is_same::Type>::value; + constexpr bool test2 = nanovdb::is_same::type>::value; EXPECT_FALSE(test2); EXPECT_EQ(sizeof(nanovdb::ValueMask), sizeof(openvdb::ValueMask)); } @@ -416,13 +390,18 @@ TEST_F(TestOpenVDB, BasicGrid) { // init Grid auto* data = grid->data(); { - openvdb::math::UniformScaleTranslateMap map(2.0, openvdb::Vec3R(0.0, 0.0, 0.0)); - auto affineMap = map.getAffineMap(); - data->mVoxelSize = affineMap->voxelSize(); + openvdb::math::UniformScaleTranslateMap map(2.0, openvdb::Vec3d(0.0, 0.0, 0.0)); + auto affineMap = map.getAffineMap(); const auto mat = affineMap->getMat4(), invMat = mat.inverse(); //for (int i=0; i<4; ++i) std::cout << "Row("<init({nanovdb::GridFlags::HasMinMax}, bytes[8], dstMap, nanovdb::GridType::Float); +#else data->mMap.set(mat, invMat, 1.0); + data->mVoxelSize = affineMap->voxelSize(); data->setFlagsOff(); data->setMinMaxOn(); data->mGridIndex = 0; @@ -433,12 +412,13 @@ TEST_F(TestOpenVDB, BasicGrid) data->mGridType = nanovdb::GridType::Float; data->mMagic = NANOVDB_MAGIC_NUMBER; data->mVersion = nanovdb::Version(); +#endif memcpy(data->mGridName, name.c_str(), name.size() + 1); } EXPECT_EQ(tree, &grid->tree()); - const openvdb::Vec3R p1(1.0, 2.0, 3.0); + const openvdb::Vec3d p1(1.0, 2.0, 3.0); const auto p2 = grid->worldToIndex(p1); - EXPECT_EQ(openvdb::Vec3R(0.5, 1.0, 1.5), p2); + EXPECT_EQ(openvdb::Vec3d(0.5, 1.0, 1.5), p2); const auto p3 = grid->indexToWorld(p2); EXPECT_EQ(p1, p3); { @@ -452,7 +432,7 @@ TEST_F(TestOpenVDB, BasicGrid) } auto const p4 = grid->worldToIndex(p3); - EXPECT_EQ(openvdb::Vec3R(0.0, 0.0, 0.0), p4); + EXPECT_EQ(openvdb::Vec3d(0.0, 0.0, 0.0), p4); const auto p5 = grid->indexToWorld(p4); EXPECT_EQ(p1, p5); } @@ -578,7 +558,7 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_Empty) { // empty grid openvdb::FloatGrid srcGrid(0.0f); auto srcAcc = srcGrid.getAccessor(); - auto handle = nanovdb::openToNanoVDB(srcGrid); + auto handle = nanovdb::createNanoGrid(srcGrid); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); EXPECT_TRUE(meta); @@ -613,7 +593,7 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_Basic1) srcAcc.setValue(openvdb::Coord(1, 2, 3), 1.0f); EXPECT_TRUE(srcAcc.isValueOn(openvdb::Coord(1, 2, 3))); EXPECT_EQ(1.0f, srcAcc.getValue(openvdb::Coord(1, 2, 3))); - auto handle = nanovdb::openToNanoVDB(srcGrid, nanovdb::StatsMode::All); + auto handle = nanovdb::createNanoGrid(srcGrid, nanovdb::StatsMode::All); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); EXPECT_TRUE(meta); @@ -633,7 +613,7 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_Basic1) EXPECT_EQ(sizeof(nanovdb::NanoGrid) + sizeof(nanovdb::NanoTree) + (const char*)handle.data(), (const char*)&dstGrid->tree().root()); - EXPECT_EQ(nanovdb::Vec3R(1.0), dstGrid->voxelSize()); + EXPECT_EQ(nanovdb::Vec3d(1.0), dstGrid->voxelSize()); EXPECT_EQ(1.0f, dstGrid->tree().getValue(nanovdb::Coord(1, 2, 3))); auto dstAcc = dstGrid->getAccessor(); EXPECT_TRUE(dstAcc.isActive(nanovdb::Coord(1, 2, 3))); @@ -652,8 +632,7 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_Model) { auto srcGrid = this->getSrcGrid(false); //mTimer.start("Generating NanoVDB grid"); - //auto handle = nanovdb::openToNanoVDB(*srcGrid, nanovdb::StatsMode::Default, nanovdb::ChecksumMode::Default, 2); - auto handle = nanovdb::openToNanoVDB(*srcGrid); + auto handle = nanovdb::createNanoGrid(*srcGrid); //mTimer.start("Writing NanoVDB grid"); nanovdb::io::writeGrid("data/test.nvdb", handle, this->getCodec()); //mTimer.stop(); @@ -700,10 +679,10 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_Fp4) EXPECT_EQ(2.0f, srcAcc.getValue(openvdb::Coord(-10, 20,-50))); EXPECT_EQ(3.0f, srcAcc.getValue(openvdb::Coord( 50,-12, 30))); - nanovdb::OpenToNanoVDB converter; + nanovdb::CreateNanoGrid converter(srcGrid); //converter.setVerbose(); converter.setStats(nanovdb::StatsMode::All); - auto handle = converter(srcGrid); + auto handle = converter.getHandle();// (srcGrid); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); @@ -731,7 +710,7 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_Fp4) EXPECT_TRUE(dstGrid->isSequential<1>()); EXPECT_TRUE(dstGrid->isSequential<0>()); - EXPECT_EQ(nanovdb::Vec3R(1.0), dstGrid->voxelSize()); + EXPECT_EQ(nanovdb::Vec3d(1.0), dstGrid->voxelSize()); auto *leaf = dstGrid->tree().root().probeLeaf(nanovdb::Coord(1, 2, 3)); EXPECT_TRUE(leaf); //std::cerr << leaf->origin() << ", " << leaf->data()->mBBoxMin << std::endl; @@ -754,10 +733,10 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_Fp4) {// Model auto openGrid = this->getSrcGrid(false); const float tolerance = 0.5f*openGrid->voxelSize()[0]; - nanovdb::OpenToNanoVDB converter; + nanovdb::CreateNanoGrid converter(*openGrid); converter.enableDithering(); //converter.setVerbose(2); - auto handle = converter(*openGrid); + auto handle = converter.getHandle(); auto* nanoGrid = handle.grid(); EXPECT_TRUE(nanoGrid); @@ -795,9 +774,9 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_Fp8) EXPECT_EQ(2.0f, srcAcc.getValue(openvdb::Coord(-10, 20,-50))); EXPECT_EQ(3.0f, srcAcc.getValue(openvdb::Coord( 50,-12, 30))); - nanovdb::OpenToNanoVDB converter; + nanovdb::CreateNanoGrid converter(srcGrid); converter.setStats(nanovdb::StatsMode::All); - auto handle = converter(srcGrid); + auto handle = converter.getHandle(); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); @@ -812,7 +791,6 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_Fp8) auto* dstGrid = handle.grid(); EXPECT_TRUE(dstGrid); EXPECT_EQ("", std::string(dstGrid->gridName())); - EXPECT_EQ((const char*)handle.data(), (const char*)dstGrid); EXPECT_EQ(1.0f, dstGrid->tree().root().minimum()); EXPECT_EQ(3.0f, dstGrid->tree().root().maximum()); EXPECT_EQ(2.0f, dstGrid->tree().root().average()); @@ -825,7 +803,7 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_Fp8) EXPECT_TRUE(dstGrid->isSequential<1>()); EXPECT_TRUE(dstGrid->isSequential<0>()); - EXPECT_EQ(nanovdb::Vec3R(1.0), dstGrid->voxelSize()); + EXPECT_EQ(nanovdb::Vec3d(1.0), dstGrid->voxelSize()); EXPECT_EQ(1.0f, dstGrid->tree().getValue(nanovdb::Coord(1, 2, 3))); auto dstAcc = dstGrid->getAccessor(); EXPECT_TRUE(dstAcc.isActive(nanovdb::Coord(1, 2, 3))); @@ -837,9 +815,9 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_Fp8) } {// Model auto openGrid = this->getSrcGrid(false); - const float tolerance = 0.05f*openGrid->voxelSize()[0]; - nanovdb::OpenToNanoVDB converter; - auto handle = converter(*openGrid); + const float tolerance = 0.05f*openGrid->voxelSize()[0]; + nanovdb::CreateNanoGrid converter(*openGrid); + auto handle = converter.getHandle(); converter.enableDithering(); //converter.setVerbose(2); auto* nanoGrid = handle.grid(); @@ -880,10 +858,10 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_Fp16) EXPECT_EQ(2.0f, srcAcc.getValue(openvdb::Coord(-10, 20,-50))); EXPECT_EQ(3.0f, srcAcc.getValue(openvdb::Coord( 50,-12, 30))); - nanovdb::OpenToNanoVDB converter; + nanovdb::CreateNanoGrid converter(srcGrid); //converter.setVerbose(2); converter.setStats(nanovdb::StatsMode::All); - auto handle = converter(srcGrid); + auto handle = converter.getHandle(); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); @@ -911,7 +889,7 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_Fp16) EXPECT_TRUE(dstGrid->isSequential<1>()); EXPECT_TRUE(dstGrid->isSequential<0>()); - EXPECT_EQ(nanovdb::Vec3R(1.0), dstGrid->voxelSize()); + EXPECT_EQ(nanovdb::Vec3d(1.0), dstGrid->voxelSize()); EXPECT_EQ(1.0f, dstGrid->tree().getValue(nanovdb::Coord(1, 2, 3))); auto dstAcc = dstGrid->getAccessor(); EXPECT_TRUE(dstAcc.isActive(nanovdb::Coord(1, 2, 3))); @@ -924,9 +902,9 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_Fp16) {// Model auto openGrid = this->getSrcGrid(false); const float tolerance = 0.005f*openGrid->voxelSize()[0]; - nanovdb::OpenToNanoVDB converter; + nanovdb::CreateNanoGrid converter(*openGrid); converter.enableDithering(); - auto handle = converter(*openGrid); + auto handle = converter.getHandle(); //converter.setVerbose(2); auto* nanoGrid = handle.grid(); EXPECT_TRUE(nanoGrid); @@ -966,9 +944,9 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_FpN) EXPECT_EQ(2.0f, srcAcc.getValue(openvdb::Coord(-10, 20,-50))); EXPECT_EQ(3.0f, srcAcc.getValue(openvdb::Coord( 50,-12, 30))); - nanovdb::OpenToNanoVDB converter; + nanovdb::CreateNanoGrid converter(srcGrid); converter.setStats(nanovdb::StatsMode::All); - auto handle = converter(srcGrid); + auto handle = converter.getHandle(); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); @@ -996,7 +974,7 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_FpN) EXPECT_TRUE(dstGrid->isSequential<1>()); EXPECT_FALSE(dstGrid->isSequential<0>()); - EXPECT_EQ(nanovdb::Vec3R(1.0), dstGrid->voxelSize()); + EXPECT_EQ(nanovdb::Vec3d(1.0), dstGrid->voxelSize()); EXPECT_EQ(1.0f, dstGrid->tree().getValue(nanovdb::Coord(1, 2, 3))); auto dstAcc = dstGrid->getAccessor(); EXPECT_TRUE(dstAcc.isActive(nanovdb::Coord(1, 2, 3))); @@ -1012,14 +990,15 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_FpN) #else auto openGrid = this->getSrcGrid(true, 1, 1);// FOG volume of Disney cloud or cube #endif - nanovdb::OpenToNanoVDB converter; - converter.oracle() = nanovdb::AbsDiff( 0.05f ); + nanovdb::CreateNanoGrid converter(*openGrid); //converter.setVerbose(2); - auto handle = converter(*openGrid); + + const float tolerance = 0.05f; + nanovdb::AbsDiff oracle(tolerance); + + auto handle = converter.getHandle(oracle); auto* nanoGrid = handle.grid(); EXPECT_TRUE(nanoGrid); - const nanovdb::AbsDiff oracle = converter.oracle(); - const float tolerance = oracle.getTolerance(); nanovdb::io::writeGrid("data/test_fpN.nvdb", handle, this->getCodec()); @@ -1047,7 +1026,7 @@ TEST_F(TestOpenVDB, OpenToNanoVDB_FpN) // Generate random points by uniformly distributing points // on a unit-sphere. -inline void genPoints(const int numPoints, std::vector& points) +inline void genPoints(const int numPoints, std::vector& points) { openvdb::math::Random01 randNumber(0); const int n = int(std::sqrt(double(numPoints))); @@ -1055,7 +1034,7 @@ inline void genPoints(const int numPoints, std::vector& points) const double yScale = openvdb::math::pi() / double(n); double x, y, theta, phi; - openvdb::Vec3R pos; + openvdb::Vec3d pos; points.reserve(n * n); @@ -1082,10 +1061,10 @@ inline void genPoints(const int numPoints, std::vector& points) } // genPoints class PointList { - std::vector const* const mPoints; + std::vector const* const mPoints; public: - using PosType = openvdb::Vec3R; + using PosType = openvdb::Vec3d; PointList(const std::vector& points) : mPoints(&points) { @@ -1094,13 +1073,14 @@ class PointList void getPos(size_t n, PosType& xyz) const { xyz = (*mPoints)[n]; } }; // PointList +// make testOpenVDB && ./unittest/testOpenVDB --gtest_filter="*PointIndexGrid" --gtest_break_on_failure TEST_F(TestOpenVDB, PointIndexGrid) { const uint64_t pointCount = 40000; const float voxelSize = 0.01f; const auto transform = openvdb::math::Transform::createLinearTransform(voxelSize); - std::vector points; + std::vector points; genPoints(pointCount, points); PointList pointList(points); EXPECT_EQ(pointCount, points.size()); @@ -1109,16 +1089,16 @@ TEST_F(TestOpenVDB, PointIndexGrid) auto srcGrid = openvdb::tools::createPointIndexGrid(pointList, *transform); using MgrT = openvdb::tree::LeafManager; - MgrT leafs(srcGrid->tree()); + MgrT leafMgr(srcGrid->tree()); size_t count = 0; - for (size_t n = 0, N = leafs.leafCount(); n < N; ++n) { - count += leafs.leaf(n).indices().size(); + for (size_t n = 0, N = leafMgr.leafCount(); n < N; ++n) { + count += leafMgr.leaf(n).indices().size(); } EXPECT_EQ(pointCount, count); //mTimer.start("Generating NanoVDB grid from PointIndexGrid"); - auto handle = nanovdb::openToNanoVDB(*srcGrid, nanovdb::StatsMode::All, nanovdb::ChecksumMode::Full); + auto handle = nanovdb::createNanoGrid(*srcGrid, nanovdb::StatsMode::All, nanovdb::ChecksumMode::Full); //mTimer.stop(); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); @@ -1128,9 +1108,9 @@ TEST_F(TestOpenVDB, PointIndexGrid) auto dstGrid = handle.grid(); EXPECT_TRUE(dstGrid); EXPECT_EQ(1u, dstGrid->blindDataCount()); - auto metaData = dstGrid->blindMetaData(0); - EXPECT_EQ(pointCount, metaData.mElementCount); - EXPECT_EQ(nanovdb::GridBlindDataSemantic::Unknown, metaData.mSemantic); + const auto &metaData = dstGrid->blindMetaData(0); + EXPECT_EQ(pointCount, metaData.mValueCount); + EXPECT_EQ(nanovdb::GridBlindDataSemantic::PointId, metaData.mSemantic); EXPECT_EQ(nanovdb::GridBlindDataClass::IndexArray, metaData.mDataClass); EXPECT_EQ(nanovdb::GridType::UInt32, metaData.mDataType); @@ -1147,12 +1127,14 @@ TEST_F(TestOpenVDB, PointIndexGrid) tbb::parallel_for(srcGrid->evalActiveVoxelBoundingBox(), kernel1); //mTimer.stop(); - EXPECT_EQ(pointCount, dstGrid->blindMetaData(0).mElementCount); + EXPECT_EQ(pointCount, dstGrid->blindMetaData(0).mValueCount); + //std::cerr << "" auto kernel = [&](const MgrT::LeafRange& r) { using CoordT = const nanovdb::Coord; auto dstAcc = dstGrid->getAccessor(); nanovdb::PointAccessor pointAcc(*dstGrid); + EXPECT_TRUE(pointAcc); const uint32_t * begin2 = nullptr, *end2 = nullptr; EXPECT_EQ(pointCount, pointAcc.gridPoints(begin2, end2)); for (auto leaf = r.begin(); leaf; ++leaf) { @@ -1167,14 +1149,13 @@ TEST_F(TestOpenVDB, PointIndexGrid) EXPECT_TRUE(leaf->getIndices(ijk, begin1, end1)); EXPECT_TRUE(pointAcc.voxelPoints(*abc, begin2, end2)); EXPECT_EQ(end1 - begin1, end2 - begin2); - for (auto* i = begin1; i != end1; ++i) - EXPECT_EQ(*i, *begin2++); + for (auto* i = begin1; i != end1; ++i) EXPECT_EQ(*i, *begin2++); } } }; //mTimer.start("Parallel unit test"); - tbb::parallel_for(leafs.leafRange(), kernel); + tbb::parallel_for(leafMgr.leafRange(), kernel); //mTimer.stop(); //mTimer.start("Testing bounding box"); @@ -1195,17 +1176,17 @@ TEST_F(TestOpenVDB, PointIndexGrid) TEST_F(TestOpenVDB, PointDataGridBasic) { // Create a vector with three point positions. - std::vector positions; - positions.push_back(openvdb::Vec3R(0.0, 0.0, 0.0)); - positions.push_back(openvdb::Vec3R(0.0, 0.0, 1.0)); - positions.push_back(openvdb::Vec3R(1.34, -56.1, 5.7)); + std::vector positions; + positions.push_back(openvdb::Vec3d(0.0, 0.0, 0.0)); + positions.push_back(openvdb::Vec3d(0.0, 0.0, 1.0)); + positions.push_back(openvdb::Vec3d(1.34, -56.1, 5.7)); EXPECT_EQ( 3UL, positions.size() ); // We need to define a custom search lambda function // to account for floating-point roundoffs! auto search = [&positions](const openvdb::Vec3f &p) { for (auto it = positions.begin(); it != positions.end(); ++it) { - const openvdb::Vec3R delta = *it - p; + const openvdb::Vec3d delta = *it - p; if ( delta.length() < 1e-5 ) return it; } return positions.end(); @@ -1216,7 +1197,7 @@ TEST_F(TestOpenVDB, PointDataGridBasic) // wrapper around an stl vector wrapper here, however it is also possible to // write one for a custom data structure in order to match the interface // required. - openvdb::points::PointAttributeVector positionsWrapper(positions); + openvdb::points::PointAttributeVector positionsWrapper(positions); // This method computes a voxel-size to match the number of // points / voxel requested. Although it won't be exact, it typically offers // a good balance of memory against performance. @@ -1229,7 +1210,7 @@ TEST_F(TestOpenVDB, PointDataGridBasic) srcGrid->setName("PointDataGrid"); //mTimer.start("Generating NanoVDB grid from PointDataGrid"); - auto handle = nanovdb::openToNanoVDB(*srcGrid); + auto handle = nanovdb::createNanoGrid(*srcGrid); //mTimer.stop(); EXPECT_TRUE(handle); @@ -1237,13 +1218,22 @@ TEST_F(TestOpenVDB, PointDataGridBasic) EXPECT_TRUE(meta); EXPECT_EQ(nanovdb::GridType::UInt32, meta->gridType()); EXPECT_EQ(nanovdb::GridClass::PointData, meta->gridClass()); - auto dstGrid = handle.grid(); + + auto *dstGrid = handle.grid(); EXPECT_TRUE(dstGrid); for (int i=0; i<3; ++i) { EXPECT_EQ(srcGrid->voxelSize()[i], dstGrid->voxelSize()[i]); } + EXPECT_EQ(1u, dstGrid->blindDataCount());// only point positions + auto &metaData = dstGrid->blindMetaData(0u); + EXPECT_EQ(metaData.mValueCount, positions.size()); + EXPECT_EQ(strcmp("P", metaData.mName), 0); + EXPECT_EQ(metaData.mDataClass, nanovdb::GridBlindDataClass::AttributeArray); + EXPECT_EQ(metaData.mSemantic, nanovdb::GridBlindDataSemantic::PointPosition); + EXPECT_EQ(metaData.mDataType, nanovdb::GridType::Vec3f); nanovdb::PointAccessor acc(*dstGrid); + EXPECT_TRUE(acc); const nanovdb::Vec3f *begin = nullptr, *end = nullptr; // iterators over points in a given voxel EXPECT_EQ(positions.size(), openvdb::points::pointCount(srcGrid->tree())); EXPECT_EQ(acc.gridPoints(begin, end), positions.size()); @@ -1268,7 +1258,7 @@ TEST_F(TestOpenVDB, PointDataGridBasic) const nanovdb::Vec3f vxlDst = *begin++;// local voxel coordinates for (int i=0; i<3; ++i) { EXPECT_EQ( ijkSrc[i], ijkDst[i] ); - EXPECT_EQ( vxlSrc[i], vxlDst[i] ); + //EXPECT_EQ( vxlSrc[i], vxlDst[i] ); } // A PointDataGrid encodes local voxel coordinates // so transform those to global index coordinates! @@ -1285,7 +1275,7 @@ TEST_F(TestOpenVDB, PointDataGridBasic) EXPECT_EQ( wldSrc[i], wldDst[i] ); } - // compair to original input points + // compare to original input points auto it = search( wldSrc ); EXPECT_TRUE( it != positions.end() ); positions.erase( it ); @@ -1296,18 +1286,18 @@ TEST_F(TestOpenVDB, PointDataGridBasic) TEST_F(TestOpenVDB, PointDataGridRandom) { - std::vector positions; + std::vector positions; const size_t pointCount = 2000; - const openvdb::Vec3R wldMin(-234.3, -135.6, -503.7); - const openvdb::Vec3R wldMax( 57.8, 289.1, 0.2); - const openvdb::Vec3R wldDim = wldMax - wldMin; + const openvdb::Vec3d wldMin(-234.3, -135.6, -503.7); + const openvdb::Vec3d wldMax( 57.8, 289.1, 0.2); + const openvdb::Vec3d wldDim = wldMax - wldMin; openvdb::math::Random01 randNumber(0); // We need to define a custom search lambda function // to account for floating-point roundoffs! auto search = [&positions](const openvdb::Vec3f &p) { for (auto it = positions.begin(); it != positions.end(); ++it) { - const openvdb::Vec3R delta = *it - p; + const openvdb::Vec3d delta = *it - p; if ( delta.length() < 1e-3 ) return it; } return positions.end(); @@ -1315,8 +1305,8 @@ TEST_F(TestOpenVDB, PointDataGridRandom) // Create a vector with random point positions. for (size_t i=0; i positionsWrapper(positions); + openvdb::points::PointAttributeVector positionsWrapper(positions); // This method computes a voxel-size to match the number of // points / voxel requested. Although it won't be exact, it typically offers // a good balance of memory against performance. @@ -1340,7 +1330,7 @@ TEST_F(TestOpenVDB, PointDataGridRandom) srcGrid->setName("PointDataGrid"); //mTimer.start("Generating NanoVDB grid from PointDataGrid"); - auto handle = nanovdb::openToNanoVDB(*srcGrid); + auto handle = nanovdb::createNanoGrid(*srcGrid); //mTimer.stop(); EXPECT_TRUE(handle); @@ -1355,6 +1345,7 @@ TEST_F(TestOpenVDB, PointDataGridRandom) } nanovdb::PointAccessor acc(*dstGrid); + EXPECT_TRUE(acc); const nanovdb::Vec3f *begin = nullptr, *end = nullptr; // iterators over points in a given voxel EXPECT_EQ(positions.size(), openvdb::points::pointCount(srcGrid->tree())); EXPECT_EQ(acc.gridPoints(begin, end), positions.size()); @@ -1438,7 +1429,7 @@ TEST_F(TestOpenVDB, CNanoVDB) { auto srcGrid = this->getSrcGrid(); //mTimer.start("Generating NanoVDB grid"); - auto handle = nanovdb::openToNanoVDB(*srcGrid); + auto handle = nanovdb::createNanoGrid(*srcGrid); //mTimer.stop(); EXPECT_TRUE(handle); EXPECT_TRUE(handle.data()); @@ -1469,7 +1460,7 @@ TEST_F(TestOpenVDB, CNanoVDBTrilinear) { auto srcGrid = this->getSrcGrid(); //mTimer.start("Generating NanoVDB grid"); - auto handle = nanovdb::openToNanoVDB(*srcGrid); + auto handle = nanovdb::createNanoGrid(*srcGrid); //mTimer.stop(); EXPECT_TRUE(handle); EXPECT_TRUE(handle.data()); @@ -1511,7 +1502,7 @@ TEST_F(TestOpenVDB, CNanoVDBTrilinearStencil) { auto srcGrid = this->getSrcGrid(); //mTimer.start("Generating NanoVDB grid"); - auto handle = nanovdb::openToNanoVDB(*srcGrid); + auto handle = nanovdb::createNanoGrid(*srcGrid); //mTimer.stop(); EXPECT_TRUE(handle); EXPECT_TRUE(handle.data()); @@ -1550,15 +1541,15 @@ TEST_F(TestOpenVDB, CNanoVDBTrilinearStencil) #endif -TEST_F(TestOpenVDB, NanoToOpenVDB_GridBuilder) -{// test GridBuilder -> NanoVDB -> OpenVDB - nanovdb::GridBuilder builder(0.0f, nanovdb::GridClass::LevelSet); - auto buildAcc = builder.getAccessor(); +TEST_F(TestOpenVDB, NanoToOpenVDB_BuildGrid) +{// test build::Grid -> NanoVDB -> OpenVDB + nanovdb::build::Grid buildGrid(0.0f, "test", nanovdb::GridClass::LevelSet); + auto buildAcc = buildGrid.getAccessor(); buildAcc.setValue(nanovdb::Coord(1, 2, 3), 1.0f); buildAcc.setValue(nanovdb::Coord(2, -2, 9), 2.0f); EXPECT_EQ(1.0f, buildAcc.getValue(nanovdb::Coord(1, 2, 3))); EXPECT_EQ(2.0f, buildAcc.getValue(nanovdb::Coord(2, -2, 9))); - auto handle = builder.getHandle<>(1.0, nanovdb::Vec3d(0.0), "test"); + auto handle = nanovdb::createNanoGrid(buildGrid); EXPECT_TRUE(handle); auto* meta = handle.gridMetaData(); EXPECT_TRUE(meta); @@ -1638,10 +1629,6 @@ TEST_F(TestOpenVDB, NanoToOpenVDB) TEST_F(TestOpenVDB, File) { - { // check nanovdb::io::stringHash - EXPECT_EQ(nanovdb::io::stringHash("generated_id_0"), nanovdb::io::stringHash("generated_id_0")); - EXPECT_NE(nanovdb::io::stringHash("generated_id_0"), nanovdb::io::stringHash("generated_id_1")); - } auto srcGrid = this->getSrcGrid(); //mTimer.start("Reading NanoVDB grids from file"); @@ -1691,13 +1678,13 @@ TEST_F(TestOpenVDB, MultiFile) grid.setName("Int32 grid"); grid.tree().setValue(openvdb::Coord(-256), 10); EXPECT_EQ(1u, grid.activeVoxelCount()); - handles.push_back(nanovdb::openToNanoVDB(grid)); + handles.push_back(nanovdb::createNanoGrid(grid)); } { // 2: add an empty int32_t grid openvdb::Int32Grid grid(-4); grid.setName("Int32 grid, empty"); EXPECT_EQ(0u, grid.activeVoxelCount()); - handles.push_back(nanovdb::openToNanoVDB(grid)); + handles.push_back(nanovdb::createNanoGrid(grid)); } { // 3: add a ValueMask grid openvdb::MaskGrid grid(false); @@ -1711,7 +1698,7 @@ TEST_F(TestOpenVDB, MultiFile) grid.tree().evalActiveVoxelBoundingBox(bbox); //std::cerr << bbox << std::endl; EXPECT_EQ(openvdb::CoordBBox(min, max), bbox); - handles.push_back(nanovdb::openToNanoVDB(grid)); + handles.push_back(nanovdb::createNanoGrid(grid)); } { // 4: add a bool grid openvdb::BoolGrid grid(false); @@ -1720,7 +1707,7 @@ TEST_F(TestOpenVDB, MultiFile) EXPECT_EQ(1u, grid.activeVoxelCount()); grid.tree().setValue(openvdb::Coord( 10, 450, 90), true); EXPECT_EQ(2u, grid.activeVoxelCount()); - handles.push_back(nanovdb::openToNanoVDB(grid)); + handles.push_back(nanovdb::createNanoGrid(grid)); } { // 5: add a Vec3f grid openvdb::Vec3fGrid grid(openvdb::Vec3f(0.0f, 0.0f, -1.0f)); @@ -1729,7 +1716,7 @@ TEST_F(TestOpenVDB, MultiFile) EXPECT_EQ(0u, grid.activeVoxelCount()); grid.tree().setValue(openvdb::Coord(-256), openvdb::Vec3f(1.0f, 0.0f, 0.0f)); EXPECT_EQ(1u, grid.activeVoxelCount()); - handles.push_back(nanovdb::openToNanoVDB(grid)); + handles.push_back(nanovdb::createNanoGrid(grid)); } { // 6: add a Vec4f grid using OpenVDBVec4fGrid = openvdb::Grid::Type>; @@ -1740,7 +1727,7 @@ TEST_F(TestOpenVDB, MultiFile) EXPECT_EQ(0u, grid.activeVoxelCount()); grid.tree().setValue(openvdb::Coord(-256), openvdb::Vec4f(1.0f, 0.0f, 0.0f, 0.0f)); EXPECT_EQ(1u, grid.activeVoxelCount()); - handles.push_back(nanovdb::openToNanoVDB(grid)); + handles.push_back(nanovdb::createNanoGrid(grid)); OpenVDBVec4fGrid::unregisterGrid(); } { // 7: add an int64_t grid @@ -1748,7 +1735,7 @@ TEST_F(TestOpenVDB, MultiFile) grid.setName("Int64 grid"); grid.tree().setValue(openvdb::Coord(0), 10); EXPECT_EQ(1u, grid.activeVoxelCount()); - handles.push_back(nanovdb::openToNanoVDB(grid)); + handles.push_back(nanovdb::createNanoGrid(grid)); } for (int i = 0; i < 10; ++i) {// 8 -> 17 const float radius = 100.0f; @@ -1756,7 +1743,7 @@ TEST_F(TestOpenVDB, MultiFile) const openvdb::Vec3f center(i * 10.0f, 0.0f, 0.0f); auto srcGrid = openvdb::tools::createLevelSetSphere(radius, center, voxelSize, width); srcGrid->setName("Level set sphere at (" + std::to_string(i * 10) + ",0,0)"); - handles.push_back(nanovdb::openToNanoVDB(*srcGrid)); + handles.push_back(nanovdb::createNanoGrid(*srcGrid)); } { // 18: add a double grid openvdb::DoubleGrid grid(0.0); @@ -1764,7 +1751,7 @@ TEST_F(TestOpenVDB, MultiFile) grid.setGridClass(openvdb::GRID_FOG_VOLUME); grid.tree().setValue(openvdb::Coord(6000), 1.0); EXPECT_EQ(1u, grid.activeVoxelCount()); - handles.push_back(nanovdb::openToNanoVDB(grid)); + handles.push_back(nanovdb::createNanoGrid(grid)); } nanovdb::io::writeGrids("data/multi.nvdb", handles, this->getCodec()); @@ -2062,7 +2049,12 @@ TEST_F(TestOpenVDB, LongGridName) srcGrid.tree().setValue(openvdb::Coord(-256), 10.0f); EXPECT_EQ(1u, srcGrid.activeVoxelCount()); const bool isLong = length > limit; - auto handle = nanovdb::openToNanoVDB(srcGrid); +#if 1 + auto handle = nanovdb::createNanoGrid(srcGrid); +#else + nanovdb::CreateNanoGrid converter(srcGrid); + auto handle = converter.getHandle(); +#endif auto* dstGrid = handle.grid(); EXPECT_TRUE(dstGrid); EXPECT_EQ(1u, dstGrid->activeVoxelCount()); @@ -2090,7 +2082,7 @@ TEST_F(TestOpenVDB, LevelSetFiles) std::vector foundModels; std::ofstream os("data/ls.nvdb", std::ios::out | std::ios::binary); for (const auto& fileName : fileNames) { - //mTimer.start("Reading grid from the file \"" + fileName + "\""); + //mTimer.start("\nReading grid from the file \"" + fileName + "\""); try { openvdb::io::File file(fileName); file.open(false); //disable delayed loading @@ -2100,8 +2092,8 @@ TEST_F(TestOpenVDB, LevelSetFiles) foundModels.push_back(fileName.substr(pos, fileName.size() - pos - 4 )); //mTimer.restart("Generating NanoVDB grid"); - auto handle = nanovdb::openToNanoVDB(*srcGrid, nanovdb::StatsMode::All, nanovdb::ChecksumMode::Partial); - //auto handle = nanovdb::openToNanoVDB(*srcGrid, nanovdb::StatsMode::Disable, nanovdb::ChecksumMode::Disable, false, 1); + //auto handle = nanovdb::createNanoGrid(*srcGrid, nanovdb::StatsMode::All, nanovdb::ChecksumMode::Partial); + auto handle = nanovdb::createNanoGrid(*srcGrid, nanovdb::StatsMode::BBox, nanovdb::ChecksumMode::Disable); //mTimer.restart("Writing NanoVDB grid"); nanovdb::io::writeGrid(os, handle, this->getCodec()); @@ -2138,8 +2130,8 @@ TEST_F(TestOpenVDB, LevelSetFiles) // test reading from non-existing file EXPECT_THROW(nanovdb::io::readGrid("data/ls.vdb", getGridName(foundModels[0])), std::runtime_error); - // test reading non-existing grid from an existing file - EXPECT_FALSE(nanovdb::io::readGrid("data/ls.nvdb", "bunny")); + // test reading of non-existing grid from an existing file + EXPECT_THROW(nanovdb::io::readGrid("data/ls.nvdb", "bunny"), std::runtime_error); // test reading existing grid from an existing file { @@ -2178,8 +2170,7 @@ TEST_F(TestOpenVDB, FogFiles) foundModels.push_back(fileName.substr(pos, fileName.size() - pos - 4 )); //mTimer.restart("Generating NanoVDB grid"); - auto handle = nanovdb::openToNanoVDB(*srcGrid, nanovdb::StatsMode::All, nanovdb::ChecksumMode::Partial); - //auto handle = nanovdb::openToNanoVDB(*srcGrid, nanovdb::StatsMode::Disable, nanovdb::ChecksumMode::Disable, false, 1); + auto handle = nanovdb::createNanoGrid(*srcGrid, nanovdb::StatsMode::All, nanovdb::ChecksumMode::Partial); //mTimer.restart("Writing NanoVDB grid"); nanovdb::io::writeGrid(os, handle, this->getCodec()); @@ -2209,8 +2200,8 @@ TEST_F(TestOpenVDB, FogFiles) // test reading from non-existing file EXPECT_THROW(nanovdb::io::readGrid("data/fog.vdb", getGridName(foundModels[0])), std::runtime_error); - // test reading non-existing grid from an existing file - EXPECT_FALSE(nanovdb::io::readGrid("data/fog.nvdb", "bunny")); + // test reading of non-existing grid from an existing file + EXPECT_THROW(nanovdb::io::readGrid("data/fog.nvdb", "bunny"), std::runtime_error); // test reading existing grid from an existing file { @@ -2254,7 +2245,7 @@ TEST_F(TestOpenVDB, PointFiles) EXPECT_TRUE(positionIndex != openvdb::points::AttributeSet::INVALID_POS); //mTimer.restart("Generating NanoVDB grid from PointDataGrid"); - auto handle = nanovdb::openToNanoVDB(*srcGrid); + auto handle = nanovdb::createNanoGrid(*srcGrid); //mTimer.restart("Writing NanoVDB grid"); nanovdb::io::writeGrid(os, handle, this->getCodec()); @@ -2268,6 +2259,7 @@ TEST_F(TestOpenVDB, PointFiles) EXPECT_TRUE(dstGrid); nanovdb::PointAccessor acc(*dstGrid); + EXPECT_TRUE(acc); const nanovdb::Vec3f * begin = nullptr, *end = nullptr; // iterators over points in a given voxel EXPECT_EQ(acc.gridPoints(begin, end), openvdb::points::pointCount(srcGrid->tree())); //std::cerr << "Point count = " << acc.gridPoints(begin, end) << ", attribute count = " << attributeSet.size() << std::endl; @@ -2319,7 +2311,7 @@ TEST_F(TestOpenVDB, PointFiles) TEST_F(TestOpenVDB, Trilinear) { // create a grid so sample from - auto trilinear = [](const openvdb::Vec3R& xyz) -> float { + auto trilinear = [](const openvdb::Vec3d& xyz) -> float { return 0.34 + 1.6 * xyz[0] + 6.7 * xyz[1] - 3.5 * xyz[2]; // world coordinates }; @@ -2334,7 +2326,7 @@ TEST_F(TestOpenVDB, Trilinear) acc.setValue(ijk, trilinear(srcGrid->indexToWorld(ijk))); } //mTimer.restart("Generating NanoVDB grid"); - auto handle = nanovdb::openToNanoVDB(*srcGrid); + auto handle = nanovdb::createNanoGrid(*srcGrid); //mTimer.restart("Writing NanoVDB grid"); nanovdb::io::writeGrid("data/tmp.nvdb", handle); //mTimer.stop(); @@ -2351,7 +2343,7 @@ TEST_F(TestOpenVDB, Trilinear) EXPECT_FALSE(handles[0].grid()); EXPECT_EQ(voxelSize, dstGrid->voxelSize()[0]); - const openvdb::Vec3R ijk(13.4, 24.67, 5.23); // in index space + const openvdb::Vec3d ijk(13.4, 24.67, 5.23); // in index space const float exact = trilinear(srcGrid->indexToWorld(ijk)); const float approx = trilinear(srcGrid->indexToWorld(openvdb::Coord(13, 25, 5))); //std::cerr << "Trilinear: exact = " << exact << ", approx = " << approx << std::endl; @@ -2384,7 +2376,7 @@ TEST_F(TestOpenVDB, Trilinear) TEST_F(TestOpenVDB, Triquadratic) { // create a grid so sample from - auto triquadratic = [](const openvdb::Vec3R& xyz) -> double { + auto triquadratic = [](const openvdb::Vec3d& xyz) -> double { return 0.34 + 1.6 * xyz[0] + 2.7 * xyz[1] + 1.5 * xyz[2] + 0.025 * xyz[0] * xyz[1] * xyz[2] - 0.013 * xyz[0] * xyz[0]; // world coordinates }; @@ -2400,7 +2392,7 @@ TEST_F(TestOpenVDB, Triquadratic) acc.setValue(ijk, triquadratic(srcGrid->indexToWorld(ijk))); } //mTimer.restart("Generating NanoVDB grid"); - auto handle = nanovdb::openToNanoVDB(*srcGrid); + auto handle = nanovdb::createNanoGrid(*srcGrid); //mTimer.restart("Writing NanoVDB grid"); nanovdb::io::writeGrid("data/tmp.nvdb", handle); //mTimer.stop(); @@ -2416,7 +2408,7 @@ TEST_F(TestOpenVDB, Triquadratic) auto* dstGrid = handles[0].grid(); EXPECT_TRUE(dstGrid); - const openvdb::Vec3R ijk(3.4, 4.67, 5.23); // in index space + const openvdb::Vec3d ijk(3.4, 4.67, 5.23); // in index space const float exact = triquadratic(srcGrid->indexToWorld(ijk)); const float approx = triquadratic(srcGrid->indexToWorld(openvdb::Coord(3, 5, 5))); //std::cerr << "Trilinear: exact = " << exact << ", approx = " << approx << std::endl; @@ -2444,7 +2436,7 @@ TEST_F(TestOpenVDB, Triquadratic) TEST_F(TestOpenVDB, Tricubic) { // create a grid so sample from - auto tricubic = [](const openvdb::Vec3R& xyz) -> double { + auto tricubic = [](const openvdb::Vec3d& xyz) -> double { return 0.34 + 1.6 * xyz[0] + 2.7 * xyz[1] + 1.5 * xyz[2] + 0.025 * xyz[0] * xyz[1] * xyz[2] - 0.013 * xyz[0] * xyz[0] * xyz[0]; // world coordinates }; @@ -2459,7 +2451,7 @@ TEST_F(TestOpenVDB, Tricubic) acc.setValue(ijk, tricubic(srcGrid->indexToWorld(ijk))); } //mTimer.restart("Generating NanoVDB grid"); - auto handle = nanovdb::openToNanoVDB(*srcGrid); + auto handle = nanovdb::createNanoGrid(*srcGrid); //mTimer.restart("Writing NanoVDB grid"); nanovdb::io::writeGrid("data/tmp.nvdb", handle); //mTimer.stop(); @@ -2475,7 +2467,7 @@ TEST_F(TestOpenVDB, Tricubic) auto* dstGrid = handles[0].grid(); EXPECT_TRUE(dstGrid); - const openvdb::Vec3R ijk(3.4, 4.67, 5.23); // in index space + const openvdb::Vec3d ijk(3.4, 4.67, 5.23); // in index space const float exact = tricubic(srcGrid->indexToWorld(ijk)); const float approx = tricubic(srcGrid->indexToWorld(openvdb::Coord(3, 5, 5))); //std::cerr << "Trilinear: exact = " << exact << ", approx = " << approx << std::endl; @@ -2503,7 +2495,7 @@ TEST_F(TestOpenVDB, Tricubic) TEST_F(TestOpenVDB, GridValidator) { auto srcGrid = this->getSrcGrid(); - auto handle = nanovdb::openToNanoVDB(*srcGrid, nanovdb::StatsMode::All, nanovdb::ChecksumMode::Full, 0); + auto handle = nanovdb::createNanoGrid(*srcGrid, nanovdb::StatsMode::All, nanovdb::ChecksumMode::Full); //mTimer.stop(); EXPECT_TRUE(handle); EXPECT_TRUE(handle.data()); @@ -2572,17 +2564,18 @@ TEST_F(TestOpenVDB, DenseIndexGrid) // read openvdb::FloatGrid auto srcGrid = this->getSrcGrid(false, 0, 0);// level set of a dragon if available, else an octahedron auto& srcTree = srcGrid->tree(); - + nanovdb::CreateNanoGrid builder(*srcGrid); + builder.setStats(nanovdb::StatsMode::All); // openvdb::FloatGrid -> nanovdb::FloatGrid - auto handle = nanovdb::openToNanoVDB(*srcGrid, nanovdb::StatsMode::All); + auto handle = builder.getHandle(); EXPECT_TRUE(handle); auto* fltGrid = handle.grid(); + builder.setStats();// reset //std::cerr << "FloatGrid footprint: " << (fltGrid->gridSize()>>20) << "MB" << std::endl; - // nanovdb::FloatGrid -> nanovdb::IndexGrid - nanovdb::IndexGridBuilder builder(*fltGrid, true, true); + // openvdb::FloatGrid -> nanovdb::IndexGrid //mTimer.start("Create IndexGrid"); - auto handle2 = builder.getHandle(); + auto handle2 = builder.getHandle(1u, true, true); //mTimer.stop(); auto *idxGrid = handle2.grid(); auto idxAcc = idxGrid->getAccessor(); @@ -2593,8 +2586,8 @@ TEST_F(TestOpenVDB, DenseIndexGrid) // create external value buffer //mTimer.start("Create value buffer"); - auto buffer = builder.getValues(1);// only allocate one channel - const float *values = reinterpret_cast(buffer.data()); + const float *values = idxGrid->getBlindData(0); + EXPECT_TRUE(values); //mTimer.stop(); //std::cerr << "Value buffer footprint: " << (buffer.size()>>20) << "MB" << std::endl; @@ -2612,9 +2605,11 @@ TEST_F(TestOpenVDB, DenseIndexGrid) auto fltAcc = fltGrid->getAccessor();// NOT thread-safe! for (auto i=r.begin(); i!=r.end(); ++i){ auto *idxLeaf = idxLeaf0 + i; - auto *srcLeaf = fltAcc.probeLeaf(idxLeaf->origin()); - EXPECT_TRUE(srcLeaf); - EXPECT_EQ(values[idxLeaf->minimum()], srcLeaf->minimum()); + auto *fltLeaf = fltAcc.probeLeaf(idxLeaf->origin()); + EXPECT_TRUE(fltLeaf); + // since idxGrid was created from an OpenVDB Grid stats were not available + EXPECT_EQ(values[idxLeaf->minimum()], srcGrid->tree().root().background()); + //EXPECT_EQ(values[idxLeaf->minimum()], fltLeaf->minimum());// only if idxGrid was created from fltGrid for (auto vox = idxLeaf->beginValueOn(); vox; ++vox) { EXPECT_EQ(values[*vox], fltAcc.getValue(vox.getCoord())); } @@ -2626,34 +2621,22 @@ TEST_F(TestOpenVDB, SparseIndexGrid) { // read openvdb::FloatGrid auto srcGrid = this->getSrcGrid(false, 0, 0);// level set of a dragon if available, else an octahedron - //auto& srcTree = srcGrid->tree(); - // openvdb::FloatGrid -> nanovdb::FloatGrid - auto handle = nanovdb::openToNanoVDB(*srcGrid, nanovdb::StatsMode::All); - EXPECT_TRUE(handle); - auto* fltGrid = handle.grid(); - //std::cerr << "FloatGrid footprint: " << (fltGrid->gridSize()>>20) << "MB" << std::endl; - - // nanovdb::FloatGrid -> nanovdb::IndexGrid - nanovdb::IndexGridBuilder builder(*fltGrid, false, false); + // openvdb::FloatGrid -> nanovdb::IndexGrid + nanovdb::CreateNanoGrid builder(*srcGrid); //mTimer.start("Create IndexGrid"); - auto handle2 = builder.getHandle(); + auto handle2 = builder.getHandle(1u, false, false); //mTimer.stop(); auto *idxGrid = handle2.grid(); auto idxAcc = idxGrid->getAccessor(); EXPECT_TRUE(idxGrid); - const uint64_t vCount = idxGrid->data()->mData1; + const uint64_t vCount = idxGrid->valueCount(); //std::cerr << "IndexGrid value count = " << vCount << std::endl; //std::cerr << "IndexGrid footprint: " << (idxGrid->gridSize()>>20) << "MB" << std::endl; - // create external value buffer - //mTimer.start("Create value buffer"); - auto buffer = builder.getValues(1);// only allocate one channel - const float *values = reinterpret_cast(buffer.data()); - //mTimer.stop(); - //std::cerr << "Value buffer footprint: " << (buffer.size()>>20) << "MB" << std::endl; - // unit-test sparse value buffer + const float *values = idxGrid->getBlindData(0u); + EXPECT_TRUE(values); //mTimer.start("Testing sparse active values"); for (auto it = srcGrid->tree().cbeginValueOn(); it; ++it) { const openvdb::Coord ijk = it.getCoord(); @@ -2665,6 +2648,116 @@ TEST_F(TestOpenVDB, SparseIndexGrid) }// SparseIndexGrid +TEST_F(TestOpenVDB, BuildNodeManager) +{ + {// test NodeManager with build::Grid + using GridT = nanovdb::build::Grid; + GridT grid(0.0f); + nanovdb::build::NodeManager mgr(grid); + using TreeT = GridT::TreeType; + static const bool test = nanovdb::is_same::type, TreeT::LeafNodeType>::value; + EXPECT_TRUE(test); + } + {// test NodeManager with openvdb::Grid + using GridT = openvdb::FloatGrid; + GridT grid(0.0f); + nanovdb::build::NodeManager mgr(grid); + using TreeT = GridT::TreeType; + static const bool test = nanovdb::is_same::type, TreeT::LeafNodeType>::value; + EXPECT_TRUE(test); + } + {// test NodeTrait on nanovdb::Grid + using GridT = nanovdb::NanoGrid; + using TreeT = GridT::TreeType; + static const bool test = nanovdb::is_same::type, TreeT::LeafNodeType>::value; + EXPECT_TRUE(test); + } +}// BuildNodeManager + +#if 0// toggle to enable benchmark tests + +class NanoPointList +{ + size_t mSize; + const openvdb::Vec3f *mPoints; +public: + using PosType = openvdb::Vec3f; + using value_type = openvdb::Vec3f; + NanoPointList(const nanovdb::Vec3f *points, size_t size) : mSize(size), mPoints(reinterpret_cast(points)) {} + size_t size() const {return mSize;} + void getPos(size_t n, PosType& xyz) const {xyz = mPoints[n];} +}; // NanoPointList + +// make -j && ./unittest/testNanoVDB --gtest_filter="*CudaPointsToGrid_PointID" --gtest_repeat=3 && ./unittest/testOpenVDB --gtest_filter="*PointIndexGrid*" --gtest_repeat=3 +TEST_F(TestOpenVDB, Benchmark_OpenVDB_PointIndexGrid) +{ + const double voxelSize = 0.5; + + nanovdb::CpuTimer timer("Generate sphere with points"); + auto pointsHandle = nanovdb::createPointSphere(8, 100.0, nanovdb::Vec3d(0.0), voxelSize); + timer.stop(); + + auto *pointGrid = pointsHandle.grid(); + EXPECT_TRUE(pointGrid); + std::cerr << "nanovdb::bbox = " << pointGrid->indexBBox() << " voxel count = " << pointGrid->activeVoxelCount() << std::endl; + + nanovdb::PointAccessor acc2(*pointGrid); + EXPECT_TRUE(acc2); + const nanovdb::Vec3f *begin, *end; + const size_t pointCount = acc2.gridPoints(begin, end); + EXPECT_TRUE(begin); + EXPECT_TRUE(end); + EXPECT_LT(begin, end); + + // construct data structure + timer.start("Building openvdb::PointIndexGrid on CPU from "+std::to_string(pointCount)+" points"); + using PointIndexGrid = openvdb::tools::PointIndexGrid; + const openvdb::math::Transform::Ptr transform = openvdb::math::Transform::createLinearTransform(voxelSize); + NanoPointList pointList(begin, pointCount); + auto pointGridPtr = openvdb::tools::createPointIndexGrid(pointList, *transform); + timer.stop(); + openvdb::CoordBBox bbox; + pointGridPtr->tree().evalActiveVoxelBoundingBox(bbox); + std::cerr << "openvdb::bbox = " << bbox << " voxel count = " << pointGridPtr->tree().activeVoxelCount() << std::endl; + +}// Benchmark_OpenVDB_PointIndexGrid + +TEST_F(TestOpenVDB, Benchmark_OpenVDB_PointDataGrid) +{ + const double voxelSize = 0.5; + + nanovdb::CpuTimer timer("Generate sphere with points"); + auto pointsHandle = nanovdb::createPointSphere(8, 100.0, nanovdb::Vec3d(0.0), voxelSize); + timer.stop(); + + auto *pointGrid = pointsHandle.grid(); + EXPECT_TRUE(pointGrid); + std::cerr << "nanovdb::bbox = " << pointGrid->indexBBox() << " voxel count = " << pointGrid->activeVoxelCount() << std::endl; + + nanovdb::PointAccessor acc2(*pointGrid); + EXPECT_TRUE(acc2); + const nanovdb::Vec3f *begin, *end; + const size_t pointCount = acc2.gridPoints(begin, end); + EXPECT_TRUE(begin); + EXPECT_TRUE(end); + EXPECT_LT(begin, end); + + // construct data structure + timer.start("Building openvdb::PointDataGrid on CPU from "+std::to_string(pointCount)+" points"); + using PointIndexGrid = openvdb::tools::PointIndexGrid; + const auto transform = openvdb::math::Transform::createLinearTransform(voxelSize); + NanoPointList pointList(begin, pointCount); + auto pointIndexGridPtr = openvdb::tools::createPointIndexGrid(pointList, *transform); + auto pointDataGridPtr = openvdb::points::createPointDataGrid,// corresponds to PointType::Voxel8 + openvdb::points::PointDataGrid, NanoPointList, PointIndexGrid>(*pointIndexGridPtr, pointList, *transform); + timer.stop(); + openvdb::CoordBBox bbox; + pointDataGridPtr->tree().evalActiveVoxelBoundingBox(bbox); + std::cerr << "openvdb::bbox = " << bbox << " voxel count = " << pointDataGridPtr->tree().activeVoxelCount() << std::endl; + +}// Benchmark_OpenVDB_PointDataGrid +#endif + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); diff --git a/nanovdb/nanovdb/unittest/pnanovdb_validate_strides.h b/nanovdb/nanovdb/unittest/pnanovdb_validate_strides.h index 4fc778d755..02f27c1ea3 100644 --- a/nanovdb/nanovdb/unittest/pnanovdb_validate_strides.h +++ b/nanovdb/nanovdb/unittest/pnanovdb_validate_strides.h @@ -83,7 +83,7 @@ static void compute_node_strides( pnanovdb_uint32_t minmaxStride = pnanovdb_grid_type_minmax_strides_bits[grid_type] / 8u; pnanovdb_uint32_t minmaxAlign = pnanovdb_grid_type_minmax_aligns_bits[grid_type] / 8u; pnanovdb_uint32_t statStride = pnanovdb_grid_type_stat_strides_bits[grid_type] / 8u; - pnanovdb_uint32_t postStatStride = 0u; + pnanovdb_uint32_t indexMaskStride = 0u; if (nodeLevel == 0u) { if (pnanovdb_grid_type_leaf_type[grid_type] == PNANOVDB_LEAF_TYPE_LITE) @@ -106,17 +106,33 @@ static void compute_node_strides( minmaxStride = 0u; minmaxAlign = 0u; statStride = 0u; - postStatStride = 8u; tableAlign = 8u; tableFullStride = 8u; } + else if (pnanovdb_grid_type_leaf_type[grid_type] == PNANOVDB_LEAF_TYPE_INDEXMASK) + { + minmaxStride = 0u; + minmaxAlign = 0u; + statStride = 0u; + tableAlign = 8u; + tableFullStride = 8u; + indexMaskStride = 64u; + } + else if (pnanovdb_grid_type_leaf_type[grid_type] == PNANOVDB_LEAF_TYPE_POINTINDEX) + { + minmaxStride = 8u; + minmaxAlign = 8u; + statStride = 0u; + tableAlign = 2u; + tableFullStride = (16u * node_elements[nodeLevel]) / 8u; + } } *min_off = allocate(&offset, minmaxStride, minmaxAlign); *max_off = allocate(&offset, minmaxStride, minmaxAlign); *ave_off = allocate(&offset, statStride, statStride); *stddev_off = allocate(&offset, statStride, statStride); - allocate(&offset, postStatStride, postStatStride); *table_off = allocate(&offset, tableFullStride, tableAlign); + allocate(&offset, indexMaskStride, tableAlign); *total_size = allocate(&offset, 0u, 32u); } diff --git a/nanovdb/nanovdb/util/CpuTimer.h b/nanovdb/nanovdb/util/CpuTimer.h new file mode 100644 index 0000000000..44bf155287 --- /dev/null +++ b/nanovdb/nanovdb/util/CpuTimer.h @@ -0,0 +1,83 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +/// @file CpuTimer.h +/// +/// @author Ken Museth +/// +/// @brief A simple timing class (in case openvdb::util::CpuTimer is unavailable) + +#ifndef NANOVDB_CPU_TIMER_H_HAS_BEEN_INCLUDED +#define NANOVDB_CPU_TIMER_H_HAS_BEEN_INCLUDED + +#include +#include + +namespace nanovdb { + +class CpuTimer +{ + std::chrono::high_resolution_clock::time_point mStart; +public: + /// @brief Default constructor + CpuTimer() {} + + /// @brief Constructor that starts the timer + /// @param msg string message to be printed when timer is started + /// @param os output stream for the message above + CpuTimer(const std::string &msg, std::ostream& os = std::cerr) {this->start(msg, os);} + + /// @brief Start the timer + /// @param msg string message to be printed when timer is started + /// @param os output stream for the message above + void start(const std::string &msg, std::ostream& os = std::cerr) + { + os << msg << " ... " << std::flush; + mStart = std::chrono::high_resolution_clock::now(); + } + + /// @brief elapsed time (since start) in miliseconds + template + auto elapsed() + { + auto end = std::chrono::high_resolution_clock::now(); + return std::chrono::duration_cast(end - mStart).count(); + } + + /// @brief stop the timer + /// @tparam AccuracyT Template parameter defining the accuracy of the reported times + /// @param os output stream for the message above + template + void stop(std::ostream& os = std::cerr) + { + auto end = std::chrono::high_resolution_clock::now(); + auto diff = std::chrono::duration_cast(end - mStart).count(); + os << "completed in " << diff; + if (std::is_same::value) {// resolved at compile-time + os << " microseconds" << std::endl; + } else if (std::is_same::value) { + os << " milliseconds" << std::endl; + } else if (std::is_same::value) { + os << " seconds" << std::endl; + } else { + os << " unknown time unit" << std::endl; + } + } + + /// @brief stop and start the timer + /// @tparam AccuracyT Template parameter defining the accuracy of the reported times + /// @param msg string message to be printed when timer is started + /// @param os output stream for the message above + template + void restart(const std::string &msg, std::ostream& os = std::cerr) + { + this->stop(); + this->start(msg, os); + } + + +};// CpuTimer + +} // namespace nanovdb + +#endif // NANOVDB_CPU_TIMER_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/CreateNanoGrid.h b/nanovdb/nanovdb/util/CreateNanoGrid.h new file mode 100644 index 0000000000..7ad71c57d4 --- /dev/null +++ b/nanovdb/nanovdb/util/CreateNanoGrid.h @@ -0,0 +1,2075 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +/*! + \file CreateNanoGrid.h + + \author Ken Museth + + \date June 26, 2020 + + \note In the examples below we assume that @c srcGrid is a exiting grid of type + SrcGridT = @c openvdb::FloatGrid, @c openvdb::FloatGrid or @c nanovdb::build::FloatGrid. + + \brief Convert any grid to a nanovdb grid of the same type, e.g. float->float + \code + auto handle = nanovdb::createNanoGrid(srcGrid); + auto *dstGrid = handle.grid(); + \endcode + + \brief Convert a grid to a nanovdb grid of a different type, e.g. float->half + \code + auto handle = nanovdb::createNanoGrid(srcGrid); + auto *dstGrid = handle.grid(); + \endcode + + \brief Convert a grid to a nanovdb grid of the same type but using a CUDA buffer + \code + auto handle = nanovdb::createNanoGrid(srcGrid); + auto *dstGrid = handle.grid(); + \endcode + + \brief Create a nanovdb grid that indices values in an existing source grid of any type. + If DstBuildT = nanovdb::ValueIndex both active and in-active values are indexed + and if DstBuildT = nanovdb::ValueOnIndex only active values are indexed. + \code + using DstBuildT = nanovdb::ValueIndex;// index both active an inactive values + auto handle = nanovdb::createNanoGridSrcGridT,DstBuildT>(srcGrid,0,false,false);//no blind data, tile values or stats + auto *dstGrid = handle.grid(); + \endcode + + \brief Create a NanoVDB grid from scratch + \code +#if defined(NANOVDB_USE_OPENVDB) && !defined(__CUDACC__) + using SrcGridT = openvdb::FloatGrid; +#else + using SrcGridT = nanovdb::build::FloatGrid; +#endif + SrcGridT srcGrid(0.0f);// create an empty source grid + auto srcAcc = srcGrid.getAccessor();// create an accessor + srcAcc.setValue(nanovdb::Coord(1,2,3), 1.0f);// set a voxel value + + auto handle = nanovdb::createNanoGrid(srcGrid);// convert source grid to a grid handle + auto dstGrid = handle.grid();// get a pointer to the destination grid + \endcode + + \brief Convert a base-pointer to an openvdb grid, denoted srcGrid, to a nanovdb + grid of the same type, e.g. float -> float or openvdb::Vec3f -> nanovdb::Vec3f + \code + auto handle = nanovdb::openToNanoVDB(*srcGrid);// convert source grid to a grid handle + auto dstGrid = handle.grid();// get a pointer to the destination grid + \endcode + + \brief Converts any existing grid to a NanoVDB grid, for example: + nanovdb::build::Grid -> nanovdb::Grid + nanovdb::Grid -> nanovdb::Grid + nanovdb::Grid -> nanovdb::Grid + openvdb::Grid -> nanovdb::Grid + openvdb::Grid -> nanovdb::Grid + openvdb::Grid -> nanovdb::Grid + openvdb::Grid -> nanovdb::Grid + + \note This files replaces GridBuilder.h, IndexGridBuilder.h and OpenToNanoVDB.h +*/ + +#ifndef NANOVDB_CREATE_NANOGRID_H_HAS_BEEN_INCLUDED +#define NANOVDB_CREATE_NANOGRID_H_HAS_BEEN_INCLUDED + +#if defined(NANOVDB_USE_OPENVDB) && !defined(__CUDACC__) +#include +#include +#include +#endif + +#include "GridBuilder.h" +#include "NodeManager.h" +#include "GridHandle.h" +#include "GridStats.h" +#include "GridChecksum.h" +#include "Range.h" +#include "Invoke.h" +#include "ForEach.h" +#include "Reduce.h" +#include "PrefixSum.h" +#include "DitherLUT.h"// for nanovdb::DitherLUT + +#include +#include +#include +#include // for memcpy +#include + +namespace nanovdb { + +// Forward declarations (defined below) +template class CreateNanoGrid; +class AbsDiff; +template struct MapToNano; + +//================================================================================================ + +#if defined(NANOVDB_USE_OPENVDB) && !defined(__CUDACC__) +/// @brief Forward declaration of free-standing function that converts an OpenVDB GridBase into a NanoVDB GridHandle +/// @tparam BufferT Type of the buffer used to allocate the destination grid +/// @param base Shared pointer to a base openvdb grid to be converted +/// @param sMode Mode for computing statistics of the destination grid +/// @param cMode Mode for computing checksums of the destination grid +/// @param verbose Mode of verbosity +/// @return Handle to the destination NanoGrid +template +GridHandle +openToNanoVDB(const openvdb::GridBase::Ptr& base, + StatsMode sMode = StatsMode::Default, + ChecksumMode cMode = ChecksumMode::Default, + int verbose = 0); +#endif + +//================================================================================================ + +/// @brief Freestanding function that creates a NanoGrid from any source grid +/// @tparam SrcGridT Type of in input (source) grid, e.g. openvdb::Grid or nanovdb::Grid +/// @tparam DstBuildT Type of values in the output (destination) nanovdb Grid, e.g. float or nanovdb::Fp16 +/// @tparam BufferT Type of the buffer used ti allocate the destination grid +/// @param srcGrid Input (source) grid to be converted +/// @param sMode Mode for computing statistics of the destination grid +/// @param cMode Mode for computing checksums of the destination grid +/// @param verbose Mode of verbosity +/// @param buffer Instance of a buffer used for allocation +/// @return Handle to the destination NanoGrid +template::type, + typename BufferT = HostBuffer> +typename disable_if::is_index || BuildTraits::is_Fp, GridHandle>::type +createNanoGrid(const SrcGridT &srcGrid, + StatsMode sMode = StatsMode::Default, + ChecksumMode cMode = ChecksumMode::Default, + int verbose = 0, + const BufferT &buffer = BufferT()); + +//================================================================================================ + +/// @brief Freestanding function that creates a NanoGrid or NanoGrid from any source grid +/// @tparam SrcGridT Type of in input (source) grid, e.g. openvdb::Grid or nanovdb::Grid +/// @tparam DstBuildT If ValueIndex all (active and inactive) values are indexed and if +/// it is ValueOnIndex only active values are indexed. +/// @tparam BufferT BufferT Type of the buffer used ti allocate the destination grid +/// @param channels If non-zero the values (active or all) in @c srcGrid are encoded as blind +/// data in the output index grid. @c channels indicates the number of copies +/// of these blind data +/// @param includeStats If true all tree nodes will includes indices for stats, i.e. min/max/avg/std-div +/// @param includeTiles If false on values in leaf nodes are indexed +/// @param verbose Mode of verbosity +/// @param buffer Instance of a buffer used for allocation +/// @return Handle to the destination NanoGrid where T = ValueIndex or ValueOnIndex +template::type, + typename BufferT = HostBuffer> +typename enable_if::is_index, GridHandle>::type +createNanoGrid(const SrcGridT &srcGrid, + uint32_t channels = 0u, + bool includeStats = true, + bool includeTiles = true, + int verbose = 0, + const BufferT &buffer = BufferT()); + +//================================================================================================ + +/// @brief Freestanding function to create a NanoGrid from any source grid +/// @tparam SrcGridT Type of in input (source) grid, e.g. openvdb::Grid or nanovdb::Grid +/// @tparam DstBuildT = FpN, i.e. variable bit-width of the output grid +/// @tparam OracleT Type of the oracle used to determine the local bit-width, i.e. N in FpN +/// @tparam BufferT Type of the buffer used to allocate the destination grid +/// @param srcGrid Input (source) grid to be converted +/// @param ditherOn switch to enable or disable dithering of quantization error +/// @param sMode Mode for computing statistics of the destination grid +/// @param cMode Mode for computing checksums of the destination grid +/// @param verbose Mode of verbosity +/// @param oracle Instance of a oracle used to determine the local bit-width, i.e. N in FpN +/// @param buffer Instance of a buffer used for allocation +/// @return Handle to the destination NanoGrid +template::type, + typename OracleT = AbsDiff, + typename BufferT = HostBuffer> +typename enable_if::value, GridHandle>::type +createNanoGrid(const SrcGridT &srcGrid, + StatsMode sMode = StatsMode::Default, + ChecksumMode cMode = ChecksumMode::Default, + bool ditherOn = false, + int verbose = 0, + const OracleT &oracle = OracleT(), + const BufferT &buffer = BufferT()); + +//================================================================================================ + +/// @brief Freestanding function to create a NanoGrid from any source grid, X=4,8,16 +/// @tparam SrcGridT Type of in input (source) grid, e.g. openvdb::Grid or nanovdb::Grid +/// @tparam DstBuildT = Fp4, Fp8 or Fp16, i.e. quantization bit-width of the output grid +/// @tparam BufferT Type of the buffer used to allocate the destination grid +/// @param srcGrid Input (source) grid to be converted +/// @param ditherOn switch to enable or disable dithering of quantization error +/// @param sMode Mode for computing statistics of the destination grid +/// @param cMode Mode for computing checksums of the destination grid +/// @param verbose Mode of verbosity +/// @param buffer Instance of a buffer used for allocation +/// @return Handle to the destination NanoGrid +template::type, + typename BufferT = HostBuffer> +typename enable_if::is_FpX, GridHandle>::type +createNanoGrid(const SrcGridT &srcGrid, + StatsMode sMode = StatsMode::Default, + ChecksumMode cMode = ChecksumMode::Default, + bool ditherOn = false, + int verbose = 0, + const BufferT &buffer = BufferT()); + +//================================================================================================ + +/// @brief Compression oracle based on absolute difference +class AbsDiff +{ + float mTolerance;// absolute error tolerance +public: + /// @note The default value of -1 means it's un-initialized! + AbsDiff(float tolerance = -1.0f) : mTolerance(tolerance) {} + AbsDiff(const AbsDiff&) = default; + ~AbsDiff() = default; + operator bool() const {return mTolerance>=0.0f;} + void init(nanovdb::GridClass gClass, float background) { + if (gClass == GridClass::LevelSet) { + static const float halfWidth = 3.0f; + mTolerance = 0.1f * background / halfWidth;// range of ls: [-3dx; 3dx] + } else if (gClass == GridClass::FogVolume) { + mTolerance = 0.01f;// range of FOG volumes: [0;1] + } else { + mTolerance = 0.0f; + } + } + void setTolerance(float tolerance) { mTolerance = tolerance; } + float getTolerance() const { return mTolerance; } + /// @brief Return true if the approximate value is within the accepted + /// absolute error bounds of the exact value. + /// + /// @details Required member method + bool operator()(float exact, float approx) const + { + return Abs(exact - approx) <= mTolerance; + } +};// AbsDiff + +inline std::ostream& operator<<(std::ostream& os, const AbsDiff& diff) +{ + os << "Absolute tolerance: " << diff.getTolerance(); + return os; +} + +//================================================================================================ + +/// @brief Compression oracle based on relative difference +class RelDiff +{ + float mTolerance;// relative error tolerance +public: + /// @note The default value of -1 means it's un-initialized! + RelDiff(float tolerance = -1.0f) : mTolerance(tolerance) {} + RelDiff(const RelDiff&) = default; + ~RelDiff() = default; + operator bool() const {return mTolerance>=0.0f;} + void setTolerance(float tolerance) { mTolerance = tolerance; } + float getTolerance() const { return mTolerance; } + /// @brief Return true if the approximate value is within the accepted + /// relative error bounds of the exact value. + /// + /// @details Required member method + bool operator()(float exact, float approx) const + { + return Abs(exact - approx)/Max(Abs(exact), Abs(approx)) <= mTolerance; + } +};// RelDiff + +inline std::ostream& operator<<(std::ostream& os, const RelDiff& diff) +{ + os << "Relative tolerance: " << diff.getTolerance(); + return os; +} + +//================================================================================================ + +/// @brief The NodeAccessor provides a uniform API for accessing nodes got NanoVDB, OpenVDB and build Grids +/// +/// @note General implementation that works with nanovdb::build::Grid +template +class NodeAccessor +{ +public: + static constexpr bool IS_OPENVDB = false; + static constexpr bool IS_NANOVDB = false; + using BuildType = typename GridT::BuildType; + using ValueType = typename GridT::ValueType; + using GridType = GridT; + using TreeType = typename GridT::TreeType; + using RootType = typename TreeType::RootNodeType; + template + using NodeType = typename NodeTrait::type; + NodeAccessor(const GridT &grid) : mMgr(const_cast(grid)) {} + const GridType& grid() const {return mMgr.grid();} + const TreeType& tree() const {return mMgr.tree();} + const RootType& root() const {return mMgr.root();} + uint64_t nodeCount(int level) const { return mMgr.nodeCount(level); } + template + const NodeType& node(uint32_t i) const {return mMgr.template node(i); } + const std::string& getName() const {return this->grid().getName();}; + bool hasLongGridName() const {return this->grid().getName().length() >= GridData::MaxNameSize;} + const nanovdb::Map& map() const {return this->grid().map();} + GridClass gridClass() const {return this->grid().gridClass();} +private: + build::NodeManager mMgr; +};// NodeAccessor + +//================================================================================================ + +/// @brief Template specialization for nanovdb::Grid which is special since its NodeManage +/// uses a handle in order to support node access on the GPU! +template +class NodeAccessor< NanoGrid > +{ +public: + static constexpr bool IS_OPENVDB = false; + static constexpr bool IS_NANOVDB = true; + using BuildType = BuildT; + using BufferType = HostBuffer; + using GridType = NanoGrid; + using ValueType = typename GridType::ValueType; + using TreeType = typename GridType::TreeType; + using RootType = typename TreeType::RootType; + template + using NodeType = typename NodeTrait::type; + NodeAccessor(const GridType &grid) + : mHandle(createNodeManager(grid)) + , mMgr(*(mHandle.template mgr())) {} + const GridType& grid() const {return mMgr.grid();} + const TreeType& tree() const {return mMgr.tree();} + const RootType& root() const {return mMgr.root();} + uint64_t nodeCount(int level) const { return mMgr.nodeCount(level); } + template + const NodeType& node(uint32_t i) const {return mMgr.template node(i); } + std::string getName() const {return std::string(this->grid().gridName());}; + bool hasLongGridName() const {return this->grid().hasLongGridName();} + const nanovdb::Map& map() const {return this->grid().map();} + GridClass gridClass() const {return this->grid().gridClass();} +private: + NodeManagerHandle mHandle; + const NodeManager &mMgr; +};// NodeAccessor + +//================================================================================================ + +/// @brief Trait that maps any type to the corresponding nanovdb type +/// @tparam T Type to be mapped +template +struct MapToNano { using type = T; }; + +#if defined(NANOVDB_USE_OPENVDB) && !defined(__CUDACC__) + +template<> +struct MapToNano {using type = nanovdb::ValueMask;}; +template +struct MapToNano>{using type = nanovdb::Vec3;}; +template +struct MapToNano>{using type = nanovdb::Vec4;}; +template<> +struct MapToNano {using type = uint32_t;}; +template<> +struct MapToNano {using type = uint32_t;}; + +/// Templated Grid with default 32->16->8 configuration +template +using OpenLeaf = openvdb::tree::LeafNode; +template +using OpenLower = openvdb::tree::InternalNode,4>; +template +using OpenUpper = openvdb::tree::InternalNode,5>; +template +using OpenRoot = openvdb::tree::RootNode>; +template +using OpenTree = openvdb::tree::Tree>; +template +using OpenGrid = openvdb::Grid>; + +//================================================================================================ + +/// @brief Template specialization for openvdb::Grid +template +class NodeAccessor> +{ +public: + static constexpr bool IS_OPENVDB = true; + static constexpr bool IS_NANOVDB = false; + using BuildType = BuildT; + using GridType = OpenGrid; + using ValueType = typename GridType::ValueType; + using TreeType = OpenTree; + using RootType = OpenRoot; + template + using NodeType = typename NodeTrait::type; + NodeAccessor(const GridType &grid) : mMgr(const_cast(grid)) { + const auto mat4 = this->grid().transform().baseMap()->getAffineMap()->getMat4(); + mMap.set(mat4, mat4.inverse()); + } + const GridType& grid() const {return mMgr.grid();} + const TreeType& tree() const {return mMgr.tree();} + const RootType& root() const {return mMgr.root();} + uint64_t nodeCount(int level) const { return mMgr.nodeCount(level); } + template + const NodeType& node(uint32_t i) const {return mMgr.template node(i); } + std::string getName() const { return this->grid().getName(); }; + bool hasLongGridName() const {return this->grid().getName().length() >= GridData::MaxNameSize;} + const nanovdb::Map& map() const {return mMap;} + GridClass gridClass() const { + switch (this->grid().getGridClass()) { + case openvdb::GRID_LEVEL_SET: + if (!is_floating_point::value) OPENVDB_THROW(openvdb::ValueError, "processGrid: Level sets are expected to be floating point types"); + return GridClass::LevelSet; + case openvdb::GRID_FOG_VOLUME: + return GridClass::FogVolume; + case openvdb::GRID_STAGGERED: + return GridClass::Staggered; + default: + return GridClass::Unknown; + } + } +private: + build::NodeManager mMgr; + nanovdb::Map mMap; +};// NodeAccessor> + +//================================================================================================ + +/// @brief Template specialization for openvdb::tools::PointIndexGrid +template <> +class NodeAccessor +{ +public: + static constexpr bool IS_OPENVDB = true; + static constexpr bool IS_NANOVDB = false; + using BuildType = openvdb::PointIndex32; + using GridType = openvdb::tools::PointIndexGrid; + using TreeType = openvdb::tools::PointIndexTree; + using RootType = typename TreeType::RootNodeType; + using ValueType = typename GridType::ValueType; + template + using NodeType = typename NodeTrait::type; + NodeAccessor(const GridType &grid) : mMgr(const_cast(grid)) { + const auto mat4 = this->grid().transform().baseMap()->getAffineMap()->getMat4(); + mMap.set(mat4, mat4.inverse()); + } + const GridType& grid() const {return mMgr.grid();} + const TreeType& tree() const {return mMgr.tree();} + const RootType& root() const {return mMgr.root();} + uint64_t nodeCount(int level) const { return mMgr.nodeCount(level); } + template + const NodeType& node(uint32_t i) const {return mMgr.template node(i); } + std::string getName() const { return this->grid().getName(); }; + bool hasLongGridName() const {return this->grid().getName().length() >= GridData::MaxNameSize;} + const nanovdb::Map& map() const {return mMap;} + GridClass gridClass() const {return GridClass::PointIndex;} +private: + build::NodeManager mMgr; + nanovdb::Map mMap; +};// NodeAccessor + +//================================================================================================ + +// @brief Template specialization for openvdb::points::PointDataGrid +template <> +class NodeAccessor +{ +public: + static constexpr bool IS_OPENVDB = true; + static constexpr bool IS_NANOVDB = false; + using BuildType = openvdb::PointDataIndex32; + using GridType = openvdb::points::PointDataGrid; + using TreeType = openvdb::points::PointDataTree; + using RootType = typename TreeType::RootNodeType; + using ValueType = typename GridType::ValueType; + template + using NodeType = typename NodeTrait::type; + NodeAccessor(const GridType &grid) : mMgr(const_cast(grid)) { + const auto mat4 = this->grid().transform().baseMap()->getAffineMap()->getMat4(); + mMap.set(mat4, mat4.inverse()); + } + const GridType& grid() const {return mMgr.grid();} + const TreeType& tree() const {return mMgr.tree();} + const RootType& root() const {return mMgr.root();} + uint64_t nodeCount(int level) const { return mMgr.nodeCount(level); } + template + const NodeType& node(uint32_t i) const {return mMgr.template node(i); } + std::string getName() const { return this->grid().getName(); }; + bool hasLongGridName() const {return this->grid().getName().length() >= GridData::MaxNameSize;} + const nanovdb::Map& map() const {return mMap;} + GridClass gridClass() const {return GridClass::PointData;} +private: + build::NodeManager mMgr; + nanovdb::Map mMap; +};// NodeAccessor + +#endif// NANOVDB_USE_OPENVDB + +//================================================================================================ + +/// @brief Creates any nanovdb Grid from any source grid (certain combinations are obviously not allowed) +template +class CreateNanoGrid +{ +public: + // SrcGridT can be either openvdb::Grid, nanovdb::Grid or nanovdb::build::Grid + using SrcNodeAccT = NodeAccessor; + using SrcBuildT = typename SrcNodeAccT::BuildType; + using SrcValueT = typename SrcNodeAccT::ValueType; + using SrcTreeT = typename SrcNodeAccT::TreeType; + using SrcRootT = typename SrcNodeAccT::RootType; + template + using SrcNodeT = typename NodeTrait::type; + + /// @brief Constructor from a source grid + /// @param srcGrid Source grid of type SrcGridT + CreateNanoGrid(const SrcGridT &srcGrid); + + /// @brief Constructor from a source node accessor (defined above) + /// @param srcNodeAcc Source node accessor of type SrcNodeAccT + CreateNanoGrid(const SrcNodeAccT &srcNodeAcc); + + /// @brief Set the level of verbosity + /// @param mode level of verbosity, mode=0 means quiet + void setVerbose(int mode = 1) { mVerbose = mode; } + + /// @brief Enable or disable dithering, i.e. randomization of the quantization error. + /// @param on enable or disable dithering + /// @warning Dithering only has an affect when DstBuildT = {Fp4, Fp8, Fp16, FpN} + void enableDithering(bool on = true) { mDitherOn = on; } + + /// @brief Set the mode used for computing statistics of the destination grid + /// @param mode specify the mode of statistics + void setStats(StatsMode mode = StatsMode::Default) { mStats = mode; } + + /// @brief Set the mode used for computing checksums of the destination grid + /// @param mode specify the mode of checksum + void setChecksum(ChecksumMode mode = ChecksumMode::Default) { mChecksum = mode; } + + /// @brief Converts the source grid into a nanovdb grid with the specified destination build type + /// @tparam DstBuildT build type of the destination, output, grid + /// @tparam BufferT Type of the buffer used for allocating the destination grid + /// @param buffer instance of the buffer use for allocation + /// @return Return an instance of a GridHandle (invoking move semantics) + /// @note This version is when DstBuildT != {FpN, ValueIndex, ValueOnIndex} + template::type, typename BufferT = HostBuffer> + typename disable_if::value || + BuildTraits::is_index, GridHandle>::type + getHandle(const BufferT &buffer = BufferT()); + + /// @brief Converts the source grid into a nanovdb grid with variable bit quantization + /// @tparam DstBuildT FpN, i.e. the destination grid uses variable bit quantization + /// @tparam OracleT Type of oracle used to determine the N in FpN + /// @tparam BufferT Type of the buffer used for allocating the destination grid + /// @param oracle Instance of the oracle used to determine the N in FpN + /// @param buffer instance of the buffer use for allocation + /// @return Return an instance of a GridHandle (invoking move semantics) + /// @note This version assumes DstBuildT == FpN + template::type, typename OracleT = AbsDiff, typename BufferT = HostBuffer> + typename enable_if::value, GridHandle>::type + getHandle(const OracleT &oracle = OracleT(), + const BufferT &buffer = BufferT()); + + /// @brief Converts the source grid into a nanovdb grid with indices to external arrays of values + /// @tparam DstBuildT ValueIndex or ValueOnIndex, i.e. index all or just active values + /// @tparam BufferT Type of the buffer used for allocating the destination grid + /// @param channels Number of copies of values encoded as blind data in the destination grid + /// @param includeStats Specify if statics should be indexed + /// @param includeTiles Specify if tile values, i.e. non-leaf-node-values, should be indexed + /// @param buffer instance of the buffer use for allocation + /// @return Return an instance of a GridHandle (invoking move semantics) + template::type, typename BufferT = HostBuffer> + typename enable_if::is_index, GridHandle>::type + getHandle(uint32_t channels = 0u, + bool includeStats = true, + bool includeTiles = true, + const BufferT &buffer = BufferT()); + + /// @brief Add blind data to the destination grid + /// @param name String name of the blind data + /// @param dataSemantic Semantics of the blind data + /// @param dataClass Class of the blind data + /// @param dataType Type of the blind data + /// @param count Element count of the blind data + /// @param size Size of each element of the blind data + /// @return Return the index used to access the blind data + uint64_t addBlindData(const std::string& name, + GridBlindDataSemantic dataSemantic, + GridBlindDataClass dataClass, + GridType dataType, + size_t count, size_t size) + { + const size_t order = mBlindMetaData.size(); + mBlindMetaData.emplace(name, dataSemantic, dataClass, dataType, order, count, size); + return order; + } + + /// @brief This method only has affect when getHandle was called with DstBuildT = ValueIndex or ValueOnIndex + /// @return Return the number of indexed values. If called before getHandle was called with + /// DstBuildT = ValueIndex or ValueOnIndex the return value is zero. Else it is a value larger than zero. + uint64_t valueCount() const {return mValIdx[0].empty() ? 0u : mValIdx[0].back();} + + /// @brief Copy values from the source grid into a provided buffer + /// @tparam DstBuildT Must be ValueIndex or ValueOnIndex, i.e. a index grid + /// @param buffer point in which to write values + template + typename enable_if::is_index>::type + copyValues(SrcValueT *buffer); + +private: + + // ========================================================= + + template + typename enable_if::value&&LEVEL==0), typename NodeTrait, LEVEL>::type*>::type + dstNode(uint64_t i) const { + static_assert(LEVEL==0 || LEVEL==1 || LEVEL==2, "Expected LEVEL== {0,1,2}"); + using NodeT = typename NodeTrait, LEVEL>::type; + return PtrAdd(mBufferPtr, mOffset[5-LEVEL]) + i; + } + template + typename enable_if::value && LEVEL==0, NanoLeaf*>::type + dstNode(uint64_t i) const {return PtrAdd>(mBufferPtr, mCodec[i].offset);} + + template NanoRoot* dstRoot() const {return PtrAdd>(mBufferPtr, mOffset.root);} + template NanoTree* dstTree() const {return PtrAdd>(mBufferPtr, mOffset.tree);} + template NanoGrid* dstGrid() const {return PtrAdd>(mBufferPtr, mOffset.grid);} + GridBlindMetaData* dstMeta(uint32_t i) const { return PtrAdd(mBufferPtr, mOffset.meta) + i;}; + + // ========================================================= + + template + typename disable_if::value || BuildTraits::is_index>::type + preProcess(); + + template + typename enable_if::is_index>::type + preProcess(uint32_t channels); + + template + typename enable_if::value>::type + preProcess(OracleT oracle); + + // ========================================================= + + // Below are private methods use to serialize nodes into NanoVDB + template + GridHandle initHandle(const BufferT& buffer); + + // ========================================================= + + template + inline typename enable_if::is_index>::type + postProcess(uint32_t channels); + + template + inline typename disable_if::is_index>::type + postProcess(); + + // ======================================================== + + template + typename disable_if::is_special>::type + processLeafs(); + + template + typename enable_if::is_index>::type + processLeafs(); + + template + typename enable_if::is_FpX>::type + processLeafs(); + + template + typename enable_if::value>::type + processLeafs(); + + template + typename enable_if::value>::type + processLeafs(); + + template + typename enable_if::value>::type + processLeafs(); + + // ========================================================= + + template + typename enable_if::is_index>::type + processInternalNodes(); + + template + typename enable_if::is_index>::type + processInternalNodes(); + + // ========================================================= + + template + typename enable_if::is_index>::type + processRoot(); + + template + typename enable_if::is_index>::type + processRoot(); + + // ========================================================= + + template + void processTree(); + + template + void processGrid(); + + template + typename enable_if::is_index, uint64_t>::type + countTileValues(uint64_t valueCount); + + template + typename enable_if::is_index, uint64_t>::type + countValues(); + +#if defined(NANOVDB_USE_OPENVDB) && !defined(__CUDACC__) + template + typename disable_if::value || + is_same::value, uint64_t>::type + countPoints() const; + + template + typename enable_if::value || + is_same::value, uint64_t>::type + countPoints() const; + + template + typename enable_if::value>::type + copyPointAttribute(size_t attIdx, AttT *attPtr); +#else + uint64_t countPoints() const {return 0u;} +#endif + + uint8_t* mBufferPtr;// pointer to the beginning of the destination nanovdb grid buffer + struct BufferOffsets { + uint64_t grid, tree, root, upper, lower, leaf, meta, blind, size; + uint64_t operator[](int i) const { return *(reinterpret_cast(this)+i); } + } mOffset; + int mVerbose; + uint64_t mLeafNodeSize;// non-trivial when DstBuiltT = FpN + + std::unique_ptr mSrcNodeAccPtr;// placeholder for potential local instance + const SrcNodeAccT &mSrcNodeAcc; + struct BlindMetaData; // forward declaration + std::set mBlindMetaData; // sorted according to BlindMetaData.order + struct Codec { float min, max; uint64_t offset; uint8_t log2; };// used for adaptive bit-rate quantization + std::unique_ptr mCodec;// defines a codec per leaf node when DstBuildT = FpN + StatsMode mStats; + ChecksumMode mChecksum; + bool mDitherOn, mIncludeStats, mIncludeTiles; + std::vector mValIdx[3];// store id of first value in node +}; // CreateNanoGrid + +//================================================================================================ + +template +CreateNanoGrid::CreateNanoGrid(const SrcGridT &srcGrid) + : mVerbose(0) + , mSrcNodeAccPtr(new SrcNodeAccT(srcGrid)) + , mSrcNodeAcc(*mSrcNodeAccPtr) + , mStats(StatsMode::Default) + , mChecksum(ChecksumMode::Default) + , mDitherOn(false) + , mIncludeStats(true) + , mIncludeTiles(true) +{ +} + +//================================================================================================ + +template +CreateNanoGrid::CreateNanoGrid(const SrcNodeAccT &srcNodeAcc) + : mVerbose(0) + , mSrcNodeAccPtr(nullptr) + , mSrcNodeAcc(srcNodeAcc) + , mStats(StatsMode::Default) + , mChecksum(ChecksumMode::Default) + , mDitherOn(false) + , mIncludeStats(true) + , mIncludeTiles(true) +{ +} + +//================================================================================================ + +template +struct CreateNanoGrid::BlindMetaData +{ + BlindMetaData(const std::string& name,// name + used to derive GridBlindDataSemantic + const std::string& type,// used to derive GridType of blind data + GridBlindDataClass dataClass, + size_t i, size_t valueCount, size_t valueSize) + : metaData(reinterpret_cast(new char[sizeof(GridBlindMetaData)])) + , order(i)// sorted id of meta data + , size(AlignUp(valueCount * valueSize)) + { + std::memset(metaData, 0, sizeof(GridBlindMetaData));// zero out all meta data + if (name.length()>=GridData::MaxNameSize) throw std::runtime_error("blind data name exceeds limit"); + std::memcpy(metaData->mName, name.c_str(), name.length() + 1); + metaData->mValueCount = valueCount; + metaData->mSemantic = BlindMetaData::mapToSemantics(name); + metaData->mDataClass = dataClass; + metaData->mDataType = BlindMetaData::mapToType(type); + metaData->mValueSize = valueSize; + NANOVDB_ASSERT(metaData->isValid()); + } + BlindMetaData(const std::string& name,// only name + GridBlindDataSemantic dataSemantic, + GridBlindDataClass dataClass, + GridType dataType, + size_t i, size_t valueCount, size_t valueSize) + : metaData(reinterpret_cast(new char[sizeof(GridBlindMetaData)])) + , order(i)// sorted id of meta data + , size(AlignUp(valueCount * valueSize)) + { + std::memset(metaData, 0, sizeof(GridBlindMetaData));// zero out all meta data + if (name.length()>=GridData::MaxNameSize) throw std::runtime_error("blind data name exceeds character limit"); + std::memcpy(metaData->mName, name.c_str(), name.length() + 1); + metaData->mValueCount = valueCount; + metaData->mSemantic = dataSemantic; + metaData->mDataClass = dataClass; + metaData->mDataType = dataType; + metaData->mValueSize = valueSize; + NANOVDB_ASSERT(metaData->isValid()); + } + ~BlindMetaData(){ delete [] reinterpret_cast(metaData); } + bool operator<(const BlindMetaData& other) const { return order < other.order; } // required by std::set + static GridType mapToType(const std::string& name) + { + GridType type = GridType::Unknown; + if ("uint32_t" == name) { + type = GridType::UInt32; + } else if ("float" == name) { + type = GridType::Float; + } else if ("vec3s"== name) { + type = GridType::Vec3f; + } else if ("int32" == name) { + type = GridType::Int32; + } else if ("int64" == name) { + type = GridType::Int64; + } + return type; + } + static GridBlindDataSemantic mapToSemantics(const std::string& name) + { + GridBlindDataSemantic semantic = GridBlindDataSemantic::Unknown; + if ("P" == name) { + semantic = GridBlindDataSemantic::PointPosition; + } else if ("V" == name) { + semantic = GridBlindDataSemantic::PointVelocity; + } else if ("Cd" == name) { + semantic = GridBlindDataSemantic::PointColor; + } else if ("N" == name) { + semantic = GridBlindDataSemantic::PointNormal; + } else if ("id" == name) { + semantic = GridBlindDataSemantic::PointId; + } + return semantic; + } + GridBlindMetaData *metaData; + const size_t order, size; +}; // CreateNanoGrid::BlindMetaData + +//================================================================================================ + +template +template +typename disable_if::value || + BuildTraits::is_index, GridHandle>::type +CreateNanoGrid::getHandle(const BufferT& pool) +{ + this->template preProcess(); + auto handle = this->template initHandle(pool); + this->template postProcess(); + return handle; +} // CreateNanoGrid::getHandle + +//================================================================================================ + +template +template +typename enable_if::value, GridHandle>::type +CreateNanoGrid::getHandle(const OracleT& oracle, const BufferT& pool) +{ + this->template preProcess(oracle); + auto handle = this->template initHandle(pool); + this->template postProcess(); + return handle; +} // CreateNanoGrid::getHandle + +//================================================================================================ + +template +template +typename enable_if::is_index, GridHandle>::type +CreateNanoGrid::getHandle(uint32_t channels, + bool includeStats, + bool includeTiles, + const BufferT &pool) +{ + mIncludeStats = includeStats; + mIncludeTiles = includeTiles; + this->template preProcess(channels); + auto handle = this->template initHandle(pool); + this->template postProcess(channels); + return handle; +}// CreateNanoGrid::getHandle + +//================================================================================================ + +template +template +GridHandle CreateNanoGrid::initHandle(const BufferT& pool) +{ + mOffset.grid = 0;// grid is always stored at the start of the buffer! + mOffset.tree = NanoGrid::memUsage(); // grid ends and tree begins + mOffset.root = mOffset.tree + NanoTree::memUsage(); // tree ends and root node begins + mOffset.upper = mOffset.root + NanoRoot::memUsage(mSrcNodeAcc.root().getTableSize()); // root node ends and upper internal nodes begin + mOffset.lower = mOffset.upper + NanoUpper::memUsage()*mSrcNodeAcc.nodeCount(2); // upper internal nodes ends and lower internal nodes begin + mOffset.leaf = mOffset.lower + NanoLower::memUsage()*mSrcNodeAcc.nodeCount(1); // lower internal nodes ends and leaf nodes begin + mOffset.meta = mOffset.leaf + mLeafNodeSize;// leaf nodes end and blind meta data begins + mOffset.blind = mOffset.meta + sizeof(GridBlindMetaData)*mBlindMetaData.size(); // meta data ends and blind data begins + mOffset.size = mOffset.blind;// end of buffer + for (const auto& b : mBlindMetaData) mOffset.size += b.size; // accumulate all the blind data + + auto buffer = BufferT::create(mOffset.size, &pool); + mBufferPtr = buffer.data(); + + // Concurrent processing of all tree levels! + invoke( [&](){this->template processLeafs();}, + [&](){this->template processInternalNodes();}, + [&](){this->template processInternalNodes();}, + [&](){this->template processRoot();}, + [&](){this->template processTree();}, + [&](){this->template processGrid();} ); + + return GridHandle(std::move(buffer)); +} // CreateNanoGrid::initHandle + +//================================================================================================ + +template +template +inline typename disable_if::value || BuildTraits::is_index>::type +CreateNanoGrid::preProcess() +{ + if (const uint64_t pointCount = this->countPoints()) { +#if defined(NANOVDB_USE_OPENVDB) && !defined(__CUDACC__) + if constexpr(is_same::value) { + if (!mBlindMetaData.empty()) throw std::runtime_error("expected no blind meta data"); + this->addBlindData("index", + GridBlindDataSemantic::PointId, + GridBlindDataClass::IndexArray, + GridType::UInt32, + pointCount, + sizeof(uint32_t)); + } else if constexpr(is_same::value) { + if (!mBlindMetaData.empty()) throw std::runtime_error("expected no blind meta data"); + auto &srcLeaf = mSrcNodeAcc.template node<0>(0); + const auto& attributeSet = srcLeaf.attributeSet(); + const auto& descriptor = attributeSet.descriptor(); + const auto& nameMap = descriptor.map(); + for (auto it = nameMap.begin(); it != nameMap.end(); ++it) { + const size_t index = it->second; + auto& attArray = srcLeaf.constAttributeArray(index); + mBlindMetaData.emplace(it->first, // name used to derive semantics + descriptor.valueType(index), // type + it->first == "id" ? GridBlindDataClass::IndexArray : GridBlindDataClass::AttributeArray, // class + index, // order + pointCount, // element count + attArray.valueTypeSize()); // element size + } + } +#endif// end NANOVDB_USE_OPENVDB + } + if (mSrcNodeAcc.hasLongGridName()) { + this->addBlindData("grid name", + GridBlindDataSemantic::Unknown, + GridBlindDataClass::GridName, + GridType::Unknown, + mSrcNodeAcc.getName().length() + 1, 1); + } + mLeafNodeSize = mSrcNodeAcc.nodeCount(0)*NanoLeaf::DataType::memUsage(); +}// CreateNanoGrid::preProcess + +//================================================================================================ + +template +template +inline typename enable_if::value>::type +CreateNanoGrid::preProcess(OracleT oracle) +{ + static_assert(is_same::value, "preProcess: expected SrcValueT == float"); + + const size_t leafCount = mSrcNodeAcc.nodeCount(0); + if (leafCount==0) { + mLeafNodeSize = 0u; + return; + } + mCodec.reset(new Codec[leafCount]); + + if constexpr(is_same::value) { + if (!oracle) oracle.init(mSrcNodeAcc.gridClass(), mSrcNodeAcc.root().background()); + } + + DitherLUT lut(mDitherOn); + forEach(0, leafCount, 4, [&](const Range1D &r) { + for (auto i=r.begin(); i!=r.end(); ++i) { + const auto &srcLeaf = mSrcNodeAcc.template node<0>(i); + float &min = mCodec[i].min = std::numeric_limits::max(); + float &max = mCodec[i].max = -min; + for (int j=0; j<512; ++j) { + float v = srcLeaf.getValue(j); + if (vmax) max = v; + } + const float range = max - min; + uint8_t &logBitWidth = mCodec[i].log2 = 0;// 0,1,2,3,4 => 1,2,4,8,16 bits + while (range > 0.0f && logBitWidth < 4u) { + const uint32_t mask = (uint32_t(1) << (uint32_t(1) << logBitWidth)) - 1u; + const float encode = mask/range; + const float decode = range/mask; + int j = 0; + do { + const float exact = srcLeaf.getValue(j);//data[j];// exact value + const uint32_t code = uint32_t(encode*(exact - min) + lut(j)); + const float approx = code * decode + min;// approximate value + j += oracle(exact, approx) ? 1 : 513; + } while(j < 512); + if (j == 512) break; + ++logBitWidth; + } + } + }); + + auto getOffset = [&](size_t i){ + --i; + return mCodec[i].offset + NanoLeaf::DataType::memUsage(1u << mCodec[i].log2); + }; + mCodec[0].offset = NanoGrid::memUsage() + + NanoTree::memUsage() + + NanoRoot::memUsage(mSrcNodeAcc.root().getTableSize()) + + NanoUpper::memUsage()*mSrcNodeAcc.nodeCount(2) + + NanoLower::memUsage()*mSrcNodeAcc.nodeCount(1); + for (size_t i=1; iaddBlindData("grid name", + GridBlindDataSemantic::Unknown, + GridBlindDataClass::GridName, + GridType::Unknown, + mSrcNodeAcc.getName().length() + 1, 1); + } +}// CreateNanoGrid::preProcess + +//================================================================================================ + +template +template +inline typename enable_if::is_index, uint64_t>::type +CreateNanoGrid::countTileValues(uint64_t valueCount) +{ + const uint64_t stats = mIncludeStats ? 4u : 0u;// minimum, maximum, average, and deviation + mValIdx[LEVEL].clear(); + mValIdx[LEVEL].resize(mSrcNodeAcc.nodeCount(LEVEL) + 1, stats);// minimum 1 entry + forEach(1, mValIdx[LEVEL].size(), 8, [&](const Range1D& r){ + for (auto i = r.begin(); i!=r.end(); ++i) { + auto &srcNode = mSrcNodeAcc.template node(i-1); + if constexpr(BuildTraits::is_onindex) {// resolved at compile time + mValIdx[LEVEL][i] += srcNode.getValueMask().countOn(); + } else { + static const uint64_t maxTileCount = uint64_t(1u) << 3*srcNode.LOG2DIM; + mValIdx[LEVEL][i] += maxTileCount - srcNode.getChildMask().countOn(); + } + } + }); + mValIdx[LEVEL][0] = valueCount; + for (size_t i=1; i + +//================================================================================================ + +template +template +inline typename enable_if::is_index, uint64_t>::type +CreateNanoGrid::countValues() +{ + const uint64_t stats = mIncludeStats ? 4u : 0u;// minimum, maximum, average, and deviation + uint64_t valueCount = 1u;// offset 0 corresponds to the background value + if (mIncludeTiles) { + if constexpr(BuildTraits::is_onindex) { + for (auto it = mSrcNodeAcc.root().cbeginValueOn(); it; ++it) ++valueCount; + } else { + for (auto it = mSrcNodeAcc.root().cbeginValueAll(); it; ++it) ++valueCount; + } + valueCount += stats;// optionally append stats for the root node + valueCount = countTileValues(valueCount); + valueCount = countTileValues(valueCount); + } + mValIdx[0].clear(); + mValIdx[0].resize(mSrcNodeAcc.nodeCount(0) + 1, 512u + stats);// minimum 1 entry + if constexpr(BuildTraits::is_onindex) { + forEach(1, mValIdx[0].size(), 8, [&](const Range1D& r) { + for (auto i = r.begin(); i != r.end(); ++i) { + mValIdx[0][i] = stats; + mValIdx[0][i] += mSrcNodeAcc.template node<0>(i-1).getValueMask().countOn(); + } + }); + } + mValIdx[0][0] = valueCount; + prefixSum(mValIdx[0], true);// inclusive prefix sum + return mValIdx[0].back(); +}// CreateNanoGrid::countValues() + +//================================================================================================ + +template +template +inline typename enable_if::is_index>::type +CreateNanoGrid::preProcess(uint32_t channels) +{ + const uint64_t valueCount = this->template countValues(); + mLeafNodeSize = mSrcNodeAcc.nodeCount(0)*NanoLeaf::DataType::memUsage(); + + uint32_t order = mBlindMetaData.size(); + for (uint32_t i=0; i()), + GridBlindDataClass::AttributeArray, + order++, + valueCount, + sizeof(SrcValueT)); + } + if (mSrcNodeAcc.hasLongGridName()) { + this->addBlindData("grid name", + GridBlindDataSemantic::Unknown, + GridBlindDataClass::GridName, + GridType::Unknown, + mSrcNodeAcc.getName().length() + 1, 1); + } +}// preProcess + +//================================================================================================ + +template +template +inline typename disable_if::is_special>::type +CreateNanoGrid::processLeafs() +{ + using DstDataT = typename NanoLeaf::DataType; + using DstValueT = typename DstDataT::ValueType; + static_assert(DstDataT::FIXED_SIZE, "Expected destination LeafNode to have fixed size"); + forEach(0, mSrcNodeAcc.nodeCount(0), 8, [&](const Range1D& r) { + auto *dstData = this->template dstNode(r.begin())->data(); + for (auto i = r.begin(); i != r.end(); ++i, ++dstData) { + auto &srcLeaf = mSrcNodeAcc.template node<0>(i); + if (DstDataT::padding()>0u) { + // Cast to void* to avoid compiler warning about missing trivial copy-assignment + std::memset(reinterpret_cast(dstData), 0, DstDataT::memUsage()); + } else { + dstData->mBBoxDif[0] = dstData->mBBoxDif[1] = dstData->mBBoxDif[2] = 0u; + dstData->mFlags = 0u;// enable rendering, no bbox, no stats + dstData->mMinimum = dstData->mMaximum = typename DstDataT::ValueType(); + dstData->mAverage = dstData->mStdDevi = 0; + } + dstData->mBBoxMin = srcLeaf.origin(); // copy origin of node + dstData->mValueMask = srcLeaf.getValueMask(); // copy value mask + DstValueT *dst = dstData->mValues; + if constexpr(is_same::value && SrcNodeAccT::IS_OPENVDB) { + const SrcValueT *src = srcLeaf.buffer().data(); + for (auto *end = dst + 512u; dst != end; dst += 4, src += 4) { + dst[0] = src[0]; // copy *all* voxel values in sets of four, i.e. loop-unrolling + dst[1] = src[1]; + dst[2] = src[2]; + dst[3] = src[3]; + } + } else { + for (uint32_t j=0; j<512u; ++j) *dst++ = static_cast(srcLeaf.getValue(j)); + } + } + }); +} // CreateNanoGrid::processLeafs + +//================================================================================================ + +template +template +inline typename enable_if::is_index>::type +CreateNanoGrid::processLeafs() +{ + using DstDataT = typename NanoLeaf::DataType; + static_assert(DstDataT::FIXED_SIZE, "Expected destination LeafNode to have fixed size"); + static_assert(DstDataT::padding()==0u, "Expected leaf nodes to have no padding"); + + forEach(0, mSrcNodeAcc.nodeCount(0), 8, [&](const Range1D& r) { + const uint8_t flags = mIncludeStats ? 16u : 0u;// 4th bit indicates stats + DstDataT *dstData = this->template dstNode(r.begin())->data();// fixed size + for (auto i = r.begin(); i != r.end(); ++i, ++dstData) { + auto &srcLeaf = mSrcNodeAcc.template node<0>(i); + dstData->mBBoxMin = srcLeaf.origin(); // copy origin of node + dstData->mBBoxDif[0] = dstData->mBBoxDif[1] = dstData->mBBoxDif[2] = 0u; + dstData->mFlags = flags; + dstData->mValueMask = srcLeaf.getValueMask(); // copy value mask + dstData->mOffset = mValIdx[0][i]; + if constexpr(BuildTraits::is_onindex) { + const uint64_t *w = dstData->mValueMask.words(); +#ifdef USE_OLD_VALUE_ON_INDEX + int32_t sum = CountOn(*w++); + uint8_t *p = reinterpret_cast(&dstData->mPrefixSum), *q = p + 7; + for (int j=0; j<7; ++j) { + *p++ = sum & 255u; + *q |= (sum >> 8) << j; + sum += CountOn(*w++); + } +#else + uint64_t &prefixSum = dstData->mPrefixSum, sum = CountOn(*w++); + prefixSum = sum; + for (int n = 9; n < 55; n += 9) {// n=i*9 where i=1,2,..6 + sum += CountOn(*w++); + prefixSum |= sum << n;// each pre-fixed sum is encoded in 9 bits + } +#endif + } else { + dstData->mPrefixSum = 0u; + } + if constexpr(BuildTraits::is_indexmask) dstData->mMask = dstData->mValueMask; + } + }); +} // CreateNanoGrid::processLeafs + +//================================================================================================ + +template +template +inline typename enable_if::value>::type +CreateNanoGrid::processLeafs() +{ + using DstDataT = typename NanoLeaf::DataType; + static_assert(DstDataT::FIXED_SIZE, "Expected destination LeafNode to have fixed size"); + forEach(0, mSrcNodeAcc.nodeCount(0), 8, [&](const Range1D& r) { + auto *dstData = this->template dstNode(r.begin())->data(); + for (auto i = r.begin(); i != r.end(); ++i, ++dstData) { + auto &srcLeaf = mSrcNodeAcc.template node<0>(i); + if (DstDataT::padding()>0u) { + // Cast to void* to avoid compiler warning about missing trivial copy-assignment + std::memset(reinterpret_cast(dstData), 0, DstDataT::memUsage()); + } else { + dstData->mBBoxDif[0] = dstData->mBBoxDif[1] = dstData->mBBoxDif[2] = 0u; + dstData->mFlags = 0u;// enable rendering, no bbox, no stats + dstData->mPadding[0] = dstData->mPadding[1] = 0u; + } + dstData->mBBoxMin = srcLeaf.origin(); // copy origin of node + dstData->mValueMask = srcLeaf.getValueMask(); // copy value mask + } + }); +} // CreateNanoGrid::processLeafs + +//================================================================================================ + +template +template +inline typename enable_if::value>::type +CreateNanoGrid::processLeafs() +{ + using DstDataT = typename NanoLeaf::DataType; + static_assert(DstDataT::FIXED_SIZE, "Expected destination LeafNode to have fixed size"); + forEach(0, mSrcNodeAcc.nodeCount(0), 8, [&](const Range1D& r) { + auto *dstData = this->template dstNode(r.begin())->data(); + for (auto i = r.begin(); i != r.end(); ++i, ++dstData) { + auto &srcLeaf = mSrcNodeAcc.template node<0>(i); + if (DstDataT::padding()>0u) { + // Cast to void* to avoid compiler warning about missing trivial copy-assignment + std::memset(reinterpret_cast(dstData), 0, DstDataT::memUsage()); + } else { + dstData->mBBoxDif[0] = dstData->mBBoxDif[1] = dstData->mBBoxDif[2] = 0u; + dstData->mFlags = 0u;// enable rendering, no bbox, no stats + } + dstData->mBBoxMin = srcLeaf.origin(); // copy origin of node + dstData->mValueMask = srcLeaf.getValueMask(); // copy value mask + if constexpr(!is_same::value) { + for (int j=0; j<512; ++j) dstData->mValues.set(j, static_cast(srcLeaf.getValue(j))); + } else if constexpr(SrcNodeAccT::IS_OPENVDB) { + dstData->mValues = *reinterpret_cast*>(srcLeaf.buffer().data()); + } else if constexpr(SrcNodeAccT::IS_NANOVDB) { + dstData->mValues = srcLeaf.data()->mValues; + } else {// build::Leaf + dstData->mValues = srcLeaf.mValues; // copy value mask + } + } + }); +} // CreateNanoGrid::processLeafs + +//================================================================================================ + +template +template +inline typename enable_if::is_FpX>::type +CreateNanoGrid::processLeafs() +{ + using DstDataT = typename NanoLeaf::DataType; + static_assert(DstDataT::FIXED_SIZE, "Expected destination LeafNode to have fixed size"); + using ArrayT = typename DstDataT::ArrayType; + static_assert(is_same::value, "Expected ValueT == float"); + using FloatT = typename std::conditional=16, double, float>::type;// 16 compression and higher requires double + static constexpr FloatT UNITS = FloatT((1 << DstDataT::bitWidth()) - 1);// # of unique non-zero values + DitherLUT lut(mDitherOn); + + forEach(0, mSrcNodeAcc.nodeCount(0), 8, [&](const Range1D& r) { + auto *dstData = this->template dstNode(r.begin())->data(); + for (auto i = r.begin(); i != r.end(); ++i, ++dstData) { + auto &srcLeaf = mSrcNodeAcc.template node<0>(i); + if (DstDataT::padding()>0u) { + // Cast to void* to avoid compiler warning about missing trivial copy-assignment + std::memset(reinterpret_cast(dstData), 0, DstDataT::memUsage()); + } else { + dstData->mFlags = dstData->mBBoxDif[2] = dstData->mBBoxDif[1] = dstData->mBBoxDif[0] = 0u; + dstData->mDev = dstData->mAvg = dstData->mMax = dstData->mMin = 0u; + } + dstData->mBBoxMin = srcLeaf.origin(); // copy origin of node + dstData->mValueMask = srcLeaf.getValueMask(); // copy value mask + // compute extrema values + float min = std::numeric_limits::max(), max = -min; + for (uint32_t j=0; j<512u; ++j) { + const float v = srcLeaf.getValue(j); + if (v < min) min = v; + if (v > max) max = v; + } + dstData->init(min, max, DstDataT::bitWidth()); + // perform quantization relative to the values in the current leaf node + const FloatT encode = UNITS/(max-min); + uint32_t offset = 0; + auto quantize = [&]()->ArrayT{ + const ArrayT tmp = static_cast(encode * (srcLeaf.getValue(offset) - min) + lut(offset)); + ++offset; + return tmp; + }; + auto *code = reinterpret_cast(dstData->mCode); + if (is_same::value) {// resolved at compile-time + for (uint32_t j=0; j<128u; ++j) { + auto tmp = quantize(); + *code++ = quantize() << 4 | tmp; + tmp = quantize(); + *code++ = quantize() << 4 | tmp; + } + } else { + for (uint32_t j=0; j<128u; ++j) { + *code++ = quantize(); + *code++ = quantize(); + *code++ = quantize(); + *code++ = quantize(); + } + } + } + }); +} // CreateNanoGrid::processLeafs + +//================================================================================================ + +template +template +inline typename enable_if::value>::type +CreateNanoGrid::processLeafs() +{ + static_assert(is_same::value, "Expected SrcValueT == float"); + DitherLUT lut(mDitherOn); + forEach(0, mSrcNodeAcc.nodeCount(0), 8, [&](const Range1D& r) { + for (auto i = r.begin(); i != r.end(); ++i) { + auto &srcLeaf = mSrcNodeAcc.template node<0>(i); + auto *dstData = this->template dstNode(i)->data(); + dstData->mBBoxMin = srcLeaf.origin(); // copy origin of node + dstData->mBBoxDif[0] = dstData->mBBoxDif[1] = dstData->mBBoxDif[2] = 0u; + const uint8_t logBitWidth = mCodec[i].log2; + dstData->mFlags = logBitWidth << 5;// pack logBitWidth into 3 MSB of mFlag + dstData->mValueMask = srcLeaf.getValueMask(); // copy value mask + const float min = mCodec[i].min, max = mCodec[i].max; + dstData->init(min, max, uint8_t(1) << logBitWidth); + // perform quantization relative to the values in the current leaf node + uint32_t offset = 0; + float encode = 0.0f; + auto quantize = [&]()->uint8_t{ + const uint8_t tmp = static_cast(encode * (srcLeaf.getValue(offset) - min) + lut(offset)); + ++offset; + return tmp; + }; + auto *dst = reinterpret_cast(dstData+1); + switch (logBitWidth) { + case 0u: {// 1 bit + encode = 1.0f/(max - min); + for (int j=0; j<64; ++j) { + uint8_t a = 0; + for (int k=0; k<8; ++k) a |= quantize() << k; + *dst++ = a; + } + } + break; + case 1u: {// 2 bits + encode = 3.0f/(max - min); + for (int j=0; j<128; ++j) { + auto a = quantize(); + a |= quantize() << 2; + a |= quantize() << 4; + *dst++ = quantize() << 6 | a; + } + } + break; + case 2u: {// 4 bits + encode = 15.0f/(max - min); + for (int j=0; j<128; ++j) { + auto a = quantize(); + *dst++ = quantize() << 4 | a; + a = quantize(); + *dst++ = quantize() << 4 | a; + } + } + break; + case 3u: {// 8 bits + encode = 255.0f/(max - min); + for (int j=0; j<128; ++j) { + *dst++ = quantize(); + *dst++ = quantize(); + *dst++ = quantize(); + *dst++ = quantize(); + } + } + break; + default: {// 16 bits - special implementation using higher bit-precision + auto *dst = reinterpret_cast(dstData+1); + const double encode = 65535.0/(max - min);// note that double is required! + for (int j=0; j<128; ++j) { + *dst++ = uint16_t(encode * (srcLeaf.getValue(offset) - min) + lut(offset)); ++offset; + *dst++ = uint16_t(encode * (srcLeaf.getValue(offset) - min) + lut(offset)); ++offset; + *dst++ = uint16_t(encode * (srcLeaf.getValue(offset) - min) + lut(offset)); ++offset; + *dst++ = uint16_t(encode * (srcLeaf.getValue(offset) - min) + lut(offset)); ++offset; + } + } + }// end switch + } + });// kernel +} // CreateNanoGrid::processLeafs + +//================================================================================================ + +template +template +inline typename enable_if::is_index>::type +CreateNanoGrid::processInternalNodes() +{ + using DstNodeT = typename NanoNode::type; + using DstValueT = typename DstNodeT::ValueType; + using DstChildT = typename NanoNode::type; + static_assert(LEVEL == 1 || LEVEL == 2, "Expected internal node"); + + const uint64_t nodeCount = mSrcNodeAcc.nodeCount(LEVEL); + if (nodeCount > 0) {// compute and temporarily encode IDs of child nodes + uint64_t childCount = 0; + auto *dstData = this->template dstNode(0)->data(); + for (uint64_t i=0; i(i).getChildMask().countOn(); + } + } + + forEach(0, nodeCount, 4, [&](const Range1D& r) { + auto *dstData = this->template dstNode(r.begin())->data(); + for (auto i = r.begin(); i != r.end(); ++i, ++dstData) { + auto &srcNode = mSrcNodeAcc.template node(i); + uint64_t childID = dstData->mFlags; + if (DstNodeT::DataType::padding()>0u) { + // Cast to void* to avoid compiler warning about missing trivial copy-assignment + std::memset(reinterpret_cast(dstData), 0, DstNodeT::memUsage()); + } else { + dstData->mFlags = 0;// enable rendering, no bbox, no stats + dstData->mMinimum = dstData->mMaximum = typename DstNodeT::ValueType(); + dstData->mAverage = dstData->mStdDevi = 0; + } + dstData->mBBox[0] = srcNode.origin(); // copy origin of node + dstData->mValueMask = srcNode.getValueMask(); // copy value mask + dstData->mChildMask = srcNode.getChildMask(); // copy child mask + for (auto it = srcNode.cbeginChildAll(); it; ++it) { + SrcValueT value{}; // default initialization + if (it.probeChild(value)) { + DstChildT *dstChild = this->template dstNode(childID++);// might be Leaf + dstData->setChild(it.pos(), dstChild); + } else { + dstData->setValue(it.pos(), static_cast(value)); + } + } + } + }); +} // CreateNanoGrid::processInternalNodes + +//================================================================================================ + +template +template +inline typename enable_if::is_index>::type +CreateNanoGrid::processInternalNodes() +{ + using DstNodeT = typename NanoNode::type; + using DstChildT = typename NanoNode::type; + static_assert(LEVEL == 1 || LEVEL == 2, "Expected internal node"); + static_assert(DstNodeT::DataType::padding()==0u, "Expected internal nodes to have no padding"); + + const uint64_t nodeCount = mSrcNodeAcc.nodeCount(LEVEL); + if (nodeCount > 0) {// compute and temporarily encode IDs of child nodes + uint64_t childCount = 0; + auto *dstData = this->template dstNode(0)->data(); + for (uint64_t i=0; i(i).getChildMask().countOn(); + } + } + + forEach(0, nodeCount, 4, [&](const Range1D& r) { + auto *dstData = this->template dstNode(r.begin())->data(); + for (auto i = r.begin(); i != r.end(); ++i, ++dstData) { + auto &srcNode = mSrcNodeAcc.template node(i); + uint64_t childID = dstData->mFlags; + dstData->mFlags = 0u; + dstData->mBBox[0] = srcNode.origin(); // copy origin of node + dstData->mValueMask = srcNode.getValueMask(); // copy value mask + dstData->mChildMask = srcNode.getChildMask(); // copy child mask + uint64_t n = mIncludeTiles ? mValIdx[LEVEL][i] : 0u; + for (auto it = srcNode.cbeginChildAll(); it; ++it) { + SrcValueT value; + if (it.probeChild(value)) { + DstChildT *dstChild = this->template dstNode(childID++);// might be Leaf + dstData->setChild(it.pos(), dstChild); + } else { + uint64_t m = 0u; + if (mIncludeTiles && !((BuildTraits::is_onindex) && dstData->mValueMask.isOff(it.pos()))) m = n++; + dstData->setValue(it.pos(), m); + } + } + if (mIncludeTiles && mIncludeStats) {// stats are always placed after the tile values + dstData->mMinimum = n++; + dstData->mMaximum = n++; + dstData->mAverage = n++; + dstData->mStdDevi = n++; + } else {// if not tiles or stats set stats to the background offset + dstData->mMinimum = 0u; + dstData->mMaximum = 0u; + dstData->mAverage = 0u; + dstData->mStdDevi = 0u; + } + } + }); +} // CreateNanoGrid::processInternalNodes + +//================================================================================================ + +template +template +inline typename enable_if::is_index>::type +CreateNanoGrid::processRoot() +{ + using DstRootT = NanoRoot; + using DstValueT = typename DstRootT::ValueType; + auto &srcRoot = mSrcNodeAcc.root(); + auto *dstData = this->template dstRoot()->data(); + const uint32_t tableSize = srcRoot.getTableSize(); + // Cast to void* to avoid compiler warning about missing trivial copy-assignment + if (DstRootT::DataType::padding()>0) std::memset(reinterpret_cast(dstData), 0, DstRootT::memUsage(tableSize)); + dstData->mTableSize = tableSize; + dstData->mMinimum = dstData->mMaximum = dstData->mBackground = srcRoot.background(); + dstData->mBBox = CoordBBox(); // // set to an empty bounding box + if (tableSize==0) return; + auto *dstChild = this->template dstNode(0);// fixed size and linear in memory + auto *dstTile = dstData->tile(0);// fixed size and linear in memory + for (auto it = srcRoot.cbeginChildAll(); it; ++it, ++dstTile) { + SrcValueT value; + if (it.probeChild(value)) { + dstTile->setChild(it.getCoord(), dstChild++, dstData); + } else { + dstTile->setValue(it.getCoord(), it.isValueOn(), static_cast(value)); + } + } +} // CreateNanoGrid::processRoot + +//================================================================================================ + +template +template +inline typename enable_if::is_index>::type +CreateNanoGrid::processRoot() +{ + using DstRootT = NanoRoot; + auto &srcRoot = mSrcNodeAcc.root(); + auto *dstData = this->template dstRoot()->data(); + const uint32_t tableSize = srcRoot.getTableSize(); + // Cast to void* to avoid compiler warning about missing trivial copy-assignment + if (DstRootT::DataType::padding()>0) std::memset(reinterpret_cast(dstData), 0, DstRootT::memUsage(tableSize)); + dstData->mTableSize = tableSize; + dstData->mBackground = 0u; + uint64_t valueCount = 0u;// the first entry is always the background value + dstData->mBBox = CoordBBox(); // set to an empty/invalid bounding box + + if (tableSize>0) { + auto *dstChild = this->template dstNode(0);// fixed size and linear in memory + auto *dstTile = dstData->tile(0);// fixed size and linear in memory + for (auto it = srcRoot.cbeginChildAll(); it; ++it, ++dstTile) { + SrcValueT tmp; + if (it.probeChild(tmp)) { + dstTile->setChild(it.getCoord(), dstChild++, dstData); + } else { + dstTile->setValue(it.getCoord(), it.isValueOn(), 0u); + if (mIncludeTiles && !((BuildTraits::is_onindex) && !dstTile->state)) dstTile->value = ++valueCount; + } + } + } + if (mIncludeTiles && mIncludeStats) {// stats are always placed after the tile values + dstData->mMinimum = ++valueCount; + dstData->mMaximum = ++valueCount; + dstData->mAverage = ++valueCount; + dstData->mStdDevi = ++valueCount; + } else if (dstData->padding()==0) { + dstData->mMinimum = 0u; + dstData->mMaximum = 0u; + dstData->mAverage = 0u; + dstData->mStdDevi = 0u; + } +} // CreateNanoGrid::processRoot + +//================================================================================================ + +template +template +void CreateNanoGrid::processTree() +{ + const uint64_t nodeCount[3] = {mSrcNodeAcc.nodeCount(0), mSrcNodeAcc.nodeCount(1), mSrcNodeAcc.nodeCount(2)}; + auto *dstTree = this->template dstTree(); + auto *dstData = dstTree->data(); + dstData->setRoot( this->template dstRoot() ); + + dstData->setFirstNode(nodeCount[2] ? this->template dstNode(0) : nullptr); + dstData->setFirstNode(nodeCount[1] ? this->template dstNode(0) : nullptr); + dstData->setFirstNode(nodeCount[0] ? this->template dstNode(0) : nullptr); + + dstData->mNodeCount[0] = static_cast(nodeCount[0]); + dstData->mNodeCount[1] = static_cast(nodeCount[1]); + dstData->mNodeCount[2] = static_cast(nodeCount[2]); + + // Count number of active leaf level tiles + dstData->mTileCount[0] = reduce(Range1D(0,nodeCount[1]), uint32_t(0), [&](Range1D &r, uint32_t sum){ + for (auto i=r.begin(); i!=r.end(); ++i) sum += mSrcNodeAcc.template node<1>(i).getValueMask().countOn(); + return sum;}, std::plus()); + + // Count number of active lower internal node tiles + dstData->mTileCount[1] = reduce(Range1D(0,nodeCount[2]), uint32_t(0), [&](Range1D &r, uint32_t sum){ + for (auto i=r.begin(); i!=r.end(); ++i) sum += mSrcNodeAcc.template node<2>(i).getValueMask().countOn(); + return sum;}, std::plus()); + + // Count number of active upper internal node tiles + dstData->mTileCount[2] = 0; + for (auto it = mSrcNodeAcc.root().cbeginValueOn(); it; ++it) dstData->mTileCount[2] += 1; + + // Count number of active voxels + dstData->mVoxelCount = reduce(Range1D(0, nodeCount[0]), uint64_t(0), [&](Range1D &r, uint64_t sum){ + for (auto i=r.begin(); i!=r.end(); ++i) sum += mSrcNodeAcc.template node<0>(i).getValueMask().countOn(); + return sum;}, std::plus()); + + dstData->mVoxelCount += uint64_t(dstData->mTileCount[0]) << 9;// = 3 * 3 + dstData->mVoxelCount += uint64_t(dstData->mTileCount[1]) << 21;// = 3 * (3+4) + dstData->mVoxelCount += uint64_t(dstData->mTileCount[2]) << 36;// = 3 * (3+4+5) + +} // CreateNanoGrid::processTree + +//================================================================================================ + +template +template +void CreateNanoGrid::processGrid() +{ + auto* dstData = this->template dstGrid()->data(); + dstData->init({GridFlags::IsBreadthFirst}, mOffset.size, mSrcNodeAcc.map(), + mapToGridType(), mapToGridClass(mSrcNodeAcc.gridClass())); + dstData->mBlindMetadataCount = static_cast(mBlindMetaData.size()); + dstData->mData1 = this->valueCount(); + + if (!isValid(dstData->mGridType, dstData->mGridClass)) { +#if 1 + fprintf(stderr,"Warning: Strange combination of GridType(\"%s\") and GridClass(\"%s\"). Consider changing GridClass to \"Unknown\"\n", + toStr(dstData->mGridType), toStr(dstData->mGridClass)); +#else + throw std::runtime_error("Invalid combination of GridType("+std::to_string(int(dstData->mGridType))+ + ") and GridClass("+std::to_string(int(dstData->mGridClass))+"). See NanoVDB.h for details!"); +#endif + } + + std::memset(dstData->mGridName, '\0', GridData::MaxNameSize);//overwrite mGridName + strncpy(dstData->mGridName, mSrcNodeAcc.getName().c_str(), GridData::MaxNameSize-1); + if (mSrcNodeAcc.hasLongGridName()) dstData->setLongGridNameOn();// grid name is long so store it as blind data + + // Partially process blind meta data - they will be complete in postProcess + if (mBlindMetaData.size()>0) { + auto *metaData = this->dstMeta(0); + dstData->mBlindMetadataOffset = PtrDiff(metaData, dstData); + dstData->mBlindMetadataCount = static_cast(mBlindMetaData.size()); + char *blindData = PtrAdd(mBufferPtr, mOffset.blind); + for (const auto &b : mBlindMetaData) { + std::memcpy(metaData, b.metaData, sizeof(GridBlindMetaData)); + metaData->setBlindData(blindData);// sets metaData.mOffset + if (metaData->mDataClass == GridBlindDataClass::GridName) strcpy(blindData, mSrcNodeAcc.getName().c_str()); + ++metaData; + blindData += b.size; + } + mBlindMetaData.clear(); + } +} // CreateNanoGrid::processGrid + +//================================================================================================ + +template +template +inline typename disable_if::is_index>::type +CreateNanoGrid::postProcess() +{ + if constexpr(is_same::value) mCodec.reset(); + auto *dstGrid = this->template dstGrid(); + gridStats(*dstGrid, mStats); +#if defined(NANOVDB_USE_OPENVDB) && !defined(__CUDACC__) + auto *metaData = this->dstMeta(0); + if constexpr(is_same::value || + is_same::value) { + static_assert(is_same::value, "expected DstBuildT==uint32_t"); + auto *dstData0 = this->template dstNode(0)->data(); + dstData0->mMinimum = 0; // start of prefix sum + dstData0->mMaximum = dstData0->mValues[511u]; + for (uint32_t i=1, n=mSrcNodeAcc.nodeCount(0); imMinimum = dstData0->mMinimum + dstData0->mMaximum; + dstData1->mMaximum = dstData1->mValues[511u]; + dstData0 = dstData1; + } + for (size_t i = 0, n = dstGrid->blindDataCount(); i < n; ++i, ++metaData) { + if constexpr(is_same::value) { + if (metaData->mDataClass != GridBlindDataClass::IndexArray) continue; + if (metaData->mDataType == GridType::UInt32) { + uint32_t *blindData = const_cast(metaData->template getBlindData()); + forEach(0, mSrcNodeAcc.nodeCount(0), 16, [&](const auto& r) { + auto *dstData = this->template dstNode(r.begin())->data(); + for (auto j = r.begin(); j != r.end(); ++j, ++dstData) { + uint32_t* p = blindData + dstData->mMinimum; + for (uint32_t idx : mSrcNodeAcc.template node<0>(j).indices()) *p++ = idx; + } + }); + } + } else {// if constexpr(is_same::value) + if (metaData->mDataClass != GridBlindDataClass::AttributeArray) continue; + if (auto *blindData = dstGrid->template getBlindData(i)) { + this->template copyPointAttribute(i, blindData); + } else if (auto *blindData = dstGrid->template getBlindData(i)) { + this->template copyPointAttribute(i, reinterpret_cast(blindData)); + } else if (auto *blindData = dstGrid->template getBlindData(i)) { + this->template copyPointAttribute(i, blindData); + } else if (auto *blindData = dstGrid->template getBlindData(i)) { + this->template copyPointAttribute(i, blindData); + } else { + std::cerr << "unsupported point attribute \"" << toStr(metaData->mDataType) << "\"\n"; + } + }// if + }// loop + } else { // if + (void)metaData; + } +#endif + updateChecksum(*dstGrid, mChecksum); +}// CreateNanoGrid::postProcess + +//================================================================================================ + +template +template +inline typename enable_if::is_index>::type +CreateNanoGrid::postProcess(uint32_t channels) +{ + const std::string typeName = toStr(mapToGridType()); + const uint64_t valueCount = this->valueCount(); + auto *dstGrid = this->template dstGrid(); + for (uint32_t i=0; ifindBlindData(name.c_str()); + if (j<0) throw std::runtime_error("missing " + name); + auto *metaData = this->dstMeta(j);// partially set in processGrid + metaData->mDataClass = GridBlindDataClass::ChannelArray; + metaData->mDataType = mapToGridType(); + SrcValueT *blindData = const_cast(metaData->template getBlindData()); + if (i>0) {// concurrent copy from previous channel + nanovdb::forEach(0,valueCount,1024,[&](const nanovdb::Range1D &r){ + SrcValueT *dst=blindData+r.begin(), *end=dst+r.size(), *src=dst-valueCount; + while(dst!=end) *dst++ = *src++; + }); + } else { + this->template copyValues(blindData); + } + }// loop over channels + gridStats(*(this->template dstGrid()), std::min(StatsMode::BBox, mStats)); + updateChecksum(*dstGrid, mChecksum); +}// CreateNanoGrid::postProcess + +//================================================================================================ + +template +template +typename enable_if::is_index>::type +CreateNanoGrid::copyValues(SrcValueT *buffer) +{// copy values from the source grid into the provided buffer + assert(mBufferPtr && buffer); + using StatsT = typename FloatTraits::FloatType; + + if (this->valueCount()==0) this->template countValues(); + + auto copyNodeValues = [&](const auto &node, SrcValueT *v) { + if constexpr(BuildTraits::is_onindex) { + for (auto it = node.cbeginValueOn(); it; ++it) *v++ = *it; + } else { + for (auto it = node.cbeginValueAll(); it; ++it) *v++ = *it; + } + if (mIncludeStats) { + if constexpr(SrcNodeAccT::IS_NANOVDB) {// resolved at compile time + *v++ = node.minimum(); + *v++ = node.maximum(); + if constexpr(is_same::value) { + *v++ = node.average(); + *v++ = node.stdDeviation(); + } else {// eg when SrcValueT=Vec3f and StatsT=float + *v++ = SrcValueT(node.average()); + *v++ = SrcValueT(node.stdDeviation()); + } + } else {// openvdb and nanovdb::build::Grid have no stats + *v++ = buffer[0];// background + *v++ = buffer[0];// background + *v++ = buffer[0];// background + *v++ = buffer[0];// background + } + } + };// copyNodeValues + + const SrcRootT &root = mSrcNodeAcc.root(); + buffer[0] = root.background();// Value array always starts with the background value + if (mIncludeTiles) { + copyNodeValues(root, buffer + 1u); + forEach(0, mSrcNodeAcc.nodeCount(2), 1, [&](const Range1D& r) { + for (auto i = r.begin(); i!=r.end(); ++i) { + copyNodeValues(mSrcNodeAcc.template node<2>(i), buffer + mValIdx[2][i]); + } + }); + forEach(0, mSrcNodeAcc.nodeCount(1), 1, [&](const Range1D& r) { + for (auto i = r.begin(); i!=r.end(); ++i) { + copyNodeValues(mSrcNodeAcc.template node<1>(i), buffer + mValIdx[1][i]); + } + }); + } + forEach(0, mSrcNodeAcc.nodeCount(0), 4, [&](const Range1D& r) { + for (auto i = r.begin(); i!=r.end(); ++i) { + copyNodeValues(mSrcNodeAcc.template node<0>(i), buffer + mValIdx[0][i]); + } + }); +}// CreateNanoGrid::copyValues + + +//================================================================================================ + +#if defined(NANOVDB_USE_OPENVDB) && !defined(__CUDACC__) + +template +template +typename disable_if::value || + is_same::value, uint64_t>::type +CreateNanoGrid::countPoints() const +{ + static_assert(is_same::value, "expected default template parameter"); + return 0u; +}// CreateNanoGrid::countPoints + +template +template +typename enable_if::value || + is_same::value, uint64_t>::type +CreateNanoGrid::countPoints() const +{ + static_assert(is_same::value, "expected default template parameter"); + return reduce(0, mSrcNodeAcc.nodeCount(0), 8, uint64_t(0), [&](auto &r, uint64_t sum) { + for (auto i=r.begin(); i!=r.end(); ++i) sum += mSrcNodeAcc.template node<0>(i).getLastValue(); + return sum;}, std::plus()); +}// CreateNanoGrid::countPoints + +template +template +typename enable_if::value>::type +CreateNanoGrid::copyPointAttribute(size_t attIdx, AttT *attPtr) +{ + static_assert(std::is_same::value, "Expected default parameter"); + using HandleT = openvdb::points::AttributeHandle; + forEach(0, mSrcNodeAcc.nodeCount(0), 16, [&](const auto& r) { + auto *dstData = this->template dstNode(r.begin())->data(); + for (auto i = r.begin(); i != r.end(); ++i, ++dstData) { + auto& srcLeaf = mSrcNodeAcc.template node<0>(i); + HandleT handle(srcLeaf.constAttributeArray(attIdx)); + AttT *p = attPtr + dstData->mMinimum; + for (auto iter = srcLeaf.beginIndexOn(); iter; ++iter) *p++ = handle.get(*iter); + } + }); +}// CreateNanoGrid::copyPointAttribute + +#endif + +//================================================================================================ + +template +typename disable_if::is_index || BuildTraits::is_Fp, GridHandle>::type +createNanoGrid(const SrcGridT &srcGrid, + StatsMode sMode, + ChecksumMode cMode, + int verbose, + const BufferT &buffer) +{ + CreateNanoGrid converter(srcGrid); + converter.setStats(sMode); + converter.setChecksum(cMode); + converter.setVerbose(verbose); + return converter.template getHandle(buffer); +}// createNanoGrid + +//================================================================================================ + +template +typename enable_if::is_index, GridHandle>::type +createNanoGrid(const SrcGridT &srcGrid, + uint32_t channels, + bool includeStats, + bool includeTiles, + int verbose, + const BufferT &buffer) +{ + CreateNanoGrid converter(srcGrid); + converter.setVerbose(verbose); + return converter.template getHandle(channels, includeStats, includeTiles, buffer); +} + +//================================================================================================ + +template +typename enable_if::value, GridHandle>::type +createNanoGrid(const SrcGridT &srcGrid, + StatsMode sMode, + ChecksumMode cMode, + bool ditherOn, + int verbose, + const OracleT &oracle, + const BufferT &buffer) +{ + CreateNanoGrid converter(srcGrid); + converter.setStats(sMode); + converter.setChecksum(cMode); + converter.enableDithering(ditherOn); + converter.setVerbose(verbose); + return converter.template getHandle(oracle, buffer); +}// createNanoGrid + +//================================================================================================ + +template +typename enable_if::is_FpX, GridHandle>::type +createNanoGrid(const SrcGridT &srcGrid, + StatsMode sMode, + ChecksumMode cMode, + bool ditherOn, + int verbose, + const BufferT &buffer) +{ + CreateNanoGrid converter(srcGrid); + converter.setStats(sMode); + converter.setChecksum(cMode); + converter.enableDithering(ditherOn); + converter.setVerbose(verbose); + return converter.template getHandle(buffer); +}// createNanoGrid + +//================================================================================================ + +#if defined(NANOVDB_USE_OPENVDB) && !defined(__CUDACC__) +template +GridHandle +openToNanoVDB(const openvdb::GridBase::Ptr& base, + StatsMode sMode, + ChecksumMode cMode, + int verbose) +{ + // We need to define these types because they are not defined in OpenVDB + using openvdb_Vec4fTree = typename openvdb::tree::Tree4::Type; + using openvdb_Vec4dTree = typename openvdb::tree::Tree4::Type; + using openvdb_Vec4fGrid = openvdb::Grid; + using openvdb_Vec4dGrid = openvdb::Grid; + using openvdb_UInt32Grid = openvdb::Grid; + + if (auto grid = openvdb::GridBase::grid(base)) { + return createNanoGrid(*grid, sMode, cMode, verbose); + } else if (auto grid = openvdb::GridBase::grid(base)) { + return createNanoGrid(*grid, sMode, cMode, verbose); + } else if (auto grid = openvdb::GridBase::grid(base)) { + return createNanoGrid(*grid, sMode, cMode, verbose); + } else if (auto grid = openvdb::GridBase::grid(base)) { + return createNanoGrid(*grid, sMode, cMode, verbose); + } else if (auto grid = openvdb::GridBase::grid(base)) { + return createNanoGrid(*grid, sMode, cMode, verbose); + } else if (auto grid = openvdb::GridBase::grid(base)) { + return createNanoGrid(*grid, sMode, cMode, verbose); + } else if (auto grid = openvdb::GridBase::grid(base)) { + return createNanoGrid(*grid, sMode, cMode, verbose); + } else if (auto grid = openvdb::GridBase::grid(base)) { + return createNanoGrid(*grid, sMode, cMode, verbose); + } else if (auto grid = openvdb::GridBase::grid(base)) { + return createNanoGrid(*grid, sMode, cMode, verbose); + } else if (auto grid = openvdb::GridBase::grid(base)) { + return createNanoGrid(*grid, sMode, cMode, verbose); + } else if (auto grid = openvdb::GridBase::grid(base)) { + return createNanoGrid(*grid, sMode, cMode, verbose); + } else if (auto grid = openvdb::GridBase::grid(base)) { + return createNanoGrid(*grid, sMode, cMode, verbose); + } else if (auto grid = openvdb::GridBase::grid(base)) { + return createNanoGrid(*grid, sMode, cMode, verbose); + } else { + OPENVDB_THROW(openvdb::RuntimeError, "Unrecognized OpenVDB grid type"); + } +}// openToNanoVDB +#endif + +} // namespace nanovdb + +#endif // NANOVDB_CREATE_NANOGRID_H_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/CudaDeviceBuffer.h b/nanovdb/nanovdb/util/CudaDeviceBuffer.h deleted file mode 100644 index 542a7519cb..0000000000 --- a/nanovdb/nanovdb/util/CudaDeviceBuffer.h +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: MPL-2.0 - -/*! - \file CudaDeviceBuffer.h - - \author Ken Museth - - \date January 8, 2020 - - \brief Implements a simple CUDA allocator! - - CudaDeviceBuffer - a class for simple cuda buffer allocation and management -*/ - -#ifndef NANOVDB_CUDA_DEVICE_BUFFER_H_HAS_BEEN_INCLUDED -#define NANOVDB_CUDA_DEVICE_BUFFER_H_HAS_BEEN_INCLUDED - -#include "HostBuffer.h" // for BufferTraits - -#include // for cudaMalloc/cudaMallocManaged/cudaFree - -#if defined(DEBUG) || defined(_DEBUG) - static inline void gpuAssert(cudaError_t code, const char* file, int line, bool abort = true) - { - if (code != cudaSuccess) { - fprintf(stderr, "CUDA Runtime Error: %s %s %d\n", cudaGetErrorString(code), file, line); - if (abort) exit(code); - } - } - static inline void ptrAssert(void* ptr, const char* msg, const char* file, int line, bool abort = true) - { - if (ptr == nullptr) { - fprintf(stderr, "NULL pointer error: %s %s %d\n", msg, file, line); - if (abort) exit(1); - } - if (uint64_t(ptr) % NANOVDB_DATA_ALIGNMENT) { - fprintf(stderr, "Pointer misalignment error: %s %s %d\n", msg, file, line); - if (abort) exit(1); - } - } -#else - static inline void gpuAssert(cudaError_t, const char*, int, bool = true){} - static inline void ptrAssert(void*, const char*, const char*, int, bool = true){} -#endif - -// Convenience function for checking CUDA runtime API results -// can be wrapped around any runtime API call. No-op in release builds. -#define cudaCheck(ans) \ - { \ - gpuAssert((ans), __FILE__, __LINE__); \ - } - -#define checkPtr(ptr, msg) \ - { \ - ptrAssert((ptr), (msg), __FILE__, __LINE__); \ - } - -namespace nanovdb { - -// ----------------------------> CudaDeviceBuffer <-------------------------------------- - -/// @brief Simple memory buffer using un-managed pinned host memory when compiled with NVCC. -/// Obviously this class is making explicit used of CUDA so replace it with your own memory -/// allocator if you are not using CUDA. -/// @note While CUDA's pinned host memory allows for asynchronous memory copy between host and device -/// it is significantly slower then cached (un-pinned) memory on the host. -class CudaDeviceBuffer -{ - uint64_t mSize; // total number of bytes for the NanoVDB grid. - uint8_t *mCpuData, *mGpuData; // raw buffer for the NanoVDB grid. - -public: - CudaDeviceBuffer(uint64_t size = 0) - : mSize(0) - , mCpuData(nullptr) - , mGpuData(nullptr) - { - this->init(size); - } - /// @brief Disallow copy-construction - CudaDeviceBuffer(const CudaDeviceBuffer&) = delete; - /// @brief Move copy-constructor - CudaDeviceBuffer(CudaDeviceBuffer&& other) noexcept - : mSize(other.mSize) - , mCpuData(other.mCpuData) - , mGpuData(other.mGpuData) - { - other.mSize = 0; - other.mCpuData = nullptr; - other.mGpuData = nullptr; - } - /// @brief Disallow copy assignment operation - CudaDeviceBuffer& operator=(const CudaDeviceBuffer&) = delete; - /// @brief Move copy assignment operation - CudaDeviceBuffer& operator=(CudaDeviceBuffer&& other) noexcept - { - clear(); - mSize = other.mSize; - mCpuData = other.mCpuData; - mGpuData = other.mGpuData; - other.mSize = 0; - other.mCpuData = nullptr; - other.mGpuData = nullptr; - return *this; - } - /// @brief Destructor frees memory on both the host and device - ~CudaDeviceBuffer() { this->clear(); }; - - void init(uint64_t size); - - // @brief Retuns a pointer to the raw memory buffer managed by this allocator. - /// - /// @warning Note that the pointer can be NULL is the allocator was not initialized! - uint8_t* data() const { return mCpuData; } - uint8_t* deviceData() const { return mGpuData; } - - /// @brief Copy grid from the CPU/host to the GPU/device. If @c sync is false the memory copy is asynchronous! - /// - /// @note This will allocate memory on the GPU/device if it is not already allocated - void deviceUpload(void* stream = 0, bool sync = true) const; - - /// @brief Copy grid from the GPU/device to the CPU/host. If @c sync is false the memory copy is asynchronous! - void deviceDownload(void* stream = 0, bool sync = true) const; - - /// @brief Returns the size in bytes of the raw memory buffer managed by this allocator. - uint64_t size() const { return mSize; } - - /// @brief Returns true if this allocator is empty, i.e. has no allocated memory - bool empty() const { return mSize == 0; } - - /// @brief De-allocate all memory managed by this allocator and set all pointer to NULL - void clear(); - - static CudaDeviceBuffer create(uint64_t size, const CudaDeviceBuffer* context = nullptr); - -}; // CudaDeviceBuffer class - -template<> -struct BufferTraits -{ - static const bool hasDeviceDual = true; -}; - -// --------------------------> Implementations below <------------------------------------ - -inline CudaDeviceBuffer CudaDeviceBuffer::create(uint64_t size, const CudaDeviceBuffer*) -{ - return CudaDeviceBuffer(size); -} - -inline void CudaDeviceBuffer::init(uint64_t size) -{ - if (size == mSize) - return; - if (mSize > 0) - this->clear(); - if (size == 0) - return; - mSize = size; - cudaCheck(cudaMallocHost((void**)&mCpuData, size)); // un-managed pinned memory on the host (can be slow to access!). Always 32B aligned - checkPtr(mCpuData, "failed to allocate host data"); -} // CudaDeviceBuffer::init - -inline void CudaDeviceBuffer::deviceUpload(void* stream, bool sync) const -{ - checkPtr(mCpuData, "uninitialized cpu data"); - if (mGpuData == nullptr) - cudaCheck(cudaMalloc((void**)&mGpuData, mSize)); // un-managed memory on the device, always 32B aligned! - checkPtr(mGpuData, "uninitialized gpu data"); - cudaCheck(cudaMemcpyAsync(mGpuData, mCpuData, mSize, cudaMemcpyHostToDevice, reinterpret_cast(stream))); - if (sync) - cudaCheck(cudaStreamSynchronize(reinterpret_cast(stream))); -} // CudaDeviceBuffer::gpuUpload - -inline void CudaDeviceBuffer::deviceDownload(void* stream, bool sync) const -{ - checkPtr(mCpuData, "uninitialized cpu data"); - checkPtr(mGpuData, "uninitialized gpu data"); - cudaCheck(cudaMemcpyAsync(mCpuData, mGpuData, mSize, cudaMemcpyDeviceToHost, reinterpret_cast(stream))); - if (sync) - cudaCheck(cudaStreamSynchronize(reinterpret_cast(stream))); -} // CudaDeviceBuffer::gpuDownload - -inline void CudaDeviceBuffer::clear() -{ - if (mGpuData) - cudaCheck(cudaFree(mGpuData)); - if (mCpuData) - cudaCheck(cudaFreeHost(mCpuData)); - mCpuData = mGpuData = nullptr; - mSize = 0; -} // CudaDeviceBuffer::clear - -} // namespace nanovdb - -#endif // end of NANOVDB_CUDA_DEVICE_BUFFER_H_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/DitherLUT.h b/nanovdb/nanovdb/util/DitherLUT.h index 885480c7fd..69c3b33031 100644 --- a/nanovdb/nanovdb/util/DitherLUT.h +++ b/nanovdb/nanovdb/util/DitherLUT.h @@ -12,7 +12,7 @@ #ifndef NANOVDB_DITHERLUT_HAS_BEEN_INCLUDED #define NANOVDB_DITHERLUT_HAS_BEEN_INCLUDED -#include "../NanoVDB.h"// for __hosedev__, Vec3, Min, Max, Pow2, Pow3, Pow4 +#include // for __hostdev__, Vec3, Min, Max, Pow2, Pow3, Pow4 namespace nanovdb { diff --git a/nanovdb/nanovdb/util/GridBuilder.h b/nanovdb/nanovdb/util/GridBuilder.h index 28514a5669..30fba27f94 100644 --- a/nanovdb/nanovdb/util/GridBuilder.h +++ b/nanovdb/nanovdb/util/GridBuilder.h @@ -8,1154 +8,946 @@ \date June 26, 2020 - \brief Generates a NanoVDB grid from any volume or function. - - \note This is only intended as a simple tool to generate nanovdb grids without - any dependency on openvdb. + \brief This file defines a minimum set of tree nodes and tools that + can be used (instead of OpenVDB) to build nanovdb grids on the CPU. */ -#ifndef NANOVDB_GRIDBUILDER_H_HAS_BEEN_INCLUDED -#define NANOVDB_GRIDBUILDER_H_HAS_BEEN_INCLUDED +#ifndef NANOVDB_GRID_BUILDER_H_HAS_BEEN_INCLUDED +#define NANOVDB_GRID_BUILDER_H_HAS_BEEN_INCLUDED -#include "GridHandle.h" -#include "GridStats.h" -#include "GridChecksum.h" -#include "Range.h" -#include "Invoke.h" -#include "ForEach.h" -#include "Reduce.h" -#include "DitherLUT.h"// for nanovdb::DitherLUT +#include #include #include #include // for stringstream #include #include // for memcpy +#include +#include +#include + +#include +#include "Range.h" +#include "ForEach.h" namespace nanovdb { -/// @brief Compression oracle based on absolute difference -class AbsDiff -{ - float mTolerance;// absolute error tolerance -public: - /// @note The default value of -1 means it's un-initialized! - AbsDiff(float tolerance = -1.0f) : mTolerance(tolerance) {} - AbsDiff(const AbsDiff&) = default; - ~AbsDiff() = default; - void setTolerance(float tolerance) { mTolerance = tolerance; } - float getTolerance() const { return mTolerance; } - /// @brief Return true if the approximate value is within the accepted - /// absolute error bounds of the exact value. - /// - /// @details Required member method - bool operator()(float exact, float approx) const - { - return Abs(exact - approx) <= mTolerance; - } -};// AbsDiff +namespace build { -inline std::ostream& operator<<(std::ostream& os, const AbsDiff& diff) -{ - os << "Absolute tolerance: " << diff.getTolerance(); - return os; -} +// ----------------------------> Froward decelerations of random access methods <-------------------------------------- -/// @brief Compression oracle based on relative difference -class RelDiff -{ - float mTolerance;// relative error tolerance -public: - /// @note The default value of -1 means it's un-initialized! - RelDiff(float tolerance = -1.0f) : mTolerance(tolerance) {} - RelDiff(const RelDiff&) = default; - ~RelDiff() = default; - void setTolerance(float tolerance) { mTolerance = tolerance; } - float getTolerance() const { return mTolerance; } - /// @brief Return true if the approximate value is within the accepted - /// relative error bounds of the exact value. - /// - /// @details Required member method - bool operator()(float exact, float approx) const - { - return Abs(exact - approx)/Max(Abs(exact), Abs(approx)) <= mTolerance; - } -};// RelDiff +template struct GetValue; +template struct SetValue; +template struct TouchLeaf; +template struct GetState; +template struct ProbeValue; -inline std::ostream& operator<<(std::ostream& os, const RelDiff& diff) -{ - os << "Relative tolerance: " << diff.getTolerance(); - return os; -} +// ----------------------------> RootNode <-------------------------------------- -/// @brief Allows for the construction of NanoVDB grids without any dependency -template> -class GridBuilder +template +struct RootNode { - struct BuildLeaf; - template - struct BuildNode; - template - struct BuildRoot; - - struct Codec {float min, max; uint16_t log2, size;};// used for adaptive bit-rate quantization - - using SrcNode0 = BuildLeaf; - using SrcNode1 = BuildNode; - using SrcNode2 = BuildNode; - using SrcRootT = BuildRoot; - - using DstNode0 = NanoLeaf< BuildT>;// nanovdb::LeafNode; // leaf - using DstNode1 = NanoLower;// nanovdb::InternalNode; // lower - using DstNode2 = NanoUpper;// nanovdb::InternalNode; // upper - using DstRootT = NanoRoot< BuildT>;// nanovdb::RootNode; - using DstTreeT = NanoTree< BuildT>; - using DstGridT = NanoGrid< BuildT>; - - ValueT mDelta; // skip node if: node.max < -mDelta || node.min > mDelta - uint8_t* mBufferPtr;// pointer to the beginning of the buffer - uint64_t mBufferOffsets[9];//grid, tree, root, upper, lower, leafs, meta data, blind data, buffer size - int mVerbose; - uint64_t mBlindDataSize; - SrcRootT mRoot;// this root supports random write - std::vector mArray0; // leaf nodes - std::vector mArray1; // lower internal nodes - std::vector mArray2; // upper internal nodes - std::unique_ptr mCodec;// defines a codec per leaf node - GridClass mGridClass; - StatsMode mStats; - ChecksumMode mChecksum; - bool mDitherOn; - - // Below are private methods use to serialize nodes into NanoVDB - template< typename OracleT, typename BufferT> - GridHandle initHandle(const OracleT &oracle, const BufferT& buffer); - - template - inline typename std::enable_if::value>::type - compression(uint64_t&, OracleT) {}// no-op - - template - inline typename std::enable_if::value>::type - compression(uint64_t &offset, OracleT oracle); - - template - typename std::enable_if::value && - !is_same::value && - !is_same::value && - !is_same::value>::type - processLeafs(std::vector&); - - template - typename std::enable_if::value || - is_same::value || - is_same::value>::type - processLeafs(std::vector&); - - template - typename std::enable_if::value>::type - processLeafs(std::vector&); - - template - void processNodes(std::vector&); - - DstRootT* processRoot(); - - DstTreeT* processTree(); - - DstGridT* processGrid(const Map&, const std::string&); - - template - typename std::enable_if::value>::type - setFlag(const T&, const T&, FlagT& flag) const { flag &= ~FlagT(1); } // unset first bit - - template - typename std::enable_if::value>::type - setFlag(const T& min, const T& max, FlagT& flag) const; - -public: - struct ValueAccessor; - - GridBuilder(ValueT background = ValueT(), - GridClass gClass = GridClass::Unknown, - uint64_t blindDataSize = 0); + using ValueType = typename ChildT::ValueType; + using BuildType = typename ChildT::BuildType; + using ChildNodeType = ChildT; + using LeafNodeType = typename ChildT::LeafNodeType; + static constexpr uint32_t LEVEL = 1 + ChildT::LEVEL; // level 0 = leaf + struct Tile { + Tile(ChildT* c = nullptr) : child(c) {} + Tile(const ValueType& v, bool s) : child(nullptr), value(v), state(s) {} + bool isChild() const { return child!=nullptr; } + bool isValue() const { return child==nullptr; } + bool isActive() const { return child==nullptr && state; } + ChildT* child; + ValueType value; + bool state; + }; + using MapT = std::map; + MapT mTable; + ValueType mBackground; - ValueAccessor getAccessor() { return ValueAccessor(mRoot); } + Tile* probeTile(const Coord &ijk) { + auto iter = mTable.find(CoordToKey(ijk)); + return iter == mTable.end() ? nullptr : &(iter->second); + } - /// @brief Performs multi-threaded bottom-up signed-distance flood-filling and changes GridClass to LevelSet - /// - /// @warning Only call this method once this GridBuilder contains a valid signed distance field - void sdfToLevelSet(); + const Tile* probeTile(const Coord &ijk) const { + auto iter = mTable.find(CoordToKey(ijk)); + return iter == mTable.end() ? nullptr : &(iter->second); + } - /// @brief Performs multi-threaded bottom-up signed-distance flood-filling followed by level-set -> FOG volume - /// conversion. It also changes the GridClass to FogVolume - /// - /// @warning Only call this method once this GridBuilder contains a valid signed distance field - void sdfToFog(); + class ChildIterator + { + const RootNode *mParent; + typename MapT::const_iterator mIter; + public: + ChildIterator() : mParent(nullptr), mIter() {} + ChildIterator(const RootNode *parent) : mParent(parent), mIter(parent->mTable.begin()) { + while (mIter!=parent->mTable.end() && mIter->second.child==nullptr) ++mIter; + } + ChildIterator& operator=(const ChildIterator&) = default; + ChildT& operator*() const {NANOVDB_ASSERT(*this); return *mIter->second.child;} + ChildT* operator->() const {NANOVDB_ASSERT(*this); return mIter->second.child;} + Coord getOrigin() const { NANOVDB_ASSERT(*this); return mIter->first;} + Coord getCoord() const { NANOVDB_ASSERT(*this); return mIter->first;} + operator bool() const {return mParent && mIter!=mParent->mTable.end();} + ChildIterator& operator++() { + NANOVDB_ASSERT(mParent); + ++mIter; + while (mIter!=mParent->mTable.end() && mIter->second.child==nullptr) ++mIter; + return *this; + } + ChildIterator operator++(int) { + auto tmp = *this; + ++(*this); + return tmp; + } + uint32_t pos() const { + NANOVDB_ASSERT(mParent); + return uint32_t(std::distance(mParent->mTable.begin(), mIter)); + } + }; // Member class ChildIterator - void setVerbose(int mode = 1) { mVerbose = mode; } + ChildIterator cbeginChild() const {return ChildIterator(this);} + ChildIterator cbeginChildOn() const {return ChildIterator(this);}// match openvdb - void enableDithering(bool on = true) { mDitherOn = on; } + class ValueIterator + { + const RootNode *mParent; + typename MapT::const_iterator mIter; + public: + ValueIterator() : mParent(nullptr), mIter() {} + ValueIterator(const RootNode *parent) : mParent(parent), mIter(parent->mTable.begin()) { + while (mIter!=parent->mTable.end() && mIter->second.child!=nullptr) ++mIter; + } + ValueIterator& operator=(const ValueIterator&) = default; + ValueType operator*() const {NANOVDB_ASSERT(*this); return mIter->second.value;} + bool isActive() const {NANOVDB_ASSERT(*this); return mIter->second.state;} + Coord getOrigin() const { NANOVDB_ASSERT(*this); return mIter->first;} + Coord getCoord() const { NANOVDB_ASSERT(*this); return mIter->first;} + operator bool() const {return mParent && mIter!=mParent->mTable.end();} + ValueIterator& operator++() { + NANOVDB_ASSERT(mParent); + ++mIter; + while (mIter!=mParent->mTable.end() && mIter->second.child!=nullptr) ++mIter; + return *this;; + } + ValueIterator operator++(int) { + auto tmp = *this; + ++(*this); + return tmp; + } + uint32_t pos() const { + NANOVDB_ASSERT(mParent); + return uint32_t(std::distance(mParent->mTable.begin(), mIter)); + } + }; // Member class ValueIterator - void setStats(StatsMode mode = StatsMode::Default) { mStats = mode; } + ValueIterator beginValue() {return ValueIterator(this);} + ValueIterator cbeginValueAll() const {return ValueIterator(this);} - void setChecksum(ChecksumMode mode = ChecksumMode::Default) { mChecksum = mode; } + class ValueOnIterator + { + const RootNode *mParent; + typename MapT::const_iterator mIter; + public: + ValueOnIterator() : mParent(nullptr), mIter() {} + ValueOnIterator(const RootNode *parent) : mParent(parent), mIter(parent->mTable.begin()) { + while (mIter!=parent->mTable.end() && (mIter->second.child!=nullptr || !mIter->second.state)) ++mIter; + } + ValueOnIterator& operator=(const ValueOnIterator&) = default; + ValueType operator*() const {NANOVDB_ASSERT(*this); return mIter->second.value;} + Coord getOrigin() const { NANOVDB_ASSERT(*this); return mIter->first;} + Coord getCoord() const { NANOVDB_ASSERT(*this); return mIter->first;} + operator bool() const {return mParent && mIter!=mParent->mTable.end();} + ValueOnIterator& operator++() { + NANOVDB_ASSERT(mParent); + ++mIter; + while (mIter!=mParent->mTable.end() && (mIter->second.child!=nullptr || !mIter->second.state)) ++mIter; + return *this;; + } + ValueOnIterator operator++(int) { + auto tmp = *this; + ++(*this); + return tmp; + } + uint32_t pos() const { + NANOVDB_ASSERT(mParent); + return uint32_t(std::distance(mParent->mTable.begin(), mIter)); + } + }; // Member class ValueOnIterator - void setGridClass(GridClass mode = GridClass::Unknown) { mGridClass = mode; } + ValueOnIterator beginValueOn() {return ValueOnIterator(this);} + ValueOnIterator cbeginValueOn() const {return ValueOnIterator(this);} - /// @brief Return an instance of a GridHandle (invoking move semantics) - template - GridHandle getHandle(double voxelSize = 1.0, - const Vec3d& gridOrigin = Vec3d(0), - const std::string& name = "", - const OracleT& oracle = OracleT(), - const BufferT& buffer = BufferT()); + class TileIterator + { + const RootNode *mParent; + typename MapT::const_iterator mIter; + public: + TileIterator() : mParent(nullptr), mIter() {} + TileIterator(const RootNode *parent) : mParent(parent), mIter(parent->mTable.begin()) { + NANOVDB_ASSERT(mParent); + } + TileIterator& operator=(const TileIterator&) = default; + const Tile& operator*() const {NANOVDB_ASSERT(*this); return mIter->second;} + const Tile* operator->() const {NANOVDB_ASSERT(*this); return &(mIter->second);} + Coord getOrigin() const { NANOVDB_ASSERT(*this); return mIter->first;} + Coord getCoord() const { NANOVDB_ASSERT(*this); return mIter->first;} + operator bool() const {return mParent && mIter!=mParent->mTable.end();} + const ChildT* probeChild(ValueType &value) { + NANOVDB_ASSERT(*this); + const ChildT *child = mIter->second.child; + if (child==nullptr) value = mIter->second.value; + return child; + } + bool isValueOn() const {return mIter->second.child==nullptr && mIter->second.state;} + TileIterator& operator++() { + NANOVDB_ASSERT(mParent); + ++mIter; + return *this; + } + TileIterator operator++(int) { + auto tmp = *this; + ++(*this); + return tmp; + } + uint32_t pos() const { + NANOVDB_ASSERT(mParent); + return uint32_t(std::distance(mParent->mTable.begin(), mIter)); + } + }; // Member class TileIterator - /// @brief Return an instance of a GridHandle (invoking move semantics) - template - GridHandle getHandle(const Map& map, - const std::string& name = "", - const OracleT& oracle = OracleT(), - const BufferT& buffer = BufferT()); + TileIterator beginTile() {return TileIterator(this);} + TileIterator cbeginChildAll() const {return TileIterator(this);} - /// @brief Sets grids values in domain of the @a bbox to those returned by the specified @a func with the - /// expected signature [](const Coord&)->ValueT. - /// - /// @note If @a func returns a value equal to the background value (specified in the constructor) at a - /// specific voxel coordinate, then the active state of that coordinate is left off! Else the value - /// value is set and the active state is on. This is done to allow for sparse grids to be generated. - /// - /// @param func Functor used to evaluate the grid values in the @a bbox - /// @param bbox Coordinate bounding-box over which the grid values will be set. - /// @param delta Specifies a lower threshold value for rendering (optional). Typically equals the voxel size - /// for level sets and otherwise it's zero. - template - void operator()(const Func& func, const CoordBBox& bbox, ValueT delta = ValueT(0)); + //class DenseIterator : public TileIterator -}; // GridBuilder + RootNode(const ValueType& background) : mBackground(background) {} + RootNode(const RootNode&) = delete; // disallow copy-construction + RootNode(RootNode&&) = default; // allow move construction + RootNode& operator=(const RootNode&) = delete; // disallow copy assignment + RootNode& operator=(RootNode&&) = default; // allow move assignment -//================================================================================================ + ~RootNode() { this->clear(); } -template -GridBuilder:: -GridBuilder(ValueT background, GridClass gClass, uint64_t blindDataSize) - : mDelta(0) - , mVerbose(0) - , mBlindDataSize(blindDataSize) - , mRoot(background) - , mGridClass(gClass) - , mStats(StatsMode::Default) - , mChecksum(ChecksumMode::Default) - , mDitherOn(false) -{ -} + uint32_t tileCount() const { return uint32_t(mTable.size()); } + uint32_t getTableSize() const { return uint32_t(mTable.size()); }// match openvdb + const ValueType& background() const {return mBackground;} -template -template -void GridBuilder:: -operator()(const Func& func, const CoordBBox& voxelBBox, ValueT delta) -{ - static_assert(is_same::type>::value, "GridBuilder: mismatched ValueType"); - mDelta = delta; // delta = voxel size for level sets, else 0 - - using LeafT = BuildLeaf; - const CoordBBox leafBBox(voxelBBox[0] >> LeafT::TOTAL, voxelBBox[1] >> LeafT::TOTAL); - std::mutex mutex; - auto kernel = [&](const CoordBBox& b) { - LeafT* leaf = nullptr; - for (auto it = b.begin(); it; ++it) { - Coord min(*it << LeafT::TOTAL), max(min + Coord(LeafT::DIM - 1)); - const CoordBBox bbox(min.maxComponent(voxelBBox.min()), - max.minComponent(voxelBBox.max()));// crop - if (leaf == nullptr) { - leaf = new LeafT(bbox[0], mRoot.mBackground, false); - } else { - leaf->mOrigin = bbox[0] & ~LeafT::MASK; - NANOVDB_ASSERT(leaf->mValueMask.isOff()); - } - leaf->mDstOffset = 0;// no prune - for (auto ijk = bbox.begin(); ijk; ++ijk) { - const auto v = func(*ijk); - if (v == mRoot.mBackground) {// don't insert background values - continue; - } - leaf->setValue(*ijk, v); - } - if (!leaf->mValueMask.isOff()) {// has active values - if (leaf->mValueMask.isOn()) {// only active values - const auto first = leaf->getFirstValue(); - int n=1; - while (n<512) {// 8^3 = 512 - if (leaf->mValues[n++] != first) break; - } - if (n == 512) leaf->mDstOffset = 1;// prune below - } - std::lock_guard guard(mutex); - NANOVDB_ASSERT(leaf != nullptr); - mRoot.addNode(leaf); - NANOVDB_ASSERT(leaf == nullptr); - } - }// loop over sub-part of leafBBox - if (leaf) { - delete leaf; + void nodeCount(std::array &count) const + { + for (auto it = this->cbeginChild(); it; ++it) { + count[ChildT::LEVEL] += 1; + it->nodeCount(count); } - }; // kernel - forEach(leafBBox, kernel); - - // Prune leaf and tile nodes - for (auto it2 = mRoot.mTable.begin(); it2 != mRoot.mTable.end(); ++it2) { - if (auto *upper = it2->second.child) {//upper level internal node - for (auto it1 = upper->mChildMask.beginOn(); it1; ++it1) { - auto *lower = upper->mTable[*it1].child;// lower level internal node - for (auto it0 = lower->mChildMask.beginOn(); it0; ++it0) { - auto *leaf = lower->mTable[*it0].child;// leaf nodes - if (leaf->mDstOffset) { - lower->mTable[*it0].value = leaf->getFirstValue(); - lower->mChildMask.setOff(*it0); - lower->mValueMask.setOn(*it0); - delete leaf; - } - }// loop over leaf nodes - if (lower->mChildMask.isOff()) {//only tiles - const auto first = lower->getFirstValue(); - int n=1; - while (n < 4096) {// 16^3 = 4096 - if (lower->mTable[n++].value != first) break; - } - if (n == 4096) {// identical tile values so prune - upper->mTable[*it1].value = first; - upper->mChildMask.setOff(*it1); - upper->mValueMask.setOn(*it1); - delete lower; - } - } - }// loop over lower internal nodes - if (upper->mChildMask.isOff()) {//only tiles - const auto first = upper->getFirstValue(); - int n=1; - while (n < 32768) {// 32^3 = 32768 - if (upper->mTable[n++].value != first) break; - } - if (n == 32768) {// identical tile values so prune - it2->second.value = first; - it2->second.state = upper->mValueMask.isOn(); - it2->second.child = nullptr; - delete upper; - } - } - }// is child node of the root - }// loop over root table -} - -//================================================================================================ - -template -template -GridHandle GridBuilder:: -initHandle(const OracleT &oracle, const BufferT& buffer) -{ - mArray0.clear(); - mArray1.clear(); - mArray2.clear(); - mArray0.reserve(mRoot.template nodeCount()); - mArray1.reserve(mRoot.template nodeCount()); - mArray2.reserve(mRoot.template nodeCount()); - - uint64_t offset[3] = {0}; - for (auto it2 = mRoot.mTable.begin(); it2 != mRoot.mTable.end(); ++it2) { - if (SrcNode2 *upper = it2->second.child) { - upper->mDstOffset = offset[2]; - mArray2.emplace_back(upper); - offset[2] += DstNode2::memUsage(); - for (auto it1 = upper->mChildMask.beginOn(); it1; ++it1) { - SrcNode1 *lower = upper->mTable[*it1].child; - lower->mDstOffset = offset[1]; - mArray1.emplace_back(lower); - offset[1] += DstNode1::memUsage(); - for (auto it0 = lower->mChildMask.beginOn(); it0; ++it0) { - SrcNode0 *leaf = lower->mTable[*it0].child; - leaf->mDstOffset = offset[0];// dummy if BuildT = FpN - mArray0.emplace_back(leaf); - offset[0] += sizeof(DstNode0);// dummy if BuildT = FpN - }// loop over leaf nodes - }// loop over lower internal nodes - }// is child node of the root - }// loop over root table - - this->template compression(offset[0], oracle);// no-op unless BuildT = FpN + } - mBufferOffsets[0] = 0;// grid is always stored at the start of the buffer! - mBufferOffsets[1] = DstGridT::memUsage(); // tree - mBufferOffsets[2] = DstTreeT::memUsage(); // root - mBufferOffsets[3] = DstRootT::memUsage(static_cast(mRoot.mTable.size())); // upper internal nodes - mBufferOffsets[4] = offset[2]; // lower internal nodes - mBufferOffsets[5] = offset[1]; // leaf nodes - mBufferOffsets[6] = offset[0]; // blind meta data - mBufferOffsets[7] = GridBlindMetaData::memUsage(mBlindDataSize > 0 ? 1 : 0); // blind data - mBufferOffsets[8] = mBlindDataSize;// end of buffer + bool empty() const { return mTable.empty(); } - // Compute the prefixed sum - for (int i = 2; i < 9; ++i) { - mBufferOffsets[i] += mBufferOffsets[i - 1]; + void clear() + { + for (auto iter = mTable.begin(); iter != mTable.end(); ++iter) delete iter->second.child; + mTable.clear(); } - GridHandle handle(BufferT::create(mBufferOffsets[8], &buffer)); - mBufferPtr = handle.data(); - return handle; -} // GridBuilder::initHandle - -//================================================================================================ + static Coord CoordToKey(const Coord& ijk) { return ijk & ~ChildT::MASK; } -template -template -inline typename std::enable_if::value>::type -GridBuilder::compression(uint64_t &offset, OracleT oracle) -{ - static_assert(is_same::value, "compression: expected BuildT == float"); - static_assert(is_same::value, "compression: expected ValueT == float"); - if (is_same::value && oracle.getTolerance() < 0.0f) {// default tolerance for level set and fog volumes - if (mGridClass == GridClass::LevelSet) { - static const float halfWidth = 3.0f; - oracle.setTolerance(0.1f * mRoot.mBackground / halfWidth);// range of ls: [-3dx; 3dx] - } else if (mGridClass == GridClass::FogVolume) { - oracle.setTolerance(0.01f);// range of FOG volumes: [0;1] +#ifdef NANOVDB_NEW_ACCESSOR_METHODS + template + auto get(const Coord& ijk, ArgsT&&... args) const + { + if (const Tile *tile = this->probeTile(ijk)) { + if (auto *child = tile->child) return child->template get(ijk, args...); + return OpT::get(*tile, args...); + } + return OpT::get(*this, args...); + } + template + auto set(const Coord& ijk, ArgsT&&... args) + { + ChildT* child = nullptr; + const Coord key = CoordToKey(ijk); + auto iter = mTable.find(key); + if (iter == mTable.end()) { + child = new ChildT(ijk, mBackground, false); + mTable[key] = Tile(child); + } else if (iter->second.child != nullptr) { + child = iter->second.child; } else { - oracle.setTolerance(0.0f); + child = new ChildT(ijk, iter->second.value, iter->second.state); + iter->second.child = child; } + NANOVDB_ASSERT(child); + return child->template set(ijk, args...); } - - const size_t size = mArray0.size(); - mCodec.reset(new Codec[size]); - - DitherLUT lut(mDitherOn); - auto kernel = [&](const Range1D &r) { - for (auto i=r.begin(); i!=r.end(); ++i) { - const float *data = mArray0[i]->mValues; - float min = std::numeric_limits::max(), max = -min; - for (int j=0; j<512; ++j) { - float v = data[j]; - if (vmax) max = v; - } - mCodec[i].min = min; - mCodec[i].max = max; - const float range = max - min; - uint16_t logBitWidth = 0;// 0,1,2,3,4 => 1,2,4,8,16 bits - while (range > 0.0f && logBitWidth < 4u) { - const uint32_t mask = (uint32_t(1) << (uint32_t(1) << logBitWidth)) - 1u; - const float encode = mask/range; - const float decode = range/mask; - int j = 0; - do { - const float exact = data[j];// exact value - const uint32_t code = uint32_t(encode*(exact - min) + lut(j)); - const float approx = code * decode + min;// approximate value - j += oracle(exact, approx) ? 1 : 513; - } while(j < 512); - if (j == 512) break; - ++logBitWidth; - } - mCodec[i].log2 = logBitWidth; - mCodec[i].size = DstNode0::DataType::memUsage(1u << logBitWidth); - } - };// kernel - forEach(0, size, 4, kernel); - - if (mVerbose) { - uint32_t counters[5+1] = {0}; - ++counters[mCodec[0].log2]; - for (size_t i=1; imDstOffset = mArray0[i-1]->mDstOffset + mCodec[i-1].size; - } - std::cout << "\n" << oracle << std::endl; - std::cout << "Dithering: " << (mDitherOn ? "enabled" : "disabled") << std::endl; - float avg = 0.0f; - for (uint32_t i=0; i<=5; ++i) { - if (uint32_t n = counters[i]) { - avg += n * float(1 << i); - printf("%2i bits: %6u leaf nodes, i.e. %4.1f%%\n",1< + auto getAndCache(const Coord& ijk, const AccT& acc, ArgsT&&... args) const + { + if (const Tile *tile = this->probeTile(ijk)) { + if (auto *child = tile->child) { + acc.insert(ijk, child); + return child->template get(ijk, args...); } + return OpT::get(*tile, args...); } - printf("%4.1f bits per value on average\n", avg/float(size)); - } else { - for (size_t i=1; imDstOffset = mArray0[i-1]->mDstOffset + mCodec[i-1].size; - } + return OpT::get(*this, args...); } - offset = mArray0[size-1]->mDstOffset + mCodec[size-1].size; -}// GridBuilder::compression -//================================================================================================ - -template -void GridBuilder:: - sdfToLevelSet() -{ - mArray0.clear(); - mArray1.clear(); - mArray2.clear(); - mArray0.reserve(mRoot.template nodeCount()); - mArray1.reserve(mRoot.template nodeCount()); - mArray2.reserve(mRoot.template nodeCount()); - - for (auto it2 = mRoot.mTable.begin(); it2 != mRoot.mTable.end(); ++it2) { - if (SrcNode2 *upper = it2->second.child) { - mArray2.emplace_back(upper); - for (auto it1 = upper->mChildMask.beginOn(); it1; ++it1) { - SrcNode1 *lower = upper->mTable[*it1].child; - mArray1.emplace_back(lower); - for (auto it0 = lower->mChildMask.beginOn(); it0; ++it0) { - mArray0.emplace_back(lower->mTable[*it0].child); - }// loop over leaf nodes - }// loop over lower internal nodes - }// is child node of the root - }// loop over root table - - // Note that the bottom-up flood filling is essential - const ValueT outside = mRoot.mBackground; - forEach(mArray0, 8, [&](const Range1D& r) { - for (auto i = r.begin(); i != r.end(); ++i) - mArray0[i]->signedFloodFill(outside); - }); - forEach(mArray1, 1, [&](const Range1D& r) { - for (auto i = r.begin(); i != r.end(); ++i) - mArray1[i]->signedFloodFill(outside); - }); - forEach(mArray2, 1, [&](const Range1D& r) { - for (auto i = r.begin(); i != r.end(); ++i) - mArray2[i]->signedFloodFill(outside); - }); - mRoot.signedFloodFill(outside); - mGridClass = GridClass::LevelSet; -} // GridBuilder::sdfToLevelSet - -//================================================================================================ - -template -template -GridHandle GridBuilder:: - getHandle(double dx, //voxel size - const Vec3d& p0, // origin - const std::string& name, - const OracleT& oracle, - const BufferT& buffer) -{ - if (dx <= 0) { - throw std::runtime_error("GridBuilder: voxel size is zero or negative"); + template + auto setAndCache(const Coord& ijk, const AccT& acc, ArgsT&&... args) + { + ChildT* child = nullptr; + const Coord key = CoordToKey(ijk); + auto iter = mTable.find(key); + if (iter == mTable.end()) { + child = new ChildT(ijk, mBackground, false); + mTable[key] = Tile(child); + } else if (iter->second.child != nullptr) { + child = iter->second.child; + } else { + child = new ChildT(ijk, iter->second.value, iter->second.state); + iter->second.child = child; + } + NANOVDB_ASSERT(child); + acc.insert(ijk, child); + return child->template setAndCache(ijk, acc, args...); } - Map map; // affine map - map.set(dx, p0, 1.0); - return this->getHandle(map, name, oracle, buffer); -} // GridBuilder::getHandle - -//================================================================================================ - -template -template< typename OracleT, typename BufferT> -GridHandle GridBuilder:: - getHandle(const Map& map, - const std::string& name, - const OracleT& oracle, - const BufferT& buffer) -{ - if (mGridClass == GridClass::LevelSet && !is_floating_point::value) { - throw std::runtime_error("Level sets are expected to be floating point types"); - } else if (mGridClass == GridClass::FogVolume && !is_floating_point::value) { - throw std::runtime_error("Fog volumes are expected to be floating point types"); + ValueType getValue(const Coord& ijk) const {return this->template get>(ijk);} + ValueType getValue(int i, int j, int k) const {return this->template get>(Coord(i,j,k));} + ValueType operator()(const Coord& ijk) const {return this->template get>(ijk);} + ValueType operator()(int i, int j, int k) const {return this->template get>(Coord(i,j,k));} + void setValue(const Coord& ijk, const ValueType& value) {this->template set>(ijk, value);} + bool probeValue(const Coord& ijk, ValueType& value) const {return this->template get>(ijk, value);} + bool isActive(const Coord& ijk) const {return this->template get>(ijk);} +#else + ValueType getValue(const Coord& ijk) const + { +#if 1 + if (auto *tile = this->probeTile(ijk)) return tile->child ? tile->child->getValue(ijk) : tile->value; + return mBackground; +#else + auto iter = mTable.find(CoordToKey(ijk)); + if (iter == mTable.end()) { + return mBackground; + } else if (iter->second.child) { + return iter->second.child->getValue(ijk); + } else { + return iter->second.value; + } +#endif } + ValueType getValue(int i, int j, int k) const {return this->getValue(Coord(i,j,k));} - auto handle = this->template initHandle(oracle, buffer);// initialize the arrays of nodes - - this->processLeafs(mArray0); - - this->processNodes(mArray1); - - this->processNodes(mArray2); - - auto *grid = this->processGrid(map, name); - - gridStats(*grid, mStats); - - updateChecksum(*grid, mChecksum); - - return handle; -} // GridBuilder::getHandle - -//================================================================================================ - -template -template -inline typename std::enable_if::value>::type -GridBuilder:: - setFlag(const T& min, const T& max, FlagT& flag) const -{ - if (mDelta > 0 && (min > mDelta || max < -mDelta)) { - flag |= FlagT(1); // set first bit - } else { - flag &= ~FlagT(1); // unset first bit + void setValue(const Coord& ijk, const ValueType& value) + { + ChildT* child = nullptr; + const Coord key = CoordToKey(ijk); + auto iter = mTable.find(key); + if (iter == mTable.end()) { + child = new ChildT(ijk, mBackground, false); + mTable[key] = Tile(child); + } else if (iter->second.child != nullptr) { + child = iter->second.child; + } else { + child = new ChildT(ijk, iter->second.value, iter->second.state); + iter->second.child = child; + } + NANOVDB_ASSERT(child); + child->setValue(ijk, value); } -} -//================================================================================================ + template + bool isActiveAndCache(const Coord& ijk, AccT& acc) const + { + auto iter = mTable.find(CoordToKey(ijk)); + if (iter == mTable.end()) + return false; + if (iter->second.child) { + acc.insert(ijk, iter->second.child); + return iter->second.child->isActiveAndCache(ijk, acc); + } + return iter->second.state; + } -template -inline void GridBuilder:: - sdfToFog() -{ - this->sdfToLevelSet(); // performs signed flood fill + template + ValueType getValueAndCache(const Coord& ijk, AccT& acc) const + { + auto iter = mTable.find(CoordToKey(ijk)); + if (iter == mTable.end()) + return mBackground; + if (iter->second.child) { + acc.insert(ijk, iter->second.child); + return iter->second.child->getValueAndCache(ijk, acc); + } + return iter->second.value; + } - const ValueT d = -mRoot.mBackground, w = 1.0f / d; - auto op = [&](ValueT& v) -> bool { - if (v > ValueT(0)) { - v = ValueT(0); - return false; + template + void setValueAndCache(const Coord& ijk, const ValueType& value, AccT& acc) + { + ChildT* child = nullptr; + const Coord key = CoordToKey(ijk); + auto iter = mTable.find(key); + if (iter == mTable.end()) { + child = new ChildT(ijk, mBackground, false); + mTable[key] = Tile(child); + } else if (iter->second.child != nullptr) { + child = iter->second.child; + } else { + child = new ChildT(ijk, iter->second.value, iter->second.state); + iter->second.child = child; } - v = v > d ? v * w : ValueT(1); - return true; - }; - auto kernel0 = [&](const Range1D& r) { - for (auto i = r.begin(); i != r.end(); ++i) { - SrcNode0* node = mArray0[i]; - for (uint32_t i = 0; i < SrcNode0::SIZE; ++i) - node->mValueMask.set(i, op(node->mValues[i])); + NANOVDB_ASSERT(child); + acc.insert(ijk, child); + child->setValueAndCache(ijk, value, acc); + } + template + void setValueOnAndCache(const Coord& ijk, AccT& acc) + { + ChildT* child = nullptr; + const Coord key = CoordToKey(ijk); + auto iter = mTable.find(key); + if (iter == mTable.end()) { + child = new ChildT(ijk, mBackground, false); + mTable[key] = Tile(child); + } else if (iter->second.child != nullptr) { + child = iter->second.child; + } else { + child = new ChildT(ijk, iter->second.value, iter->second.state); + iter->second.child = child; } - }; - auto kernel1 = [&](const Range1D& r) { - for (auto i = r.begin(); i != r.end(); ++i) { - SrcNode1* node = mArray1[i]; - for (uint32_t i = 0; i < SrcNode1::SIZE; ++i) { - if (node->mChildMask.isOn(i)) { - SrcNode0* leaf = node->mTable[i].child; - if (leaf->mValueMask.isOff()) { - node->mTable[i].value = leaf->getFirstValue(); - node->mChildMask.setOff(i); - delete leaf; - } - } else { - node->mValueMask.set(i, op(node->mTable[i].value)); - } - } + NANOVDB_ASSERT(child); + acc.insert(ijk, child); + child->setValueOnAndCache(ijk, acc); + } + template + void touchLeafAndCache(const Coord &ijk, AccT& acc) + { + ChildT* child = nullptr; + const Coord key = CoordToKey(ijk); + auto iter = mTable.find(key); + if (iter == mTable.end()) { + child = new ChildT(ijk, mBackground, false); + mTable[key] = Tile(child); + } else if (iter->second.child != nullptr) { + child = iter->second.child; + } else { + child = new ChildT(ijk, iter->second.value, iter->second.state); + iter->second.child = child; } - }; - auto kernel2 = [&](const Range1D& r) { - for (auto i = r.begin(); i != r.end(); ++i) { - SrcNode2* node = mArray2[i]; - for (uint32_t i = 0; i < SrcNode2::SIZE; ++i) { - if (node->mChildMask.isOn(i)) { - SrcNode1* child = node->mTable[i].child; - if (child->mChildMask.isOff() && child->mValueMask.isOff()) { - node->mTable[i].value = child->getFirstValue(); - node->mChildMask.setOff(i); - delete child; - } - } else { - node->mValueMask.set(i, op(node->mTable[i].value)); - } + acc.insert(ijk, child); + child->touchLeafAndCache(ijk, acc); + } +#endif// NANOVDB_NEW_ACCESSOR_METHODS + + template + uint32_t nodeCount() const + { + static_assert(is_same::value, "Root::getNodes: Invalid type"); + static_assert(NodeT::LEVEL < LEVEL, "Root::getNodes: LEVEL error"); + uint32_t sum = 0; + for (auto iter = mTable.begin(); iter != mTable.end(); ++iter) { + if (iter->second.child == nullptr) continue; // skip tiles + if constexpr(is_same::value) { //resolved at compile-time + ++sum; + } else { + sum += iter->second.child->template nodeCount(); } } - }; - forEach(mArray0, 8, kernel0); - forEach(mArray1, 1, kernel1); - forEach(mArray2, 1, kernel2); + return sum; + } - for (auto it = mRoot.mTable.begin(); it != mRoot.mTable.end(); ++it) { - SrcNode2* child = it->second.child; - if (child == nullptr) { - it->second.state = op(it->second.value); - } else if (child->mChildMask.isOff() && child->mValueMask.isOff()) { - it->second.value = child->getFirstValue(); - it->second.state = false; - it->second.child = nullptr; - delete child; + template + void getNodes(std::vector& array) + { + static_assert(is_same::value, "Root::getNodes: Invalid type"); + static_assert(NodeT::LEVEL < LEVEL, "Root::getNodes: LEVEL error"); + for (auto iter = mTable.begin(); iter != mTable.end(); ++iter) { + if (iter->second.child == nullptr) + continue; + if constexpr(is_same::value) { //resolved at compile-time + array.push_back(reinterpret_cast(iter->second.child)); + } else { + iter->second.child->getNodes(array); + } } } - mGridClass = GridClass::FogVolume; -} // GridBuilder::sdfToFog -//================================================================================================ + void addChild(ChildT*& child) + { + NANOVDB_ASSERT(child); + const Coord key = CoordToKey(child->mOrigin); + auto iter = mTable.find(key); + if (iter != mTable.end() && iter->second.child != nullptr) { // existing child node + delete iter->second.child; + iter->second.child = child; + } else { + mTable[key] = Tile(child); + } + child = nullptr; + } -template -template -inline typename std::enable_if::value && - !is_same::value && - !is_same::value && - !is_same::value>::type -GridBuilder:: - processLeafs(std::vector& srcLeafs) -{ - static_assert(!is_same::value, "Does not yet support bool leafs"); - static_assert(!is_same::value, "Does not yet support mask leafs"); - auto kernel = [&](const Range1D& r) { - auto *ptr = mBufferPtr + mBufferOffsets[5]; - for (auto i = r.begin(); i != r.end(); ++i) { - auto *srcLeaf = srcLeafs[i]; - auto *dstLeaf = PtrAdd(ptr, srcLeaf->mDstOffset); - auto *data = dstLeaf->data(); - if (DstNode0::DataType::padding()>0u) { - std::memset(data, 0, DstNode0::DataType::memUsage()); + /// @brief Add a tile containing voxel (i, j, k) at the specified tree level, + /// creating a new branch if necessary. Delete any existing lower-level nodes + /// that contain (x, y, z). + /// @tparam level tree level at which the tile is inserted. Must be 1, 2 or 3. + /// @param ijk Index coordinate that map to the tile being inserted + /// @param value Value of the tile + /// @param state Binary state of the tile + template + void addTile(const Coord& ijk, const ValueType& value, bool state) + { + static_assert(level > 0 && level <= LEVEL, "invalid template value of level"); + const Coord key = CoordToKey(ijk); + auto iter = mTable.find(key); + if constexpr(level == LEVEL) { + if (iter == mTable.end()) { + mTable[key] = Tile(value, state); + } else if (iter->second.child == nullptr) { + iter->second.value = value; + iter->second.state = state; } else { - data->mBBoxDif[0] = 0u; - data->mBBoxDif[1] = 0u; - data->mBBoxDif[2] = 0u; - data->mFlags = 0u;// enable rendering, no bbox - data->mMinimum = data->mMaximum = ValueT(); - data->mAverage = data->mStdDevi = 0; + delete iter->second.child; + iter->second.child = nullptr; + iter->second.value = value; + iter->second.state = state; } - srcLeaf->mDstNode = dstLeaf; - data->mBBoxMin = srcLeaf->mOrigin; // copy origin of node - data->mValueMask = srcLeaf->mValueMask; // copy value mask - const ValueT* src = srcLeaf->mValues; - for (ValueT *dst = data->mValues, *end = dst + SrcNode0::SIZE; dst != end; dst += 4, src += 4) { - dst[0] = src[0]; // copy *all* voxel values in sets of four, i.e. loop-unrolling - dst[1] = src[1]; - dst[2] = src[2]; - dst[3] = src[3]; + } else if constexpr(level < LEVEL) { + ChildT* child = nullptr; + if (iter == mTable.end()) { + child = new ChildT(ijk, mBackground, false); + mTable[key] = Tile(child); + } else if (iter->second.child != nullptr) { + child = iter->second.child; + } else { + child = new ChildT(ijk, iter->second.value, iter->second.state); + iter->second.child = child; } + child->template addTile(ijk, value, state); } - }; - forEach(srcLeafs, 8, kernel); -} // GridBuilder::processLeafs - -//================================================================================================ + } -template -template -inline typename std::enable_if::value || - is_same::value || - is_same::value>::type -GridBuilder:: - processLeafs(std::vector& srcLeafs) -{ - static_assert(is_same::value, "Expected ValueT == float"); - using ArrayT = typename DstNode0::DataType::ArrayType; - using FloatT = typename std::conditional=16, double, float>::type;// 16 compression and higher requires double - static constexpr FloatT UNITS = FloatT((1 << DstNode0::DataType::bitWidth()) - 1);// # of unique non-zero values - DitherLUT lut(mDitherOn); - - auto kernel = [&](const Range1D& r) { - uint8_t* ptr = mBufferPtr + mBufferOffsets[5]; - for (auto i = r.begin(); i != r.end(); ++i) { - auto *srcLeaf = srcLeafs[i]; - auto *dstLeaf = PtrAdd(ptr, srcLeaf->mDstOffset); - srcLeaf->mDstNode = dstLeaf; - auto *data = dstLeaf->data(); - if (DstNode0::DataType::padding()>0u) { - std::memset(data, 0, DstNode0::DataType::memUsage()); + template + void addNode(NodeT*& node) + { + if constexpr(is_same::value) { //resolved at compile-time + this->addChild(reinterpret_cast(node)); + } else { + ChildT* child = nullptr; + const Coord key = CoordToKey(node->mOrigin); + auto iter = mTable.find(key); + if (iter == mTable.end()) { + child = new ChildT(node->mOrigin, mBackground, false); + mTable[key] = Tile(child); + } else if (iter->second.child != nullptr) { + child = iter->second.child; } else { - data->mFlags = data->mBBoxDif[2] = data->mBBoxDif[1] = data->mBBoxDif[0] = 0u; - data->mDev = data->mAvg = data->mMax = data->mMin = 0u; - } - data->mBBoxMin = srcLeaf->mOrigin; // copy origin of node - data->mValueMask = srcLeaf->mValueMask; // copy value mask - const float* src = srcLeaf->mValues; - // compute extrema values - float min = std::numeric_limits::max(), max = -min; - for (int i=0; i<512; ++i) { - const float v = src[i]; - if (v < min) min = v; - if (v > max) max = v; + child = new ChildT(node->mOrigin, iter->second.value, iter->second.state); + iter->second.child = child; } - data->init(min, max, DstNode0::DataType::bitWidth()); - // perform quantization relative to the values in the current leaf node - const FloatT encode = UNITS/(max-min); - auto *code = reinterpret_cast(data->mCode); - int offset = 0; - if (is_same::value) {// resolved at compile-time - for (int j=0; j<128; ++j) { - auto tmp = ArrayT(encode * (*src++ - min) + lut(offset++)); - *code++ = ArrayT(encode * (*src++ - min) + lut(offset++)) << 4 | tmp; - tmp = ArrayT(encode * (*src++ - min) + lut(offset++)); - *code++ = ArrayT(encode * (*src++ - min) + lut(offset++)) << 4 | tmp; - } + child->addNode(node); + } + } + + void merge(RootNode &other) + { + for (auto iter1 = other.mTable.begin(); iter1 != other.mTable.end(); ++iter1) { + if (iter1->second.child == nullptr) continue;// ignore input tiles + auto iter2 = mTable.find(iter1->first); + if (iter2 == mTable.end() || iter2->second.child == nullptr) { + mTable[iter1->first] = Tile(iter1->second.child); + iter1->second.child = nullptr; } else { - for (int j=0; j<128; ++j) { - *code++ = ArrayT(encode * (*src++ - min) + lut(offset++)); - *code++ = ArrayT(encode * (*src++ - min) + lut(offset++)); - *code++ = ArrayT(encode * (*src++ - min) + lut(offset++)); - *code++ = ArrayT(encode * (*src++ - min) + lut(offset++)); - } + iter2->second.child->merge(*iter1->second.child); } } - }; - forEach(srcLeafs, 8, kernel); -} // GridBuilder::processLeafs + other.clear(); + } + + template + typename std::enable_if::value>::type + signedFloodFill(T outside); + +}; // build::RootNode //================================================================================================ -template +template template -inline typename std::enable_if::value>::type -GridBuilder:: - processLeafs(std::vector& srcLeafs) +inline typename std::enable_if::value>::type +RootNode::signedFloodFill(T outside) { - static_assert(is_same::value, "Expected ValueT == float"); + std::map nodeKeys; + for (auto iter = mTable.begin(); iter != mTable.end(); ++iter) { + if (iter->second.child == nullptr) + continue; + nodeKeys.insert(std::pair(iter->first, iter->second.child)); + } - DitherLUT lut(mDitherOn); - auto kernel = [&](const Range1D& r) { - uint8_t* ptr = mBufferPtr + mBufferOffsets[5]; - for (auto i = r.begin(); i != r.end(); ++i) { - auto *srcLeaf = srcLeafs[i]; - auto *dstLeaf = PtrAdd(ptr, srcLeaf->mDstOffset); - auto *data = dstLeaf->data(); - data->mBBoxMin = srcLeaf->mOrigin; // copy origin of node - data->mBBoxDif[0] = 0u; - data->mBBoxDif[1] = 0u; - data->mBBoxDif[2] = 0u; - srcLeaf->mDstNode = dstLeaf; - const uint8_t logBitWidth = uint8_t(mCodec[i].log2); - data->mFlags = logBitWidth << 5;// pack logBitWidth into 3 MSB of mFlag - data->mValueMask = srcLeaf->mValueMask; // copy value mask - const float* src = srcLeaf->mValues; - const float min = mCodec[i].min, max = mCodec[i].max; - data->init(min, max, uint8_t(1) << logBitWidth); - // perform quantization relative to the values in the current leaf node - int offset = 0; - switch (logBitWidth) { - case 0u: {// 1 bit - auto *dst = reinterpret_cast(data+1); - const float encode = 1.0f/(max - min); - for (int j=0; j<64; ++j) { - uint8_t a = 0; - for (int k=0; k<8; ++k) { - a |= uint8_t(encode * (*src++ - min) + lut(offset++)) << k; - } - *dst++ = a; - } - } - break; - case 1u: {// 2 bits - auto *dst = reinterpret_cast(data+1); - const float encode = 3.0f/(max - min); - for (int j=0; j<128; ++j) { - auto a = uint8_t(encode * (*src++ - min) + lut(offset++)); - a |= uint8_t(encode * (*src++ - min) + lut(offset++)) << 2; - a |= uint8_t(encode * (*src++ - min) + lut(offset++)) << 4; - *dst++ = uint8_t(encode * (*src++ - min) + lut(offset++)) << 6 | a; - } - } - break; - case 2u: {// 4 bits - auto *dst = reinterpret_cast(data+1); - const float encode = 15.0f/(max - min); - for (int j=0; j<128; ++j) { - auto a = uint8_t(encode * (*src++ - min) + lut(offset++)); - *dst++ = uint8_t(encode * (*src++ - min) + lut(offset++)) << 4 | a; - a = uint8_t(encode * (*src++ - min) + lut(offset++)); - *dst++ = uint8_t(encode * (*src++ - min) + lut(offset++)) << 4 | a; - } - } - break; - case 3u: {// 8 bits - auto *dst = reinterpret_cast(data+1); - const float encode = 255.0f/(max - min); - for (int j=0; j<128; ++j) { - *dst++ = uint8_t(encode * (*src++ - min) + lut(offset++)); - *dst++ = uint8_t(encode * (*src++ - min) + lut(offset++)); - *dst++ = uint8_t(encode * (*src++ - min) + lut(offset++)); - *dst++ = uint8_t(encode * (*src++ - min) + lut(offset++)); - } - } - break; - default: {// 16 bits - auto *dst = reinterpret_cast(data+1); - const double encode = 65535.0/(max - min);// note that double is required! - for (int j=0; j<128; ++j) { - *dst++ = uint16_t(encode * (*src++ - min) + lut(offset++)); - *dst++ = uint16_t(encode * (*src++ - min) + lut(offset++)); - *dst++ = uint16_t(encode * (*src++ - min) + lut(offset++)); - *dst++ = uint16_t(encode * (*src++ - min) + lut(offset++)); - } - } - }// end switch + // We employ a simple z-scanline algorithm that inserts inactive tiles with + // the inside value if they are sandwiched between inside child nodes only! + auto b = nodeKeys.begin(), e = nodeKeys.end(); + if (b == e) + return; + for (auto a = b++; b != e; ++a, ++b) { + Coord d = b->first - a->first; // delta of neighboring coordinates + if (d[0] != 0 || d[1] != 0 || d[2] == int(ChildT::DIM)) + continue; // not same z-scanline or neighbors + const ValueType fill[] = {a->second->getLastValue(), b->second->getFirstValue()}; + if (!(fill[0] < 0) || !(fill[1] < 0)) + continue; // scanline isn't inside + Coord c = a->first + Coord(0u, 0u, ChildT::DIM); + for (; c[2] != b->first[2]; c[2] += ChildT::DIM) { + const Coord key = RootNode::CoordToKey(c); + mTable[key] = typename RootNode::Tile(-outside, false); // inactive tile } - };// kernel - forEach(srcLeafs, 8, kernel); -} // GridBuilder::processLeafs + } +} // build::RootNode::signedFloodFill -//================================================================================================ +// ----------------------------> InternalNode <-------------------------------------- -template -template -void GridBuilder:: - processNodes(std::vector& srcNodes) +template +struct InternalNode { - using DstNodeT = typename SrcNodeT::NanoNodeT; - static_assert(DstNodeT::LEVEL == 1 || DstNodeT::LEVEL == 2, "Expected internal node"); - auto kernel = [&](const Range1D& r) { - uint8_t* ptr = mBufferPtr + mBufferOffsets[5 - DstNodeT::LEVEL];// 3 or 4 - for (auto i = r.begin(); i != r.end(); ++i) { - SrcNodeT *srcNode = srcNodes[i]; - DstNodeT *dstNode = PtrAdd(ptr, srcNode->mDstOffset); - auto *data = dstNode->data(); - if (DstNodeT::DataType::padding()>0u) std::memset(data, 0, DstNodeT::memUsage()); - srcNode->mDstNode = dstNode; - data->mBBox[0] = srcNode->mOrigin; // copy origin of node - data->mValueMask = srcNode->mValueMask; // copy value mask - data->mChildMask = srcNode->mChildMask; // copy child mask - for (uint32_t j = 0; j != SrcNodeT::SIZE; ++j) { - if (data->mChildMask.isOn(j)) { - data->setChild(j, srcNode->mTable[j].child->mDstNode); - } else - data->setValue(j, srcNode->mTable[j].value); + using ValueType = typename ChildT::ValueType; + using BuildType = typename ChildT::BuildType; + using ChildNodeType = ChildT; + using LeafNodeType = typename ChildT::LeafNodeType; + static constexpr uint32_t LOG2DIM = ChildT::LOG2DIM + 1; + static constexpr uint32_t TOTAL = LOG2DIM + ChildT::TOTAL; //dimension in index space + static constexpr uint32_t DIM = 1u << TOTAL; + static constexpr uint32_t SIZE = 1u << (3 * LOG2DIM); //number of tile values (or child pointers) + static constexpr uint32_t MASK = DIM - 1; + static constexpr uint32_t LEVEL = 1 + ChildT::LEVEL; // level 0 = leaf + static constexpr uint64_t NUM_VALUES = uint64_t(1) << (3 * TOTAL); // total voxel count represented by this node + using MaskT = Mask; + template + using MaskIterT = typename MaskT::template Iterator; + using NanoNodeT = typename NanoNode::Type; + + struct Tile { + Tile(ChildT* c = nullptr) : child(c) {} + Tile(const ValueType& v) : value(v) {} + union{ + ChildT* child; + ValueType value; + }; + }; + Coord mOrigin; + MaskT mValueMask; + MaskT mChildMask; + Tile mTable[SIZE]; + + union { + NanoNodeT *mDstNode; + uint64_t mDstOffset; + }; + + /// @brief Visits child nodes of this node only + class ChildIterator : public MaskIterT + { + using BaseT = MaskIterT; + const InternalNode *mParent; + public: + ChildIterator() : BaseT(), mParent(nullptr) {} + ChildIterator(const InternalNode* parent) : BaseT(parent->mChildMask.beginOn()), mParent(parent) {} + ChildIterator& operator=(const ChildIterator&) = default; + const ChildT& operator*() const {NANOVDB_ASSERT(*this); return *mParent->mTable[BaseT::pos()].child;} + const ChildT* operator->() const {NANOVDB_ASSERT(*this); return mParent->mTable[BaseT::pos()].child;} + Coord getCoord() const { NANOVDB_ASSERT(*this); return (*this)->origin();} + }; // Member class ChildIterator + + ChildIterator beginChild() {return ChildIterator(this);} + ChildIterator cbeginChildOn() const {return ChildIterator(this);}// match openvdb + + /// @brief Visits all tile values in this node, i.e. both inactive and active tiles + class ValueIterator : public MaskIterT + { + using BaseT = MaskIterT; + const InternalNode *mParent; + public: + ValueIterator() : BaseT(), mParent(nullptr) {} + ValueIterator(const InternalNode* parent) : BaseT(parent->mChildMask.beginOff()), mParent(parent) {} + ValueIterator& operator=(const ValueIterator&) = default; + ValueType operator*() const {NANOVDB_ASSERT(*this); return mParent->mTable[BaseT::pos()].value;} + Coord getCoord() const { NANOVDB_ASSERT(*this); return mParent->offsetToGlobalCoord(BaseT::pos());} + bool isActive() const { NANOVDB_ASSERT(*this); return mParent->mValueMask.isOn(BaseT::pos());} + }; // Member class ValueIterator + + ValueIterator beginValue() {return ValueIterator(this);} + ValueIterator cbeginValueAll() const {return ValueIterator(this);} + + /// @brief Visits active tile values of this node only + class ValueOnIterator : public MaskIterT + { + using BaseT = MaskIterT; + const InternalNode *mParent; + public: + ValueOnIterator() : BaseT(), mParent(nullptr) {} + ValueOnIterator(const InternalNode* parent) : BaseT(parent->mValueMask.beginOn()), mParent(parent) {} + ValueOnIterator& operator=(const ValueOnIterator&) = default; + ValueType operator*() const {NANOVDB_ASSERT(*this); return mParent->mTable[BaseT::pos()].value;} + Coord getCoord() const { NANOVDB_ASSERT(*this); return mParent->offsetToGlobalCoord(BaseT::pos());} + }; // Member class ValueOnIterator + + ValueOnIterator beginValueOn() {return ValueOnIterator(this);} + ValueOnIterator cbeginValueOn() const {return ValueOnIterator(this);} + + /// @brief Visits all tile values and child nodes of this node + class DenseIterator : public MaskT::DenseIterator + { + using BaseT = typename MaskT::DenseIterator; + const InternalNode *mParent; + public: + DenseIterator() : BaseT(), mParent(nullptr) {} + DenseIterator(const InternalNode* parent) : BaseT(0), mParent(parent) {} + DenseIterator& operator=(const DenseIterator&) = default; + ChildT* probeChild(ValueType& value) const + { + NANOVDB_ASSERT(mParent && bool(*this)); + ChildT *child = nullptr; + if (mParent->mChildMask.isOn(BaseT::pos())) { + child = mParent->mTable[BaseT::pos()].child; + } else { + value = mParent->mTable[BaseT::pos()].value; } + return child; } - }; - forEach(srcNodes, 4, kernel); -} // GridBuilder::processNodes + Coord getCoord() const { NANOVDB_ASSERT(mParent && bool(*this)); return mParent->offsetToGlobalCoord(BaseT::pos());} + }; // Member class DenseIterator -//================================================================================================ + DenseIterator beginDense() {return DenseIterator(this);} + DenseIterator cbeginChildAll() const {return DenseIterator(this);}// matches openvdb -template -NanoRoot* GridBuilder::processRoot() -{ - auto *dstRoot = reinterpret_cast(mBufferPtr + mBufferOffsets[2]); - auto *data = dstRoot->data(); - if (data->padding()>0) std::memset(data, 0, DstRootT::memUsage(uint32_t(mRoot.mTable.size()))); - data->mTableSize = uint32_t(mRoot.mTable.size()); - data->mMinimum = data->mMaximum = data->mBackground = mRoot.mBackground; - data->mBBox = CoordBBox(); // // set to an empty bounding box - - uint32_t tileID = 0; - for (auto iter = mRoot.mTable.begin(); iter != mRoot.mTable.end(); ++iter) { - auto *dstTile = data->tile(tileID++); - if (auto* srcChild = iter->second.child) { - dstTile->setChild(srcChild->mOrigin, srcChild->mDstNode, data); - } else { - dstTile->setValue(iter->first, iter->second.state, iter->second.value); + InternalNode(const Coord& origin, const ValueType& value, bool state) + : mOrigin(origin & ~MASK) + , mValueMask(state) + , mChildMask() + , mDstOffset(0) + { + for (uint32_t i = 0; i < SIZE; ++i) mTable[i].value = value; + } + InternalNode(const InternalNode&) = delete; // disallow copy-construction + InternalNode(InternalNode&&) = delete; // disallow move construction + InternalNode& operator=(const InternalNode&) = delete; // disallow copy assignment + InternalNode& operator=(InternalNode&&) = delete; // disallow move assignment + ~InternalNode() + { + for (auto iter = mChildMask.beginOn(); iter; ++iter) { + delete mTable[*iter].child; } } - return dstRoot; -} // GridBuilder::processRoot - -//================================================================================================ + const MaskT& getValueMask() const {return mValueMask;} + const MaskT& valueMask() const {return mValueMask;} + const MaskT& getChildMask() const {return mChildMask;} + const MaskT& childMask() const {return mChildMask;} + const Coord& origin() const {return mOrigin;} -template -NanoTree* GridBuilder::processTree() -{ - auto *dstTree = reinterpret_cast(mBufferPtr + mBufferOffsets[1]); - auto *data = dstTree->data(); - data->setRoot( this->processRoot() ); - - DstNode2 *node2 = mArray2.empty() ? nullptr : reinterpret_cast(mBufferPtr + mBufferOffsets[3]); - data->setFirstNode(node2); - - DstNode1 *node1 = mArray1.empty() ? nullptr : reinterpret_cast(mBufferPtr + mBufferOffsets[4]); - data->setFirstNode(node1); - - DstNode0 *node0 = mArray0.empty() ? nullptr : reinterpret_cast(mBufferPtr + mBufferOffsets[5]); - data->setFirstNode(node0); - - data->mNodeCount[0] = static_cast(mArray0.size()); - data->mNodeCount[1] = static_cast(mArray1.size()); - data->mNodeCount[2] = static_cast(mArray2.size()); - - // Count number of active leaf level tiles - data->mTileCount[0] = reduce(mArray1, uint32_t(0), [&](Range1D &r, uint32_t sum){ - for (auto i=r.begin(); i!=r.end(); ++i) sum += mArray1[i]->mValueMask.countOn(); - return sum;}, std::plus()); + void nodeCount(std::array &count) const + { + count[ChildT::LEVEL] += mChildMask.countOn(); + if constexpr(ChildT::LEVEL>0) { + for (auto it = const_cast(this)->beginChild(); it; ++it) it->nodeCount(count); + } + } - // Count number of active lower internal node tiles - data->mTileCount[1] = reduce(mArray2, uint32_t(0), [&](Range1D &r, uint32_t sum){ - for (auto i=r.begin(); i!=r.end(); ++i) sum += mArray2[i]->mValueMask.countOn(); - return sum;}, std::plus()); + static uint32_t CoordToOffset(const Coord& ijk) + { + return (((ijk[0] & int32_t(MASK)) >> ChildT::TOTAL) << (2 * LOG2DIM)) + + (((ijk[1] & int32_t(MASK)) >> ChildT::TOTAL) << (LOG2DIM)) + + ((ijk[2] & int32_t(MASK)) >> ChildT::TOTAL); + } - // Count number of active upper internal node tiles - uint32_t sum = 0; - for (auto &tile : mRoot.mTable) { - if (tile.second.child==nullptr && tile.second.state) ++sum; + static Coord OffsetToLocalCoord(uint32_t n) + { + NANOVDB_ASSERT(n < SIZE); + const uint32_t m = n & ((1 << 2 * LOG2DIM) - 1); + return Coord(n >> 2 * LOG2DIM, m >> LOG2DIM, m & ((1 << LOG2DIM) - 1)); } - data->mTileCount[2] = sum; - // Count number of active voxels - data->mVoxelCount = reduce(mArray0, uint64_t(0), [&](Range1D &r, uint64_t sum){ - for (auto i=r.begin(); i!=r.end(); ++i) sum += mArray0[i]->mValueMask.countOn(); - return sum;}, std::plus()); + void localToGlobalCoord(Coord& ijk) const + { + ijk <<= ChildT::TOTAL; + ijk += mOrigin; + } - data->mVoxelCount += data->mTileCount[0]*DstNode0::NUM_VALUES; - data->mVoxelCount += data->mTileCount[1]*DstNode1::NUM_VALUES; - data->mVoxelCount += data->mTileCount[2]*DstNode2::NUM_VALUES; + Coord offsetToGlobalCoord(uint32_t n) const + { + Coord ijk = InternalNode::OffsetToLocalCoord(n); + this->localToGlobalCoord(ijk); + return ijk; + } - return dstTree; -} // GridBuilder::processTree + ValueType getFirstValue() const { return mChildMask.isOn(0) ? mTable[0].child->getFirstValue() : mTable[0].value; } + ValueType getLastValue() const { return mChildMask.isOn(SIZE - 1) ? mTable[SIZE - 1].child->getLastValue() : mTable[SIZE - 1].value; } -//================================================================================================ + template + auto get(const Coord& ijk, ArgsT&&... args) const + { + const uint32_t n = CoordToOffset(ijk); + if (mChildMask.isOn(n)) return mTable[n].child->template get(ijk, args...); + return OpT::get(*this, n, args...); + } -template -NanoGrid* GridBuilder:: -processGrid(const Map& map, - const std::string& name) -{ - auto *dstGrid = reinterpret_cast(mBufferPtr + mBufferOffsets[0]); - this->processTree(); - auto* data = dstGrid->data(); - data->mMagic = NANOVDB_MAGIC_NUMBER; - data->mChecksum = 0u; - data->mVersion = Version(); - data->mFlags = static_cast(GridFlags::IsBreadthFirst); - data->mGridIndex = 0; - data->mGridCount = 1; - data->mGridSize = mBufferOffsets[8]; - data->mWorldBBox = BBox(); - data->mBlindMetadataOffset = 0; - data->mBlindMetadataCount = 0; - data->mGridClass = mGridClass; - data->mGridType = mapToGridType(); - data->mData0 = 0u; - data->mData1 = 0u; - data->mData2 = 0u; - - if (!isValid(data->mGridType, data->mGridClass)) { - std::stringstream ss; - ss << "Invalid combination of GridType("<mGridType) - << ") and GridClass("<mGridClass)<<"). See NanoVDB.h for details!"; - throw std::runtime_error(ss.str()); - } - - std::memset(data->mGridName, '\0', GridData::MaxNameSize);//overwrite mGridName - strncpy(data->mGridName, name.c_str(), GridData::MaxNameSize-1); - if (name.length() >= GridData::MaxNameSize) {// currently we don't support long grid names - std::stringstream ss; - ss << "Grid name \"" << name << "\" is more then " << GridData::MaxNameSize << " characters"; - throw std::runtime_error(ss.str()); - } - - data->mVoxelSize = map.applyMap(Vec3d(1)) - map.applyMap(Vec3d(0)); - data->mMap = map; - - if (mBlindDataSize>0) { - auto *metaData = reinterpret_cast(mBufferPtr + mBufferOffsets[6]); - data->mBlindMetadataOffset = PtrDiff(metaData, dstGrid); - data->mBlindMetadataCount = 1u;// we currently support only 1 set of blind data - auto *blindData = reinterpret_cast(mBufferPtr + mBufferOffsets[7]); - metaData->setBlindData(blindData); - } - - return dstGrid; -} // GridBuilder::processGrid + template + auto set(const Coord& ijk, ArgsT&&... args) + { + const uint32_t n = CoordToOffset(ijk); + ChildT* child = nullptr; + if (mChildMask.isOn(n)) { + child = mTable[n].child; + } else { + child = new ChildT(ijk, mTable[n].value, mValueMask.isOn(n)); + mTable[n].child = child; + mChildMask.setOn(n); + } + NANOVDB_ASSERT(child); + return child->template set(ijk, args...); + } -//================================================================================================ + template + auto getAndCache(const Coord& ijk, const AccT& acc, ArgsT&&... args) const + { + const uint32_t n = CoordToOffset(ijk); + if (mChildMask.isOff(n)) return OpT::get(*this, n, args...); + ChildT* child = mTable[n].child; + acc.insert(ijk, child); + if constexpr(ChildT::LEVEL == 0) { + return child->template get(ijk, args...); + } else { + return child->template getAndCache(ijk, acc, args...); + } + } -template -template -struct GridBuilder::BuildRoot -{ - using ValueType = typename ChildT::ValueType; - using ChildType = ChildT; - static constexpr uint32_t LEVEL = 1 + ChildT::LEVEL; // level 0 = leaf - struct Tile + template + auto setAndCache(const Coord& ijk, const AccT& acc, ArgsT&&... args) { - Tile(ChildT* c = nullptr) - : child(c) - { + const uint32_t n = CoordToOffset(ijk); + ChildT* child = nullptr; + if (mChildMask.isOn(n)) { + child = mTable[n].child; + } else { + child = new ChildT(ijk, mTable[n].value, mValueMask.isOn(n)); + mTable[n].child = child; + mChildMask.setOn(n); } - Tile(const ValueT& v, bool s) - : child(nullptr) - , value(v) - , state(s) - { + NANOVDB_ASSERT(child); + acc.insert(ijk, child); + if constexpr(ChildT::LEVEL == 0) { + return child->template set(ijk, args...); + } else { + return child->template setAndCache(ijk, acc, args...); } - ChildT* child; - ValueT value; - bool state; - }; - using MapT = std::map; - MapT mTable; - ValueT mBackground; + } - BuildRoot(const ValueT& background) - : mBackground(background) +#ifdef NANOVDB_NEW_ACCESSOR_METHODS + ValueType getValue(const Coord& ijk) const {return this->template get>(ijk);} + LeafNodeType& setValue(const Coord& ijk, const ValueType& value){return this->template set>(ijk, value);} +#else + ValueType getValue(const Coord& ijk) const { + const uint32_t n = CoordToOffset(ijk); + if (mChildMask.isOn(n)) { + return mTable[n].child->getValue(ijk); + } + return mTable[n].value; } - BuildRoot(const BuildRoot&) = delete; // disallow copy-construction - BuildRoot(BuildRoot&&) = default; // allow move construction - BuildRoot& operator=(const BuildRoot&) = delete; // disallow copy assignment - BuildRoot& operator=(BuildRoot&&) = default; // allow move assignment - - ~BuildRoot() { this->clear(); } - - bool empty() const { return mTable.empty(); } - - void clear() + void setValue(const Coord& ijk, const ValueType& value) { - for (auto iter = mTable.begin(); iter != mTable.end(); ++iter) - delete iter->second.child; - mTable.clear(); + const uint32_t n = CoordToOffset(ijk); + ChildT* child = nullptr; + if (mChildMask.isOn(n)) { + child = mTable[n].child; + } else { + child = new ChildT(ijk, mTable[n].value, mValueMask.isOn(n)); + mTable[n].child = child; + mChildMask.setOn(n); + } + child->setValue(ijk, value); } - static Coord CoordToKey(const Coord& ijk) { return ijk & ~ChildT::MASK; } - template - bool isActiveAndCache(const Coord& ijk, AccT& acc) const + ValueType getValueAndCache(const Coord& ijk, AccT& acc) const { - auto iter = mTable.find(CoordToKey(ijk)); - if (iter == mTable.end()) - return false; - if (iter->second.child) { - acc.insert(ijk, iter->second.child); - return iter->second.child->isActiveAndCache(ijk, acc); + const uint32_t n = CoordToOffset(ijk); + if (mChildMask.isOn(n)) { + acc.insert(ijk, const_cast(mTable[n].child)); + return mTable[n].child->getValueAndCache(ijk, acc); } - return iter->second.state; + return mTable[n].value; } - const ValueT& getValue(const Coord& ijk) const + template + void setValueAndCache(const Coord& ijk, const ValueType& value, AccT& acc) { - auto iter = mTable.find(CoordToKey(ijk)); - if (iter == mTable.end()) { - return mBackground; - } else if (iter->second.child) { - return iter->second.child->getValue(ijk); + const uint32_t n = CoordToOffset(ijk); + ChildT* child = nullptr; + if (mChildMask.isOn(n)) { + child = mTable[n].child; } else { - return iter->second.value; + child = new ChildT(ijk, mTable[n].value, mValueMask.isOn(n)); + mTable[n].child = child; + mChildMask.setOn(n); } + acc.insert(ijk, child); + child->setValueAndCache(ijk, value, acc); } template - const ValueT& getValueAndCache(const Coord& ijk, AccT& acc) const + void setValueOnAndCache(const Coord& ijk, AccT& acc) { - auto iter = mTable.find(CoordToKey(ijk)); - if (iter == mTable.end()) - return mBackground; - if (iter->second.child) { - acc.insert(ijk, iter->second.child); - return iter->second.child->getValueAndCache(ijk, acc); + const uint32_t n = CoordToOffset(ijk); + ChildT* child = nullptr; + if (mChildMask.isOn(n)) { + child = mTable[n].child; + } else { + child = new ChildT(ijk, mTable[n].value, mValueMask.isOn(n)); + mTable[n].child = child; + mChildMask.setOn(n); } - return iter->second.value; + acc.insert(ijk, child); + child->setValueOnAndCache(ijk, acc); } template - void setValueAndCache(const Coord& ijk, const ValueT& value, AccT& acc) + void touchLeafAndCache(const Coord &ijk, AccT& acc) { - ChildT* child = nullptr; - const Coord key = CoordToKey(ijk); - auto iter = mTable.find(key); - if (iter == mTable.end()) { - child = new ChildT(ijk, mBackground, false); - mTable[key] = Tile(child); - } else if (iter->second.child != nullptr) { - child = iter->second.child; + const uint32_t n = CoordToOffset(ijk); + ChildT* child = nullptr; + if (mChildMask.isOn(n)) { + child = mTable[n].child; } else { - child = new ChildT(ijk, iter->second.value, iter->second.state); - iter->second.child = child; + child = new ChildT(ijk, mTable[n].value, mValueMask.isOn(n)); + mTable[n].child = child; + mChildMask.setOn(n); } - NANOVDB_ASSERT(child); acc.insert(ijk, child); - child->setValueAndCache(ijk, value, acc); + if constexpr(LEVEL>1) child->touchLeafAndCache(ijk, acc); + } + template + bool isActiveAndCache(const Coord& ijk, AccT& acc) const + { + const uint32_t n = CoordToOffset(ijk); + if (mChildMask.isOn(n)) { + acc.insert(ijk, const_cast(mTable[n].child)); + return mTable[n].child->isActiveAndCache(ijk, acc); + } + return mValueMask.isOn(n); } +#endif template uint32_t nodeCount() const { - static_assert(is_same::value, "Root::getNodes: Invalid type"); - static_assert(NodeT::LEVEL < LEVEL, "Root::getNodes: LEVEL error"); + static_assert(is_same::value, "Node::getNodes: Invalid type"); + NANOVDB_ASSERT(NodeT::LEVEL < LEVEL); uint32_t sum = 0; - for (auto iter = mTable.begin(); iter != mTable.end(); ++iter) { - if (iter->second.child == nullptr) - continue; // skip tiles - if (is_same::value) { //resolved at compile-time - ++sum; - } else { - sum += iter->second.child->template nodeCount(); + if constexpr(is_same::value) { // resolved at compile-time + sum += mChildMask.countOn(); + } else if constexpr(LEVEL>1) { + for (auto iter = mChildMask.beginOn(); iter; ++iter) { + sum += mTable[*iter].child->template nodeCount(); } } return sum; @@ -1164,52 +956,93 @@ struct GridBuilder::BuildRoot template void getNodes(std::vector& array) { - static_assert(is_same::value, "Root::getNodes: Invalid type"); - static_assert(NodeT::LEVEL < LEVEL, "Root::getNodes: LEVEL error"); - for (auto iter = mTable.begin(); iter != mTable.end(); ++iter) { - if (iter->second.child == nullptr) - continue; - if (is_same::value) { //resolved at compile-time - array.push_back(reinterpret_cast(iter->second.child)); - } else { - iter->second.child->getNodes(array); + static_assert(is_same::value, "Node::getNodes: Invalid type"); + NANOVDB_ASSERT(NodeT::LEVEL < LEVEL); + for (auto iter = mChildMask.beginOn(); iter; ++iter) { + if constexpr(is_same::value) { // resolved at compile-time + array.push_back(reinterpret_cast(mTable[*iter].child)); + } else if constexpr(LEVEL>1) { + mTable[*iter].child->getNodes(array); } } } void addChild(ChildT*& child) { - NANOVDB_ASSERT(child); - const Coord key = CoordToKey(child->mOrigin); - auto iter = mTable.find(key); - if (iter != mTable.end() && iter->second.child != nullptr) { // existing child node - delete iter->second.child; - iter->second.child = child; + NANOVDB_ASSERT(child && (child->mOrigin & ~MASK) == this->mOrigin); + const uint32_t n = CoordToOffset(child->mOrigin); + if (mChildMask.isOn(n)) { + delete mTable[n].child; } else { - mTable[key] = Tile(child); + mChildMask.setOn(n); } + mTable[n].child = child; child = nullptr; } + /// @brief Add a tile containing voxel (i, j, k) at the specified tree level, + /// creating a new branch if necessary. Delete any existing lower-level nodes + /// that contain (x, y, z). + /// @tparam level tree level at which the tile is inserted. Must be 1 or 2. + /// @param ijk Index coordinate that map to the tile being inserted + /// @param value Value of the tile + /// @param state Binary state of the tile + template + void addTile(const Coord& ijk, const ValueType& value, bool state) + { + static_assert(level > 0 && level <= LEVEL, "invalid template value of level"); + const uint32_t n = CoordToOffset(ijk); + if constexpr(level == LEVEL) { + if (mChildMask.isOn(n)) { + delete mTable[n].child; + mTable[n] = Tile(value); + } else { + mValueMask.set(n, state); + mTable[n].value = value; + } + } else if constexpr(level < LEVEL) { + ChildT* child = nullptr; + if (mChildMask.isOn(n)) { + child = mTable[n].child; + } else { + child = new ChildT(ijk, value, state); + mTable[n].child = child; + mChildMask.setOn(n); + } + child->template addTile(ijk, value, state); + } + } + template void addNode(NodeT*& node) { - if (is_same::value) { //resolved at compile-time + if constexpr(is_same::value) { //resolved at compile-time this->addChild(reinterpret_cast(node)); - } else { - ChildT* child = nullptr; - const Coord key = CoordToKey(node->mOrigin); - auto iter = mTable.find(key); - if (iter == mTable.end()) { - child = new ChildT(node->mOrigin, mBackground, false); - mTable[key] = Tile(child); - } else if (iter->second.child != nullptr) { - child = iter->second.child; + } else if constexpr(LEVEL>1) { + const uint32_t n = CoordToOffset(node->mOrigin); + ChildT* child = nullptr; + if (mChildMask.isOn(n)) { + child = mTable[n].child; } else { - child = new ChildT(node->mOrigin, iter->second.value, iter->second.state); - iter->second.child = child; + child = new ChildT(node->mOrigin, mTable[n].value, mValueMask.isOn(n)); + mTable[n].child = child; + mChildMask.setOn(n); + } + child->addNode(node); + } + } + + void merge(InternalNode &other) + { + for (auto iter = other.mChildMask.beginOn(); iter; ++iter) { + const uint32_t n = *iter; + if (mChildMask.isOn(n)) { + mTable[n].child->merge(*other.mTable[n].child); + } else { + mTable[n].child = other.mTable[n].child; + other.mChildMask.setOff(n); + mChildMask.setOn(n); } - child->addNode(node); } } @@ -1217,362 +1050,523 @@ struct GridBuilder::BuildRoot typename std::enable_if::value>::type signedFloodFill(T outside); - template - typename std::enable_if::value>::type - signedFloodFill(T) {} // no-op for none floating point values -}; // GridBuilder::BuildRoot +}; // build::InternalNode //================================================================================================ -template template template inline typename std::enable_if::value>::type -GridBuilder::BuildRoot:: - signedFloodFill(T outside) +InternalNode::signedFloodFill(T outside) { - std::map nodeKeys; - for (auto iter = mTable.begin(); iter != mTable.end(); ++iter) { - if (iter->second.child == nullptr) - continue; - nodeKeys.insert(std::pair(iter->first, iter->second.child)); - } - - // We employ a simple z-scanline algorithm that inserts inactive tiles with - // the inside value if they are sandwiched between inside child nodes only! - auto b = nodeKeys.begin(), e = nodeKeys.end(); - if (b == e) - return; - for (auto a = b++; b != e; ++a, ++b) { - Coord d = b->first - a->first; // delta of neighboring coordinates - if (d[0] != 0 || d[1] != 0 || d[2] == int(ChildT::DIM)) - continue; // not same z-scanline or neighbors - const ValueT fill[] = {a->second->getLastValue(), b->second->getFirstValue()}; - if (!(fill[0] < 0) || !(fill[1] < 0)) - continue; // scanline isn't inside - Coord c = a->first + Coord(0u, 0u, ChildT::DIM); - for (; c[2] != b->first[2]; c[2] += ChildT::DIM) { - const Coord key = SrcRootT::CoordToKey(c); - mTable[key] = typename SrcRootT::Tile(-outside, false); // inactive tile + const uint32_t first = *mChildMask.beginOn(); + if (first < NUM_VALUES) { + bool xInside = mTable[first].child->getFirstValue() < 0; + bool yInside = xInside, zInside = xInside; + for (uint32_t x = 0; x != (1 << LOG2DIM); ++x) { + const uint32_t x00 = x << (2 * LOG2DIM); // offset for block(x, 0, 0) + if (mChildMask.isOn(x00)) { + xInside = mTable[x00].child->getLastValue() < 0; + } + yInside = xInside; + for (uint32_t y = 0; y != (1u << LOG2DIM); ++y) { + const uint32_t xy0 = x00 + (y << LOG2DIM); // offset for block(x, y, 0) + if (mChildMask.isOn(xy0)) + yInside = mTable[xy0].child->getLastValue() < 0; + zInside = yInside; + for (uint32_t z = 0; z != (1 << LOG2DIM); ++z) { + const uint32_t xyz = xy0 + z; // offset for block(x, y, z) + if (mChildMask.isOn(xyz)) { + zInside = mTable[xyz].child->getLastValue() < 0; + } else { + mTable[xyz].value = zInside ? -outside : outside; + } + } + } } } -} // Root::signedFloodFill +} // build::InternalNode::signedFloodFill -//================================================================================================ +// ----------------------------> LeafNode <-------------------------------------- -template -template -struct GridBuilder:: - BuildNode +template +struct LeafNode { - using ValueType = ValueT; using BuildType = BuildT; - using ChildType = ChildT; - static constexpr uint32_t LOG2DIM = ChildT::LOG2DIM + 1; - static constexpr uint32_t TOTAL = LOG2DIM + ChildT::TOTAL; //dimension in index space + using ValueType = typename BuildToValueMap::type; + using LeafNodeType = LeafNode; + static constexpr uint32_t LOG2DIM = 3; + static constexpr uint32_t TOTAL = LOG2DIM; // needed by parent nodes static constexpr uint32_t DIM = 1u << TOTAL; - static constexpr uint32_t SIZE = 1u << (3 * LOG2DIM); //number of tile values (or child pointers) - static constexpr int32_t MASK = DIM - 1; - static constexpr uint32_t LEVEL = 1 + ChildT::LEVEL; // level 0 = leaf + static constexpr uint32_t SIZE = 1u << 3 * LOG2DIM; // total number of voxels represented by this node + static constexpr uint32_t MASK = DIM - 1; // mask for bit operations + static constexpr uint32_t LEVEL = 0; // level 0 = leaf static constexpr uint64_t NUM_VALUES = uint64_t(1) << (3 * TOTAL); // total voxel count represented by this node - using MaskT = Mask; - using NanoNodeT = typename NanoNode::Type; - - struct Tile - { - Tile(ChildT* c = nullptr) - : child(c) - { - } - union - { - ChildT* child; - ValueT value; - }; - }; - Coord mOrigin; - MaskT mValueMask; - MaskT mChildMask; - Tile mTable[SIZE]; + using NodeMaskType = Mask; + template + using MaskIterT = typename Mask::template Iterator; + using NanoLeafT = typename NanoNode::Type; + Coord mOrigin; + Mask mValueMask; + ValueType mValues[SIZE]; union { - NanoNodeT *mDstNode; + NanoLeafT *mDstNode; uint64_t mDstOffset; }; - BuildNode(const Coord& origin, const ValueT& value, bool state) - : mOrigin(origin & ~MASK) - , mValueMask(state) - , mChildMask() - , mDstOffset(0) + /// @brief Visits all active values in a leaf node + class ValueOnIterator : public MaskIterT + { + using BaseT = MaskIterT; + const LeafNode *mParent; + public: + ValueOnIterator() : BaseT(), mParent(nullptr) {} + ValueOnIterator(const LeafNode* parent) : BaseT(parent->mValueMask.beginOn()), mParent(parent) {} + ValueOnIterator& operator=(const ValueOnIterator&) = default; + ValueType operator*() const {NANOVDB_ASSERT(*this); return mParent->mValues[BaseT::pos()];} + Coord getCoord() const { NANOVDB_ASSERT(*this); return mParent->offsetToGlobalCoord(BaseT::pos());} + }; // Member class ValueOnIterator + + ValueOnIterator beginValueOn() {return ValueOnIterator(this);} + ValueOnIterator cbeginValueOn() const {return ValueOnIterator(this);} + + /// @brief Visits all inactive values in a leaf node + class ValueOffIterator : public MaskIterT + { + using BaseT = MaskIterT; + const LeafNode *mParent; + public: + ValueOffIterator() : BaseT(), mParent(nullptr) {} + ValueOffIterator(const LeafNode* parent) : BaseT(parent->mValueMask.beginOff()), mParent(parent) {} + ValueOffIterator& operator=(const ValueOffIterator&) = default; + ValueType operator*() const {NANOVDB_ASSERT(*this); return mParent->mValues[BaseT::pos()];} + Coord getCoord() const { NANOVDB_ASSERT(*this); return mParent->offsetToGlobalCoord(BaseT::pos());} + }; // Member class ValueOffIterator + + ValueOffIterator beginValueOff() {return ValueOffIterator(this);} + ValueOffIterator cbeginValueOff() const {return ValueOffIterator(this);} + + /// @brief Visits all values in a leaf node, i.e. both active and inactive values + class ValueIterator { - for (uint32_t i = 0; i < SIZE; ++i) { - mTable[i].value = value; + const LeafNode *mParent; + uint32_t mPos; + public: + ValueIterator() : mParent(nullptr), mPos(1u << 3 * LOG2DIM) {} + ValueIterator(const LeafNode* parent) : mParent(parent), mPos(0) {NANOVDB_ASSERT(parent);} + ValueIterator& operator=(const ValueIterator&) = default; + ValueType operator*() const { NANOVDB_ASSERT(*this); return mParent->mValues[mPos];} + Coord getCoord() const { NANOVDB_ASSERT(*this); return mParent->offsetToGlobalCoord(mPos);} + bool isActive() const { NANOVDB_ASSERT(*this); return mParent->isActive(mPos);} + operator bool() const {return mPos < SIZE;} + ValueIterator& operator++() {++mPos; return *this;} + ValueIterator operator++(int) { + auto tmp = *this; + ++(*this); + return tmp; } - } - BuildNode(const BuildNode&) = delete; // disallow copy-construction - BuildNode(BuildNode&&) = delete; // disallow move construction - BuildNode& operator=(const BuildNode&) = delete; // disallow copy assignment - BuildNode& operator=(BuildNode&&) = delete; // disallow move assignment - ~BuildNode() + }; // Member class ValueIterator + + ValueIterator beginValue() {return ValueIterator(this);} + ValueIterator cbeginValueAll() const {return ValueIterator(this);} + + LeafNode(const Coord& ijk, const ValueType& value, bool state) + : mOrigin(ijk & ~MASK) + , mValueMask(state) //invalid + , mDstOffset(0) { - for (auto iter = mChildMask.beginOn(); iter; ++iter) { - delete mTable[*iter].child; + ValueType* target = mValues; + uint32_t n = SIZE; + while (n--) { + *target++ = value; } } + LeafNode(const LeafNode&) = delete; // disallow copy-construction + LeafNode(LeafNode&&) = delete; // disallow move construction + LeafNode& operator=(const LeafNode&) = delete; // disallow copy assignment + LeafNode& operator=(LeafNode&&) = delete; // disallow move assignment + ~LeafNode() = default; + + const Mask& getValueMask() const {return mValueMask;} + const Mask& valueMask() const {return mValueMask;} + const Coord& origin() const {return mOrigin;} + /// @brief Return the linear offset corresponding to the given coordinate static uint32_t CoordToOffset(const Coord& ijk) { - return (((ijk[0] & MASK) >> ChildT::TOTAL) << (2 * LOG2DIM)) + - (((ijk[1] & MASK) >> ChildT::TOTAL) << (LOG2DIM)) + - ((ijk[2] & MASK) >> ChildT::TOTAL); + return ((ijk[0] & int32_t(MASK)) << (2 * LOG2DIM)) + + ((ijk[1] & int32_t(MASK)) << LOG2DIM) + + (ijk[2] & int32_t(MASK)); } static Coord OffsetToLocalCoord(uint32_t n) { NANOVDB_ASSERT(n < SIZE); - const uint32_t m = n & ((1 << 2 * LOG2DIM) - 1); - return Coord(n >> 2 * LOG2DIM, m >> LOG2DIM, m & ((1 << LOG2DIM) - 1)); + const int32_t m = n & ((1 << 2 * LOG2DIM) - 1); + return Coord(n >> 2 * LOG2DIM, m >> LOG2DIM, m & int32_t(MASK)); } void localToGlobalCoord(Coord& ijk) const { - ijk <<= ChildT::TOTAL; ijk += mOrigin; } Coord offsetToGlobalCoord(uint32_t n) const { - Coord ijk = BuildNode::OffsetToLocalCoord(n); + Coord ijk = LeafNode::OffsetToLocalCoord(n); this->localToGlobalCoord(ijk); return ijk; } + ValueType getFirstValue() const { return mValues[0]; } + ValueType getLastValue() const { return mValues[SIZE - 1]; } + const ValueType& getValue(uint32_t i) const {return mValues[i];} + const ValueType& getValue(const Coord& ijk) const {return mValues[CoordToOffset(ijk)];} + + template + auto get(const Coord& ijk, ArgsT&&... args) const {return OpT::get(*this, CoordToOffset(ijk), args...);} + + template + auto set(const Coord& ijk, ArgsT&&... args) {return OpT::set(*this, CoordToOffset(ijk), args...);} + +#ifndef NANOVDB_NEW_ACCESSOR_METHODS template - bool isActiveAndCache(const Coord& ijk, AccT& acc) const + const ValueType& getValueAndCache(const Coord& ijk, const AccT&) const { - const uint32_t n = CoordToOffset(ijk); - if (mChildMask.isOn(n)) { - acc.insert(ijk, const_cast(mTable[n].child)); - return mTable[n].child->isActiveAndCache(ijk, acc); - } - return mValueMask.isOn(n); + return mValues[CoordToOffset(ijk)]; } - ValueT getFirstValue() const { return mChildMask.isOn(0) ? mTable[0].child->getFirstValue() : mTable[0].value; } - ValueT getLastValue() const { return mChildMask.isOn(SIZE - 1) ? mTable[SIZE - 1].child->getLastValue() : mTable[SIZE - 1].value; } - - const ValueT& getValue(const Coord& ijk) const + template + void setValueAndCache(const Coord& ijk, const ValueType& value, const AccT&) { const uint32_t n = CoordToOffset(ijk); - if (mChildMask.isOn(n)) { - return mTable[n].child->getValue(ijk); - } - return mTable[n].value; + mValueMask.setOn(n); + mValues[n] = value; } template - const ValueT& getValueAndCache(const Coord& ijk, AccT& acc) const + void setValueOnAndCache(const Coord& ijk, const AccT&) { const uint32_t n = CoordToOffset(ijk); - if (mChildMask.isOn(n)) { - acc.insert(ijk, const_cast(mTable[n].child)); - return mTable[n].child->getValueAndCache(ijk, acc); - } - return mTable[n].value; + mValueMask.setOn(n); } - void setValue(const Coord& ijk, const ValueT& value) + template + bool isActiveAndCache(const Coord& ijk, const AccT&) const { - const uint32_t n = CoordToOffset(ijk); - ChildT* child = nullptr; - if (mChildMask.isOn(n)) { - child = mTable[n].child; - } else { - child = new ChildT(ijk, mTable[n].value, mValueMask.isOn(n)); - mTable[n].child = child; - mChildMask.setOn(n); - } - child->setValue(ijk, value); + return mValueMask.isOn(CoordToOffset(ijk)); } +#endif - template - void setValueAndCache(const Coord& ijk, const ValueT& value, AccT& acc) + void setValue(uint32_t n, const ValueType& value) { - const uint32_t n = CoordToOffset(ijk); - ChildT* child = nullptr; - if (mChildMask.isOn(n)) { - child = mTable[n].child; - } else { - child = new ChildT(ijk, mTable[n].value, mValueMask.isOn(n)); - mTable[n].child = child; - mChildMask.setOn(n); - } - acc.insert(ijk, child); - child->setValueAndCache(ijk, value, acc); + mValueMask.setOn(n); + mValues[n] = value; } + void setValue(const Coord& ijk, const ValueType& value){this->setValue(CoordToOffset(ijk), value);} - template - uint32_t nodeCount() const + void merge(LeafNode &other) { - static_assert(is_same::value, "Node::getNodes: Invalid type"); - NANOVDB_ASSERT(NodeT::LEVEL < LEVEL); - uint32_t sum = 0; - if (is_same::value) { //resolved at compile-time - sum += mChildMask.countOn(); - } else { - for (auto iter = mChildMask.beginOn(); iter; ++iter) { - sum += mTable[*iter].child->template nodeCount(); - } + other.mValueMask -= mValueMask; + for (auto iter = other.mValueMask.beginOn(); iter; ++iter) { + const uint32_t n = *iter; + mValues[n] = other.mValues[n]; } - return sum; + mValueMask |= other.mValueMask; } - template - void getNodes(std::vector& array) + template + typename std::enable_if::value>::type + signedFloodFill(T outside); + +}; // build::LeafNode + +//================================================================================================ + +template <> +struct LeafNode +{ + using ValueType = bool; + using BuildType = ValueMask; + using LeafNodeType = LeafNode; + static constexpr uint32_t LOG2DIM = 3; + static constexpr uint32_t TOTAL = LOG2DIM; // needed by parent nodes + static constexpr uint32_t DIM = 1u << TOTAL; + static constexpr uint32_t SIZE = 1u << 3 * LOG2DIM; // total number of voxels represented by this node + static constexpr uint32_t MASK = DIM - 1; // mask for bit operations + static constexpr uint32_t LEVEL = 0; // level 0 = leaf + static constexpr uint64_t NUM_VALUES = uint64_t(1) << (3 * TOTAL); // total voxel count represented by this node + using NodeMaskType = Mask; + template + using MaskIterT = typename Mask::template Iterator; + using NanoLeafT = typename NanoNode::Type; + + Coord mOrigin; + Mask mValueMask; + union { + NanoLeafT *mDstNode; + uint64_t mDstOffset; + }; + + /// @brief Visits all active values in a leaf node + class ValueOnIterator : public MaskIterT { - static_assert(is_same::value, "Node::getNodes: Invalid type"); - NANOVDB_ASSERT(NodeT::LEVEL < LEVEL); - for (auto iter = mChildMask.beginOn(); iter; ++iter) { - if (is_same::value) { //resolved at compile-time - array.push_back(reinterpret_cast(mTable[*iter].child)); - } else { - mTable[*iter].child->getNodes(array); - } + using BaseT = MaskIterT; + const LeafNode *mParent; + public: + ValueOnIterator() : BaseT(), mParent(nullptr) {} + ValueOnIterator(const LeafNode* parent) : BaseT(parent->mValueMask.beginOn()), mParent(parent) {} + ValueOnIterator& operator=(const ValueOnIterator&) = default; + bool operator*() const {NANOVDB_ASSERT(*this); return true;} + Coord getCoord() const { NANOVDB_ASSERT(*this); return mParent->offsetToGlobalCoord(BaseT::pos());} + }; // Member class ValueOnIterator + + ValueOnIterator beginValueOn() {return ValueOnIterator(this);} + ValueOnIterator cbeginValueOn() const {return ValueOnIterator(this);} + + /// @brief Visits all inactive values in a leaf node + class ValueOffIterator : public MaskIterT + { + using BaseT = MaskIterT; + const LeafNode *mParent; + public: + ValueOffIterator() : BaseT(), mParent(nullptr) {} + ValueOffIterator(const LeafNode* parent) : BaseT(parent->mValueMask.beginOff()), mParent(parent) {} + ValueOffIterator& operator=(const ValueOffIterator&) = default; + bool operator*() const {NANOVDB_ASSERT(*this); return false;} + Coord getCoord() const { NANOVDB_ASSERT(*this); return mParent->offsetToGlobalCoord(BaseT::pos());} + }; // Member class ValueOffIterator + + ValueOffIterator beginValueOff() {return ValueOffIterator(this);} + ValueOffIterator cbeginValueOff() const {return ValueOffIterator(this);} + + /// @brief Visits all values in a leaf node, i.e. both active and inactive values + class ValueIterator + { + const LeafNode *mParent; + uint32_t mPos; + public: + ValueIterator() : mParent(nullptr), mPos(1u << 3 * LOG2DIM) {} + ValueIterator(const LeafNode* parent) : mParent(parent), mPos(0) {NANOVDB_ASSERT(parent);} + ValueIterator& operator=(const ValueIterator&) = default; + bool operator*() const { NANOVDB_ASSERT(*this); return mParent->mValueMask.isOn(mPos);} + Coord getCoord() const { NANOVDB_ASSERT(*this); return mParent->offsetToGlobalCoord(mPos);} + bool isActive() const { NANOVDB_ASSERT(*this); return mParent->mValueMask.isOn(mPos);} + operator bool() const {return mPos < SIZE;} + ValueIterator& operator++() {++mPos; return *this;} + ValueIterator operator++(int) { + auto tmp = *this; + ++(*this); + return tmp; } + }; // Member class ValueIterator + + ValueIterator beginValue() {return ValueIterator(this);} + ValueIterator cbeginValueAll() const {return ValueIterator(this);} + + LeafNode(const Coord& ijk, const ValueType&, bool state) + : mOrigin(ijk & ~MASK) + , mValueMask(state) //invalid + , mDstOffset(0) + { } + LeafNode(const LeafNode&) = delete; // disallow copy-construction + LeafNode(LeafNode&&) = delete; // disallow move construction + LeafNode& operator=(const LeafNode&) = delete; // disallow copy assignment + LeafNode& operator=(LeafNode&&) = delete; // disallow move assignment + ~LeafNode() = default; - void addChild(ChildT*& child) + const Mask& valueMask() const {return mValueMask;} + const Mask& getValueMask() const {return mValueMask;} + const Coord& origin() const {return mOrigin;} + + /// @brief Return the linear offset corresponding to the given coordinate + static uint32_t CoordToOffset(const Coord& ijk) { - NANOVDB_ASSERT(child && (child->mOrigin & ~MASK) == this->mOrigin); - const uint32_t n = CoordToOffset(child->mOrigin); - if (mChildMask.isOn(n)) { - delete mTable[n].child; - } else { - mChildMask.setOn(n); - } - mTable[n].child = child; - child = nullptr; + return ((ijk[0] & int32_t(MASK)) << (2 * LOG2DIM)) + + ((ijk[1] & int32_t(MASK)) << LOG2DIM) + + (ijk[2] & int32_t(MASK)); } - template - void addNode(NodeT*& node) + static Coord OffsetToLocalCoord(uint32_t n) { - if (is_same::value) { //resolved at compile-time - this->addChild(reinterpret_cast(node)); - } else { - const uint32_t n = CoordToOffset(node->mOrigin); - ChildT* child = nullptr; - if (mChildMask.isOn(n)) { - child = mTable[n].child; - } else { - child = new ChildT(node->mOrigin, mTable[n].value, mValueMask.isOn(n)); - mTable[n].child = child; - mChildMask.setOn(n); - } - child->addNode(node); - } + NANOVDB_ASSERT(n < SIZE); + const int32_t m = n & ((1 << 2 * LOG2DIM) - 1); + return Coord(n >> 2 * LOG2DIM, m >> LOG2DIM, m & int32_t(MASK)); + } + + void localToGlobalCoord(Coord& ijk) const {ijk += mOrigin;} + + Coord offsetToGlobalCoord(uint32_t n) const + { + Coord ijk = LeafNode::OffsetToLocalCoord(n); + this->localToGlobalCoord(ijk); + return ijk; + } + + bool getFirstValue() const { return mValueMask.isOn(0); } + bool getLastValue() const { return mValueMask.isOn(SIZE - 1); } + bool getValue(uint32_t i) const {return mValueMask.isOn(i);} + bool getValue(const Coord& ijk) const {return mValueMask.isOn(CoordToOffset(ijk));} + + template + auto get(const Coord& ijk, ArgsT&&... args) const {return OpT::get(*this, CoordToOffset(ijk), args...);} + + template + auto set(const Coord& ijk, ArgsT&&... args) {return OpT::set(*this, CoordToOffset(ijk), args...);} + +#ifndef NANOVDB_NEW_ACCESSOR_METHODS + template + bool getValueAndCache(const Coord& ijk, const AccT&) const + { + return mValueMask.isOn(CoordToOffset(ijk)); + } + + template + void setValueAndCache(const Coord& ijk, bool, const AccT&) + { + const uint32_t n = CoordToOffset(ijk); + mValueMask.setOn(n); + } + + template + void setValueOnAndCache(const Coord& ijk, const AccT&) + { + const uint32_t n = CoordToOffset(ijk); + mValueMask.setOn(n); } - template - typename std::enable_if::value>::type - signedFloodFill(T outside); - template - typename std::enable_if::value>::type - signedFloodFill(T) {} // no-op for none floating point values -}; // GridBuilder::BuildNode + template + bool isActiveAndCache(const Coord& ijk, const AccT&) const + { + return mValueMask.isOn(CoordToOffset(ijk)); + } +#endif -//================================================================================================ + void setValue(uint32_t n, bool) {mValueMask.setOn(n);} + void setValue(const Coord& ijk) {mValueMask.setOn(CoordToOffset(ijk));} -template -template -template -inline typename std::enable_if::value>::type -GridBuilder::BuildNode:: - signedFloodFill(T outside) -{ - const uint32_t first = *mChildMask.beginOn(); - if (first < NUM_VALUES) { - bool xInside = mTable[first].child->getFirstValue() < 0; - bool yInside = xInside, zInside = xInside; - for (uint32_t x = 0; x != (1 << LOG2DIM); ++x) { - const uint32_t x00 = x << (2 * LOG2DIM); // offset for block(x, 0, 0) - if (mChildMask.isOn(x00)) { - xInside = mTable[x00].child->getLastValue() < 0; - } - yInside = xInside; - for (uint32_t y = 0; y != (1u << LOG2DIM); ++y) { - const uint32_t xy0 = x00 + (y << LOG2DIM); // offset for block(x, y, 0) - if (mChildMask.isOn(xy0)) - yInside = mTable[xy0].child->getLastValue() < 0; - zInside = yInside; - for (uint32_t z = 0; z != (1 << LOG2DIM); ++z) { - const uint32_t xyz = xy0 + z; // offset for block(x, y, z) - if (mChildMask.isOn(xyz)) { - zInside = mTable[xyz].child->getLastValue() < 0; - } else { - mTable[xyz].value = zInside ? -outside : outside; - } - } - } - } + void merge(LeafNode &other) + { + mValueMask |= other.mValueMask; } -} // Node::signedFloodFill + +}; // build::LeafNode //================================================================================================ -template -struct GridBuilder:: - BuildLeaf +template <> +struct LeafNode { - using ValueType = ValueT; - using BuildType = BuildT; + using ValueType = bool; + using BuildType = ValueMask; + using LeafNodeType = LeafNode; static constexpr uint32_t LOG2DIM = 3; static constexpr uint32_t TOTAL = LOG2DIM; // needed by parent nodes static constexpr uint32_t DIM = 1u << TOTAL; static constexpr uint32_t SIZE = 1u << 3 * LOG2DIM; // total number of voxels represented by this node - static constexpr int32_t MASK = DIM - 1; // mask for bit operations + static constexpr uint32_t MASK = DIM - 1; // mask for bit operations static constexpr uint32_t LEVEL = 0; // level 0 = leaf static constexpr uint64_t NUM_VALUES = uint64_t(1) << (3 * TOTAL); // total voxel count represented by this node using NodeMaskType = Mask; - using NanoLeafT = typename NanoNode::Type; + template + using MaskIterT = typename Mask::template Iterator; + using NanoLeafT = typename NanoNode::Type; Coord mOrigin; - Mask mValueMask; - ValueT mValues[SIZE]; + Mask mValueMask, mValues; union { NanoLeafT *mDstNode; uint64_t mDstOffset; }; - BuildLeaf(const Coord& ijk, const ValueT& value, bool state) + /// @brief Visits all active values in a leaf node + class ValueOnIterator : public MaskIterT + { + using BaseT = MaskIterT; + const LeafNode *mParent; + public: + ValueOnIterator() : BaseT(), mParent(nullptr) {} + ValueOnIterator(const LeafNode* parent) : BaseT(parent->mValueMask.beginOn()), mParent(parent) {} + ValueOnIterator& operator=(const ValueOnIterator&) = default; + bool operator*() const {NANOVDB_ASSERT(*this); return mParent->mValues.isOn(BaseT::pos());} + Coord getCoord() const { NANOVDB_ASSERT(*this); return mParent->offsetToGlobalCoord(BaseT::pos());} + }; // Member class ValueOnIterator + + ValueOnIterator beginValueOn() {return ValueOnIterator(this);} + ValueOnIterator cbeginValueOn() const {return ValueOnIterator(this);} + + /// @brief Visits all inactive values in a leaf node + class ValueOffIterator : public MaskIterT + { + using BaseT = MaskIterT; + const LeafNode *mParent; + public: + ValueOffIterator() : BaseT(), mParent(nullptr) {} + ValueOffIterator(const LeafNode* parent) : BaseT(parent->mValueMask.beginOff()), mParent(parent) {} + ValueOffIterator& operator=(const ValueOffIterator&) = default; + bool operator*() const {NANOVDB_ASSERT(*this); return mParent->mValues.isOn(BaseT::pos());} + Coord getCoord() const { NANOVDB_ASSERT(*this); return mParent->offsetToGlobalCoord(BaseT::pos());} + }; // Member class ValueOffIterator + + ValueOffIterator beginValueOff() {return ValueOffIterator(this);} + ValueOffIterator cbeginValueOff() const {return ValueOffIterator(this);} + + /// @brief Visits all values in a leaf node, i.e. both active and inactive values + class ValueIterator + { + const LeafNode *mParent; + uint32_t mPos; + public: + ValueIterator() : mParent(nullptr), mPos(1u << 3 * LOG2DIM) {} + ValueIterator(const LeafNode* parent) : mParent(parent), mPos(0) {NANOVDB_ASSERT(parent);} + ValueIterator& operator=(const ValueIterator&) = default; + bool operator*() const { NANOVDB_ASSERT(*this); return mParent->mValues.isOn(mPos);} + Coord getCoord() const { NANOVDB_ASSERT(*this); return mParent->offsetToGlobalCoord(mPos);} + bool isActive() const { NANOVDB_ASSERT(*this); return mParent->mValueMask.isOn(mPos);} + operator bool() const {return mPos < SIZE;} + ValueIterator& operator++() {++mPos; return *this;} + ValueIterator operator++(int) { + auto tmp = *this; + ++(*this); + return tmp; + } + }; // Member class ValueIterator + + ValueIterator beginValue() {return ValueIterator(this);} + ValueIterator cbeginValueAll() const {return ValueIterator(this);} + + LeafNode(const Coord& ijk, bool value, bool state) : mOrigin(ijk & ~MASK) - , mValueMask(state) //invalid + , mValueMask(state) + , mValues(value) , mDstOffset(0) { - ValueT* target = mValues; - uint32_t n = SIZE; - while (n--) { - *target++ = value; - } } - BuildLeaf(const BuildLeaf&) = delete; // disallow copy-construction - BuildLeaf(BuildLeaf&&) = delete; // disallow move construction - BuildLeaf& operator=(const BuildLeaf&) = delete; // disallow copy assignment - BuildLeaf& operator=(BuildLeaf&&) = delete; // disallow move assignment - ~BuildLeaf() = default; + LeafNode(const LeafNode&) = delete; // disallow copy-construction + LeafNode(LeafNode&&) = delete; // disallow move construction + LeafNode& operator=(const LeafNode&) = delete; // disallow copy assignment + LeafNode& operator=(LeafNode&&) = delete; // disallow move assignment + ~LeafNode() = default; + + const Mask& valueMask() const {return mValueMask;} + const Mask& getValueMask() const {return mValueMask;} + const Coord& origin() const {return mOrigin;} /// @brief Return the linear offset corresponding to the given coordinate static uint32_t CoordToOffset(const Coord& ijk) { - return ((ijk[0] & MASK) << (2 * LOG2DIM)) + ((ijk[1] & MASK) << LOG2DIM) + (ijk[2] & MASK); + return ((ijk[0] & int32_t(MASK)) << (2 * LOG2DIM)) + + ((ijk[1] & int32_t(MASK)) << LOG2DIM) + + (ijk[2] & int32_t(MASK)); } static Coord OffsetToLocalCoord(uint32_t n) { NANOVDB_ASSERT(n < SIZE); const int32_t m = n & ((1 << 2 * LOG2DIM) - 1); - return Coord(n >> 2 * LOG2DIM, m >> LOG2DIM, m & MASK); + return Coord(n >> 2 * LOG2DIM, m >> LOG2DIM, m & int32_t(MASK)); } void localToGlobalCoord(Coord& ijk) const @@ -1582,74 +1576,68 @@ struct GridBuilder:: Coord offsetToGlobalCoord(uint32_t n) const { - Coord ijk = BuildLeaf::OffsetToLocalCoord(n); + Coord ijk = LeafNode::OffsetToLocalCoord(n); this->localToGlobalCoord(ijk); return ijk; } + bool getFirstValue() const { return mValues.isOn(0); } + bool getLastValue() const { return mValues.isOn(SIZE - 1); } + bool getValue(uint32_t i) const {return mValues.isOn(i);} + bool getValue(const Coord& ijk) const + { + return mValues.isOn(CoordToOffset(ijk)); + } +#ifndef NANOVDB_NEW_ACCESSOR_METHODS template bool isActiveAndCache(const Coord& ijk, const AccT&) const { return mValueMask.isOn(CoordToOffset(ijk)); } - ValueT getFirstValue() const { return mValues[0]; } - ValueT getLastValue() const { return mValues[SIZE - 1]; } - - const ValueT& getValue(const Coord& ijk) const + template + bool getValueAndCache(const Coord& ijk, const AccT&) const { - return mValues[CoordToOffset(ijk)]; + return mValues.isOn(CoordToOffset(ijk)); } template - const ValueT& getValueAndCache(const Coord& ijk, const AccT&) const + void setValueAndCache(const Coord& ijk, bool value, const AccT&) { - return mValues[CoordToOffset(ijk)]; + const uint32_t n = CoordToOffset(ijk); + mValueMask.setOn(n); + mValues.setOn(n); } template - void setValueAndCache(const Coord& ijk, const ValueT& value, const AccT&) + void setValueOnAndCache(const Coord& ijk, const AccT&) { const uint32_t n = CoordToOffset(ijk); mValueMask.setOn(n); - mValues[n] = value; } +#endif - void setValue(const Coord& ijk, const ValueT& value) + void setValue(uint32_t n, bool value) { - const uint32_t n = CoordToOffset(ijk); mValueMask.setOn(n); - mValues[n] = value; + mValues.set(n, value); } + void setValue(const Coord& ijk, bool value) {return this->setValue(CoordToOffset(ijk), value);} - template - void getNodes(std::vector&) { NANOVDB_ASSERT(false); } - - template - void addNode(NodeT*&) {} - - template - uint32_t nodeCount() const + void merge(LeafNode &other) { - NANOVDB_ASSERT(false);// should never get called - return 1; + mValues |= other.mValues; + mValueMask |= other.mValueMask; } - template - typename std::enable_if::value>::type - signedFloodFill(T outside); - template - typename std::enable_if::value>::type - signedFloodFill(T) {} // no-op for none floating point values -}; // BuildLeaf +}; // build::LeafNode //================================================================================================ -template +template template inline typename std::enable_if::value>::type -GridBuilder::BuildLeaf:: - signedFloodFill(T outside) +LeafNode::signedFloodFill(T outside) { const uint32_t first = *mValueMask.beginOn(); if (first < SIZE) { @@ -1675,74 +1663,652 @@ GridBuilder::BuildLeaf:: } } } -} // BuildLeaf::signedFloodFill +} // build::LeafNode::signedFloodFill -//================================================================================================ +// ----------------------------> ValueAccessor <-------------------------------------- -template -struct GridBuilder:: - ValueAccessor +template +struct ValueAccessor { - ValueAccessor(SrcRootT& root) - : mKeys{Coord(Maximum::value()), Coord(Maximum::value()), Coord(Maximum::value())} - , mNode{nullptr, nullptr, nullptr, &root} + using ValueType = typename BuildToValueMap::type; + using LeafT = build::LeafNode; + using Node1 = build::InternalNode; + using Node2 = build::InternalNode; + using RootNodeType = build::RootNode; + using LeafNodeType = typename RootNodeType::LeafNodeType; + + ValueAccessor(RootNodeType& root) + : mRoot(root) + , mKeys{Coord(Maximum::value()), Coord(Maximum::value()), Coord(Maximum::value())} + , mNode{nullptr, nullptr, nullptr} { } + ValueAccessor(ValueAccessor&&) = default; // allow move construction + ValueAccessor(const ValueAccessor&) = delete; // disallow copy construction + ValueType getValue(int i, int j, int k) const {return this->getValue(Coord(i,j,k));} template bool isCached(const Coord& ijk) const { - return (ijk[0] & ~NodeT::MASK) == mKeys[NodeT::LEVEL][0] && - (ijk[1] & ~NodeT::MASK) == mKeys[NodeT::LEVEL][1] && - (ijk[2] & ~NodeT::MASK) == mKeys[NodeT::LEVEL][2]; + return (ijk[0] & int32_t(~NodeT::MASK)) == mKeys[NodeT::LEVEL][0] && + (ijk[1] & int32_t(~NodeT::MASK)) == mKeys[NodeT::LEVEL][1] && + (ijk[2] & int32_t(~NodeT::MASK)) == mKeys[NodeT::LEVEL][2]; + } + + template + auto get(const Coord& ijk, ArgsT&&... args) const + { + if (this->template isCached(ijk)) { + return ((const LeafT*)mNode[0])->template get(ijk, args...); + } else if (this->template isCached(ijk)) { + return ((const Node1*)mNode[1])->template getAndCache(ijk, *this, args...); + } else if (this->template isCached(ijk)) { + return ((const Node2*)mNode[2])->template getAndCache(ijk, *this, args...); + } + return mRoot.template getAndCache(ijk, *this, args...); + } + + template + auto set(const Coord& ijk, ArgsT&&... args) const + { + if (this->template isCached(ijk)) { + return ((LeafT*)mNode[0])->template set(ijk, args...); + } else if (this->template isCached(ijk)) { + return ((Node1*)mNode[1])->template setAndCache(ijk, *this, args...); + } else if (this->template isCached(ijk)) { + return ((Node2*)mNode[2])->template setAndCache(ijk, *this, args...); + } + return mRoot.template setAndCache(ijk, *this, args...); } - const ValueT& getValue(const Coord& ijk) + +#ifdef NANOVDB_NEW_ACCESSOR_METHODS + ValueType getValue(const Coord& ijk) const {return this->template get>(ijk);} + LeafT* setValue(const Coord& ijk, const ValueType& value) {return this->template set>(ijk, value);} + LeafT* setValueOn(const Coord& ijk) {return this->template set>(ijk);} + LeafT& touchLeaf(const Coord& ijk) {return this->template set>(ijk);} + bool isActive(const Coord& ijk) const {return this->template get>(ijk);} +#else + ValueType getValue(const Coord& ijk) const { - if (this->isCached(ijk)) { - return ((SrcNode0*)mNode[0])->getValueAndCache(ijk, *this); - } else if (this->isCached(ijk)) { - return ((SrcNode1*)mNode[1])->getValueAndCache(ijk, *this); - } else if (this->isCached(ijk)) { - return ((SrcNode2*)mNode[2])->getValueAndCache(ijk, *this); + if (this->template isCached(ijk)) { + return ((LeafT*)mNode[0])->getValueAndCache(ijk, *this); + } else if (this->template isCached(ijk)) { + return ((Node1*)mNode[1])->getValueAndCache(ijk, *this); + } else if (this->template isCached(ijk)) { + return ((Node2*)mNode[2])->getValueAndCache(ijk, *this); } - return ((SrcRootT*)mNode[3])->getValueAndCache(ijk, *this); + return mRoot.getValueAndCache(ijk, *this); } + /// @brief Sets value in a leaf node and returns it. - SrcNode0* setValue(const Coord& ijk, const ValueT& value) - { - if (this->isCached(ijk)) { - ((SrcNode0*)mNode[0])->setValueAndCache(ijk, value, *this); - } else if (this->isCached(ijk)) { - ((SrcNode1*)mNode[1])->setValueAndCache(ijk, value, *this); - } else if (this->isCached(ijk)) { - ((SrcNode2*)mNode[2])->setValueAndCache(ijk, value, *this); + LeafT* setValue(const Coord& ijk, const ValueType& value) + { + if (this->template isCached(ijk)) { + ((LeafT*)mNode[0])->setValueAndCache(ijk, value, *this); + } else if (this->template isCached(ijk)) { + ((Node1*)mNode[1])->setValueAndCache(ijk, value, *this); + } else if (this->template isCached(ijk)) { + ((Node2*)mNode[2])->setValueAndCache(ijk, value, *this); + } else { + mRoot.setValueAndCache(ijk, value, *this); + } + NANOVDB_ASSERT(this->isCached(ijk)); + return (LeafT*)mNode[0]; + } + void setValueOn(const Coord& ijk) + { + if (this->template isCached(ijk)) { + ((LeafT*)mNode[0])->setValueOnAndCache(ijk, *this); + } else if (this->template isCached(ijk)) { + ((Node1*)mNode[1])->setValueOnAndCache(ijk, *this); + } else if (this->template isCached(ijk)) { + ((Node2*)mNode[2])->setValueOnAndCache(ijk, *this); + } else { + mRoot.setValueOnAndCache(ijk, *this); + } + } + void touchLeaf(const Coord& ijk) const + { + if (this->template isCached(ijk)) { + return; + } else if (this->template isCached(ijk)) { + ((Node1*)mNode[1])->touchLeafAndCache(ijk, *this); + } else if (this->template isCached(ijk)) { + ((Node2*)mNode[2])->touchLeafAndCache(ijk, *this); } else { - ((SrcRootT*)mNode[3])->setValueAndCache(ijk, value, *this); + mRoot.touchLeafAndCache(ijk, *this); } - NANOVDB_ASSERT(this->isCached(ijk)); - return (SrcNode0*)mNode[0]; } - bool isActive(const Coord& ijk) + bool isActive(const Coord& ijk) const { - if (this->isCached(ijk)) { - return ((SrcNode0*)mNode[0])->isActiveAndCache(ijk, *this); - } else if (this->isCached(ijk)) { - return ((SrcNode1*)mNode[1])->isActiveAndCache(ijk, *this); - } else if (this->isCached(ijk)) { - return ((SrcNode2*)mNode[2])->isActiveAndCache(ijk, *this); + if (this->template isCached(ijk)) { + return ((LeafT*)mNode[0])->isActiveAndCache(ijk, *this); + } else if (this->template isCached(ijk)) { + return ((Node1*)mNode[1])->isActiveAndCache(ijk, *this); + } else if (this->template isCached(ijk)) { + return ((Node2*)mNode[2])->isActiveAndCache(ijk, *this); } - return ((SrcRootT*)mNode[3])->isActiveAndCache(ijk, *this); + return mRoot.isActiveAndCache(ijk, *this); } - bool isValueOn(const Coord& ijk) { return this->isActive(ijk); } +#endif + + bool isValueOn(const Coord& ijk) const { return this->isActive(ijk); } template - void insert(const Coord& ijk, NodeT* node) + void insert(const Coord& ijk, NodeT* node) const { mKeys[NodeT::LEVEL] = ijk & ~NodeT::MASK; mNode[NodeT::LEVEL] = node; } - Coord mKeys[3]; - void* mNode[4]; -}; // ValueAccessor + RootNodeType& mRoot; + mutable Coord mKeys[3]; + mutable void* mNode[3]; +}; // build::ValueAccessor + +// ----------------------------> Tree <-------------------------------------- + +template +struct Tree +{ + using ValueType = typename BuildToValueMap::type; + using Node0 = build::LeafNode; + using Node1 = build::InternalNode; + using Node2 = build::InternalNode; + using RootNodeType = build::RootNode; + using LeafNodeType = typename RootNodeType::LeafNodeType; + struct WriteAccessor; + + RootNodeType mRoot; + std::mutex mMutex; + + Tree(const ValueType &background) : mRoot(background) {} + Tree(const Tree&) = delete; // disallow copy construction + Tree(Tree&&) = delete; // disallow move construction + Tree& tree() {return *this;} + RootNodeType& root() {return mRoot;} + ValueType getValue(const Coord& ijk) const {return mRoot.getValue(ijk);} + ValueType getValue(int i, int j, int k) const {return this->getValue(Coord(i,j,k));} + void setValue(const Coord& ijk, const ValueType &value) {mRoot.setValue(ijk, value);} + std::array nodeCount() const + { + std::array count{0,0,0}; + mRoot.nodeCount(count); + return count; + } + /// @brief regular accessor for thread-safe reading and non-thread-safe writing + ValueAccessor getAccessor() { return ValueAccessor(mRoot); } + /// @brief special accessor for thread-safe writing only + WriteAccessor getWriteAccessor() { return WriteAccessor(mRoot, mMutex); } +};// build::Tree + +// ----------------------------> Tree::WriteAccessor <-------------------------------------- + +template +struct Tree::WriteAccessor +{ + using AccT = ValueAccessor; + using ValueType = typename AccT::ValueType; + using LeafT = typename AccT::LeafT; + using Node1 = typename AccT::Node1; + using Node2 = typename AccT::Node2; + using RootNodeType = typename AccT::RootNodeType; + + WriteAccessor(RootNodeType& parent, std::mutex &mx) + : mParent(parent) + , mRoot(parent.mBackground) + , mAcc(mRoot) + , mMutex(mx) + { + } + WriteAccessor(const WriteAccessor&) = delete; // disallow copy construction + WriteAccessor(WriteAccessor&&) = default; // allow move construction + ~WriteAccessor() { this->merge(); } + void merge() + { + mMutex.lock(); + mParent.merge(mRoot); + mMutex.unlock(); + } + inline void setValueOn(const Coord& ijk) {mAcc.setValueOn(ijk);} + inline void setValue(const Coord& ijk, const ValueType &value) {mAcc.setValue(ijk, value);} + + RootNodeType &mParent, mRoot; + AccT mAcc; + std::mutex &mMutex; +}; // build::Tree::WriteAccessor + +// ----------------------------> Grid <-------------------------------------- + +template +struct Grid : public Tree +{ + using BuildType = BuildT; + using ValueType = typename BuildToValueMap::type; + using TreeType = Tree; + using Node0 = build::LeafNode; + using Node1 = build::InternalNode; + using Node2 = build::InternalNode; + using RootNodeType = build::RootNode; + + GridClass mGridClass; + GridType mGridType; + Map mMap; + std::string mName; + + Grid(const ValueType &background, const std::string &name = "", GridClass gClass = GridClass::Unknown) + : TreeType(background) + , mGridClass(gClass) + , mGridType(mapToGridType()) + , mName(name) + { + mMap.set(1.0, Vec3d(0.0), 1.0); + } + TreeType& tree() {return *this;} + const GridType& gridType() const { return mGridType; } + const GridClass& gridClass() const { return mGridClass; } + const Map& map() const { return mMap; } + void setTransform(double scale=1.0, const Vec3d &translation = Vec3d(0.0)) {mMap.set(scale, translation, 1.0);} + const std::string& gridName() const { return mName; } + const std::string& getName() const { return mName; } + void setName(const std::string &name) { mName = name; } + /// @brief Sets grids values in domain of the @a bbox to those returned by the specified @a func with the + /// expected signature [](const Coord&)->ValueType. + /// + /// @note If @a func returns a value equal to the background value of the input grid at a + /// specific voxel coordinate, then the active state of that coordinate is off! Else the value + /// value is set and the active state is on. This is done to allow for sparse grids to be generated. + /// + /// @param func Functor used to evaluate the grid values in the @a bbox + /// @param bbox Coordinate bounding-box over which the grid values will be set. + /// @param delta Specifies a lower threshold value for rendering (optional). Typically equals the voxel size + /// for level sets and otherwise it's zero. + template + void operator()(const Func& func, const CoordBBox& bbox, ValueType delta = ValueType(0)); +};// build::Grid + +template +template +void Grid::operator()(const Func& func, const CoordBBox& bbox, ValueType delta) +{ + auto &root = this->tree().root(); +#if __cplusplus >= 201703L + static_assert(is_same::type>::value, "GridBuilder: mismatched ValueType"); +#else// invoke_result was introduced in C++17 and result_of was removed in C++20 + static_assert(is_same::type>::value, "GridBuilder: mismatched ValueType"); +#endif + const CoordBBox leafBBox(bbox[0] >> Node0::TOTAL, bbox[1] >> Node0::TOTAL); + std::mutex mutex; + forEach(leafBBox, [&](const CoordBBox& b) { + Node0* leaf = nullptr; + for (auto it = b.begin(); it; ++it) { + Coord min(*it << Node0::TOTAL), max(min + Coord(Node0::DIM - 1)); + const CoordBBox b(min.maxComponent(bbox.min()), + max.minComponent(bbox.max()));// crop + if (leaf == nullptr) { + leaf = new Node0(b[0], root.mBackground, false); + } else { + leaf->mOrigin = b[0] & ~Node0::MASK; + NANOVDB_ASSERT(leaf->mValueMask.isOff()); + } + leaf->mDstOffset = 0;// no prune + for (auto ijk = b.begin(); ijk; ++ijk) { + const auto v = func(*ijk);// call functor + if (v != root.mBackground) leaf->setValue(*ijk, v);// don't insert background values + } + if (!leaf->mValueMask.isOff()) {// has active values + if (leaf->mValueMask.isOn()) {// only active values + const auto first = leaf->getFirstValue(); + int n=1; + while (n<512) {// 8^3 = 512 + if (leaf->mValues[n++] != first) break; + } + if (n == 512) leaf->mDstOffset = 1;// prune below + } + std::lock_guard guard(mutex); + NANOVDB_ASSERT(leaf != nullptr); + root.addNode(leaf); + NANOVDB_ASSERT(leaf == nullptr); + } + }// loop over sub-part of leafBBox + if (leaf) delete leaf; + }); + + // Prune leaf and tile nodes + for (auto it2 = root.mTable.begin(); it2 != root.mTable.end(); ++it2) { + if (auto *upper = it2->second.child) {//upper level internal node + for (auto it1 = upper->mChildMask.beginOn(); it1; ++it1) { + auto *lower = upper->mTable[*it1].child;// lower level internal node + for (auto it0 = lower->mChildMask.beginOn(); it0; ++it0) { + auto *leaf = lower->mTable[*it0].child;// leaf nodes + if (leaf->mDstOffset) { + lower->mTable[*it0].value = leaf->getFirstValue(); + lower->mChildMask.setOff(*it0); + lower->mValueMask.setOn(*it0); + delete leaf; + } + }// loop over leaf nodes + if (lower->mChildMask.isOff()) {//only tiles + const auto first = lower->getFirstValue(); + int n=1; + while (n < 4096) {// 16^3 = 4096 + if (lower->mTable[n++].value != first) break; + } + if (n == 4096) {// identical tile values so prune + upper->mTable[*it1].value = first; + upper->mChildMask.setOff(*it1); + upper->mValueMask.setOn(*it1); + delete lower; + } + } + }// loop over lower internal nodes + if (upper->mChildMask.isOff()) {//only tiles + const auto first = upper->getFirstValue(); + int n=1; + while (n < 32768) {// 32^3 = 32768 + if (upper->mTable[n++].value != first) break; + } + if (n == 32768) {// identical tile values so prune + it2->second.value = first; + it2->second.state = upper->mValueMask.isOn(); + it2->second.child = nullptr; + delete upper; + } + } + }// is child node of the root + }// loop over root table +}// build::Grid::operator() + +//================================================================================================ + +template +using BuildLeaf = LeafNode; +template +using BuildLower = InternalNode>; +template +using BuildUpper = InternalNode>; +template +using BuildRoot = RootNode>; +template +using BuildTile = typename BuildRoot::Tile; + +using FloatGrid = Grid; +using Fp4Grid = Grid; +using Fp8Grid = Grid; +using Fp16Grid = Grid; +using FpNGrid = Grid; +using DoubleGrid = Grid; +using Int32Grid = Grid; +using UInt32Grid = Grid; +using Int64Grid = Grid; +using Vec3fGrid = Grid; +using Vec3dGrid = Grid; +using Vec4fGrid = Grid; +using Vec4dGrid = Grid; +using MaskGrid = Grid; +using IndexGrid = Grid; +using OnIndexGrid = Grid; +using BoolGrid = Grid; + +// ----------------------------> NodeManager <-------------------------------------- + +// GridT can be openvdb::Grid and nanovdb::build::Grid +template +class NodeManager +{ +public: + + using ValueType = typename GridT::ValueType; + using BuildType = typename GridT::BuildType; + using GridType = GridT; + using TreeType = typename GridT::TreeType; + using RootNodeType = typename TreeType::RootNodeType; + static_assert(RootNodeType::LEVEL == 3, "NodeManager expected LEVEL=3"); + using Node2 = typename RootNodeType::ChildNodeType; + using Node1 = typename Node2::ChildNodeType; + using Node0 = typename Node1::ChildNodeType; + + NodeManager(GridT &grid) : mGrid(grid) {this->init();} + void init() + { + mArray0.clear(); + mArray1.clear(); + mArray2.clear(); + auto counts = mGrid.tree().nodeCount(); + mArray0.reserve(counts[0]); + mArray1.reserve(counts[1]); + mArray2.reserve(counts[2]); + + for (auto it2 = mGrid.tree().root().cbeginChildOn(); it2; ++it2) { + Node2 &upper = const_cast(*it2); + mArray2.emplace_back(&upper); + for (auto it1 = upper.cbeginChildOn(); it1; ++it1) { + Node1 &lower = const_cast(*it1); + mArray1.emplace_back(&lower); + for (auto it0 = lower.cbeginChildOn(); it0; ++it0) { + Node0 &leaf = const_cast(*it0); + mArray0.emplace_back(&leaf); + }// loop over leaf nodes + }// loop over lower internal nodes + }// loop over root node + } + + /// @brief Return the number of tree nodes at the specified level + /// @details 0 is leaf, 1 is lower internal, and 2 is upper internal level + uint64_t nodeCount(int level) const + { + NANOVDB_ASSERT(level==0 || level==1 || level==2); + return level==0 ? mArray0.size() : level==1 ? mArray1.size() : mArray2.size(); + } + + template + typename enable_if::type node(int i) {return *mArray0[i];} + template + typename enable_if::type node(int i) const {return *mArray0[i];} + template + typename enable_if::type node(int i) {return *mArray1[i];} + template + typename enable_if::type node(int i) const {return *mArray1[i];} + template + typename enable_if::type node(int i) {return *mArray2[i];} + template + typename enable_if::type node(int i) const {return *mArray2[i];} + + /// @brief Return the i'th leaf node with respect to breadth-first ordering + const Node0& leaf(uint32_t i) const { return *mArray0[i]; } + Node0& leaf(uint32_t i) { return *mArray0[i]; } + uint64_t leafCount() const {return mArray0.size();} + + /// @brief Return the i'th lower internal node with respect to breadth-first ordering + const Node1& lower(uint32_t i) const { return *mArray1[i]; } + Node1& lower(uint32_t i) { return *mArray1[i]; } + uint64_t lowerCount() const {return mArray1.size();} + + /// @brief Return the i'th upper internal node with respect to breadth-first ordering + const Node2& upper(uint32_t i) const { return *mArray2[i]; } + Node2& upper(uint32_t i) { return *mArray2[i]; } + uint64_t upperCount() const {return mArray2.size();} + + RootNodeType& root() {return mGrid.tree().root();} + const RootNodeType& root() const {return mGrid.tree().root();} + + TreeType& tree() {return mGrid.tree();} + const TreeType& tree() const {return mGrid.tree();} + + GridType& grid() {return mGrid;} + const GridType& grid() const {return mGrid;} + +protected: + + GridT &mGrid; + std::vector mArray0; // leaf nodes + std::vector mArray1; // lower internal nodes + std::vector mArray2; // upper internal nodes + +};// NodeManager + +template +typename enable_if::value>::type +sdfToLevelSet(NodeManagerT &mgr) +{ + mgr.grid().mGridClass = GridClass::LevelSet; + // Note that the bottom-up flood filling is essential + const auto outside = mgr.root().mBackground; + forEach(0, mgr.leafCount(), 8, [&](const Range1D& r) { + for (auto i = r.begin(); i != r.end(); ++i) mgr.leaf(i).signedFloodFill(outside); + }); + forEach(0, mgr.lowerCount(), 1, [&](const Range1D& r) { + for (auto i = r.begin(); i != r.end(); ++i) mgr.lower(i).signedFloodFill(outside); + }); + forEach(0, mgr.upperCount(), 1, [&](const Range1D& r) { + for (auto i = r.begin(); i != r.end(); ++i) mgr.upper(i).signedFloodFill(outside); + }); + mgr.root().signedFloodFill(outside); +}// sdfToLevelSet + +template +void levelSetToFog(NodeManagerT &mgr, bool rebuild = true) +{ + using ValueType = typename NodeManagerT::ValueType; + mgr.grid().mGridClass = GridClass::FogVolume; + const ValueType d = -mgr.root().mBackground, w = 1.0f / d; + std::atomic_bool prune{false}; + auto op = [&](ValueType& v) -> bool { + if (v > ValueType(0)) { + v = ValueType(0); + return false; + } + v = v > d ? v * w : ValueType(1); + return true; + }; + forEach(0, mgr.leafCount(), 8, [&](const Range1D& r) { + for (auto i = r.begin(); i != r.end(); ++i) { + auto& leaf = mgr.leaf(i); + for (uint32_t i = 0; i < 512u; ++i) leaf.mValueMask.set(i, op(leaf.mValues[i])); + } + }); + forEach(0, mgr.lowerCount(), 1, [&](const Range1D& r) { + for (auto i = r.begin(); i != r.end(); ++i) { + auto& node = mgr.lower(i); + for (uint32_t i = 0; i < 4096u; ++i) { + if (node.mChildMask.isOn(i)) { + auto* leaf = node.mTable[i].child; + if (leaf->mValueMask.isOff()) {// prune leaf node + node.mTable[i].value = leaf->getFirstValue(); + node.mChildMask.setOff(i); + delete leaf; + prune = true; + } + } else { + node.mValueMask.set(i, op(node.mTable[i].value)); + } + } + } + }); + forEach(0, mgr.upperCount(), 1, [&](const Range1D& r) { + for (auto i = r.begin(); i != r.end(); ++i) { + auto& node = mgr.upper(i); + for (uint32_t i = 0; i < 32768u; ++i) { + if (node.mChildMask.isOn(i)) {// prune lower internal node + auto* child = node.mTable[i].child; + if (child->mChildMask.isOff() && child->mValueMask.isOff()) { + node.mTable[i].value = child->getFirstValue(); + node.mChildMask.setOff(i); + delete child; + prune = true; + } + } else { + node.mValueMask.set(i, op(node.mTable[i].value)); + } + } + } + }); + + for (auto it = mgr.root().mTable.begin(); it != mgr.root().mTable.end(); ++it) { + auto* child = it->second.child; + if (child == nullptr) { + it->second.state = op(it->second.value); + } else if (child->mChildMask.isOff() && child->mValueMask.isOff()) { + it->second.value = child->getFirstValue(); + it->second.state = false; + it->second.child = nullptr; + delete child; + prune = true; + } + } + if (rebuild && prune) mgr.init(); +}// levelSetToFog + +// ----------------------------> Implementations of random access methods <-------------------------------------- + +template +struct TouchLeaf { + static BuildLeaf& set(BuildLeaf &leaf, uint32_t) {return leaf;} +};// TouchLeaf + +/// @brief Implements Tree::getValue(Coord), i.e. return the value associated with a specific coordinate @c ijk. +/// @tparam BuildT Build type of the grid being called +/// @details The value at a coordinate maps to the background, a tile value or a leaf value. +template +struct GetValue { + static auto get(const BuildRoot &root) {return root.mBackground;} + static auto get(const BuildTile &tile) {return tile.value;} + static auto get(const BuildUpper &node, uint32_t n) {return node.mTable[n].value;} + static auto get(const BuildLower &node, uint32_t n) {return node.mTable[n].value;} + static auto get(const BuildLeaf &leaf, uint32_t n) {return leaf.getValue(n);} +};// GetValue + +/// @brief Implements Tree::isActive(Coord) +/// @tparam T Build type of the grid being called +template +struct GetState { + static bool get(const BuildRoot&) {return false;} + static bool get(const BuildTile &tile) {return tile.state;} + static bool get(const BuildUpper &node, uint32_t n) {return node.mValueMask.isOn(n);} + static bool get(const BuildLower &node, uint32_t n) {return node.mValueMask.isOn(n);} + static bool get(const BuildLeaf &leaf, uint32_t n) {return leaf.mValueMask.isOn(n);} +};// GetState + +/// @brief Set the value and its state at the leaf level mapped to by ijk, and create the leaf node and branch if needed. +/// @tparam T BuildType of the corresponding tree +template +struct SetValue { + static BuildLeaf* set(BuildLeaf &leaf, uint32_t n) { + leaf.mValueMask.setOn(n);// always set the active bit + return &leaf; + } + static BuildLeaf* set(BuildLeaf &leaf, uint32_t n, const typename BuildLeaf::ValueType &v) { + leaf.setValue(n, v); + return &leaf; + } +};// SetValue + +/// @brief Implements Tree::probeLeaf(Coord) +/// @tparam T Build type of the grid being called +template +struct ProbeValue { + using ValueT = typename BuildLeaf::ValueType; + static bool get(const BuildRoot &root, ValueT &v) { + v = root.mBackground; + return false; + } + static bool get(const BuildTile &tile, ValueT &v) { + v = tile.value; + return tile.state; + } + static bool get(const BuildUpper &node, uint32_t n, ValueT &v) { + v = node.mTable[n].value; + return node.mValueMask.isOn(n); + } + static bool get(const BuildLower &node, uint32_t n, ValueT &v) { + v = node.mTable[n].value; + return node.mValueMask.isOn(n); + } + static bool get(const BuildLeaf &leaf, uint32_t n, ValueT &v) { + v = leaf.getValue(n); + return leaf.isActive(n); + } +};// ProbeValue + +} // namespace build } // namespace nanovdb -#endif // NANOVDB_GRIDBUILDER_H_HAS_BEEN_INCLUDED +#endif // NANOVDB_GRID_BUILDER_H_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/GridChecksum.h b/nanovdb/nanovdb/util/GridChecksum.h index e5672364b8..531a6f674b 100644 --- a/nanovdb/nanovdb/util/GridChecksum.h +++ b/nanovdb/nanovdb/util/GridChecksum.h @@ -6,7 +6,7 @@ \author Ken Museth - \brief Computes a pair of 32bit checksums, og a Grid, by means of Cyclic Redundancy Check (CRC) + \brief Computes a pair of 32bit checksums, of a Grid, by means of Cyclic Redundancy Check (CRC) \details A CRC32 is the 32 bit remainder, or residue, of binary division of a message, by a polynomial. */ @@ -21,12 +21,17 @@ #include // offsetof macro #include #include +#include // for std::unique_ptr -#include "../NanoVDB.h" -#include "GridHandle.h" +#include #include "ForEach.h" #include "NodeManager.h" +// Define log of block size for FULL CRC32 computation. +// A value of 12 corresponds to a block size of 4KB (2^12 = 4096). +// Undefine to use old checksum computation +#define NANOVDB_CRC32_LOG2_BLOCK_SIZE 12 + namespace nanovdb { /// @brief List of different modes for computing for a checksum @@ -36,227 +41,288 @@ enum class ChecksumMode : uint32_t { Disable = 0,// no computation Default = 1,// defaults to Partial End = 3 };// marks the end of the enum list -/// @brief Return the (2 x CRC32) checksum of the specified @a grid -/// +/// @brief Return the (2 x CRC32) checksum of the specified @a grid +/// @tparam BuildT Template parameter used to build NanoVDB grid. /// @param grid Grid from which the checksum is computed. /// @param mode Defines the mode of computation for the checksum. -template -uint64_t checksum(const NanoGrid &grid, ChecksumMode mode = ChecksumMode::Default); +/// @return Return the (2 x CRC32) checksum of the specified @a grid +template +uint64_t checksum(const NanoGrid &grid, ChecksumMode mode = ChecksumMode::Default); /// @brief Return true if the checksum of the @a grid matches the expected /// value already encoded into the grid's meta data. -/// +/// @tparam BuildT Template parameter used to build NanoVDB grid. /// @param grid Grid whose checksum is validated. /// @param mode Defines the mode of computation for the checksum. -template -bool validateChecksum(const NanoGrid &grid, ChecksumMode mode = ChecksumMode::Default); +template +bool validateChecksum(const NanoGrid &grid, ChecksumMode mode = ChecksumMode::Default); /// @brief Updates the checksum of a grid /// /// @param grid Grid whose checksum will be updated. /// @param mode Defines the mode of computation for the checksum. -template -void updateChecksum(NanoGrid &grid, ChecksumMode mode = ChecksumMode::Default); - -/// @brief Return the CRC32 checksum of the raw @a data of @a size -/// @param data The beginning of the raw data. -/// @param size Size of the data to bytes! -inline std::uint_fast32_t crc32(const void *data, size_t size); - -/// @brief Return the CRC32 checksum of the content pointed to be the iterator -/// @param begin Beginning of the iterator range -/// @param end End of the iterator range (exclusive) -/// @warning The dereference of the iterator must be convertible to a uint8_t -template -std::uint_fast32_t crc32(IterT begin, IterT end); - -/// @brief Class that computes the Cyclic Redundancy Check (CRC) -class CRC32 -{ - using ByteT = std::uint_fast8_t; - using HashT = std::uint_fast32_t; - HashT mChecksum; +template +void updateChecksum(NanoGrid &grid, ChecksumMode mode = ChecksumMode::Default); - static std::array INIT() - { - HashT n = 0; - auto kernel = [&n]()->HashT{ - HashT checksum = n++; - for (int i = 0; i < 8; ++i) checksum = (checksum >> 1) ^ ((checksum & 0x1u) ? HashT{0xEDB88320uL} : 0); - return checksum; - }; - std::array LUT{}; - std::generate(LUT.begin(), LUT.end(), kernel); - return LUT;// move semantic should prevent a deep copy - } +namespace crc32 { -public: - - static const HashT EMPTY = ~HashT{0} & HashT{0xFFFFFFFFuL};// All bits are on - - CRC32() : mChecksum(EMPTY) {} - - void reset() { mChecksum = EMPTY; } +/// @brief Initiate single entry in look-up-table for CRC32 computations +/// @param lut pointer of size 256 for look-up-table +/// @param n entry in table (assumed n < 256) +inline __hostdev__ void initLut(uint32_t lut[256], uint32_t n) +{ + uint32_t &cs = lut[n] = n; + for (int i = 0; i < 8; ++i) cs = (cs >> 1) ^ ((cs & 1) ? 0xEDB88320 : 0); +} - HashT checksum() const { return HashT{0xFFFFFFFFuL} & ~mChecksum; } +/// @brief Initiate entire look-up-table for CRC32 computations +/// @param lut pointer of size 256 for look-up-table +inline __hostdev__ void initLut(uint32_t lut[256]){for (uint32_t n = 0u; n < 256u; ++n) initLut(lut, n);} - template - void operator()(IterT begin, IterT end) - { - static const auto LUT = INIT();// scoped static initialization is thread-safe since C++11 - auto kernel = [](HashT checksum, ByteT value){return LUT[(checksum ^ value) & 0xFFu] ^ (checksum >> 8);}; - mChecksum = std::accumulate(begin, end, mChecksum, kernel); - } +/// @brief Create and initiate entire look-up-table for CRC32 computations +/// @return returns a unique pointer to the lookup table of size 256. +inline std::unique_ptr createLut() +{ + std::unique_ptr lut(new uint32_t[256]); + initLut(lut.get()); + return lut; +} - void operator()(const void *data, size_t byteSize) - { - const ByteT *begin = static_cast(data); - this->operator()(begin, begin + byteSize); +/// @brief Compute crc32 checksum of @c data of @c size bytes (without a lookup table)) +/// @param data pointer to beginning of data +/// @param size byte size of data +/// @param crc initial value of crc32 checksum +/// @return return crc32 checksum of @c data +inline __hostdev__ uint32_t checksum(const void* data, size_t size, uint32_t crc = 0) +{ + crc = ~crc; + for (auto *p = (const uint8_t*)data, *q = p + size; p != q; ++p) { + crc ^= *p; + for (int j = 0; j < 8; ++j) crc = (crc >> 1) ^ (0xEDB88320 & (-(crc & 1))); } + return ~crc; +} - template - void operator()(const T &data) {(*this)(&data, sizeof(T));} -};// CRC32 +/// @brief Compute crc32 checksum of data between @c begin and @c end +/// @param begin points to beginning of data +/// @param end points to end of @data, (exclusive) +/// @param crc initial value of crc32 checksum +/// @return return crc32 checksum +inline __hostdev__ uint32_t checksum(const void *begin, const void *end, uint32_t crc = 0) +{ + NANOVDB_ASSERT(begin && end); + NANOVDB_ASSERT(end >= begin); + return checksum(begin, (const char*)end - (const char*)begin, crc); +} -inline std::uint_fast32_t crc32(const void *data, size_t byteSize) +/// @brief Compute crc32 checksum of @c data with @c size bytes using a lookup table +/// @param data pointer to begenning of data +/// @param size byte size +/// @param lut pointer to loopup table for accelerated crc32 computation +/// @param crc initial value of the checksum +/// @return crc32 checksum of @c data with @c size bytes +inline __hostdev__ uint32_t checksum(const void *data, size_t size, const uint32_t lut[256], uint32_t crc = 0) { - CRC32 crc; - crc(data, byteSize); - return crc.checksum(); + crc = ~crc; + for (auto *p = (const uint8_t*)data, *q = p + size; p != q; ++p) crc = lut[(crc ^ *p) & 0xFF] ^ (crc >> 8); + return ~crc; } -template -inline std::uint_fast32_t crc32(IterT begin, IterT end) +/// @brief Compute crc32 checksum of data between @c begin and @c end using a lookup table +/// @param begin points to beginning of data +/// @param end points to end of @data, (exclusive) +/// @param lut pointer to loopup table for accelerated crc32 computation +/// @param crc initial value of crc32 checksum +/// @return return crc32 checksum +inline __hostdev__ uint32_t checksum(const void *begin, const void *end, const uint32_t lut[256], uint32_t crc = 0) { - CRC32 crc; - crc(begin, end); - return crc.checksum(); + NANOVDB_ASSERT(begin && end); + NANOVDB_ASSERT(end >= begin); + return checksum(begin, (const char*)end - (const char*)begin, lut, crc); } +}// namespace crc32 + /// @brief Class that encapsulates two CRC32 checksums, one for the Grid, Tree and Root node meta data /// and one for the remaining grid nodes. class GridChecksum { - union {uint32_t mCRC[2]; uint64_t mChecksum; }; + /// Three types of checksums: + /// 1) Empty: all 64 bits are on (used to signify no checksum) + /// 2) Partial: Upper 32 bits are on and not all of lower 32 bits are on (lower 32 bits checksum head of grid) + /// 3) Full: Not all of the 64 bits are one (lower 32 bits checksum head of grid and upper 32 bits checksum tail of grid) + union {uint32_t mCRC[2]; uint64_t mChecksum; };// mCRC[0] is checksum of Grid, Tree and Root, and mCRC[1] is checksum of nodes + static constexpr uint32_t EMPTY32 = ~uint32_t{0}; public: - static const uint64_t EMPTY = (static_cast(CRC32::EMPTY) << 32) | static_cast(CRC32::EMPTY); + static constexpr uint64_t EMPTY = ~uint64_t(0); - GridChecksum() : mCRC{CRC32::EMPTY, CRC32::EMPTY} {} + /// @brief default constructor initiates checksum to EMPTY + GridChecksum() : mCRC{EMPTY32, EMPTY32} {} + /// @brief Constructor that allows the two 32bit checksums to be initiated explicitly + /// @param head Initial 32bit CRC checksum of grid, tree and root data + /// @param tail Initial 32bit CRC checksum of all the nodes and blind data GridChecksum(uint32_t head, uint32_t tail) : mCRC{head, tail} {} + /// @brief + /// @param checksum + /// @param mode GridChecksum(uint64_t checksum, ChecksumMode mode = ChecksumMode::Full) : mChecksum{mode == ChecksumMode::Disable ? EMPTY : checksum} { - if (mode == ChecksumMode::Partial) mCRC[1] = CRC32::EMPTY; + if (mode == ChecksumMode::Partial) mCRC[1] = EMPTY32; } + /// @brief return the 64 bit checksum of this instance uint64_t checksum() const { return mChecksum; } - uint32_t crc32(int i) const {assert(i==0 || i==1); return mCRC[i]; } + /// @brief return 32 bit (crc32) checksum of this instance + /// @param i index of value 0 or 1 indicated the 32 bit checksum of the head or nodes + /// @return non-const reference of the i'th 32bit checksum + uint32_t& checksum(int i) {NANOVDB_ASSERT(i==0 || i==1); return mCRC[i]; } + + /// @brief return 32 bit (crc32) checksum of this instance + /// @param i index of value 0 or 1 indicated the 32 bit checksum of the head or nodes + /// @return copy of the i'th 32bit checksum + uint32_t checksum(int i) const {NANOVDB_ASSERT(i==0 || i==1); return mCRC[i]; } - bool isFull() const { return mCRC[0] != CRC32::EMPTY && mCRC[1] != CRC32::EMPTY; } + /// @brief return true if the 64 bit checksum is partial, i.e. of head only + bool isPartial() const { return mCRC[0] != EMPTY32 && mCRC[1] == EMPTY32; } + /// @brief return true if the 64 bit checksum is fill, i.e. of both had and nodes + bool isFull() const { return mCRC[0] != EMPTY32 && mCRC[1] != EMPTY32; } + + /// @brief return true if the 64 bit checksum is disables (unset) bool isEmpty() const { return mChecksum == EMPTY; } + /// @brief return the mode of the 64 bit checksum ChecksumMode mode() const { return mChecksum == EMPTY ? ChecksumMode::Disable : - mCRC[1] == CRC32::EMPTY ? ChecksumMode::Partial : ChecksumMode::Full; + mCRC[1] == EMPTY32 ? ChecksumMode::Partial : ChecksumMode::Full; } - +#ifdef NANOVDB_CRC32_LOG2_BLOCK_SIZE + /// @brief compute checksum of @c gridData using a 4KB blocked approach + /// @param gridData Reference to GridData + /// @param mode Mode of the checksum computation + ChecksumMode operator()(const GridData &gridData, ChecksumMode mode = ChecksumMode::Full); +#else + /// @brief Compute checksum using old (node-based) approach + /// @tparam ValueT Build type of the grid + /// @param grid Reference to Grid + /// @param mode Mode of the checksum computation template void operator()(const NanoGrid &grid, ChecksumMode mode = ChecksumMode::Full); - +#endif + /// @brief return true if the checksums are identical + /// @param rhs other GridChecksum bool operator==(const GridChecksum &rhs) const {return mChecksum == rhs.mChecksum;} + + /// @brief return true if the checksums are not identical + /// @param rhs other GridChecksum bool operator!=(const GridChecksum &rhs) const {return mChecksum != rhs.mChecksum;} };// GridChecksum // [GridData][TreeData]---[RootData][ROOT TILES...]---[NodeData<5>]---[NodeData<4>]---[LeafData<3>]---[BLINDMETA...]---[BLIND0]---[BLIND1]---etc. + +#ifdef NANOVDB_CRC32_LOG2_BLOCK_SIZE + +inline ChecksumMode GridChecksum::operator()(const GridData &gridData, ChecksumMode mode) +{ + mChecksum = EMPTY; + + if (mode == ChecksumMode::Disable) return ChecksumMode::Disable; + + auto lut = crc32::createLut(); + const uint8_t *begin = (const uint8_t*)(&gridData), *mid = gridData.template nodePtr<2>(), *end = begin + gridData.mGridSize;// what about empty grids? + if (mid == nullptr) {// no (upper) nodes + if (gridData.mBlindMetadataCount) { + mid = begin + gridData.mBlindMetadataOffset;// exclude blind data from Partial checksum + } else { + mid = end;// no nodes or blind data, so Partial checksum is computed on the entire grid buffer + } + } + mCRC[0] = crc32::checksum(begin + 16, mid, lut.get());// GridData, TreeData. RootData but exclude GridData::mMagic and GridData::mChecksum + + if (mode != ChecksumMode::Full || mid == end) return ChecksumMode::Partial; + + uint64_t size = end - mid;// includes blind data + const uint64_t blockCount = size >> NANOVDB_CRC32_LOG2_BLOCK_SIZE;// number of 4 KB (4096 byte) blocks + std::unique_ptr checksums(new uint32_t[blockCount]); + forEach(0, blockCount, 64, [&](const Range1D &r) { + uint32_t blockSize = 1 << NANOVDB_CRC32_LOG2_BLOCK_SIZE; + uint32_t *p = checksums.get() + r.begin(); + for (auto i = r.begin(); i != r.end(); ++i) { + if (i+1 == blockCount) blockSize += size - (blockCount< void GridChecksum::operator()(const NanoGrid &grid, ChecksumMode mode) { // Validate the assumed memory layout -#if 0 - NANOVDB_ASSERT(NANOVDB_OFFSETOF(GridData, mMagic) == 0); - NANOVDB_ASSERT(NANOVDB_OFFSETOF(GridData, mChecksum) == 8); - NANOVDB_ASSERT(NANOVDB_OFFSETOF(GridData, mVersion) == 16); -#else// the static asserts below generate compiler warnings static_assert(offsetof(GridData, mMagic) == 0, "Unexpected offset to magic number"); static_assert(offsetof(GridData, mChecksum) == 8, "Unexpected offset to checksum"); static_assert(offsetof(GridData, mVersion) == 16, "Unexpected offset to version number"); -#endif - static const size_t offset = 16; mChecksum = EMPTY; if (mode == ChecksumMode::Disable) return; - const auto &tree = grid.tree(); - const auto &root = tree.root(); - CRC32 crc; + auto lut = crc32::createLut(); + const uint8_t *begin = reinterpret_cast(&grid), *mid = grid.template nodePtr<2>(); - // process Grid + Tree + Root but exclude mMagic and mChecksum - const uint8_t *begin = reinterpret_cast(&grid); - const uint8_t *end = begin + grid.memUsage() + tree.memUsage() + root.memUsage(); - crc(begin + offset, end); + mCRC[0] = crc32::checksum(begin + 16, mid, lut.get());// process Grid + Tree + Root but exclude mMagic and mChecksum - mCRC[0] = crc.checksum(); - - if (mode == ChecksumMode::Partial || tree.isEmpty()) return; + if (mode != ChecksumMode::Full || grid.isEmpty()) return; + const auto &tree = grid.tree(); + const auto &root = tree.root(); auto nodeMgrHandle = createNodeManager(grid); auto *nodeMgr = nodeMgrHandle.template mgr(); assert(isValid(nodeMgr)); const auto nodeCount = tree.nodeCount(0) + tree.nodeCount(1) + tree.nodeCount(2); - std::vector checksums(nodeCount, 0); - + std::vector checksums(nodeCount, 0); // process upper internal nodes auto kernel2 = [&](const Range1D &r) { - CRC32 local; - std::uint_fast32_t *p = checksums.data() + r.begin(); + uint32_t *p = checksums.data() + r.begin(); for (auto i = r.begin(); i != r.end(); ++i) { const auto &node = nodeMgr->upper(static_cast(i)); - local(node); - *p++ = local.checksum(); - local.reset(); + *p++ = crc32::checksum(&node, node.memUsage(), lut.get()); } }; - // process lower internal nodes auto kernel1 = [&](const Range1D &r) { - CRC32 local; - std::uint_fast32_t *p = checksums.data() + r.begin() + tree.nodeCount(2); + uint32_t *p = checksums.data() + r.begin() + tree.nodeCount(2); for (auto i = r.begin(); i != r.end(); ++i) { const auto &node = nodeMgr->lower(static_cast(i)); - local(node); - *p++ = local.checksum(); - local.reset(); + *p++ = crc32::checksum(&node, node.memUsage(), lut.get()); } }; - // process leaf nodes auto kernel0 = [&](const Range1D &r) { - CRC32 local; - std::uint_fast32_t *p = checksums.data() + r.begin() + tree.nodeCount(1) + tree.nodeCount(2); + uint32_t *p = checksums.data() + r.begin() + tree.nodeCount(1) + tree.nodeCount(2); for (auto i = r.begin(); i != r.end(); ++i) { const auto &leaf = nodeMgr->leaf(static_cast(i)); - local(leaf); - *p++ = local.checksum(); - local.reset(); + *p++ = crc32::checksum(&leaf, leaf.memUsage(), lut.get()); } }; - forEach(0, tree.nodeCount(2), 1, kernel2); forEach(0, tree.nodeCount(1), 1, kernel1); forEach(0, tree.nodeCount(0), 8, kernel0); - - crc.reset(); - crc(checksums.data(), sizeof(std::uint_fast32_t)*checksums.size() ); - mCRC[1] = crc.checksum(); + mCRC[1] = crc32::checksum(checksums.data(), sizeof(uint32_t)*checksums.size(), lut.get()); }// GridChecksum::operator() +#endif// NANOVDB_CRC32_LOG2_BLOCK_SIZE + template uint64_t checksum(const NanoGrid &grid, ChecksumMode mode) { @@ -281,6 +347,116 @@ void updateChecksum(NanoGrid &grid, ChecksumMode mode) grid.data()->mChecksum = cs.checksum(); } +inline bool updateChecksum(GridData &gridData, ChecksumMode mode) +{ +#ifdef NANOVDB_CRC32_LOG2_BLOCK_SIZE + GridChecksum cs; + cs(gridData, mode); + gridData.mChecksum = cs.checksum(); +#else + if (mode == ChecksumMode::Disable) return false; + switch (data->mGridType){ + case GridType::Float: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::Double: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::Int16: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::Int32: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::Int64: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::Vec3f: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::Vec3d: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::UInt32: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::Mask: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::Index: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::OnIndex: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::IndexMask: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::OnIndexMask: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::Boolean: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::RGBA8: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::Fp4: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::Fp8: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::Fp16: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::FpN: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::Vec4f: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + case GridType::Vec4d: + updateChecksum(*reinterpret_cast*>(data), mode); + break; + default: { + std::stringstream ss; + ss << "Cannot update checksum for grid of unknown type \"" << toStr(data->mGridType); + throw std::runtime_error(ss.str() + "\""); + } + }// switch +#endif + return true; +}// updateChecksum(GridData *data, ChecksumMode mode) + +/// @brief Preserve the existing mode of the checksum and update it if it's not disabled +/// @param data +/// @return +inline bool updateChecksum(GridData *data) +{ + GridChecksum cs(data->mChecksum); + const auto mode = cs.mode(); + return updateChecksum(*data, mode); +}// updateChecksum(GridData *data) + +/// @brief Updates the ground index and count, as well as the partial checksum if needed +/// @param data Pointer to grid data +/// @param gridIndex New value of the index +/// @param gridCount New value of the grid count +/// @return returns true if the checksum was updated +inline bool updateGridCount(GridData *data, uint32_t gridIndex, uint32_t gridCount) +{ + NANOVDB_ASSERT(gridIndex < gridCount); + if (data->mGridIndex == gridIndex && data->mGridCount == gridCount) return false;// nothing to update + data->mGridIndex = gridIndex; + data->mGridCount = gridCount; + GridChecksum cs(data->mChecksum); + if (cs.isEmpty()) return false;// no checksum to update + updateChecksum(*data, ChecksumMode::Partial);// only update the checksum of the grid since we only modified the GridData + reinterpret_cast(&(data->mChecksum))->checksum(1) = cs.checksum(1);// copy the old checksum of the tree nodes since it was set to EMPTY during the update + return true; +} + } // namespace nanovdb #endif // NANOVDB_GRIDCHECKSUM_H_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/GridHandle.h b/nanovdb/nanovdb/util/GridHandle.h index 7414314311..4ccd019787 100644 --- a/nanovdb/nanovdb/util/GridHandle.h +++ b/nanovdb/nanovdb/util/GridHandle.h @@ -8,102 +8,89 @@ \date January 8, 2020 - \brief Defines two classes, a GridRegister the defines the value type (e.g. Double, Float etc) - of a NanoVDB grid, and a GridHandle and manages the memory of a NanoVDB grid. - - \note This file has NO dependency on OpenVDB. + \brief Defines GridHandle, which manages a host, and possibly a device, + memory buffer containing one or more NanoVDB grids. */ #ifndef NANOVDB_GRID_HANDLE_H_HAS_BEEN_INCLUDED #define NANOVDB_GRID_HANDLE_H_HAS_BEEN_INCLUDED -#include "../NanoVDB.h"// for mapToGridType -#include "HostBuffer.h" - -namespace nanovdb { - -// --------------------------> GridHandleBase <------------------------------------ - -class GridHandleBase -{ -public: - virtual ~GridHandleBase() {} - - /// @brief Returns the size in bytes of the raw memory buffer managed by this GridHandle's allocator. - virtual uint64_t size() const = 0; - - virtual uint8_t* data() = 0; - virtual const uint8_t* data() const = 0; - - /// @brief Return true if this handle is empty, i.e. has no allocated memory - bool empty() const { return size() == 0; } - - /// @brief Return true if this handle contains a grid - operator bool() const { return !this->empty(); } - - /// @brief Returns a const point to the grid meta data (see definition above). - /// - /// @warning Note that the return pointer can be NULL if the GridHandle was not initialized - const GridMetaData* gridMetaData() const { return reinterpret_cast(data()); } +#include // for std::ifstream +#include // for std::cerr/cout +#include +#include - /// @brief Returns the GridType handled by this instance, and GridType::End if empty - GridType gridType() const - { - const GridMetaData* ptr = this->gridMetaData(); - return ptr ? ptr->gridType() : GridType::End; - } +#include // for mapToGridType +#include +#include // for updateGridCount - /// @brief Return the number of grids contained in this buffer - uint32_t gridCount() const - { - auto *ptr = this->gridMetaData(); - return ptr ? ptr->gridCount() : 0; - } -};// GridHandleBase +namespace nanovdb { // --------------------------> GridHandle <------------------------------------ -/// @brief This class serves to manage a raw memory buffer of a NanoVDB Grid. +struct GridHandleMetaData {uint64_t offset, size; GridType gridType;}; + +/// @brief This class serves to manage a buffer containing one or more NanoVDB Grids. /// /// @note It is important to note that this class does NOT depend on OpenVDB. template -class GridHandle : public GridHandleBase +class GridHandle { + std::vector mMetaData; BufferT mBuffer; - template - const NanoGrid* getGrid(uint32_t n = 0) const; - - template - typename std::enable_if::hasDeviceDual, const NanoGrid*>::type - getDeviceGrid(uint32_t n = 0) const; - template static T* no_const(const T* ptr) { return const_cast(ptr); } public: using BufferType = BufferT; - /// @brief Move constructor from a buffer - GridHandle(BufferT&& buffer) { mBuffer = std::move(buffer); } - /// @brief Empty ctor + /// @brief Move constructor from a host buffer + /// @param buffer buffer containing one or more NanoGrids that will be moved into this GridHandle + /// @throw Will throw and error with the buffer does not contain a valid NanoGrid! + template::hasDeviceDual, int>::type = 0> + GridHandle(T&& buffer); + + /// @brief Move constructor from a dual host-device buffer + /// @param buffer buffer containing one or more NanoGrids that will be moved into this GridHandle + /// @throw Will throw and error with the buffer does not contain a valid NanoGrid! + template::hasDeviceDual, int>::type = 0> + GridHandle(T&& buffer); + + /// @brief Constructs an empty GridHandle GridHandle() = default; + /// @brief Disallow copy-construction GridHandle(const GridHandle&) = delete; + + /// @brief Move copy-constructor + GridHandle(GridHandle&& other) noexcept { + mBuffer = std::move(other.mBuffer); + mMetaData = std::move(other.mMetaData); + } + + /// @brief clear this GridHandle to an empty handle + void reset() { + mBuffer.clear(); + mMetaData.clear(); + } + /// @brief Disallow copy assignment operation GridHandle& operator=(const GridHandle&) = delete; + /// @brief Move copy assignment operation - GridHandle& operator=(GridHandle&& other) noexcept - { - mBuffer = std::move(other.mBuffer); + GridHandle& operator=(GridHandle&& other) noexcept { + mBuffer = std::move(other.mBuffer); + mMetaData = std::move(other.mMetaData); return *this; } - /// @brief Move copy-constructor - GridHandle(GridHandle&& other) noexcept { mBuffer = std::move(other.mBuffer); } - /// @brief Default destructor - ~GridHandle() override { reset(); } - /// @brief clear the buffer - void reset() { mBuffer.clear(); } + + /// @brief Performs a deep copy of the GridHandle, possibly templated on a different buffer type + /// @tparam OtherBufferT Buffer type of the deep copy + /// @param buffer optional buffer used for allocation + /// @return A new handle of the specified buffer type that contains a deep copy of the current handle + template + GridHandle copy(const OtherBufferT& buffer = OtherBufferT()) const; /// @brief Return a reference to the buffer BufferT& buffer() { return mBuffer; } @@ -112,102 +99,397 @@ class GridHandle : public GridHandleBase const BufferT& buffer() const { return mBuffer; } /// @brief Returns a non-const pointer to the data. - /// /// @warning Note that the return pointer can be NULL if the GridHandle was not initialized - uint8_t* data() override { return mBuffer.data(); } + uint8_t* data() { return mBuffer.data(); } /// @brief Returns a const pointer to the data. - /// /// @warning Note that the return pointer can be NULL if the GridHandle was not initialized - const uint8_t* data() const override { return mBuffer.data(); } + const uint8_t* data() const { return mBuffer.data(); } + + template + typename enable_if::hasDeviceDual, const uint8_t*>::type + deviceData() const { return mBuffer.deviceData(); } + template + typename enable_if::hasDeviceDual, uint8_t*>::type + deviceData() { return mBuffer.deviceData(); } + + /// @brief Returns the size in bytes of the raw memory buffer managed by this GridHandle. + uint64_t size() const { return mBuffer.size(); } - /// @brief Returns the size in bytes of the raw memory buffer managed by this GridHandle's allocator. - uint64_t size() const override { return mBuffer.size(); } + //@{ + /// @brief Return true if this handle is empty, i.e. has no allocated memory + bool empty() const { return this->size() == 0; } + bool isEmpty() const { return this->size() == 0; } + //@} + + /// @brief Return true if this handle contains any grids + operator bool() const { return !this->empty(); } - /// @brief Returns a const pointer to the @a n'th NanoVDB grid encoded in this GridHandle. - /// - /// @warning Note that the return pointer can be NULL if the GridHandle was not initialized, @a n is invalid + /// @brief Returns a const host pointer to the @a n'th NanoVDB grid encoded in this GridHandle. + /// @tparam ValueT Value type of the grid point to be returned + /// @param n Index of the (host) grid pointer to be returned + /// @warning Note that the return pointer can be NULL if the GridHandle no host grid, @a n is invalid /// or if the template parameter does not match the specified grid! template - const NanoGrid* grid(uint32_t n = 0) const { return this->template getGrid(n); } + const NanoGrid* grid(uint32_t n = 0) const; - /// @brief Returns a pointer to the @a n'th NanoVDB grid encoded in this GridHandle. - /// - /// @warning Note that the return pointer can be NULL if the GridHandle was not initialized, @a n is invalid + /// @brief Returns a host pointer to the @a n'th NanoVDB grid encoded in this GridHandle. + /// @tparam ValueT Value type of the grid point to be returned + /// @param n Index of the (host) grid pointer to be returned + /// @warning Note that the return pointer can be NULL if the GridHandle no host grid, @a n is invalid /// or if the template parameter does not match the specified grid! template - NanoGrid* grid(uint32_t n = 0) { return no_const(this->template getGrid(n)); } + NanoGrid* grid(uint32_t n = 0) {return const_cast*>(static_cast(this)->template grid(n));} /// @brief Return a const pointer to the @a n'th grid encoded in this GridHandle on the device, e.g. GPU - /// - /// @warning Note that the return pointer can be NULL if the GridHandle was not initialized, @a n is invalid - /// or if the template parameter does not match the specified grid! + /// @tparam ValueT Value type of the grid point to be returned + /// @param n Index of the (device) grid pointer to be returned + /// @warning Note that the return pointer can be NULL if the GridHandle has no device grid, @a n is invalid, + /// or if the template parameter does not match the specified grid. template - typename std::enable_if::hasDeviceDual, const NanoGrid*>::type - deviceGrid(uint32_t n = 0) const { return this->template getDeviceGrid(n); } + typename enable_if::hasDeviceDual, const NanoGrid*>::type + deviceGrid(uint32_t n=0) const; /// @brief Return a const pointer to the @a n'th grid encoded in this GridHandle on the device, e.g. GPU - /// - /// @warning Note that the return pointer can be NULL if the GridHandle was not initialized, @a n is invalid - /// or if the template parameter does not match the specified grid! + /// @tparam ValueT Value type of the grid point to be returned + /// @param n Index if of the grid pointer to be returned + /// @param verbose if non-zero error messages will be printed in case something failed + /// @warning Note that the return pointer can be NULL if the GridHandle was not initialized, @a n is invalid, + /// or if the template parameter does not match the specified grid. template - typename std::enable_if::hasDeviceDual, NanoGrid*>::type - deviceGrid(uint32_t n = 0) { return no_const(this->template getDeviceGrid(n)); } + typename enable_if::hasDeviceDual, NanoGrid*>::type + deviceGrid(uint32_t n=0){return const_cast*>(static_cast(this)->template deviceGrid(n));} /// @brief Upload the grid to the device, e.g. from CPU to GPU - /// /// @note This method is only available if the buffer supports devices template - typename std::enable_if::hasDeviceDual, void>::type + typename enable_if::hasDeviceDual, void>::type deviceUpload(void* stream = nullptr, bool sync = true) { mBuffer.deviceUpload(stream, sync); } /// @brief Download the grid to from the device, e.g. from GPU to CPU - /// /// @note This method is only available if the buffer supports devices template - typename std::enable_if::hasDeviceDual, void>::type + typename enable_if::hasDeviceDual, void>::type deviceDownload(void* stream = nullptr, bool sync = true) { mBuffer.deviceDownload(stream, sync); } + + /// @brief Check if the buffer is this handle has any padding, i.e. if the buffer is larger than the combined size of all its grids + /// @return true is the combined size of all grid is smaller than the buffer size + bool isPadded() const {return mMetaData.empty() ? false : mMetaData.back().offset + mMetaData.back().size != mBuffer.size();} + + /// @brief Return the total number of grids contained in this buffer + uint32_t gridCount() const {return static_cast(mMetaData.size());} + + /// @brief Return the grid size of the @a n'th grid in this GridHandle + /// @param n index of the grid (assumed to be less than gridCount()) + /// @return Return the byte size of the specified grid + uint64_t gridSize(uint32_t n = 0) const {return mMetaData[n].size; } + + /// @brief Return the GridType of the @a n'th grid in this GridHandle + /// @param n index of the grid (assumed to be less than gridCount()) + /// @return Return the GridType of the specified grid + GridType gridType(uint32_t n = 0) const {return mMetaData[n].gridType; } + + /// @brief Access to the GridData of the n'th grid in the current handle + /// @param n zero-based ID of the grid + /// @return Const pointer to the n'th GridData in the current handle + const GridData* gridData(uint32_t n = 0) const; + + /// @brief Returns a const point to the @a n'th grid meta data + /// @param n zero-based ID of the grid + /// @warning Note that the return pointer can be NULL if the GridHandle was not initialized + const GridMetaData* gridMetaData(uint32_t n = 0) const; + + /// @brief Write a specific grid in this buffer to an output stream + /// @param os output stream that the buffer will be written to + /// @param n zero-based index of the grid to be written to stream + void write(std::ostream& os, uint32_t n) const { + if (const GridData* data = this->gridData(n)) { + os.write((const char*)data, data->mGridSize); + } else { + throw std::runtime_error("GridHandle does not contain a #" + std::to_string(n) + " grid"); + } + } + + /// @brief Write the entire grid buffer to an output stream + /// @param os output stream that the buffer will be written to + void write(std::ostream& os) const { + for (uint32_t n=0; ngridCount(); ++n) this->write(os, n); + } + + /// @brief Write this entire grid buffer to a file + /// @param fileName string name of the output file + void write(const std::string &fileName) const { + std::ofstream os(fileName, std::ios::out | std::ios::binary | std::ios::trunc); + if (!os.is_open()) throw std::ios_base::failure("Unable to open file named \"" + fileName + "\" for output"); + this->write(os); + } + + /// @brief Write a specific grid to file + /// @param fileName string name of the output file + /// @param n zero-based index of the grid to be written to file + void write(const std::string &fileName, uint32_t n) const { + std::ofstream os(fileName, std::ios::out | std::ios::binary | std::ios::trunc); + if (!os.is_open()) throw std::ios_base::failure("Unable to open file named \"" + fileName + "\" for output"); + this->write(os, n); + } + + /// @brief Read an entire raw grid buffer from an input stream + /// @param is input stream containing a raw grid buffer + /// @param pool optional pool from which to allocate the new grid buffer + /// @throw Will throw a std::logic_error if the stream does not contain a valid raw grid + void read(std::istream& is, const BufferT& pool = BufferT()); + + /// @brief Read a specific grid from an input stream containing a raw grid buffer + /// @param is input stream containing a raw grid buffer + /// @param n zero-based index of the grid to be read + /// @param pool optional pool from which to allocate the new grid buffer + /// @throw Will throw a std::logic_error if the stream does not contain a valid raw grid + void read(std::istream& is, uint32_t n, const BufferT& pool = BufferT()); + + /// @brief Read a specific grid from an input stream containing a raw grid buffer + /// @param is input stream containing a raw grid buffer + /// @param gridName string name of the grid to be read + /// @param pool optional pool from which to allocate the new grid buffer + /// @throw Will throw a std::logic_error if the stream does not contain a valid raw grid with the speficied name + void read(std::istream& is, const std::string &gridName, const BufferT& pool = BufferT()); + + /// @brief Read a raw grid buffer from a file + /// @param filename string name of the input file containing a raw grid buffer + /// @param pool optional pool from which to allocate the new grid buffe + void read(const std::string &fileName, const BufferT& pool = BufferT()) { + std::ifstream is(fileName, std::ios::in | std::ios::binary); + if (!is.is_open()) throw std::ios_base::failure("Unable to open file named \"" + fileName + "\" for input"); + this->read(is, pool); + } + + /// @brief Read a specific grid from a file containing a raw grid buffer + /// @param filename string name of the input file containing a raw grid buffer + /// @param n zero-based index of the grid to be read + /// @param pool optional pool from which to allocate the new grid buffer + /// @throw Will throw a std::ios_base::failure if the file does not exist and a + /// std::logic_error if the files does not contain a valid raw grid + void read(const std::string &fileName, uint32_t n, const BufferT& pool = BufferT()) { + std::ifstream is(fileName, std::ios::in | std::ios::binary); + if (!is.is_open()) throw std::ios_base::failure("Unable to open file named \"" + fileName + "\" for input"); + this->read(is, n, pool); + } + + /// @brief Read a specific grid from a file containing a raw grid buffer + /// @param filename string name of the input file containing a raw grid buffer + /// @param gridName string name of the grid to be read + /// @param pool optional pool from which to allocate the new grid buffer + /// @throw Will throw a std::ios_base::failure if the file does not exist and a + /// std::logic_error if the files does not contain a valid raw grid withe the specified name + void read(const std::string &fileName, const std::string &gridName, const BufferT& pool = BufferT()) { + std::ifstream is(fileName, std::ios::in | std::ios::binary); + if (!is.is_open()) throw std::ios_base::failure("Unable to open file named \"" + fileName + "\" for input"); + this->read(is, gridName, pool); + } }; // GridHandle // --------------------------> Implementation of private methods in GridHandle <------------------------------------ template -template -inline const NanoGrid* GridHandle::getGrid(uint32_t index) const +inline const GridData* GridHandle::gridData(uint32_t n) const +{ + const uint8_t *data = this->data(); + if (data == nullptr || n >= mMetaData.size()) return nullptr; + return reinterpret_cast(data + mMetaData[n].offset); +}// const GridData* GridHandle::gridData(uint32_t n) const + +template +inline const GridMetaData* GridHandle::gridMetaData(uint32_t n) const +{ + const uint8_t *data = this->data(); + if (data == nullptr || n >= mMetaData.size()) return nullptr; + return reinterpret_cast(data + mMetaData[n].offset); +}// const GridMetaData* GridHandle::gridMetaData(uint32_t n) const + +namespace {// anonymous namespace +inline __hostdev__ void cpyMetaData(const GridData *data, GridHandleMetaData *meta) { - using GridT = const NanoGrid; - auto *data = mBuffer.data(); - GridT *grid = reinterpret_cast(data); - if (grid == nullptr || index >= grid->gridCount()) {// un-initialized or index is out of range - return nullptr; + uint64_t offset = 0; + for (auto *p=meta, *q=p+data->mGridCount; p!=q; ++p) { + *p = {offset, data->mGridSize, data->mGridType}; + offset += p->size; + data = PtrAdd(data, p->size); } - while(index != grid->gridIndex()) { - data += grid->gridSize(); - grid = reinterpret_cast(data); +}// void cpyMetaData(const GridData *data, GridHandleMetaData *meta) +}// anonymous namespace + +template +template::hasDeviceDual, int>::type> +GridHandle::GridHandle(T&& buffer) +{ + static_assert(is_same::value, "Expected U==BufferT"); + mBuffer = std::move(buffer); + if (auto *data = reinterpret_cast(mBuffer.data())) { + if (!data->isValid()) throw std::runtime_error("GridHandle was constructed with an invalid host buffer"); + mMetaData.resize(data->mGridCount); + cpyMetaData(data, mMetaData.data()); } - return grid->gridType() == mapToGridType() ? grid : nullptr; -} +}// GridHandle::GridHandle(T&& buffer) + +template +template +inline GridHandle GridHandle::copy(const OtherBufferT& other) const +{ + if (mBuffer.isEmpty()) return GridHandle();// return an empty handle + auto buffer = OtherBufferT::create(mBuffer.size(), &other); + std::memcpy(buffer.data(), mBuffer.data(), mBuffer.size());// deep copy of buffer + return GridHandle(std::move(buffer)); +}// GridHandle GridHandle::copy(const OtherBufferT& other) const + +template +template +inline const NanoGrid* GridHandle::grid(uint32_t n) const +{ + const uint8_t *data = mBuffer.data(); + if (data == nullptr || n >= mMetaData.size() || mMetaData[n].gridType != mapToGridType()) return nullptr; + return reinterpret_cast*>(data + mMetaData[n].offset); +}// const NanoGrid* GridHandle::grid(uint32_t n) const template template -inline typename std::enable_if::hasDeviceDual, const NanoGrid*>::type -GridHandle::getDeviceGrid(uint32_t index) const +inline typename enable_if::hasDeviceDual, const NanoGrid*>::type +GridHandle::deviceGrid(uint32_t n) const +{ + const uint8_t *data = mBuffer.deviceData(); + if (data == nullptr || n >= mMetaData.size() || mMetaData[n].gridType != mapToGridType()) return nullptr; + return reinterpret_cast*>(data + mMetaData[n].offset); +}// GridHandle::deviceGrid(uint32_t n) cons + +template +void GridHandle::read(std::istream& is, const BufferT& pool) +{ + GridData data; + is.read((char*)&data, 40);// only 40 bytes are required for all the data we need in GridData + if (data.isValid()) { + uint64_t size = data.mGridSize, sum = 0u; + while(data.mGridIndex + 1u < data.mGridCount) {// loop over remaining raw grids in stream + is.seekg(data.mGridSize - 40, std::ios::cur);// skip grid + is.read((char*)&data, 40);// read 40 bytes of the next GridData + sum += data.mGridSize; + } + is.seekg(-int64_t(sum + 40), std::ios::cur);// rewind to start + auto buffer = BufferT::create(size + sum, &pool); + is.read((char*)(buffer.data()), buffer.size()); + *this = GridHandle(std::move(buffer)); + } else { + is.seekg(-40, std::ios::cur);// rewind + throw std::logic_error("This stream does not contain a valid raw grid buffer"); + } +}// void GridHandle::read(std::istream& is, const BufferT& pool) + +template +void GridHandle::read(std::istream& is, uint32_t n, const BufferT& pool) +{ + GridData data; + is.read((char*)&data, 40);// only 40 bytes are required for all the data we need in GridData + if (data.isValid()) { + if (n>=data.mGridCount) throw std::runtime_error("stream does not contain a #" + std::to_string(n) + " grid"); + while(data.mGridIndex != n) { + is.seekg(data.mGridSize - 40, std::ios::cur);// skip grid + is.read((char*)&data, 40);// read 40 bytes + } + auto buffer = BufferT::create(data.mGridSize, &pool); + is.seekg(-40, std::ios::cur);// rewind + is.read((char*)(buffer.data()), data.mGridSize); + updateGridCount((GridData*)buffer.data(), 0u, 1u); + *this = GridHandle(std::move(buffer)); + } else { + is.seekg(-40, std::ios::cur);// rewind 40 bytes to undo initial read + throw std::logic_error("This file does not contain a valid raw buffer"); + } +}// void GridHandle::read(std::istream& is, uint32_t n, const BufferT& pool) + +template +void GridHandle::read(std::istream& is, const std::string &gridName, const BufferT& pool) +{ + static const std::streamsize byteSize = sizeof(GridData); + GridData data; + is.read((char*)&data, byteSize); + is.seekg(-byteSize, std::ios::cur);// rewind + if (data.isValid()) { + uint32_t n = 0; + while(data.mGridName != gridName && n++ < data.mGridCount) { + is.seekg(data.mGridSize, std::ios::cur);// skip grid + is.read((char*)&data, byteSize);// read 40 bytes + is.seekg(-byteSize, std::ios::cur);// rewind + } + if (n>data.mGridCount) throw std::runtime_error("No raw grid named \""+gridName+"\""); + auto buffer = BufferT::create(data.mGridSize, &pool); + is.read((char*)(buffer.data()), data.mGridSize); + updateGridCount((GridData*)buffer.data(), 0u, 1u); + *this = GridHandle(std::move(buffer)); + } else { + throw std::logic_error("This file does not contain a valid raw buffer"); + } +}// void GridHandle::read(std::istream& is, const std::string &gridName n, const BufferT& pool) + +// --------------------------> free-standing functions <------------------------------------ + +/// @brief Split all grids in a single GridHandle into a vector of multiple GridHandles each with a single grid +/// @tparam BufferT Type of the input and output grid buffers +/// @param handle GridHandle with grids that will be slip into individual GridHandles +/// @param pool optional pool used for allocation of output GridHandle +/// @return Vector of GridHandles each containing a single grid +template class VectorT = std::vector> +inline VectorT> +splitGrids(const GridHandle &handle, const BufferT* other = nullptr) { - using GridT = const NanoGrid; - auto *data = mBuffer.data(); - GridT *grid = reinterpret_cast(data); - if (grid == nullptr || index >= grid->gridCount()) {// un-initialized or index is out of range - return nullptr; + using HandleT = GridHandle; + const uint8_t *ptr = handle.data(); + if (ptr == nullptr) return VectorT(); + VectorT handles(handle.gridCount()); + for (auto &h : handles) { + const GridData *src = reinterpret_cast(ptr); + NANOVDB_ASSERT(src->isValid()); + auto buffer = BufferT::create(src->mGridSize, other); + GridData *dst = reinterpret_cast(buffer.data()); + std::memcpy(dst, src, src->mGridSize); + updateGridCount(dst, 0u, 1u); + h = HandleT(std::move(buffer)); + ptr += src->mGridSize; } - auto* dev = mBuffer.deviceData(); - while(index != grid->gridIndex()) { - data += grid->gridSize(); - dev += grid->gridSize(); - grid = reinterpret_cast(data); + return std::move(handles); +}// splitGrids + +/// @brief Combines (or merges) multiple GridHandles into a single GridHandle containing all grids +/// @tparam BufferT Type of the input and output grid buffers +/// @param handles Vector of GridHandles to be combined +/// @param pool optional pool used for allocation of output GridHandle +/// @return single GridHandle containing all input grids +template class VectorT> +inline GridHandle +mergeGrids(const VectorT> &handles, const BufferT* pool = nullptr) +{ + uint64_t size = 0u; + uint32_t counter = 0u, gridCount = 0u; + for (auto &h : handles) { + gridCount += h.gridCount(); + for (uint32_t n=0; n(dst); + NANOVDB_ASSERT(data->isValid()); + updateGridCount(data, counter++, gridCount); + dst += data->mGridSize; + src += data->mGridSize; + } } - return grid->gridType() == mapToGridType() ? reinterpret_cast(dev) : nullptr; -} + return GridHandle(std::move(buffer)); +}// mergeGrids } // namespace nanovdb +#if defined(__CUDACC__) +#include +#endif// defined(__CUDACC__) + #endif // NANOVDB_GRID_HANDLE_H_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/GridStats.h b/nanovdb/nanovdb/util/GridStats.h index a98e1a5ed1..267e7462e3 100644 --- a/nanovdb/nanovdb/util/GridStats.h +++ b/nanovdb/nanovdb/util/GridStats.h @@ -15,7 +15,7 @@ #ifndef NANOVDB_GRIDSTATS_H_HAS_BEEN_INCLUDED #define NANOVDB_GRIDSTATS_H_HAS_BEEN_INCLUDED -#include "../NanoVDB.h" +#include #include "Range.h" #include "ForEach.h" @@ -23,6 +23,12 @@ #include #endif +#if defined(__CUDACC__) +#include // for cuda::std::numeric_limits +#else +#include // for std::numeric_limits +#endif + #include #include @@ -59,55 +65,64 @@ class Extrema public: using ValueType = ValueT; - Extrema() + __hostdev__ Extrema() +#if defined(__CUDACC__) + : mMin(cuda::std::numeric_limits::max()) + , mMax(cuda::std::numeric_limits::lowest()) +#else : mMin(std::numeric_limits::max()) , mMax(std::numeric_limits::lowest()) +#endif { } - Extrema(const ValueT& v) + __hostdev__ Extrema(const ValueT& v) : mMin(v) , mMax(v) { } - Extrema(const ValueT& a, const ValueT& b) + __hostdev__ Extrema(const ValueT& a, const ValueT& b) : mMin(a) , mMax(b) { } - Extrema& min(const ValueT& v) + __hostdev__ Extrema& min(const ValueT& v) { - if (v < mMin) { - mMin = v; - } + if (v < mMin) mMin = v; return *this; } - Extrema& max(const ValueT& v) + __hostdev__ Extrema& max(const ValueT& v) { - if (v > mMax) { - mMax = v; - } + if (v > mMax) mMax = v; return *this; } - Extrema& add(const ValueT& v) + __hostdev__ Extrema& add(const ValueT& v) { this->min(v); this->max(v); return *this; } - Extrema& add(const ValueT& v, uint64_t) { return this->add(v); } - Extrema& add(const Extrema& other) + __hostdev__ Extrema& add(const ValueT& v, uint64_t) { return this->add(v); } + __hostdev__ Extrema& add(const Extrema& other) { this->min(other.mMin); this->max(other.mMax); return *this; } - const ValueT& min() const { return mMin; } - const ValueT& max() const { return mMax; } - operator bool() const { return mMin <= mMax; } - static constexpr bool hasMinMax() { return !std::is_same::value; } - static constexpr bool hasAverage() { return false; } - static constexpr bool hasStdDeviation() { return false; } - static constexpr size_t size() { return 0; } + __hostdev__ const ValueT& min() const { return mMin; } + __hostdev__ const ValueT& max() const { return mMax; } + __hostdev__ operator bool() const { return mMin <= mMax; } + __hostdev__ static constexpr bool hasMinMax() { return !std::is_same::value; } + __hostdev__ static constexpr bool hasAverage() { return false; } + __hostdev__ static constexpr bool hasStdDeviation() { return false; } + __hostdev__ static constexpr bool hasStats() { return !std::is_same::value; } + __hostdev__ static constexpr size_t size() { return 0; } + + template + __hostdev__ void setStats(NodeT &node) const + { + node.setMin(this->min()); + node.setMax(this->max()); + } }; // Extrema /// @brief Template specialization of Extrema on vector value types, i.e. rank = 1 @@ -121,81 +136,82 @@ class Extrema Real scalar; VecT vector; - Pair(Real s)// is only used by Extrema() default c-tor + __hostdev__ Pair(Real s)// is only used by Extrema() default c-tor : scalar(s) , vector(s) { } - Pair(const VecT& v) + __hostdev__ Pair(const VecT& v) : scalar(v.lengthSqr()) , vector(v) { } - bool operator<(const Pair& rhs) const { return scalar < rhs.scalar; } + __hostdev__ bool operator<(const Pair& rhs) const { return scalar < rhs.scalar; } } mMin, mMax; - Extrema& add(const Pair& p) + __hostdev__ Extrema& add(const Pair& p) { - if (p < mMin) { - mMin = p; - } - if (mMax < p) { - mMax = p; - } + if (p < mMin) mMin = p; + if (mMax < p) mMax = p; return *this; } public: using ValueType = VecT; - Extrema() + __hostdev__ Extrema() +#if defined(__CUDACC__) + : mMin(cuda::std::numeric_limits::max()) + , mMax(cuda::std::numeric_limits::lowest()) +#else : mMin(std::numeric_limits::max()) , mMax(std::numeric_limits::lowest()) +#endif { } - Extrema(const VecT& v) + __hostdev__ Extrema(const VecT& v) : mMin(v) , mMax(v) { } - Extrema(const VecT& a, const VecT& b) + __hostdev__ Extrema(const VecT& a, const VecT& b) : mMin(a) , mMax(b) { } - Extrema& min(const VecT& v) + __hostdev__ Extrema& min(const VecT& v) { Pair tmp(v); - if (tmp < mMin) { - mMin = tmp; - } + if (tmp < mMin) mMin = tmp; return *this; } - Extrema& max(const VecT& v) + __hostdev__ Extrema& max(const VecT& v) { Pair tmp(v); - if (mMax < tmp) { - mMax = tmp; - } + if (mMax < tmp) mMax = tmp; return *this; } - Extrema& add(const VecT& v) { return this->add(Pair(v)); } - Extrema& add(const VecT& v, uint64_t) { return this->add(Pair(v)); } - Extrema& add(const Extrema& other) + __hostdev__ Extrema& add(const VecT& v) { return this->add(Pair(v)); } + __hostdev__ Extrema& add(const VecT& v, uint64_t) { return this->add(Pair(v)); } + __hostdev__ Extrema& add(const Extrema& other) { - if (other.mMin < mMin) { - mMin = other.mMin; - } - if (mMax < other.mMax) { - mMax = other.mMax; - } + if (other.mMin < mMin) mMin = other.mMin; + if (mMax < other.mMax) mMax = other.mMax; return *this; } - const VecT& min() const { return mMin.vector; } - const VecT& max() const { return mMax.vector; } - operator bool() const { return !(mMax < mMin); } - static constexpr bool hasMinMax() { return !std::is_same::value; } - static constexpr bool hasAverage() { return false; } - static constexpr bool hasStdDeviation() { return false; } - static constexpr size_t size() { return 0; } + __hostdev__ const VecT& min() const { return mMin.vector; } + __hostdev__ const VecT& max() const { return mMax.vector; } + __hostdev__ operator bool() const { return !(mMax < mMin); } + __hostdev__ static constexpr bool hasMinMax() { return !std::is_same::value; } + __hostdev__ static constexpr bool hasAverage() { return false; } + __hostdev__ static constexpr bool hasStdDeviation() { return false; } + __hostdev__ static constexpr bool hasStats() { return !std::is_same::value; } + __hostdev__ static constexpr size_t size() { return 0; } + + template + __hostdev__ void setStats(NodeT &node) const + { + node.setMin(this->min()); + node.setMax(this->max()); + } }; // Extrema //================================================================================================ @@ -222,14 +238,14 @@ class Stats : public Extrema public: using ValueType = ValueT; - Stats() + __hostdev__ Stats() : BaseT() , mSize(0) , mAvg(0.0) , mAux(0.0) { } - Stats(const ValueT& val) + __hostdev__ Stats(const ValueT& val) : BaseT(val) , mSize(1) , mAvg(RealT(val)) @@ -237,7 +253,7 @@ class Stats : public Extrema { } /// @brief Add a single sample - Stats& add(const ValueT& val) + __hostdev__ Stats& add(const ValueT& val) { BaseT::add(val); mSize += 1; @@ -247,7 +263,7 @@ class Stats : public Extrema return *this; } /// @brief Add @a n samples with constant value @a val. - Stats& add(const ValueT& val, uint64_t n) + __hostdev__ Stats& add(const ValueT& val, uint64_t n) { const double denom = 1.0 / double(mSize + n); const double delta = double(val) - mAvg; @@ -259,7 +275,7 @@ class Stats : public Extrema } /// Add the samples from the other Stats instance. - Stats& add(const Stats& other) + __hostdev__ Stats& add(const Stats& other) { if (other.mSize > 0) { const double denom = 1.0 / double(mSize + other.mSize); @@ -272,32 +288,42 @@ class Stats : public Extrema return *this; } - static constexpr bool hasMinMax() { return !std::is_same::value; } - static constexpr bool hasAverage() { return !std::is_same::value; } - static constexpr bool hasStdDeviation() { return !std::is_same::value; } + __hostdev__ static constexpr bool hasMinMax() { return !std::is_same::value; } + __hostdev__ static constexpr bool hasAverage() { return !std::is_same::value; } + __hostdev__ static constexpr bool hasStdDeviation() { return !std::is_same::value; } + __hostdev__ static constexpr bool hasStats() { return !std::is_same::value; } - size_t size() const { return mSize; } + __hostdev__ size_t size() const { return mSize; } //@{ /// Return the arithmetic mean, i.e. average, value. - double avg() const { return mAvg; } - double mean() const { return mAvg; } + __hostdev__ double avg() const { return mAvg; } + __hostdev__ double mean() const { return mAvg; } //@} //@{ /// @brief Return the population variance. /// /// @note The unbiased sample variance = population variance * num/(num-1) - double var() const { return mSize < 2 ? 0.0 : mAux / double(mSize); } - double variance() const { return this->var(); } + __hostdev__ double var() const { return mSize < 2 ? 0.0 : mAux / double(mSize); } + __hostdev__ double variance() const { return this->var(); } //@} //@{ /// @brief Return the standard deviation (=Sqrt(variance)) as /// defined from the (biased) population variance. - double std() const { return sqrt(this->var()); } - double stdDev() const { return this->std(); } + __hostdev__ double std() const { return sqrt(this->var()); } + __hostdev__ double stdDev() const { return this->std(); } //@} + + template + __hostdev__ void setStats(NodeT &node) const + { + node.setMin(this->min()); + node.setMax(this->max()); + node.setAvg(this->avg()); + node.setDev(this->std()); + } }; // end Stats /// @brief This class computes statistics (minimum value, maximum @@ -319,7 +345,7 @@ class Stats : public Extrema public: using ValueType = ValueT; - Stats() + __hostdev__ Stats() : BaseT() , mSize(0) , mAvg(0.0) @@ -327,7 +353,7 @@ class Stats : public Extrema { } /// @brief Add a single sample - Stats& add(const ValueT& val) + __hostdev__ Stats& add(const ValueT& val) { typename BaseT::Pair tmp(val); BaseT::add(tmp); @@ -338,7 +364,7 @@ class Stats : public Extrema return *this; } /// @brief Add @a n samples with constant value @a val. - Stats& add(const ValueT& val, uint64_t n) + __hostdev__ Stats& add(const ValueT& val, uint64_t n) { typename BaseT::Pair tmp(val); const double denom = 1.0 / double(mSize + n); @@ -351,7 +377,7 @@ class Stats : public Extrema } /// Add the samples from the other Stats instance. - Stats& add(const Stats& other) + __hostdev__ Stats& add(const Stats& other) { if (other.mSize > 0) { const double denom = 1.0 / double(mSize + other.mSize); @@ -364,32 +390,42 @@ class Stats : public Extrema return *this; } - static constexpr bool hasMinMax() { return !std::is_same::value; } - static constexpr bool hasAverage() { return !std::is_same::value; } - static constexpr bool hasStdDeviation() { return !std::is_same::value; } + __hostdev__ static constexpr bool hasMinMax() { return !std::is_same::value; } + __hostdev__ static constexpr bool hasAverage() { return !std::is_same::value; } + __hostdev__ static constexpr bool hasStdDeviation() { return !std::is_same::value; } + __hostdev__ static constexpr bool hasStats() { return !std::is_same::value; } - size_t size() const { return mSize; } + __hostdev__ size_t size() const { return mSize; } //@{ /// Return the arithmetic mean, i.e. average, value. - double avg() const { return mAvg; } - double mean() const { return mAvg; } + __hostdev__ double avg() const { return mAvg; } + __hostdev__ double mean() const { return mAvg; } //@} //@{ /// @brief Return the population variance. /// /// @note The unbiased sample variance = population variance * num/(num-1) - double var() const { return mSize < 2 ? 0.0 : mAux / double(mSize); } - double variance() const { return this->var(); } + __hostdev__ double var() const { return mSize < 2 ? 0.0 : mAux / double(mSize); } + __hostdev__ double variance() const { return this->var(); } //@} //@{ /// @brief Return the standard deviation (=Sqrt(variance)) as /// defined from the (biased) population variance. - double std() const { return sqrt(this->var()); } - double stdDev() const { return this->std(); } + __hostdev__ double std() const { return sqrt(this->var()); } + __hostdev__ double stdDev() const { return this->std(); } //@} + + template + __hostdev__ void setStats(NodeT &node) const + { + node.setMin(this->min()); + node.setMax(this->max()); + node.setAvg(this->avg()); + node.setDev(this->std()); + } }; // end Stats /// @brief No-op Stats class @@ -397,20 +433,23 @@ template struct NoopStats { using ValueType = ValueT; - NoopStats() {} - NoopStats(const ValueT&) {} - NoopStats& add(const ValueT&) { return *this; } - NoopStats& add(const ValueT&, uint64_t) { return *this; } - NoopStats& add(const NoopStats&) { return *this; } - static constexpr size_t size() { return 0; } - static constexpr bool hasMinMax() { return false; } - static constexpr bool hasAverage() { return false; } - static constexpr bool hasStdDeviation() { return false; } + __hostdev__ NoopStats() {} + __hostdev__ NoopStats(const ValueT&) {} + __hostdev__ NoopStats& add(const ValueT&) { return *this; } + __hostdev__ NoopStats& add(const ValueT&, uint64_t) { return *this; } + __hostdev__ NoopStats& add(const NoopStats&) { return *this; } + __hostdev__ static constexpr size_t size() { return 0; } + __hostdev__ static constexpr bool hasMinMax() { return false; } + __hostdev__ static constexpr bool hasAverage() { return false; } + __hostdev__ static constexpr bool hasStdDeviation() { return false; } + __hostdev__ static constexpr bool hasStats() { return false; } + template + __hostdev__ void setStats(NodeT&) const{} }; // end NoopStats //================================================================================================ -/// @brief Allows for the construction of NanoVDB grids without any dependecy +/// @brief Allows for the construction of NanoVDB grids without any dependency template> class GridStats { @@ -423,7 +462,6 @@ class GridStats using Node2 = typename TreeT::Node2; // upper using RootT = typename TreeT::Node3; // root static_assert(std::is_same::value, "Mismatching type"); - static constexpr bool DO_STATS = StatsT::hasMinMax() || StatsT::hasAverage() || StatsT::hasStdDeviation(); ValueT mDelta; // skip rendering of node if: node.max < -mDelta || node.min > mDelta @@ -461,7 +499,6 @@ template struct GridStats::NodeStats { StatsT stats; - //uint64_t activeCount; CoordBBox bbox; NodeStats(): stats(), bbox() {}//activeCount(0), bbox() {}; @@ -469,7 +506,6 @@ struct GridStats::NodeStats NodeStats& add(const NodeStats &other) { stats.add( other.stats );// no-op for NoopStats?! - //activeCount += other.activeCount; bbox[0].minComponent(other.bbox[0]); bbox[1].maxComponent(other.bbox[1]); return *this; @@ -533,31 +569,19 @@ void GridStats::process( GridT &grid ) auto& data = *grid.data(); const auto& indexBBox = grid.tree().root().bbox(); if (indexBBox.empty()) { - data.mWorldBBox = BBox(); + data.mWorldBBox = BBox(); data.setBBoxOn(false); } else { // Note that below max is offset by one since CoordBBox.max is inclusive - // while bbox.max is exclusive. However, min is inclusive in both - // CoordBBox and BBox. This also guarantees that a grid with a single + // while bbox.max is exclusive. However, min is inclusive in both + // CoordBBox and BBox. This also guarantees that a grid with a single // active voxel, does not have an empty world bbox! E.g. if a grid with a // unit index-to-world transformation only contains the active voxel (0,0,0) // then indeBBox = (0,0,0) -> (0,0,0) and then worldBBox = (0.0, 0.0, 0.0) // -> (1.0, 1.0, 1.0). This is a consequence of the different definitions // of index and world bounding boxes inherited from OpenVDB! - const Coord min = indexBBox[0]; - const Coord max = indexBBox[1] + Coord(1); - - auto& worldBBox = data.mWorldBBox; - const auto& map = grid.map(); - worldBBox[0] = worldBBox[1] = map.applyMap(Vec3d(min[0], min[1], min[2])); - worldBBox.expand(map.applyMap(Vec3d(min[0], min[1], max[2]))); - worldBBox.expand(map.applyMap(Vec3d(min[0], max[1], min[2]))); - worldBBox.expand(map.applyMap(Vec3d(max[0], min[1], min[2]))); - worldBBox.expand(map.applyMap(Vec3d(max[0], max[1], min[2]))); - worldBBox.expand(map.applyMap(Vec3d(max[0], min[1], max[2]))); - worldBBox.expand(map.applyMap(Vec3d(min[0], max[1], max[2]))); - worldBBox.expand(map.applyMap(Vec3d(max[0], max[1], max[2]))); - data.setBBoxOn(true); + grid.mWorldBBox = CoordBBox(indexBBox[0], indexBBox[1].offsetBy(1)).transform(grid.map()); + grid.setBBoxOn(true); } // set bit flags @@ -584,7 +608,6 @@ void GridStats::process(RootT &root) if (data.mTableSize == 0) { // empty root node data.mMinimum = data.mMaximum = data.mBackground; data.mAverage = data.mStdDevi = 0; - //data.mActiveVoxelCount = 0; data.mBBox = CoordBBox(); } else { NodeStats total; @@ -593,21 +616,19 @@ void GridStats::process(RootT &root) if (tile->isChild()) { // process child node total.add( this->process( *data.getChild(tile) ) ); } else if (tile->state) { // active tile - //total.activeCount += ChildT::NUM_VALUES; const Coord ijk = tile->origin(); total.bbox[0].minComponent(ijk); total.bbox[1].maxComponent(ijk + Coord(ChildT::DIM - 1)); - if (DO_STATS) { // resolved at compile time + if (StatsT::hasStats()) { // resolved at compile time total.stats.add(tile->value, ChildT::NUM_VALUES); } } } this->setStats(&data, total.stats); if (total.bbox.empty()) { - std::cerr << "\nWarning: input tree only contained inactive root tiles!" + std::cerr << "\nWarning in GridStats: input tree only contained inactive root tiles!" << "\nWhile not strictly an error it's rather suspicious!\n"; } - //data.mActiveVoxelCount = total.activeCount; data.mBBox = total.bbox; } } // GridStats::process( RootNode ) @@ -629,7 +650,7 @@ GridStats::process(NodeT &node) if (const auto tileCount = data->mValueMask.countOn()) { //total.activeCount = tileCount * ChildT::NUM_VALUES; // active tiles for (auto it = data->mValueMask.beginOn(); it; ++it) { - if (DO_STATS) { // resolved at compile time + if (StatsT::hasStats()) { // resolved at compile time total.stats.add( data->mTable[*it].value, ChildT::NUM_VALUES ); } const Coord ijk = node.offsetToGlobalCoord(*it); @@ -668,7 +689,7 @@ GridStats::process(NodeT &node) data->mFlags &= ~uint32_t(2); // set 2nd bit off since node does not contain active values } else { data->mFlags |= uint32_t(2); // set 2nd bit on since node contains active values - if (DO_STATS) { // resolved at compile time + if (StatsT::hasStats()) { // resolved at compile time this->setStats(data, total.stats); this->setFlag(data->mMinimum, data->mMaximum, data->mFlags); } @@ -682,24 +703,15 @@ template typename GridStats::NodeStats GridStats::process(Node0 &leaf) { - static_assert(Node0::SIZE == 512u, "Invalid size of leaf nodes"); NodeStats local; - auto *data = leaf.data(); - if (auto activeCount = data->mValueMask.countOn()) { - //data->mFlags |= uint8_t(2); // sets 2nd bit on since leaf contains active voxel - //local.activeCount += activeCount; - leaf.updateBBox(); // optionally update active bounding box (updates data->mFlags) - local.bbox[0] = local.bbox[1] = data->mBBoxMin; - local.bbox[1] += Coord(data->mBBoxDif[0], data->mBBoxDif[1], data->mBBoxDif[2]); - if (DO_STATS) { // resolved at compile time - for (auto it = data->mValueMask.beginOn(); it; ++it) { - local.stats.add(data->getValue(*it)); - } - this->setStats(data, local.stats); - this->setFlag(data->getMin(), data->getMax(), data->mFlags); + if (leaf.updateBBox()) {// optionally update active bounding box (updates data->mFlags) + local.bbox[0] = local.bbox[1] = leaf.mBBoxMin; + local.bbox[1] += Coord(leaf.mBBoxDif[0], leaf.mBBoxDif[1], leaf.mBBoxDif[2]); + if (StatsT::hasStats()) {// resolved at compile time + for (auto it = leaf.cbeginValueOn(); it; ++it) local.stats.add(*it); + this->setStats(&leaf, local.stats); + this->setFlag(leaf.getMin(), leaf.getMax(), leaf.mFlags); } - } else { - data->mFlags &= ~uint8_t(2); // sets 2nd bit off since leaf has no bbox of active active values } return local; } // GridStats::process( LeafNode ) @@ -725,7 +737,7 @@ void gridStats(NanoGrid& grid, StatsMode mode) } else { throw std::runtime_error("gridStats: Unsupported statistics mode."); } -} +}// gridStats //================================================================================================ @@ -755,7 +767,8 @@ Mask getBBoxMask(const CoordBBox &bbox, const NodeT* node) } } return mask; -} +}// getBBoxMask + }// end of unnamed namespace /// @brief return the extrema of all the values in a grid that @@ -837,7 +850,6 @@ getExtrema(const NanoGrid& grid, const CoordBBox &bbox) return extrema; }// getExtrema - } // namespace nanovdb #endif // NANOVDB_GRIDSTATS_H_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/GridValidator.h b/nanovdb/nanovdb/util/GridValidator.h index 961074268f..fe6815bfb4 100644 --- a/nanovdb/nanovdb/util/GridValidator.h +++ b/nanovdb/nanovdb/util/GridValidator.h @@ -14,7 +14,7 @@ #ifndef NANOVDB_GRIDVALIDATOR_H_HAS_BEEN_INCLUDED #define NANOVDB_GRIDVALIDATOR_H_HAS_BEEN_INCLUDED -#include "../NanoVDB.h" +#include #include "GridChecksum.h" namespace nanovdb { @@ -59,8 +59,16 @@ std::string GridValidator::check(const GridT &grid, bool detailed) std::stringstream ss; if (!isValid(data)) { errorStr.assign("Grid is not 32B aligned"); - } else if (data->mMagic != NANOVDB_MAGIC_NUMBER) { - ss << "Incorrect magic number: Expected " << NANOVDB_MAGIC_NUMBER << ", but read " << data->mMagic; + } else if (data->mMagic != NANOVDB_MAGIC_NUMBER && data->mMagic != NANOVDB_MAGIC_GRID) { + const uint64_t magic1 = NANOVDB_MAGIC_NUMBER, magic2 = NANOVDB_MAGIC_GRID; + const char *c0 = (const char*)&(data->mMagic), *c1=(const char*)&magic1, *c2=(const char*)&magic2; + ss << "Incorrect magic number: Expected \""; + for (int i=0; i<8; ++i) ss << c1[i]; + ss << "\" or \""; + for (int i=0; i<8; ++i) ss << c2[i]; + ss << "\", but found \""; + for (int i=0; i<8; ++i) ss << c0[i]; + ss << "\""; errorStr = ss.str(); } else if (!validateChecksum(grid, detailed ? ChecksumMode::Full : ChecksumMode::Partial)) { errorStr.assign("Mis-matching checksum"); @@ -146,13 +154,13 @@ void GridValidator::checkNodes(const GridT &grid, std::string &errorStr) return errorStr.empty(); }; - for (auto it2 = grid.tree().root().beginChild(); it2; ++it2) { + for (auto it2 = grid.tree().root().cbeginChild(); it2; ++it2) { auto &node2 = *it2; if (!check(&node2, sizeof(node2))) return; - for (auto it1 = node2.beginChild(); it1; ++it1) { + for (auto it1 = node2.cbeginChild(); it1; ++it1) { auto &node1 = *it1; if (!check(&node1, sizeof(node1))) return; - for (auto it0 = node1.beginChild(); it0; ++it0) { + for (auto it0 = node1.cbeginChild(); it0; ++it0) { auto &node0 = *it0; if (!check(&node2, sizeof(node2))) return; }// loop over child nodes of the lower internal node diff --git a/nanovdb/nanovdb/util/HostBuffer.h b/nanovdb/nanovdb/util/HostBuffer.h index 7994d1a766..e0520d6983 100644 --- a/nanovdb/nanovdb/util/HostBuffer.h +++ b/nanovdb/nanovdb/util/HostBuffer.h @@ -77,16 +77,16 @@ #ifndef NANOVDB_HOSTBUFFER_H_HAS_BEEN_INCLUDED #define NANOVDB_HOSTBUFFER_H_HAS_BEEN_INCLUDED -#include "../NanoVDB.h"// for NANOVDB_DATA_ALIGNMENT; -#include // for types like int32_t etc -#include // for fprintf -#include // for std::malloc/std::realloc/std::free -#include // for std::make_shared -#include // for std::mutex -#include //for std::unordered_set -#include // for assert -#include // for std::stringstream -#include // for memcpy +#include // for NANOVDB_DATA_ALIGNMENT; +#include // for types like int32_t etc +#include // for fprintf +#include // for std::malloc/std::realloc/std::free +#include // for std::make_shared +#include // for std::mutex +#include // for std::unordered_set +#include // for assert +#include // for std::stringstream +#include // for memcpy #define checkPtr(ptr, msg) \ { \ @@ -98,7 +98,7 @@ namespace nanovdb { template struct BufferTraits { - static const bool hasDeviceDual = false; + static constexpr bool hasDeviceDual = false; }; // ----------------------------> HostBuffer <-------------------------------------- diff --git a/nanovdb/nanovdb/util/IO.h b/nanovdb/nanovdb/util/IO.h index 32246623aa..5d51cb53c6 100644 --- a/nanovdb/nanovdb/util/IO.h +++ b/nanovdb/nanovdb/util/IO.h @@ -13,13 +13,22 @@ multiple grid types. \note This file does NOT depend on OpenVDB, but optionally on ZIP and BLOSC + + \details NanoVDB files take on of two formats: + 1) multiple segments each with multiple grids (segments have easy to access metadata about its grids) + 2) starting with verion 32.6.0 nanovdb files also support a raw buffer with one or more grids (just a + dump of a raw grid buffer, so no new metadata). + + // 1: Segment: FileHeader, MetaData0, gridName0...MetaDataN, gridNameN, compress Grid0,...compressed GridN + // 2: Raw: Grid0,...GridN */ #ifndef NANOVDB_IO_H_HAS_BEEN_INCLUDED #define NANOVDB_IO_H_HAS_BEEN_INCLUDED -#include "../NanoVDB.h" +#include #include "GridHandle.h" +#include "GridChecksum.h"// for updateGridCount #include // for std::ifstream #include // for std::cerr/cout @@ -47,28 +56,60 @@ namespace nanovdb { namespace io { +// --------------------------> writeGrid(s) <------------------------------------ + +/// @brief Write a single grid to file (over-writing existing content of the file) +template +void writeGrid(const std::string& fileName, const GridHandle& handle, io::Codec codec = io::Codec::NONE, int verbose = 0); + +/// @brief Write multiple grids to file (over-writing existing content of the file) +template class VecT = std::vector> +void writeGrids(const std::string& fileName, const VecT>& handles, Codec codec = Codec::NONE, int verbose = 0); + +// --------------------------> readGrid(s) <------------------------------------ + +/// @brief Read and return one or all grids from a file into a single GridHandle +/// @tparam BufferT Type of buffer used memory allocation +/// @param fileName string name of file to be read from +/// @param n zero-based signed index of the grid to be read. +/// The default value of 0 means read only first grid. +/// A negative value of n means read all grids in the file. +/// @param verbose specify verbosity level. Default value of zero means quiet. +/// @param buffer optional buffer used for memory allocation +/// @return return a single GridHandle with one or all grids found in the file +/// @throw will throw a std::runtime_error if the file does not contain a grid with index n +template +GridHandle readGrid(const std::string& fileName, int n = 0, int verbose = 0, const BufferT& buffer = BufferT()); + +/// @brief Read and return the first grid with a specific name from a file +/// @tparam BufferT Type of buffer used memory allocation +/// @param fileName string name of file to be read from +/// @param gridName string name of the grid to be read +/// @param verbose specify verbosity level. Default value of zero means quiet. +/// @param buffer optional buffer used for memory allocation +/// @return return a single GridHandle containing the grid with the specific name +/// @throw will throw a std::runtime_error if the file does not contain a grid with the specific name +template +GridHandle readGrid(const std::string& fileName, const std::string& gridName, int verbose = 0, const BufferT& buffer = BufferT()); + +/// @brief Read all the grids in the file and return them as a vector of multiple GridHandles, each containing +/// all grids encoded in the same segment of the file (i.e. they where written together) +/// @tparam BufferT Type of buffer used memory allocation +/// @param fileName string name of file to be read from +/// @param verbose specify verbosity level. Default value of zero means quiet. +/// @param buffer optional buffer used for memory allocation +/// @return Return a vector of GridHandles each containing all grids encoded +/// in the same segment of the file (i.e. they where written together). +template class VecT = std::vector> +VecT> readGrids(const std::string& fileName, int verbose = 0, const BufferT& buffer = BufferT()); + +// ----------------------------------------------------------------------- + /// We fix a specific size for counting bytes in files so that they /// are saved the same regardless of machine precision. (Note there are /// still little/bigendian issues, however) using fileSize_t = uint64_t; -/// @brief Optional compression codecs -/// -/// @note NONE is the default, ZIP is slow but compact and BLOSC offers a great balance. -/// -/// @warning NanoVDB optionally supports ZIP and BLOSC compression and will throw an exception -/// if it support is required but missing. -enum class Codec : uint16_t { NONE = 0, - ZIP = 1, - BLOSC = 2, - END = 3 }; - -inline __hostdev__ const char* toStr(Codec codec) -{ - static const char * LUT[] = { "NONE", "ZIP", "BLOSC" , "END" }; - return LUT[static_cast(codec)]; -} - /// @brief Internal functions for compressed read/write of a NanoVDB GridHandle into a stream /// /// @warning These functions should never be called directly by client code @@ -76,107 +117,65 @@ namespace Internal { static constexpr fileSize_t MAX_SIZE = 1UL << 30; // size is 1 GB template -static fileSize_t write(std::ostream& os, const GridHandle& handle, Codec codec); +static fileSize_t write(std::ostream& os, const GridHandle& handle, Codec codec, uint32_t n); template -static void read(std::istream& is, GridHandle& handle, Codec codec); -}; // namespace Internal +static void read(std::istream& is, BufferT& buffer, Codec codec); + +static void read(std::istream& is, char* data, fileSize_t size, Codec codec); +} // namespace Internal /// @brief Standard hash function to use on strings; std::hash may vary by /// platform/implementation and is know to produce frequent collisions. uint64_t stringHash(const char* cstr); /// @brief Return a uint64_t hash key of a std::string -inline uint64_t stringHash(const std::string& str) -{ - return stringHash(str.c_str()); -} +inline uint64_t stringHash(const std::string& str){return stringHash(str.c_str());} /// @brief Return a uint64_t with its bytes reversed so we can check for endianness inline uint64_t reverseEndianness(uint64_t val) { return (((val) >> 56) & 0x00000000000000FF) | (((val) >> 40) & 0x000000000000FF00) | - (((val) >> 24) & 0x0000000000FF0000) | (((val) >> 8) & 0x00000000FF000000) | - (((val) << 8) & 0x000000FF00000000) | (((val) << 24) & 0x0000FF0000000000) | + (((val) >> 24) & 0x0000000000FF0000) | (((val) >> 8) & 0x00000000FF000000) | + (((val) << 8) & 0x000000FF00000000) | (((val) << 24) & 0x0000FF0000000000) | (((val) << 40) & 0x00FF000000000000) | (((val) << 56) & 0xFF00000000000000); } -/// @brief Data encoded at the head of each segment of a file or stream. +/// @brief This class defines the meta data stored for each grid in a segment /// -/// @note A file or stream is composed of one or more segments that each contain -// one or more grids. -// Magic number of NanoVDB files (uint64_t) | -// Version numbers of this file (uint32_t) | one header for each segment -// Number of grids in this segment (uint16_t) | -// Compression mode (uint16_t) | -struct Header -{// 16 bytes - uint64_t magic; // 8 bytes - Version version;// 4 bytes version numbers - uint16_t gridCount; // 2 bytes - Codec codec; // 2 bytes - Header(Codec c = Codec::NONE) - : magic(NANOVDB_MAGIC_NUMBER) // Magic number: "NanoVDB" in hex - , version()// major, minor and patch version numbers - , gridCount(0) - , codec(c) - { - } -}; // Header ( 16 bytes = 2 words ) - -/// @brief Data encoded for each of the grids associated with a segment. -// Grid size in memory (uint64_t) | -// Grid size on disk (uint64_t) | -// Grid name hash key (uint64_t) | -// Numer of active voxels (uint64_t) | -// Grid type (uint32_t) | -// Grid class (uint32_t) | -// Characters in grid name (uint32_t) | -// AABB in world space (2*3*double) | one per grid in file -// AABB in index space (2*3*int) | -// Size of a voxel in world units (3*double) | -// Byte size of the grid name (uint32_t) | -// Number of nodes per level (4*uint32_t) | -// Numer of active tiles per level (3*uint32_t) | -// Codec for file compression (uint16_t) | -// Padding due to 8B alignment (uint16_t) | -// Version number (uint32_t) | -struct MetaData -{// 176 bytes - uint64_t gridSize, fileSize, nameKey, voxelCount; // 4 * 8 = 32B. - GridType gridType; // 4B. - GridClass gridClass; // 4B. - BBox worldBBox; // 2 * 3 * 8 = 48B. - CoordBBox indexBBox; // 2 * 3 * 4 = 24B. - Vec3R voxelSize; // 24B. - uint32_t nameSize; // 4B. - uint32_t nodeCount[4]; //4 x 4 = 16B - uint32_t tileCount[3];// 3 x 4 = 12B - Codec codec; // 2B - uint16_t padding;// 2B, due to 8B alignment from uint64_t - Version version;// 4B -}; // MetaData - -struct GridMetaData : public MetaData -{ - static_assert(sizeof(MetaData) == 176, "Unexpected sizeof(MetaData)"); +/// @details A segment consists of a FileHeader followed by a list of FileGridMetaData +/// each followed by grid names and then finally the grids themselves. +/// +/// @note This class should not be confused with nanovdb::GridMetaData defined in NanoVDB.h +/// Also, FileMetaData is defined in NanoVDB.h. +struct FileGridMetaData : public FileMetaData +{ + static_assert(sizeof(FileMetaData) == 176, "Unexpected sizeof(FileMetaData)"); std::string gridName; void read(std::istream& is); void write(std::ostream& os) const; - GridMetaData() {} + FileGridMetaData() {} template - GridMetaData(uint64_t size, Codec c, const NanoGrid& grid); - uint64_t memUsage() const { return sizeof(MetaData) + nameSize; } -}; // GridMetaData + FileGridMetaData(uint64_t size, Codec c, const NanoGrid& grid); + uint64_t memUsage() const { return sizeof(FileMetaData) + nameSize; } +}; // FileGridMetaData +/// @brief This class defines all the data stored in segment of a file +/// +/// @details A segment consists of a FileHeader followed by a list of FileGridMetaData +/// each followed by grid names and then finally the grids themselves. struct Segment { - // Check assumptions made during read and write of Header and MetaData - static_assert(sizeof(Header) == 16u, "Unexpected sizeof(Header)"); - Header header; - std::vector meta; + // Check assumptions made during read and write of FileHeader and FileMetaData + static_assert(sizeof(FileHeader) == 16u, "Unexpected sizeof(FileHeader)"); + FileHeader header;// defined in NanoVDB.h + std::vector meta;// defined in NanoVDB.h Segment(Codec c = Codec::NONE) - : header(c) +#ifdef NANOVDB_USE_NEW_MAGIC_NUMBERS + : header{NANOVDB_MAGIC_FILE, Version(), 0u, c} +#else + : header{NANOVDB_MAGIC_NUMBER, Version(), 0u, c} +#endif , meta() { } @@ -187,56 +186,6 @@ struct Segment uint64_t memUsage() const; }; // Segment -/// @brief Write a single grid to file (over-writing existing content of the file) -template -void writeGrid(const std::string& fileName, const GridHandle& handle, Codec codec = Codec::NONE, int verbose = 0); - -/// @brief Write a single grid to stream (starting at the current position) -/// -/// @note This method can be used to append grid to an existing stream -template -void writeGrid(std::ostream& os, const GridHandle& handle, Codec codec = Codec::NONE); - -/// @brief Write multiple grids to file (over-writing existing content of the file) -template class VecT = std::vector> -void writeGrids(const std::string& fileName, const VecT>& handles, Codec codec = Codec::NONE, int verbose = 0); - -/// @brief Writes multiple grids to stream (starting at its current position) -/// -/// @note This method can be used to append multiple grids to an existing stream -template class VecT = std::vector> -void writeGrids(std::ostream& os, const VecT>& handles, Codec codec = Codec::NONE); - -/// @brief Read the n'th grid from file (defaults to first grid) -/// -/// @throw If n exceeds the number of grids in the file -template -GridHandle readGrid(const std::string& fileName, uint64_t n = 0, int verbose = 0, const BufferT& buffer = BufferT()); - -/// @brief Read the n'th grid from stream (defaults to first grid) -/// -/// @throw If n exceeds the number of grids in the stream -template -GridHandle readGrid(std::istream& is, uint64_t n = 0, const BufferT& buffer = BufferT()); - -/// @brief Read the first grid with a specific name -/// -/// @warning If not grid exists with the specified name the resulting GridHandle is empty -template -GridHandle readGrid(const std::string& fileName, const std::string& gridName, int verbose = 0, const BufferT& buffer = BufferT()); - -/// @brief Read the first grid with a specific name -template -GridHandle readGrid(std::istream& is, const std::string& gridName, const BufferT& buffer = BufferT()); - -/// @brief Read all the grids in the file -template class VecT = std::vector> -VecT> readGrids(const std::string& fileName, int verbose = 0, const BufferT& buffer = BufferT()); - -/// @brief Real all grids at the current position of the input stream -template class VecT = std::vector> -VecT> readGrids(std::istream& is, const BufferT& buffer = BufferT()); - /// @brief Return true if the file contains a grid with the specified name bool hasGrid(const std::string& fileName, const std::string& gridName); @@ -244,18 +193,18 @@ bool hasGrid(const std::string& fileName, const std::string& gridName); bool hasGrid(std::istream& is, const std::string& gridName); /// @brief Reads and returns a vector of meta data for all the grids found in the specified file -std::vector readGridMetaData(const std::string& fileName); +std::vector readGridMetaData(const std::string& fileName); /// @brief Reads and returns a vector of meta data for all the grids found in the specified stream -std::vector readGridMetaData(std::istream& is); +std::vector readGridMetaData(std::istream& is); // --------------------------> Implementations for Internal <------------------------------------ template -fileSize_t Internal::write(std::ostream& os, const GridHandle& handle, Codec codec) +fileSize_t Internal::write(std::ostream& os, const GridHandle& handle, Codec codec, unsigned int n) { - const char* data = reinterpret_cast(handle.data()); - fileSize_t total = 0, residual = handle.size(); + const char* data = reinterpret_cast(handle.gridData(n)); + fileSize_t total = 0, residual = handle.gridSize(n); switch (codec) { case Codec::ZIP: { @@ -300,18 +249,23 @@ fileSize_t Internal::write(std::ostream& os, const GridHandle& handle, os.write(data, residual); total += residual; } - if (!os) { - throw std::runtime_error("Failed to write Tree to file"); - } + if (!os) throw std::runtime_error("Failed to write Tree to file"); return total; } // Internal::write template -void Internal::read(std::istream& is, GridHandle& handle, Codec codec) +void Internal::read(std::istream& is, BufferT& buffer, Codec codec) { - char* data = reinterpret_cast(handle.buffer().data()); - fileSize_t residual = handle.buffer().size(); + Internal::read(is, reinterpret_cast(buffer.data()), buffer.size(), codec); +} // Internal::read +/// @brief read compressed grid from stream +/// @param is input stream to read from +/// @param data data buffer to write into +/// @param residual expected size of uncompressed data +/// @param codec mode of compression +void Internal::read(std::istream& is, char* data, fileSize_t residual, Codec codec) +{ // read tree using optional compression switch (codec) { case Codec::ZIP: { @@ -321,11 +275,9 @@ void Internal::read(std::istream& is, GridHandle& handle, Codec codec) std::unique_ptr tmp(new Bytef[size]); is.read(reinterpret_cast(tmp.get()), size); uLongf numBytes = residual; - int status = uncompress(reinterpret_cast(data), &numBytes, tmp.get(), static_cast(size)); - if (status != Z_OK) - std::runtime_error("Internal read error in ZIP"); - if (fileSize_t(numBytes) != residual) - throw std::runtime_error("UNZIP failed on byte size"); + int status = uncompress(reinterpret_cast(data), &numBytes, tmp.get(), static_cast(size)); + if (status != Z_OK) std::runtime_error("Internal read error in ZIP"); + if (fileSize_t(numBytes) != residual) throw std::runtime_error("UNZIP failed on byte size"); #else throw std::runtime_error("ZIP compression codec was disabled during build"); #endif @@ -353,148 +305,151 @@ void Internal::read(std::istream& is, GridHandle& handle, Codec codec) break; } default: - is.read(data, residual); - } - if (!is) { - throw std::runtime_error("Failed to read Tree from file"); + is.read(data, residual);// read uncompressed data } + if (!is) throw std::runtime_error("Failed to read Tree from file"); } // Internal::read -// --------------------------> Implementations for GridMetaData <------------------------------------ +// --------------------------> Implementations for FileGridMetaData <------------------------------------ template -inline GridMetaData::GridMetaData(uint64_t size, Codec c, const NanoGrid& grid) - : MetaData{size, // gridSize - 0, // fileSize - 0, // nameKey - grid.activeVoxelCount(), // voxelCount - grid.gridType(), // gridType - grid.gridClass(), // gridClass - grid.worldBBox(), // worldBBox - grid.tree().bbox(), // indexBBox - grid.voxelSize(), // voxelSize - 0, // nameSize - {0, 0, 0, 1}, // nodeCount[4] - {0, 0, 0}, // tileCount[3] - c, // codec - 0, // padding - Version()}// version +inline FileGridMetaData::FileGridMetaData(uint64_t size, Codec c, const NanoGrid& grid) + : FileMetaData{size, // gridSize + size, // fileSize (will typically be redefined) + 0u, // nameKey + grid.activeVoxelCount(), // voxelCount + grid.gridType(), // gridType + grid.gridClass(), // gridClass + grid.worldBBox(), // worldBBox + grid.tree().bbox(), // indexBBox + grid.voxelSize(), // voxelSize + 0, // nameSize + {0, 0, 0, 1}, // nodeCount[4] + {0, 0, 0}, // tileCount[3] + c, // codec + 0, // padding + Version()}// version , gridName(grid.gridName()) { nameKey = stringHash(gridName); nameSize = static_cast(gridName.size() + 1); // include '\0' - const uint32_t* ptr = reinterpret_cast*>(&grid.tree())->mNodeCount; - for (int i = 0; i < 3; ++i) { - MetaData::nodeCount[i] = *ptr++; - } - //MetaData::nodeCount[3] = 1;// one root node - for (int i = 0; i < 3; ++i) { - MetaData::tileCount[i] = *ptr++; - } -} + const uint32_t* ptr = reinterpret_cast(&grid.tree())->mNodeCount; + for (int i = 0; i < 3; ++i) FileMetaData::nodeCount[i] = *ptr++; + for (int i = 0; i < 3; ++i) FileMetaData::tileCount[i] = *ptr++; +}// FileGridMetaData::FileGridMetaData -inline void GridMetaData::write(std::ostream& os) const +inline void FileGridMetaData::write(std::ostream& os) const { - os.write(reinterpret_cast(this), sizeof(MetaData)); + os.write(reinterpret_cast(this), sizeof(FileMetaData)); os.write(gridName.c_str(), nameSize); - if (!os) { - throw std::runtime_error("Failed writing GridMetaData"); - } -} + if (!os) throw std::runtime_error("Failed writing FileGridMetaData"); +}// FileGridMetaData::write -inline void GridMetaData::read(std::istream& is) +inline void FileGridMetaData::read(std::istream& is) { - is.read(reinterpret_cast(this), sizeof(MetaData)); + is.read(reinterpret_cast(this), sizeof(FileMetaData)); std::unique_ptr tmp(new char[nameSize]); is.read(reinterpret_cast(tmp.get()), nameSize); gridName.assign(tmp.get()); - if (!is) { - throw std::runtime_error("Failed reading GridMetaData"); - } -} + if (!is) throw std::runtime_error("Failed reading FileGridMetaData"); +}// FileGridMetaData::read // --------------------------> Implementations for Segment <------------------------------------ inline uint64_t Segment::memUsage() const { - uint64_t sum = sizeof(Header); - for (auto& m : meta) { - sum += m.memUsage(); - } + uint64_t sum = sizeof(FileHeader); + for (auto& m : meta) sum += m.memUsage();// includes FileMetaData + grid name return sum; -} +}// Segment::memUsage template inline void Segment::add(const GridHandle& h) { - if (auto* grid = h.template grid()) { // most common - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else if (auto* grid = h.template grid()) { - meta.emplace_back(h.size(), header.codec, *grid); - } else { - throw std::runtime_error("nanovdb::io::Segment::add Cannot write grid of unknown type to file"); + for (uint32_t i = 0; i < h.gridCount(); ++i) { + if (auto* grid = h.template grid(i)) { // most common + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else if (auto* grid = h.template grid(i)) { + meta.emplace_back(h.gridSize(i), header.codec, *grid); + } else { + std::stringstream ss; + ss << "nanovdb::io::Segment::add: Cannot write grid of unknown type \""<(&header), sizeof(Header))) { - throw std::runtime_error("Failed to write Header of Segment"); + } else if (!os.write(reinterpret_cast(&header), sizeof(FileHeader))) { + throw std::runtime_error("Failed to write FileHeader of Segment"); } - for (auto& m : meta) { - m.write(os); - } -} + for (auto& m : meta) m.write(os); +}// Segment::write inline bool Segment::read(std::istream& is) { - is.read(reinterpret_cast(&header), sizeof(Header)); - if (is.eof()) { + is.read(reinterpret_cast(&header), sizeof(FileHeader)); + if (is.eof()) {// The EOF flag is only set once a read tries to read past the end of the file + is.clear(std::ios_base::eofbit);// clear eof flag so we can rewind and read again return false; } - if (!is || header.magic != NANOVDB_MAGIC_NUMBER) { + if (!header.isValid()) { // first check for byte-swapped header magic. - if (header.magic == reverseEndianness(NANOVDB_MAGIC_NUMBER)) + if (header.magic == reverseEndianness(NANOVDB_MAGIC_NUMBER) || + header.magic == reverseEndianness(NANOVDB_MAGIC_FILE)) { throw std::runtime_error("This nvdb file has reversed endianness"); - throw std::runtime_error("Magic number error: This is not a valid nvdb file"); - } else if ( header.version.getMajor() != NANOVDB_MAJOR_VERSION_NUMBER) { + } else { + throw std::runtime_error("Magic number error: This is not a valid nvdb file"); + } + } else if ( !header.version.isCompatible()) { std::stringstream ss; - if (header.version.getMajor() < NANOVDB_MAJOR_VERSION_NUMBER) { + Version v; + is.read(reinterpret_cast(&v), sizeof(Version));// read GridData::mVersion located at byte 16=sizeof(FileHeader) is stream + if ( v.getMajor() == NANOVDB_MAJOR_VERSION_NUMBER) { + ss << "This file looks like it contains a raw grid buffer and not a standard file with meta data"; + } else if ( header.version.getMajor() < NANOVDB_MAJOR_VERSION_NUMBER) { ss << "The file contains an older version of NanoVDB: " << std::string(header.version.c_str()) << "!\n\t" << "Recommendation: Re-generate this NanoVDB file with this version: " << NANOVDB_MAJOR_VERSION_NUMBER << ".X of NanoVDB"; } else { @@ -509,116 +464,176 @@ inline bool Segment::read(std::istream& is) m.version = header.version; } return true; -} +}// Segment::read -// --------------------------> Implementations for read/write <------------------------------------ +// --------------------------> writeGrid <------------------------------------ + +template +void writeGrid(std::ostream& os, const GridHandle& handle, Codec codec) +{ + Segment seg(codec); + seg.add(handle); + const auto start = os.tellp(); + seg.write(os); // write header without the correct fileSize (so it's allocated) + for (uint32_t i = 0; i < handle.gridCount(); ++i) { + seg.meta[i].fileSize = Internal::write(os, handle, codec, i); + } + os.seekp(start); + seg.write(os);// re-write header with the correct fileSize + os.seekp(0, std::ios_base::end);// skip to end +}// writeGrid template void writeGrid(const std::string& fileName, const GridHandle& handle, Codec codec, int verbose) { std::ofstream os(fileName, std::ios::out | std::ios::binary | std::ios::trunc); if (!os.is_open()) { - throw std::runtime_error("Unable to open file named \"" + fileName + "\" for output"); + throw std::ios_base::failure("Unable to open file named \"" + fileName + "\" for output"); } writeGrid(os, handle, codec); if (verbose) { std::cout << "Wrote nanovdb::Grid to file named \"" << fileName << "\"" << std::endl; } -} +}// writeGrid -template -void writeGrid(std::ostream& os, const GridHandle& handle, Codec codec) +// --------------------------> writeGrids <------------------------------------ + +template class VecT = std::vector> +void writeGrids(std::ostream& os, const VecT>& handles, Codec codec = Codec::NONE) { - Segment s(codec); - s.add(handle); - const uint64_t headerSize = s.memUsage(); - std::streamoff seek = headerSize; - os.seekp(seek, std::ios_base::cur); // skip forward from the current position - s.meta[0].fileSize = Internal::write(os, handle, codec); - seek += s.meta[0].fileSize; - os.seekp(-seek, std::ios_base::cur); // rewind to start of stream - s.write(os); // write header - os.seekp(seek - headerSize, std::ios_base::cur); // skip to end -} + for (auto& h : handles) writeGrid(os, h, codec); +}// writeGrids template class VecT> void writeGrids(const std::string& fileName, const VecT>& handles, Codec codec, int verbose) { std::ofstream os(fileName, std::ios::out | std::ios::binary | std::ios::trunc); - if (!os.is_open()) { - throw std::runtime_error("Unable to open file named \"" + fileName + "\" for output"); - } + if (!os.is_open()) throw std::ios_base::failure("Unable to open file named \"" + fileName + "\" for output"); writeGrids(os, handles, codec); - if (verbose) { - std::cout << "Wrote " << handles.size() << " nanovdb::Grid(s) to file named \"" << fileName << "\"" << std::endl; - } -} + if (verbose) std::cout << "Wrote " << handles.size() << " nanovdb::Grid(s) to file named \"" << fileName << "\"" << std::endl; +}// writeGrids -template class VecT> -void writeGrids(std::ostream& os, const VecT>& handles, Codec codec) +// --------------------------> readGrid <------------------------------------ + +template +GridHandle readGrid(std::istream& is, int n, const BufferT& pool) { - Segment s(codec); - for (auto& h : handles) { - s.add(h); - } - const uint64_t headerSize = s.memUsage(); - std::streamoff seek = headerSize; - os.seekp(seek, std::ios_base::cur); // skip forward from the current position - for (size_t i = 0; i < handles.size(); ++i) { - s.meta[i].fileSize = Internal::write(os, handles[i], codec); - seek += s.meta[i].fileSize; + GridHandle handle; + if (n<0) {// read all grids into the same buffer + try {//first try to read a raw grid buffer + handle.read(is, pool); + } catch(const std::logic_error&) { + Segment seg; + uint64_t bufferSize = 0u; + uint32_t gridCount = 0u, gridIndex = 0u; + const auto start = is.tellg(); + while (seg.read(is)) { + std::streamoff skipSize = 0; + for (auto& m : seg.meta) { + ++gridCount; + bufferSize += m.gridSize; + skipSize += m.fileSize; + }// loop over grids in segment + is.seekg(skipSize, std::ios_base::cur); // skip forward from the current position + }// loop over segments + auto buffer = BufferT::create(bufferSize, &pool); + char *ptr = (char*)buffer.data(); + is.seekg(start);// rewind + while (seg.read(is)) { + for (auto& m : seg.meta) { + Internal::read(is, ptr, m.gridSize, seg.header.codec); + updateGridCount((GridData*)ptr, gridIndex++, gridCount); + ptr += m.gridSize; + }// loop over grids in segment + }// loop over segments + return GridHandle(std::move(buffer)); + } + } else {// read a specific grid + try {//first try to read a raw grid buffer + handle.read(is, uint32_t(n), pool); + updateGridCount((GridData*)handle.data(), 0u, 1u); + } catch(const std::logic_error&) { + Segment seg; + int counter = -1; + while (seg.read(is)) { + std::streamoff seek = 0; + for (auto& m : seg.meta) { + if (++counter == n) { + auto buffer = BufferT::create(m.gridSize, &pool); + Internal::read(is, buffer, seg.header.codec); + updateGridCount((GridData*)buffer.data(), 0u, 1u); + return GridHandle(std::move(buffer)); + } else { + seek += m.fileSize; + } + }// loop over grids in segment + is.seekg(seek, std::ios_base::cur); // skip forward from the current position + }// loop over segments + if (n != counter) throw std::runtime_error("stream does not contain a #" + std::to_string(n) + " grid"); + } } - os.seekp(-seek, std::ios_base::cur); // rewind to start of stream - s.write(os); // write header - os.seekp(seek - headerSize, std::ios_base::cur); // skip to end -} + return handle; +}// readGrid /// @brief Read the n'th grid template -GridHandle readGrid(const std::string& fileName, uint64_t n, int verbose, const BufferT& buffer) +GridHandle readGrid(const std::string& fileName, int n, int verbose, const BufferT& buffer) { std::ifstream is(fileName, std::ios::in | std::ios::binary); - if (!is.is_open()) { - throw std::runtime_error("Unable to open file named \"" + fileName + "\" for input"); - } + if (!is.is_open()) throw std::ios_base::failure("Unable to open file named \"" + fileName + "\" for input"); auto handle = readGrid(is, n, buffer); if (verbose) { - std::cout << "Read NanoGrid # " << n << " from the file named \"" << fileName << "\"" << std::endl; + if (n<0) { + std::cout << "Read all NanoGrids from the file named \"" << fileName << "\"" << std::endl; + } else { + std::cout << "Read NanoGrid # " << n << " from the file named \"" << fileName << "\"" << std::endl; + } } return handle; // is converted to r-value and return value is move constructed. -} - +}// readGrid + +/// @brief Read a specific grid from an input stream given the name of the grid +/// @tparam BufferT Buffer type used for allocation +/// @param is input stream from which to read the grid +/// @param gridName string name of the (first) grid to be returned +/// @param pool optional memory pool from which to allocate the grid buffer +/// @return Return the first grid in the input stream with a specific name +/// @throw std::runtime_error with no grid exists with the specified name template -GridHandle readGrid(std::istream& is, uint64_t n, const BufferT& buffer) +GridHandle readGrid(std::istream& is, const std::string& gridName, const BufferT& pool) { - Segment s; - uint64_t counter = 0; - while (s.read(is)) { - std::streamoff seek = 0; - for (auto& m : s.meta) { - if (counter == n) { - GridHandle handle(BufferT::create(m.gridSize, &buffer)); - is.seekg(seek, std::ios_base::cur); // skip forward from the current position - Internal::read(is, handle, s.header.codec); - return handle; // is converted to r-value and return value is move constructed. - } else { - seek += m.fileSize; + try { + GridHandle handle; + handle.read(is, gridName, pool); + return handle; + } catch(const std::logic_error&) { + const auto key = stringHash(gridName); + Segment seg; + while (seg.read(is)) {// loop over all segments in stream + std::streamoff seek = 0; + for (auto& m : seg.meta) {// loop over all grids in segment + if ((m.nameKey == 0u || m.nameKey == key) && m.gridName == gridName) { // check for hash key collision + auto buffer = BufferT::create(m.gridSize, &pool); + is.seekg(seek, std::ios_base::cur); // rewind + Internal::read(is, buffer, seg.header.codec); + updateGridCount((GridData*)buffer.data(), 0u, 1u); + return GridHandle(std::move(buffer)); + } else { + seek += m.fileSize; + } } - ++counter; + is.seekg(seek, std::ios_base::cur); // skip forward from the current position } - is.seekg(seek, std::ios_base::cur); // skip forward from the current position } - throw std::runtime_error("Grid index exceeds grid count in file"); -} + throw std::runtime_error("Grid name '" + gridName + "' not found in file"); +}// readGrid /// @brief Read the first grid with a specific name template GridHandle readGrid(const std::string& fileName, const std::string& gridName, int verbose, const BufferT& buffer) { std::ifstream is(fileName, std::ios::in | std::ios::binary); - if (!is.is_open()) { - throw std::runtime_error("Unable to open file named \"" + fileName + "\" for input"); - } + if (!is.is_open()) throw std::ios_base::failure("Unable to open file named \"" + fileName + "\" for input"); auto handle = readGrid(is, gridName, buffer); if (verbose) { if (handle) { @@ -628,125 +643,154 @@ GridHandle readGrid(const std::string& fileName, const std::string& gri } } return handle; // is converted to r-value and return value is move constructed. -} +}// readGrid -template -GridHandle readGrid(std::istream& is, const std::string& gridName, const BufferT& buffer) +// --------------------------> readGrids <------------------------------------ + +template class VecT = std::vector> +VecT> readGrids(std::istream& is, const BufferT& pool = BufferT()) { - const auto key = stringHash(gridName); - Segment s; - while (s.read(is)) { - std::streamoff seek = 0; - for (auto& m : s.meta) { - if (m.nameKey == key && m.gridName == gridName) { // check for hash key collision - GridHandle handle(BufferT::create(m.gridSize, &buffer)); - is.seekg(seek, std::ios_base::cur); // rewind - Internal::read(is, handle, s.header.codec); - return handle; // is converted to r-value and return value is move constructed. - } else { - seek += m.fileSize; - } - } - is.seekg(seek, std::ios_base::cur); // skip forward from the current position - } - return GridHandle(); // empty handle -} + VecT> handles; + Segment seg; + while (seg.read(is)) { + uint64_t bufferSize = 0; + for (auto& m : seg.meta) bufferSize += m.gridSize; + auto buffer = BufferT::create(bufferSize, &pool); + uint64_t bufferOffset = 0; + for (uint16_t i = 0; i < seg.header.gridCount; ++i) { + auto *data = reinterpret_cast(buffer.data() + bufferOffset); + Internal::read(is, (char*)data, seg.meta[i].gridSize, seg.header.codec); + updateGridCount(data, uint32_t(i), uint32_t(seg.header.gridCount)); + bufferOffset += seg.meta[i].gridSize; + }// loop over grids in segment + handles.emplace_back(std::move(buffer)); // force move copy assignment + }// loop over segments + return handles; // is converted to r-value and return value is move constructed. +}// readGrids /// @brief Read all the grids template class VecT> VecT> readGrids(const std::string& fileName, int verbose, const BufferT& buffer) { std::ifstream is(fileName, std::ios::in | std::ios::binary); - if (!is.is_open()) { - throw std::runtime_error("Unable to open file named \"" + fileName + "\" for input"); - } + if (!is.is_open()) throw std::ios_base::failure("Unable to open file named \"" + fileName + "\" for input"); auto handles = readGrids(is, buffer); - if (verbose) { - std::cout << "Read " << handles.size() << " NanoGrid(s) from the file named \"" << fileName << "\"" << std::endl; - } + if (verbose) std::cout << "Read " << handles.size() << " NanoGrid(s) from the file named \"" << fileName << "\"" << std::endl; return handles; // is converted to r-value and return value is move constructed. -} +}// readGrids -template class VecT> -VecT> readGrids(std::istream& is, const BufferT& buffer) -{ - VecT> handles; - Segment seg; - while (seg.read(is)) { - for (auto& m : seg.meta) { - GridHandle handle(BufferT::create(m.gridSize, &buffer)); - Internal::read(is, handle, seg.header.codec); - handles.push_back(std::move(handle)); // force move copy assignment - } - } - return handles; // is converted to r-value and return value is move constructed. -} +// --------------------------> readGridMetaData <------------------------------------ -inline std::vector readGridMetaData(const std::string& fileName) +inline std::vector readGridMetaData(const std::string& fileName) { std::ifstream is(fileName, std::ios::in | std::ios::binary); - if (!is.is_open()) { - throw std::runtime_error("Unable to open file named \"" + fileName + "\" for input"); - } + if (!is.is_open()) throw std::ios_base::failure("Unable to open file named \"" + fileName + "\" for input"); return readGridMetaData(is); // is converted to r-value and return value is move constructed. -} +}// readGridMetaData -inline std::vector readGridMetaData(std::istream& is) +inline std::vector readGridMetaData(std::istream& is) { - std::vector meta; - Segment seg; - while (seg.read(is)) { - std::streamoff seek = 0; - for (auto& m : seg.meta) { - meta.push_back(m); - seek += m.fileSize; - } - is.seekg(seek, std::ios_base::cur); + Segment seg; + std::vector meta; + try { + GridHandle<> handle;// if stream contains a raw grid buffer we unfortunately have to load everything + handle.read(is); + seg.add(handle); + meta = std::move(seg.meta); + } catch(const std::logic_error&) { + while (seg.read(is)) { + std::streamoff skip = 0; + for (auto& m : seg.meta) { + meta.push_back(m); + skip += m.fileSize; + }// loop over grid meta data in segment + is.seekg(skip, std::ios_base::cur); + }// loop over segments } return meta; // is converted to r-value and return value is move constructed. -} +}// readGridMetaData + +// --------------------------> hasGrid <------------------------------------ inline bool hasGrid(const std::string& fileName, const std::string& gridName) { std::ifstream is(fileName, std::ios::in | std::ios::binary); - if (!is.is_open()) { - throw std::runtime_error("Unable to open file named \"" + fileName + "\" for input"); - } + if (!is.is_open()) throw std::ios_base::failure("Unable to open file named \"" + fileName + "\" for input"); return hasGrid(is, gridName); -} +}// hasGrid inline bool hasGrid(std::istream& is, const std::string& gridName) { const auto key = stringHash(gridName); - Segment s; - while (s.read(is)) { + Segment seg; + while (seg.read(is)) { std::streamoff seek = 0; - for (auto& m : s.meta) { - if (m.nameKey == key && m.gridName == gridName) { - return true; // check for hash key collision - } + for (auto& m : seg.meta) { + if (m.nameKey == key && m.gridName == gridName) return true; // check for hash key collision seek += m.fileSize; - } + }// loop over grid meta data in segment is.seekg(seek, std::ios_base::cur); - } + }// loop over segments return false; -} +}// hasGrid + +// --------------------------> stringHash <------------------------------------ -inline uint64_t stringHash(const char* cstr) +inline uint64_t stringHash(const char* c_str) { - uint64_t hash = 0; - if (!cstr) { - return hash; - } - for (auto* str = reinterpret_cast(cstr); *str; ++str) { - uint64_t overflow = hash >> (64 - 8); - hash *= 67; // Next-ish prime after 26 + 26 + 10 - hash += *str + overflow; + uint64_t hash = 0;// zero is returned when cstr = nullptr or "\0" + if (c_str) { + for (auto* str = reinterpret_cast(c_str); *str; ++str) { + uint64_t overflow = hash >> (64 - 8); + hash *= 67; // Next-ish prime after 26 + 26 + 10 + hash += *str + overflow; + } } return hash; +}// stringHash + +} // namespace io + +template +inline std::ostream& +operator<<(std::ostream& os, const BBox>& b) +{ + os << "(" << b[0][0] << "," << b[0][1] << "," << b[0][2] << ") -> " + << "(" << b[1][0] << "," << b[1][1] << "," << b[1][2] << ")"; + return os; } +inline std::ostream& +operator<<(std::ostream& os, const CoordBBox& b) +{ + os << "(" << b[0][0] << "," << b[0][1] << "," << b[0][2] << ") -> " + << "(" << b[1][0] << "," << b[1][1] << "," << b[1][2] << ")"; + return os; +} + +inline std::ostream& +operator<<(std::ostream& os, const Coord& ijk) +{ + os << "(" << ijk[0] << "," << ijk[1] << "," << ijk[2] << ")"; + return os; } -} // namespace nanovdb::io + +template +inline std::ostream& +operator<<(std::ostream& os, const Vec3& v) +{ + os << "(" << v[0] << "," << v[1] << "," << v[2] << ")"; + return os; +} + +template +inline std::ostream& +operator<<(std::ostream& os, const Vec4& v) +{ + os << "(" << v[0] << "," << v[1] << "," << v[2] << "," << v[3] << ")"; + return os; +} + +} // namespace nanovdb #endif // NANOVDB_IO_H_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/IndexGridBuilder.h b/nanovdb/nanovdb/util/IndexGridBuilder.h deleted file mode 100644 index 77b9235664..0000000000 --- a/nanovdb/nanovdb/util/IndexGridBuilder.h +++ /dev/null @@ -1,652 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: MPL-2.0 - -/*! - \file IndexGridBuilder.h - - \author Ken Museth - - \date July 8, 2022 - - \brief Generates a NanoVDB IndexGrid from any existing NanoVDB grid. - - \note An IndexGrid encodes index offsets to external value arrays -*/ - -#ifndef NANOVDB_INDEXGRIDBUILDER_H_HAS_BEEN_INCLUDED -#define NANOVDB_INDEXGRIDBUILDER_H_HAS_BEEN_INCLUDED - -#include "GridHandle.h" -#include "NodeManager.h" -#include "Range.h" -#include "ForEach.h" - -#include -#include -#include -#include // for stringstream -#include -#include // for memcpy - -namespace nanovdb { - -/// @brief Allows for the construction of NanoVDB grids without any dependency -template -class IndexGridBuilder -{ - using SrcNode0 = NanoLeaf< SrcValueT>; - using SrcNode1 = NanoLower; - using SrcNode2 = NanoUpper; - using SrcData0 = typename SrcNode0::DataType; - using SrcData1 = typename SrcNode1::DataType; - using SrcData2 = typename SrcNode2::DataType; - using SrcRootT = NanoRoot; - using SrcTreeT = NanoTree; - using SrcGridT = NanoGrid; - - using DstNode0 = NanoLeaf< ValueIndex>; - using DstNode1 = NanoLower; - using DstNode2 = NanoUpper; - using DstData0 = NanoLeaf< ValueIndex>::DataType; - using DstData1 = NanoLower::DataType; - using DstData2 = NanoUpper::DataType; - using DstRootT = NanoRoot; - using DstTreeT = NanoTree; - using DstGridT = NanoGrid; - - NodeManagerHandle<> mSrcMgrHandle; - NodeManager *mSrcMgr; - std::vector mValIdx2, mValIdx1, mValIdx0;// store id of first value in node - uint8_t* mBufferPtr;// pointer to the beginning of the buffer - uint64_t mBufferOffsets[9];//grid, tree, root, upper, lower, leafs, meta, data, buffer size - uint64_t mValueCount; - const bool mIsSparse, mIncludeStats;// include inactive values and stats - - DstNode0* getLeaf( int i=0) const {return PtrAdd(mBufferPtr, mBufferOffsets[5]) + i;} - DstNode1* getLower(int i=0) const {return PtrAdd(mBufferPtr, mBufferOffsets[4]) + i;} - DstNode2* getUpper(int i=0) const {return PtrAdd(mBufferPtr, mBufferOffsets[3]) + i;} - DstRootT* getRoot() const {return PtrAdd(mBufferPtr, mBufferOffsets[2]);} - DstTreeT* getTree() const {return PtrAdd(mBufferPtr, mBufferOffsets[1]);} - DstGridT* getGrid() const {return PtrAdd(mBufferPtr, mBufferOffsets[0]);} - - // Count the number of values (possibly only active) - void countValues(); - - // Below are private methods use to serialize nodes into NanoVDB - template - GridHandle initHandle(uint32_t channels, const BufferT& buffer); - - void processLeafs(); - - void processLower(); - - void processUpper(); - - void processRoot(); - - void processTree(); - - void processGrid(const std::string& name, uint32_t channels); - - void processChannels(uint32_t channels); - -public: - - /// @brief Constructor based on a source grid - /// - /// @param srcGrid Source grid used to generate the IndexGrid - /// @param includeInactive Include inactive values or only active values - /// @param includeStats Include min/max/avg/std per node or not - /// - /// @note For minimum memory consumption set the two boolean options to false - IndexGridBuilder(const SrcGridT& srcGrid, bool includeInactive = true, bool includeStats = true) - : mSrcMgrHandle(createNodeManager(srcGrid)) - , mSrcMgr(mSrcMgrHandle.template mgr()) - , mValueCount(0) - , mIsSparse(!includeInactive) - , mIncludeStats(includeStats) - {} - - /// @brief Return an instance of a GridHandle (invoking move semantics) - template - GridHandle getHandle(const std::string& name = "", uint32_t channels = 0u, const BufferT& buffer = BufferT()); - - /// @brief return the total number of values located in the source grid. - /// - /// @note This is minimum number of elements required for the external array that the IndexGrid - /// points to. - uint64_t getValueCount() const { return mValueCount; } - - /// @brief return a buffer with all the values in the source grid - template - BufferT getValues(uint32_t channels = 1u, const BufferT &buffer = BufferT()); - - /// @brief copy values from the source grid into the provided array and returns number of values copied - uint64_t copyValues(SrcValueT *buffer, size_t maxValueCount = -1); -}; // IndexGridBuilder - -//================================================================================================ - -template -template -GridHandle IndexGridBuilder:: -getHandle(const std::string &name, uint32_t channels, const BufferT &buffer) -{ - this->countValues(); - - auto handle = this->template initHandle(channels, buffer);// initialize the arrays of nodes - - this->processLeafs(); - - this->processLower(); - - this->processUpper(); - - this->processRoot(); - - this->processTree(); - - this->processGrid(name, channels); - - this->processChannels(channels); - - return handle; -} // IndexGridBuilder::getHandle - -//================================================================================================ - -template -void IndexGridBuilder::countValues() -{ - const uint64_t stats = mIncludeStats ? 4u : 0u; - - uint64_t valueCount = 1u + stats;//background, [minimum, maximum, average, and deviation] - - // root values - if (mIsSparse) { - for (auto it = mSrcMgr->root().beginValueOn(); it; ++it) ++valueCount; - } else { - for (auto it = mSrcMgr->root().beginValue(); it; ++it) ++valueCount; - } - - // tile values in upper internal nodes - mValIdx2.resize(mSrcMgr->nodeCount(2) + 1); - if (mIsSparse) { - forEach(1, mValIdx2.size(), 8, [&](const Range1D& r){ - for (auto i = r.begin(); i!=r.end(); ++i) { - mValIdx2[i] = stats + mSrcMgr->upper(i-1).data()->mValueMask.countOn(); - } - }); - } else { - forEach(1, mValIdx2.size(), 8, [&](const Range1D& r){ - const uint64_t n = 32768u + stats; - for (auto i = r.begin(); i!=r.end(); ++i) { - mValIdx2[i] = n - mSrcMgr->upper(i-1).data()->mChildMask.countOn(); - } - }); - } - mValIdx2[0] = valueCount; - for (size_t i=1; inodeCount(1) + 1); - if (mIsSparse) { - forEach(1, mValIdx1.size(), 8, [&](const Range1D& r){ - for (auto i = r.begin(); i!=r.end(); ++i) { - mValIdx1[i] = stats + mSrcMgr->lower(i-1).data()->mValueMask.countOn(); - } - }); - } else { - forEach(1, mValIdx1.size(), 8, [&](const Range1D& r){ - const uint64_t n = 4096u + stats; - for (auto i = r.begin(); i!=r.end(); ++i) { - mValIdx1[i] = n - mSrcMgr->lower(i-1).data()->mChildMask.countOn(); - } - }); - } - mValIdx1[0] = valueCount; - for (size_t i=1; inodeCount(0) + 1, 512u + stats); - if (mIsSparse) { - forEach(1, mValIdx0.size(), 8, [&](const Range1D& r) { - for (auto i = r.begin(); i != r.end(); ++i) { - mValIdx0[i] = stats + mSrcMgr->leaf(i-1).data()->mValueMask.countOn(); - } - }); - } - mValIdx0[0] = valueCount; - for (size_t i=1; i -uint64_t IndexGridBuilder::copyValues(SrcValueT *buffer, size_t maxValueCount) -{ - assert(mBufferPtr); - if (maxValueCount < mValueCount) return 0; - - // Value array always starts with these entries - buffer[0] = mSrcMgr->root().background(); - if (mIncludeStats) { - buffer[1] = mSrcMgr->root().minimum(); - buffer[2] = mSrcMgr->root().maximum(); - buffer[3] = mSrcMgr->root().average(); - buffer[4] = mSrcMgr->root().stdDeviation(); - } - {// copy root tile values - auto *srcData = mSrcMgr->root().data(); - SrcValueT *v = buffer + (mIncludeStats ? 5u : 1u); - for (uint32_t tileID = 0; tileID < srcData->mTableSize; ++tileID) { - auto *srcTile = srcData->tile(tileID); - if (srcTile->isChild() ||(mIsSparse&&!srcTile->state)) continue; - NANOVDB_ASSERT(v - buffer < mValueCount); - *v++ = srcTile->value; - } - } - - {// upper nodes - auto kernel = [&](const Range1D& r) { - DstData2 *dstData = this->getUpper(r.begin())->data(); - for (auto i = r.begin(); i != r.end(); ++i, ++dstData) { - SrcValueT *v = buffer + mValIdx2[i]; - const SrcNode2 &srcNode = mSrcMgr->upper(i); - if (mIncludeStats) { - *v++ = srcNode.minimum(); - *v++ = srcNode.maximum(); - *v++ = srcNode.average(); - *v++ = srcNode.stdDeviation(); - } - if (mIsSparse) { - for (auto it = srcNode.beginValueOn(); it; ++it) { - NANOVDB_ASSERT(v - buffer < mValueCount); - *v++ = *it; - } - } else { - auto *srcData = srcNode.data(); - for (uint32_t j = 0; j != 32768; ++j) { - if (srcData->mChildMask.isOn(j)) continue; - NANOVDB_ASSERT(v - buffer < mValueCount); - *v++ = srcData->getValue(j); - } - } - } - }; - forEach(0, mSrcMgr->nodeCount(2), 1, kernel); - } - - {// lower nodes - auto kernel = [&](const Range1D& r) { - DstData1 *dstData = this->getLower(r.begin())->data(); - for (auto i = r.begin(); i != r.end(); ++i, ++dstData) { - SrcValueT *v = buffer + mValIdx1[i]; - const SrcNode1 &srcNode = mSrcMgr->lower(i); - if (mIncludeStats) { - *v++ = srcNode.minimum(); - *v++ = srcNode.maximum(); - *v++ = srcNode.average(); - *v++ = srcNode.stdDeviation(); - } - if (mIsSparse) { - for (auto it = srcNode.beginValueOn(); it; ++it) { - NANOVDB_ASSERT(v - buffer < mValueCount); - *v++ = *it; - } - } else { - auto *srcData = srcNode.data(); - for (uint32_t j = 0; j != 4096; ++j) { - if (srcData->mChildMask.isOn(j)) continue; - NANOVDB_ASSERT(v - buffer < mValueCount); - *v++ = srcData->getValue(j); - } - } - } - }; - forEach(0, mSrcMgr->nodeCount(1), 4, kernel); - } - {// leaf nodes - auto kernel = [&](const Range1D& r) { - DstData0 *dstLeaf = this->getLeaf(r.begin())->data(); - for (auto i = r.begin(); i != r.end(); ++i, ++dstLeaf) { - SrcValueT *v = buffer + mValIdx0[i];// bug!? - const SrcNode0 &srcLeaf = mSrcMgr->leaf(i); - if (mIncludeStats) { - *v++ = srcLeaf.minimum(); - *v++ = srcLeaf.maximum(); - *v++ = srcLeaf.average(); - *v++ = srcLeaf.stdDeviation(); - } - if (mIsSparse) { - for (auto it = srcLeaf.beginValueOn(); it; ++it) { - NANOVDB_ASSERT(v - buffer < mValueCount); - *v++ = *it; - } - } else { - const SrcData0 *srcData = srcLeaf.data(); - for (uint32_t j = 0; j != 512; ++j) { - NANOVDB_ASSERT(v - buffer < mValueCount); - *v++ = srcData->getValue(j); - } - } - } - }; - forEach(0, mSrcMgr->nodeCount(0), 8, kernel); - } - return mValueCount; -} // IndexGridBuilder::copyValues - -template -template -BufferT IndexGridBuilder::getValues(uint32_t channels, const BufferT &buffer) -{ - assert(channels > 0); - auto values = BufferT::create(channels*sizeof(SrcValueT)*mValueCount, &buffer); - SrcValueT *p = reinterpret_cast(values.data()); - if (!this->copyValues(p, mValueCount)) { - throw std::runtime_error("getValues: insufficient channels"); - } - for (uint32_t i=1; i -template -GridHandle IndexGridBuilder:: -initHandle(uint32_t channels, const BufferT& buffer) -{ - const SrcTreeT &srcTree = mSrcMgr->tree(); - mBufferOffsets[0] = 0;// grid is always stored at the start of the buffer! - mBufferOffsets[1] = DstGridT::memUsage(); // tree - mBufferOffsets[2] = mBufferOffsets[1] + DstTreeT::memUsage(); // root - mBufferOffsets[3] = mBufferOffsets[2] + DstRootT::memUsage(srcTree.root().tileCount());// upper internal nodes - mBufferOffsets[4] = mBufferOffsets[3] + srcTree.nodeCount(2)*sizeof(DstData2); // lower internal nodes - mBufferOffsets[5] = mBufferOffsets[4] + srcTree.nodeCount(1)*sizeof(DstData1); // leaf nodes - mBufferOffsets[6] = mBufferOffsets[5] + srcTree.nodeCount(0)*sizeof(DstData0); // meta data - mBufferOffsets[7] = mBufferOffsets[6] + GridBlindMetaData::memUsage(channels); // channel values - mBufferOffsets[8] = mBufferOffsets[7] + channels*mValueCount*sizeof(SrcValueT);// total size -#if 0 - std::cerr << "grid starts at " << mBufferOffsets[0] <<" byte" << std::endl; - std::cerr << "tree starts at " << mBufferOffsets[1] <<" byte" << std::endl; - std::cerr << "root starts at " << mBufferOffsets[2] <<" byte" << std::endl; - std::cerr << "node starts at " << mBufferOffsets[3] <<" byte" << " #" << srcTree.nodeCount(2) << std::endl; - std::cerr << "node starts at " << mBufferOffsets[4] <<" byte" << " #" << srcTree.nodeCount(1) << std::endl; - std::cerr << "leaf starts at " << mBufferOffsets[5] <<" byte" << " #" << srcTree.nodeCount(0) << std::endl; - std::cerr << "meta starts at " << mBufferOffsets[6] <<" byte" << std::endl; - std::cerr << "data starts at " << mBufferOffsets[7] <<" byte" << std::endl; - std::cerr << "buffer ends at " << mBufferOffsets[8] <<" byte" << std::endl; - std::cerr << "creating buffer of size " << (mBufferOffsets[8]>>20) << "MB" << std::endl; -#endif - GridHandle handle(BufferT::create(mBufferOffsets[8], &buffer)); - mBufferPtr = handle.data(); - - return handle; -} // IndexGridBuilder::initHandle - -//================================================================================================ - -template -void IndexGridBuilder::processGrid(const std::string& name, uint32_t channels) -{ - auto *srcData = mSrcMgr->grid().data(); - auto *dstData = this->getGrid()->data(); - - dstData->mMagic = NANOVDB_MAGIC_NUMBER; - dstData->mChecksum = 0u; - dstData->mVersion = Version(); - dstData->mFlags = static_cast(GridFlags::IsBreadthFirst); - dstData->mGridIndex = 0; - dstData->mGridCount = 1; - dstData->mGridSize = mBufferOffsets[8]; - std::memset(dstData->mGridName, '\0', GridData::MaxNameSize);//overwrite mGridName - strncpy(dstData->mGridName, name.c_str(), GridData::MaxNameSize-1); - dstData->mMap = srcData->mMap; - dstData->mWorldBBox = srcData->mWorldBBox; - dstData->mVoxelSize = srcData->mVoxelSize; - dstData->mGridClass = GridClass::IndexGrid; - dstData->mGridType = mapToGridType(); - dstData->mBlindMetadataOffset = mBufferOffsets[6]; - dstData->mBlindMetadataCount = channels; - dstData->mData0 = 0u; - dstData->mData1 = mValueCount;// encode the total number of values being indexed - dstData->mData2 = 0u; - - if (name.length() >= GridData::MaxNameSize) {// currently we don't support long grid names - std::stringstream ss; - ss << "Grid name \"" << name << "\" is more then " << GridData::MaxNameSize << " characters"; - throw std::runtime_error(ss.str()); - } -} // IndexGridBuilder::processGrid - -//================================================================================================ - -template -void IndexGridBuilder::processTree() -{ - auto *srcData = mSrcMgr->tree().data(); - auto *dstData = this->getTree()->data(); - for (int i=0; i<4; ++i) dstData->mNodeOffset[i] = mBufferOffsets[5-i] - mBufferOffsets[1];// byte offset from tree to first leaf, lower, upper and root node - for (int i=0; i<3; ++i) { - dstData->mNodeCount[i] = srcData->mNodeCount[i];// total number of nodes of type: leaf, lower internal, upper internal - dstData->mTileCount[i] = srcData->mTileCount[i];// total number of active tile values at the lower internal, upper internal and root node levels - } - dstData->mVoxelCount = srcData->mVoxelCount;// total number of active voxels in the root and all its child nodes -} // IndexGridBuilder::processTree - -//================================================================================================ - -template -void IndexGridBuilder::processRoot() -{ - auto *srcData = mSrcMgr->root().data(); - auto *dstData = this->getRoot()->data(); - - if (dstData->padding()>0) std::memset(dstData, 0, DstRootT::memUsage(mSrcMgr->root().tileCount())); - dstData->mBBox = srcData->mBBox; - dstData->mTableSize = srcData->mTableSize; - dstData->mBackground = 0u; - uint64_t valueCount = 1u;// the first entry is always the background value - if (mIncludeStats) { - valueCount += 4u; - dstData->mMinimum = 1u; - dstData->mMaximum = 2u; - dstData->mAverage = 3u; - dstData->mStdDevi = 4u; - } else if (dstData->padding()==0) { - dstData->mMinimum = 0u; - dstData->mMaximum = 0u; - dstData->mAverage = 0u; - dstData->mStdDevi = 0u; - } - //uint64_t valueCount = 5u;// this is always the first available index - for (uint32_t tileID = 0, childID = 0; tileID < dstData->mTableSize; ++tileID) { - auto *srcTile = srcData->tile(tileID); - auto *dstTile = dstData->tile(tileID); - dstTile->key = srcTile->key; - if (srcTile->isChild()) { - dstTile->child = childID * sizeof(DstNode2) + mBufferOffsets[3] - mBufferOffsets[2]; - dstTile->state = false; - dstTile->value = std::numeric_limits::max(); - ++childID; - } else { - dstTile->child = 0; - dstTile->state = srcTile->state; - if (!(mIsSparse && !dstTile->state)) dstTile->value = valueCount++; - } - } -} // IndexGridBuilder::processRoot - -//================================================================================================ - -template -void IndexGridBuilder::processUpper() -{ - static_assert(DstData2::padding()==0u, "Expected upper internal nodes to have no padding"); - auto kernel = [&](const Range1D& r) { - const bool activeOnly = mIsSparse; - const bool hasStats = mIncludeStats; - auto *dstData1 = this->getLower()->data();// fixed size - auto *dstData2 = this->getUpper(r.begin())->data();// fixed size - for (auto i = r.begin(); i != r.end(); ++i, ++dstData2) { - SrcData2 *srcData2 = mSrcMgr->upper(i).data();// might vary in size due to compression - dstData2->mBBox = srcData2->mBBox; - dstData2->mFlags = srcData2->mFlags; - srcData2->mFlags = i;// encode node ID - dstData2->mChildMask = srcData2->mChildMask; - dstData2->mValueMask = srcData2->mValueMask; - uint64_t n = mValIdx2[i]; - if (mIncludeStats) { - dstData2->mMinimum = n++; - dstData2->mMaximum = n++; - dstData2->mAverage = n++; - dstData2->mStdDevi = n++; - } else { - dstData2->mMinimum = 0u; - dstData2->mMaximum = 0u; - dstData2->mAverage = 0u; - dstData2->mStdDevi = 0u; - } - for (uint32_t j = 0; j != 32768; ++j) { - if (dstData2->isChild(j)) { - SrcData1 *srcChild = srcData2->getChild(j)->data(); - DstData1 *dstChild = dstData1 + srcChild->mFlags; - dstData2->setChild(j, dstChild); - srcChild->mFlags = dstChild->mFlags;// restore - } else { - const bool test = activeOnly && !srcData2->mValueMask.isOn(j); - dstData2->setValue(j, test ? 0 : n++); - } - } - - } - }; - forEach(0, mSrcMgr->nodeCount(2), 1, kernel); -} // IndexGridBuilder::processUpper - -//================================================================================================ - -template -void IndexGridBuilder::processLower() -{ - static_assert(DstData1::padding()==0u, "Expected lower internal nodes to have no padding"); - auto kernel = [&](const Range1D& r) { - const bool activeOnly = mIsSparse; - DstData0 *dstData0 = this->getLeaf()->data();// first dst leaf node - DstData1 *dstData1 = this->getLower(r.begin())->data();// fixed size - for (auto i = r.begin(); i != r.end(); ++i, ++dstData1) { - SrcData1 *srcData1 = mSrcMgr->lower(i).data();// might vary in size due to compression - dstData1->mBBox = srcData1->mBBox; - dstData1->mFlags = srcData1->mFlags; - srcData1->mFlags = i;// encode node ID - dstData1->mChildMask = srcData1->mChildMask; - dstData1->mValueMask = srcData1->mValueMask; - uint64_t n = mValIdx1[i]; - if (mIncludeStats) { - dstData1->mMinimum = n++; - dstData1->mMaximum = n++; - dstData1->mAverage = n++; - dstData1->mStdDevi = n++; - } else { - dstData1->mMinimum = 0u; - dstData1->mMaximum = 0u; - dstData1->mAverage = 0u; - dstData1->mStdDevi = 0u; - } - for (uint32_t j = 0; j != 4096; ++j) { - if (dstData1->isChild(j)) { - SrcData0 *srcChild = srcData1->getChild(j)->data(); - DstData0 *dstChild = dstData0 + srcChild->mBBoxMin[0]; - dstData1->setChild(j, dstChild); - srcChild->mBBoxMin[0] = dstChild->mBBoxMin[0];// restore - } else { - const bool test = activeOnly && !srcData1->mValueMask.isOn(j); - dstData1->setValue(j, test ? 0 : n++); - } - } - } - }; - forEach(0, mSrcMgr->nodeCount(1), 4, kernel); -} // IndexGridBuilder::processLower - -//================================================================================================ - -template -void IndexGridBuilder::processLeafs() -{ - static_assert(DstData0::padding()==0u, "Expected leaf nodes to have no padding"); - - auto kernel = [&](const Range1D& r) { - DstData0 *dstData0 = this->getLeaf(r.begin())->data();// fixed size - const uint8_t flags = mIsSparse ? 16u : 0u;// 4th bit indicates sparseness - for (auto i = r.begin(); i != r.end(); ++i, ++dstData0) { - SrcData0 *srcData0 = mSrcMgr->leaf(i).data();// might vary in size due to compression - dstData0->mBBoxMin = srcData0->mBBoxMin; - srcData0->mBBoxMin[0] = int(i);// encode node ID - dstData0->mBBoxDif[0] = srcData0->mBBoxDif[0]; - dstData0->mBBoxDif[1] = srcData0->mBBoxDif[1]; - dstData0->mBBoxDif[2] = srcData0->mBBoxDif[2]; - dstData0->mFlags = flags | (srcData0->mFlags & 2u);// 2nd bit indicates a bbox - dstData0->mValueMask = srcData0->mValueMask; - - if (mIncludeStats) { - dstData0->mStatsOff = mValIdx0[i];// first 4 entries are leaf stats - dstData0->mValueOff = mValIdx0[i] + 4u; - } else { - dstData0->mStatsOff = 0u;// set to background which indicates no stats! - dstData0->mValueOff = mValIdx0[i]; - } - } - }; - forEach(0, mSrcMgr->nodeCount(0), 8, kernel); -} // IndexGridBuilder::processLeafs - -//================================================================================================ - -template -void IndexGridBuilder::processChannels(uint32_t channels) -{ - for (uint32_t i=0; i(mBufferPtr, mBufferOffsets[6]) + i; - auto *blindData = PtrAdd(mBufferPtr, mBufferOffsets[7]) + i*mValueCount; - metaData->setBlindData(blindData); - metaData->mElementCount = mValueCount; - metaData->mFlags = 0; - metaData->mSemantic = GridBlindDataSemantic::Unknown; - metaData->mDataClass = GridBlindDataClass::ChannelArray; - metaData->mDataType = mapToGridType(); - std::memset(metaData->mName, '\0', GridBlindMetaData::MaxNameSize); - std::stringstream ss; - ss << toStr(metaData->mDataType) << "_channel_" << i; - strncpy(metaData->mName, ss.str().c_str(), GridBlindMetaData::MaxNameSize-1); - if (i) {// deep copy from previous channel -#if 0 - this->copyValues(blindData, mValueCount); - //std::memcpy(blindData, blindData-mValueCount, mValueCount*sizeof(SrcValueT)); -#else - nanovdb::forEach(0,mValueCount,1024,[&](const nanovdb::Range1D &r){ - SrcValueT *dst=blindData+r.begin(), *end=dst+r.size(), *src=dst-mValueCount; - while(dst!=end) *dst++ = *src++; - }); -#endif - } else { - this->copyValues(blindData, mValueCount); - } - } -} - -} // namespace nanovdb - -#endif // NANOVDB_INDEXGRIDBUILDER_H_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/Invoke.h b/nanovdb/nanovdb/util/Invoke.h index 36e58b4fdf..48e1ac0a42 100644 --- a/nanovdb/nanovdb/util/Invoke.h +++ b/nanovdb/nanovdb/util/Invoke.h @@ -19,7 +19,7 @@ #ifndef NANOVDB_INVOKE_H_HAS_BEEN_INCLUDED #define NANOVDB_INVOKE_H_HAS_BEEN_INCLUDED -#include "../NanoVDB.h"// for nanovdb::CoordBBox +#include // for nanovdb::CoordBBox #ifdef NANOVDB_USE_TBB #include diff --git a/nanovdb/nanovdb/util/NanoToOpenVDB.h b/nanovdb/nanovdb/util/NanoToOpenVDB.h index 9ee0297d62..8610afb9a8 100644 --- a/nanovdb/nanovdb/util/NanoToOpenVDB.h +++ b/nanovdb/nanovdb/util/NanoToOpenVDB.h @@ -56,7 +56,7 @@ nanoToOpenVDB(const NanoGrid& grid, int verbose = 0); /// @brief Forward declaration of free-standing function that de-serializes a NanoVDB GridHandle into an OpenVDB GridBase template openvdb::GridBase::Ptr -nanoToOpenVDB(const GridHandle& handle, int verbose = 0); +nanoToOpenVDB(const GridHandle& handle, int verbose = 0, uint32_t n = 0); /// @brief This class will serialize an OpenVDB grid into a NanoVDB grid managed by a GridHandle. template @@ -304,35 +304,35 @@ nanoToOpenVDB(const NanoGrid& grid, int verbose) template openvdb::GridBase::Ptr -nanoToOpenVDB(const GridHandle& handle, int verbose) +nanoToOpenVDB(const GridHandle& handle, int verbose, uint32_t n) { - if (auto grid = handle.template grid()) { + if (auto grid = handle.template grid(n)) { return nanovdb::nanoToOpenVDB(*grid, verbose); - } else if (auto grid = handle.template grid()) { + } else if (auto grid = handle.template grid(n)) { return nanovdb::nanoToOpenVDB(*grid, verbose); - } else if (auto grid = handle.template grid()) { + } else if (auto grid = handle.template grid(n)) { return nanovdb::nanoToOpenVDB(*grid, verbose); - } else if (auto grid = handle.template grid()) { + } else if (auto grid = handle.template grid(n)) { return nanovdb::nanoToOpenVDB(*grid, verbose); - } else if (auto grid = handle.template grid()) { + } else if (auto grid = handle.template grid(n)) { return nanovdb::nanoToOpenVDB(*grid, verbose); - } else if (auto grid = handle.template grid()) { + } else if (auto grid = handle.template grid(n)) { return nanovdb::nanoToOpenVDB(*grid, verbose); - } else if (auto grid = handle.template grid()) { + } else if (auto grid = handle.template grid(n)) { return nanovdb::nanoToOpenVDB(*grid, verbose); - } else if (auto grid = handle.template grid()) { + } else if (auto grid = handle.template grid(n)) { return nanovdb::nanoToOpenVDB(*grid, verbose); - } else if (auto grid = handle.template grid()) { + } else if (auto grid = handle.template grid(n)) { return nanovdb::nanoToOpenVDB(*grid, verbose); - } else if (auto grid = handle.template grid()) { + } else if (auto grid = handle.template grid(n)) { return nanovdb::nanoToOpenVDB(*grid, verbose); - } else if (auto grid = handle.template grid()) { + } else if (auto grid = handle.template grid(n)) { return nanovdb::nanoToOpenVDB(*grid, verbose); - } else if (auto grid = handle.template grid()) { + } else if (auto grid = handle.template grid(n)) { return nanovdb::nanoToOpenVDB(*grid, verbose); - } else if (auto grid = handle.template grid()) { + } else if (auto grid = handle.template grid(n)) { return nanovdb::nanoToOpenVDB(*grid, verbose); - } else if (auto grid = handle.template grid()) { + } else if (auto grid = handle.template grid(n)) { return nanovdb::nanoToOpenVDB(*grid, verbose); } else { OPENVDB_THROW(openvdb::RuntimeError, "Unsupported NanoVDB grid type!"); diff --git a/nanovdb/nanovdb/util/NodeManager.h b/nanovdb/nanovdb/util/NodeManager.h index c11bc6fb7c..4da1eee873 100644 --- a/nanovdb/nanovdb/util/NodeManager.h +++ b/nanovdb/nanovdb/util/NodeManager.h @@ -14,7 +14,7 @@ \details The ordering of the sequential access to nodes is always breadth-first! */ -#include "../NanoVDB.h"// for NanoGrid etc +#include // for NanoGrid etc #include "HostBuffer.h"// for HostBuffer #ifndef NANOVDB_NODEMANAGER_H_HAS_BEEN_INCLUDED @@ -44,30 +44,35 @@ NodeManagerHandle createNodeManager(const NanoGrid &grid, struct NodeManagerData {// 48B = 6*8B uint64_t mMagic;// 8B + union {int64_t mPadding; uint8_t mLinear;};// 8B of which 1B is used for a binary flag void *mGrid;// 8B pointer to either host or device grid union {int64_t *mPtr[3], mOff[3];};// 24B, use mOff if mLinear!=0 - uint8_t mLinear, mPadding[7];// 7B padding to 8B boundary }; /// @brief This class serves to manage a raw memory buffer of a NanoVDB NodeManager or LeafManager. template class NodeManagerHandle { - BufferT mBuffer; + GridType mGridType{GridType::Unknown}; + BufferT mBuffer; template - const NodeManager* getMgr() const; + const NodeManager* getMgr() const { + return mGridType == mapToGridType() ? (const NodeManager*)mBuffer.data() : nullptr; + } template - typename std::enable_if::hasDeviceDual, const NodeManager*>::type - getDeviceMgr() const; + typename enable_if::hasDeviceDual, const NodeManager*>::type + getDeviceMgr() const { + return mGridType == mapToGridType() ? (const NodeManager*)mBuffer.deviceData() : nullptr; + } template static T* no_const(const T* ptr) { return const_cast(ptr); } public: /// @brief Move constructor from a buffer - NodeManagerHandle(BufferT&& buffer) { mBuffer = std::move(buffer); } + NodeManagerHandle(GridType gridType, BufferT&& buffer) : mGridType(gridType) { mBuffer = std::move(buffer); } /// @brief Empty ctor NodeManagerHandle() = default; /// @brief Disallow copy-construction @@ -75,13 +80,18 @@ class NodeManagerHandle /// @brief Disallow copy assignment operation NodeManagerHandle& operator=(const NodeManagerHandle&) = delete; /// @brief Move copy assignment operation - NodeManagerHandle& operator=(NodeManagerHandle&& other) noexcept - { + NodeManagerHandle& operator=(NodeManagerHandle&& other) noexcept { + mGridType = other.mGridType; mBuffer = std::move(other.mBuffer); + other.mGridType = GridType::Unknown; return *this; } /// @brief Move copy-constructor - NodeManagerHandle(NodeManagerHandle&& other) noexcept { mBuffer = std::move(other.mBuffer); } + NodeManagerHandle(NodeManagerHandle&& other) noexcept { + mGridType = other.mGridType; + mBuffer = std::move(other.mBuffer); + other.mGridType = GridType::Unknown; + } /// @brief Default destructor ~NodeManagerHandle() { this->reset(); } /// @brief clear the buffer @@ -122,21 +132,21 @@ class NodeManagerHandle /// /// @warning Note that the return pointer can be NULL if the template parameter does not match the specified grid! template - typename std::enable_if::hasDeviceDual, const NodeManager*>::type + typename enable_if::hasDeviceDual, const NodeManager*>::type deviceMgr() const { return this->template getDeviceMgr(); } /// @brief Return a const pointer to the NodeManager encoded in this NodeManagerHandle on the device, e.g. GPU /// /// @warning Note that the return pointer can be NULL if the template parameter does not match the specified grid! template - typename std::enable_if::hasDeviceDual, NodeManager*>::type + typename enable_if::hasDeviceDual, NodeManager*>::type deviceMgr() { return no_const(this->template getDeviceMgr()); } /// @brief Upload the NodeManager to the device, e.g. from CPU to GPU /// /// @note This method is only available if the buffer supports devices template - typename std::enable_if::hasDeviceDual, void>::type + typename enable_if::hasDeviceDual, void>::type deviceUpload(void* deviceGrid, void* stream = nullptr, bool sync = true) { assert(deviceGrid); @@ -151,7 +161,7 @@ class NodeManagerHandle /// /// @note This method is only available if the buffer supports devices template - typename std::enable_if::hasDeviceDual, void>::type + typename enable_if::hasDeviceDual, void>::type deviceDownload(void* stream = nullptr, bool sync = true) { auto *data = reinterpret_cast(mBuffer.data()); @@ -161,25 +171,6 @@ class NodeManagerHandle } };// NodeManagerHandle -template -template -inline const NodeManager* NodeManagerHandle::getMgr() const -{ - using T = const NodeManager*; - T mgr = reinterpret_cast(mBuffer.data());// host - return mgr && mgr->grid().gridType() == mapToGridType() ? mgr : nullptr; -} - -template -template -inline typename std::enable_if::hasDeviceDual, const NodeManager*>::type -NodeManagerHandle::getDeviceMgr() const -{ - using T = const NodeManager*; - T mgr = reinterpret_cast(mBuffer.data());// host - return mgr && mgr->grid().gridType() == mapToGridType() ? reinterpret_cast(mBuffer.deviceData()) : nullptr; -} - /// @brief This class allows for sequential access to nodes in a NanoVDB tree /// /// @details Nodes are always arranged breadth first during sequential access of nodes @@ -196,9 +187,9 @@ class NodeManager : private NodeManagerData using Node2 = NodeT<2>;// upper internal node using Node1 = NodeT<1>;// lower internal node using Node0 = NodeT<0>;// leaf node - static constexpr bool FIXED_SIZE = Node0::FIXED_SIZE && Node1::FIXED_SIZE && Node2::FIXED_SIZE; public: + static constexpr bool FIXED_SIZE = Node0::FIXED_SIZE && Node1::FIXED_SIZE && Node2::FIXED_SIZE; NodeManager(const NodeManager&) = delete; NodeManager(NodeManager&&) = delete; @@ -218,7 +209,7 @@ class NodeManager : private NodeManagerData __hostdev__ static uint64_t memUsage(const GridT &grid) { uint64_t size = sizeof(NodeManagerData); if (!NodeManager::isLinear(grid)) { - const uint32_t *p = grid.tree().data()->mNodeCount; + const uint32_t *p = grid.tree().mNodeCount; size += sizeof(int64_t)*(p[0]+p[1]+p[2]); } return size; @@ -228,21 +219,25 @@ class NodeManager : private NodeManagerData __hostdev__ uint64_t memUsage() const {return NodeManager::memUsage(this->grid());} /// @brief Return a reference to the grid - __hostdev__ GridT& grid() { return *reinterpret_cast(DataT::mGrid); } + __hostdev__ GridT& grid() { return *reinterpret_cast(DataT::mGrid); } __hostdev__ const GridT& grid() const { return *reinterpret_cast(DataT::mGrid); } /// @brief Return a reference to the tree - __hostdev__ TreeT& tree() { return this->grid().tree(); } + __hostdev__ TreeT& tree() { return this->grid().tree(); } __hostdev__ const TreeT& tree() const { return this->grid().tree(); } /// @brief Return a reference to the root - __hostdev__ RootT& root() { return this->tree().root(); } + __hostdev__ RootT& root() { return this->tree().root(); } __hostdev__ const RootT& root() const { return this->tree().root(); } /// @brief Return the number of tree nodes at the specified level /// @details 0 is leaf, 1 is lower internal, and 2 is upper internal level __hostdev__ uint64_t nodeCount(int level) const { return this->tree().nodeCount(level); } + __hostdev__ uint64_t leafCount() const { return this->tree().nodeCount(0); } + __hostdev__ uint64_t lowerCount() const { return this->tree().nodeCount(1); } + __hostdev__ uint64_t upperCount() const { return this->tree().nodeCount(2); } + /// @brief Return the i'th leaf node with respect to breadth-first ordering template __hostdev__ const NodeT& node(uint32_t i) const { @@ -273,15 +268,15 @@ class NodeManager : private NodeManagerData /// @brief Return the i'th leaf node with respect to breadth-first ordering __hostdev__ const Node0& leaf(uint32_t i) const { return this->node<0>(i); } - __hostdev__ Node0& leaf(uint32_t i) { return this->node<0>(i); } + __hostdev__ Node0& leaf(uint32_t i) { return this->node<0>(i); } /// @brief Return the i'th lower internal node with respect to breadth-first ordering __hostdev__ const Node1& lower(uint32_t i) const { return this->node<1>(i); } - __hostdev__ Node1& lower(uint32_t i) { return this->node<1>(i); } + __hostdev__ Node1& lower(uint32_t i) { return this->node<1>(i); } /// @brief Return the i'th upper internal node with respect to breadth-first ordering __hostdev__ const Node2& upper(uint32_t i) const { return this->node<2>(i); } - __hostdev__ Node2& upper(uint32_t i) { return this->node<2>(i); } + __hostdev__ Node2& upper(uint32_t i) { return this->node<2>(i); } }; // NodeManager class @@ -289,13 +284,18 @@ template NodeManagerHandle createNodeManager(const NanoGrid &grid, const BufferT& buffer) { - NodeManagerHandle handle(BufferT::create(NodeManager::memUsage(grid), &buffer)); + NodeManagerHandle handle(mapToGridType(), BufferT::create(NodeManager::memUsage(grid), &buffer)); auto *data = reinterpret_cast(handle.data()); NANOVDB_ASSERT(isValid(data)); - data->mMagic = NANOVDB_MAGIC_NUMBER; - data->mGrid = const_cast*>(&grid); - - if ((data->mLinear = NodeManager::isLinear(grid)?1u:0u)) { + NANOVDB_ASSERT(mapToGridType() == grid.gridType()); +#ifdef NANOVDB_USE_NEW_MAGIC_NUMBERS + *data = NodeManagerData{NANOVDB_MAGIC_NODE, 0u, (void*)&grid, {0u,0u,0u}}; +#else + *data = NodeManagerData{NANOVDB_MAGIC_NUMBER, 0u, (void*)&grid, {0u,0u,0u}}; +#endif + + if (NodeManager::isLinear(grid)) { + data->mLinear = uint8_t(1u); data->mOff[0] = PtrDiff(grid.tree().template getFirstNode<0>(), &grid); data->mOff[1] = PtrDiff(grid.tree().template getFirstNode<1>(), &grid); data->mOff[2] = PtrDiff(grid.tree().template getFirstNode<2>(), &grid); @@ -304,11 +304,11 @@ NodeManagerHandle createNodeManager(const NanoGrid &grid, int64_t *ptr1 = data->mPtr[1] = data->mPtr[0] + grid.tree().nodeCount(0); int64_t *ptr2 = data->mPtr[2] = data->mPtr[1] + grid.tree().nodeCount(1); // Performs depth first traversal but breadth first insertion - for (auto it2 = grid.tree().root().beginChild(); it2; ++it2) { + for (auto it2 = grid.tree().root().cbeginChild(); it2; ++it2) { *ptr2++ = PtrDiff(&*it2, &grid); - for (auto it1 = it2->beginChild(); it1; ++it1) { + for (auto it1 = it2->cbeginChild(); it1; ++it1) { *ptr1++ = PtrDiff(&*it1, &grid); - for (auto it0 = it1->beginChild(); it0; ++it0) { + for (auto it0 = it1->cbeginChild(); it0; ++it0) { *ptr0++ = PtrDiff(&*it0, &grid); }// loop over child nodes of the lower internal node }// loop over child nodes of the upper internal node @@ -320,4 +320,8 @@ NodeManagerHandle createNodeManager(const NanoGrid &grid, } // namespace nanovdb +#if defined(__CUDACC__) +#include +#endif// defined(__CUDACC__) + #endif // NANOVDB_NODEMANAGER_H_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/OpenToNanoVDB.h b/nanovdb/nanovdb/util/OpenToNanoVDB.h index 3dd1c331b2..ea6c2c94d7 100644 --- a/nanovdb/nanovdb/util/OpenToNanoVDB.h +++ b/nanovdb/nanovdb/util/OpenToNanoVDB.h @@ -8,1491 +8,8 @@ \date January 8, 2020 - \brief This class will serialize an OpenVDB grid into a NanoVDB grid. -*/ - -#include -#include -#include - -#include "GridHandle.h" // manages and streams the raw memory buffer of a NanoVDB grid. -#include "GridChecksum.h" // for nanovdb::checksum -#include "GridStats.h" // for nanovdb::Extrema -#include "GridBuilder.h" // for nanovdb::AbsDiff -#include "ForEach.h"// for nanovdb::forEach -#include "Reduce.h"// for nanovdb::reduce -#include "Invoke.h"// for nanovdb::invoke -#include "DitherLUT.h"// for nanovdb::DitherLUT - -#include - -#ifndef NANOVDB_OPENTONANOVDB_H_HAS_BEEN_INCLUDED -#define NANOVDB_OPENTONANOVDB_H_HAS_BEEN_INCLUDED - -namespace nanovdb { - -/// @brief Converts OpenVDB types to NanoVDB types, e.g. openvdb::Vec3f to nanovdb::Vec3f -/// Template specializations are defined below. -template -struct OpenToNanoType { using Type = T; }; - -//================================================================================================ - -/// @brief Forward declaration of free-standing function that converts an OpenVDB GridBase into a NanoVDB GridHandle -template -GridHandle -openToNanoVDB(const openvdb::GridBase::Ptr& base, - StatsMode sMode = StatsMode::Default, - ChecksumMode cMode = ChecksumMode::Default, - int verbose = 0); - -//================================================================================================ - -/// @brief Forward declaration of free-standing function that converts a typed OpenVDB Grid into a NanoVDB GridHandle -/// -/// @details Unlike the function above that takes a base openvdb grid, this method is strongly typed and allows -/// for compression, e.g. openToNanoVDB -template::Type> -GridHandle -openToNanoVDB(const openvdb::Grid& grid, - StatsMode sMode = StatsMode::Default, - ChecksumMode cMode = ChecksumMode::Default, - int verbose = 0); - -//================================================================================================ - -/// @brief Template specialization for openvdb::Coord -template<> -struct OpenToNanoType -{ - using Type = nanovdb::Coord; - static_assert(sizeof(Type) == sizeof(openvdb::Coord), "Mismatching sizeof"); -}; - -/// @brief Template specialization for openvdb::CoordBBox -template<> -struct OpenToNanoType -{ - using Type = nanovdb::CoordBBox; - static_assert(sizeof(Type) == sizeof(openvdb::CoordBBox), "Mismatching sizeof"); -}; - -/// @brief Template specialization for openvdb::math::BBox -template -struct OpenToNanoType> -{ - using Type = nanovdb::BBox; - static_assert(sizeof(Type) == sizeof(openvdb::math::BBox), "Mismatching sizeof"); -}; - -/// @brief Template specialization for openvdb::math::Vec3 -template -struct OpenToNanoType> -{ - using Type = nanovdb::Vec3; - static_assert(sizeof(Type) == sizeof(openvdb::math::Vec3), "Mismatching sizeof"); -}; - -/// @brief Template specialization for openvdb::math::Vec4 -template -struct OpenToNanoType> -{ - using Type = nanovdb::Vec4; - static_assert(sizeof(Type) == sizeof(openvdb::math::Vec4), "Mismatching sizeof"); -}; - -/// @brief Template specialization for openvdb::ValueMask -template<> -struct OpenToNanoType -{ - using Type = nanovdb::ValueMask; -}; - -template<> -struct OpenToNanoType -{ - using Type = uint32_t; -}; - -template<> -struct OpenToNanoType -{ - using Type = uint32_t; -}; - -//================================================================================================ - -/// @brief Grid trait that defines OpenVDB grids with the exact same configuration as NanoVDB grids -template -struct OpenGridType -{ - using GridT = openvdb::Grid::Type>; - using TreeT = typename GridT::TreeType; - using RootT = typename TreeT::RootNodeType; - using UpperT = typename RootT::ChildNodeType; - using LowerT = typename UpperT::ChildNodeType; - using LeafT = typename LowerT::ChildNodeType; - using ValueT = typename LeafT::ValueType; -}; - -/// @brief Template specialization for the PointIndexGrid -template <> -struct OpenGridType -{ - using GridT = openvdb::tools::PointIndexGrid;// 5, 4, 3 - using TreeT = typename GridT::TreeType; - using RootT = typename TreeT::RootNodeType; - using UpperT = typename RootT::ChildNodeType; - using LowerT = typename UpperT::ChildNodeType; - using LeafT = typename LowerT::ChildNodeType; - using ValueT = typename LeafT::ValueType; -}; - -/// @brief Template specialization for the PointDataGrid -template <> -struct OpenGridType -{ - using GridT = openvdb::points::PointDataGrid;// 5, 4, 3 - using TreeT = typename GridT::TreeType; - using RootT = typename TreeT::RootNodeType; - using UpperT = typename RootT::ChildNodeType; - using LowerT = typename UpperT::ChildNodeType; - using LeafT = typename LowerT::ChildNodeType; - using ValueT = typename LeafT::ValueType; -}; - -//================================================================================================ - -/// @brief This class will convert an OpenVDB grid into a NanoVDB grid managed by a GridHandle. -/// -/// @note Note that this converter assumes a 5,4,3 tree configuration of BOTH the OpenVDB and NanoVDB -/// grids. This is a consequence of the fact that the OpenVDB tree is defined in OpenGridType and -/// that all NanoVDB trees are by design always 5,4,3! -/// -/// @details While NanoVDB allows root, internal and leaf nodes to reside anywhere in the memory buffer -/// this conversion tool uses the following memory layout: -/// -/// -/// Grid | Tree Root... Node2... Node1... Leaf... BlindMetaData... BlindData... -/// where "..." means size may vary and "|" means "no gap" - -template -class OpenToNanoVDB -{ - struct BlindMetaData; // forward declerations - template struct NodePair; - struct Codec {float min, max; uint16_t log2, size;};// used for adaptive bit-rate quantization - - using OpenGridT = typename OpenGridType::GridT;// OpenVDB grid - using OpenTreeT = typename OpenGridType::TreeT;// OpenVDB tree - using OpenRootT = typename OpenGridType::RootT;// OpenVDB root node - using OpenUpperT= typename OpenGridType::UpperT;// OpenVDB upper internal node - using OpenLowerT= typename OpenGridType::LowerT;// OpenVDB lower internal node - using OpenLeafT = typename OpenGridType::LeafT;// OpenVDB leaf node - using OpenValueT= typename OpenGridType::ValueT; - - using NanoValueT= typename BuildToValueMap::Type;// e.g. maps from Fp16 to float - using NanoLeafT = NanoLeaf; - using NanoLowerT= NanoLower; - using NanoUpperT= NanoUpper; - using NanoRootT = NanoRoot; - using NanoTreeT = NanoTree; - using NanoGridT = NanoGrid; - - static_assert(sizeof(NanoValueT) == sizeof(OpenValueT), "Mismatching sizeof"); - static_assert(is_same::Type>::value, "Mismatching ValueT"); - - NanoValueT mDelta; // skip node if: node.max < -mDelta || node.min > mDelta - uint8_t* mBufferPtr;// pointer to the beginning of the buffer - uint64_t mBufferOffsets[9];//grid, tree, root, upper. lower, leafs, meta data, blind data, buffer size - int mVerbose; - std::set mBlindMetaData; // sorted according to index - std::vector> mArray0; // leaf nodes - std::vector> mArray1; // lower internal nodes - std::vector> mArray2; // upper internal nodes - std::unique_ptr mCodec;// defines a codec per leaf node - StatsMode mStats; - ChecksumMode mChecksum; - bool mDitherOn; - OracleT mOracle;// used for adaptive bit-rate quantization - -public: - /// @brief Default c-tor - OpenToNanoVDB(); - - /// @brief return a reference to the compression oracle - /// - /// @note Note, the oracle is only used when NanoBuildT = nanovdb::FpN! - OracleT& oracle() { return mOracle; } - - void setVerbose(int mode = 1) { mVerbose = mode; } - - void enableDithering(bool on = true) { mDitherOn = on; } - - void setStats(StatsMode mode = StatsMode::Default) { mStats = mode; } - - void setChecksum(ChecksumMode mode = ChecksumMode::Default) { mChecksum = mode; } - - /// @brief Return a shared pointer to a NanoVDB grid handle constructed from the specified OpenVDB grid - GridHandle operator()(const OpenGridT& grid, - const BufferT& allocator = BufferT()); - - GridHandle operator()(const OpenGridT& grid, - StatsMode sMode, - ChecksumMode cMode, - int verbose, - const BufferT& allocator = BufferT()); - -private: - - /// @brief Allocates and return a handle for the buffer - GridHandle initHandle(const OpenGridT& openGrid, const BufferT& allocator); - - template - inline typename std::enable_if::value>::type - compression(const OpenGridT&, uint64_t&) {}// no-op - - template - inline typename std::enable_if::value>::type - compression(const OpenGridT& openGrid, uint64_t &offset); - - /// @brief Private method to process the grid - NanoGridT* processGrid(const OpenGridT& openGrid); - - // @brief Private method to process the tree - NanoTreeT* processTree(const OpenTreeT& openTree); - - /// @brief Private method to process the root node - NanoRootT* processRoot(const OpenRootT& openRoot); - - template - void processNodes(std::vector> &nodes); - - ////////////////////// - - template - typename std::enable_if::LeafT, typename T::OpenNodeT>::value && - !std::is_same::LeafT, typename T::OpenNodeT>::value && - !std::is_same::value && - !std::is_same::value && - !std::is_same::value && - !std::is_same::value>::type - processLeafs(std::vector &leafs); - - template - typename std::enable_if::value || - std::is_same::value || - std::is_same::value>::type - processLeafs(std::vector &leafs); - - template - typename std::enable_if::value>::type - processLeafs(std::vector &leafs); - - template - typename std::enable_if::LeafT>::value>::type - processLeafs(std::vector> &leafs); - - template - typename std::enable_if::LeafT>::value>::type - processLeafs(std::vector> &leafs); - - ////////////////////// - - /// @brief Private methods to pre-process the bind metadata - template - typename std::enable_if::value && - !std::is_same::value>::type - preProcessMetadata(const T& openGrid); - - template - typename std::enable_if::value>::type - preProcessMetadata(const T& openGrid); - - template - typename std::enable_if::value>::type - preProcessMetadata(const T& openGrid); - - ////////////////////// - - /// @brief Private methods to process the blind metadata - template - typename std::enable_if::value && - !std::is_same::value, GridBlindMetaData*>::type - processMetadata(const T& openGrid); - - template - typename std::enable_if::value, GridBlindMetaData*>::type - processMetadata(const T& openGrid); - - template - typename std::enable_if::value, GridBlindMetaData*>::type - processMetadata(const T& openGrid); - - ////////////////////// - - uint64_t pointCount(); - - template - void copyPointAttribute(size_t attIdx, AttT *attPtr); - - /// @brief Performs: nanoNode.origin = openNode.origin - /// openNode.origin = nanoNode offset - template - void encode(const OpenNodeT *openNode, NanoNodeT *nanoNode); - - /// @brief Performs: nanoNode offset = openNode.origin - /// openNode.origin = nanoNode.origin - /// return nanoNode offset - template - typename NanoNode::Type* decode(const OpenNodeT *openNode); - -}; // OpenToNanoVDB class - -//================================================================================================ - -template -OpenToNanoVDB::OpenToNanoVDB() - : mVerbose(0) - , mStats(StatsMode::Default) - , mChecksum(ChecksumMode::Default) - , mDitherOn(false) - , mOracle() -{ -} - -//================================================================================================ - -template -inline GridHandle -OpenToNanoVDB:: - operator()(const OpenGridT& openGrid, - StatsMode sMode, - ChecksumMode cMode, - int verbose, - const BufferT& allocator) -{ - this->setStats(sMode); - this->setChecksum(cMode); - this->setVerbose(verbose); - return (*this)(openGrid, allocator); -} - -//================================================================================================ - -template -inline GridHandle -OpenToNanoVDB:: - operator()(const OpenGridT& openGrid, - const BufferT& allocator) -{ - //mVerbose = 2; - std::unique_ptr timer(mVerbose > 1 ? new openvdb::util::CpuTimer() : nullptr); - - if (timer) timer->start("Allocating memory for the NanoVDB buffer"); - auto handle = this->initHandle(openGrid, allocator); - if (timer) timer->stop(); - - if (timer) timer->start("Processing leaf nodes"); - this->processLeafs(mArray0); - if (timer) timer->stop(); - - if (timer) timer->start("Processing lower internal nodes"); - this->processNodes(mArray1); - if (timer) timer->stop(); - - if (timer) timer->start("Processing upper internal nodes"); - this->processNodes(mArray2); - if (timer) timer->stop(); - - if (timer) timer->start("Processing grid, tree and root node"); - NanoGridT *nanoGrid = this->processGrid(openGrid); - if (timer) timer->stop(); - - // Point grids already make use of min/max so they shouldn't be re-computed - if (std::is_same::value || - std::is_same::value) { - if (mStats > StatsMode::BBox) mStats = StatsMode::BBox; - } - - if (timer) timer->start("GridStats"); - gridStats(*nanoGrid, mStats); - if (timer) timer->stop(); - - if (timer) timer->start("Checksum"); - updateChecksum(*nanoGrid, mChecksum); - if (timer) timer->stop(); - - return handle; // invokes move constructor -} // OpenToNanoVDB::operator() - -//================================================================================================ - -template -template -inline typename std::enable_if::value>::type -OpenToNanoVDB:: - compression(const OpenGridT& openGrid, uint64_t &offset) -{ - static_assert(is_same::value, "compression: expected OpenBuildT == float"); - static_assert(is_same::value, "compression: expected NanoBuildT == FpN"); - if (is_same::value && mOracle.getTolerance() < 0.0f) {// default tolerance for level set and fog volumes - if (openGrid.getGridClass() == openvdb::GRID_LEVEL_SET) { - mOracle.setTolerance(0.1f * float(openGrid.voxelSize()[0]));// range of ls: [-3dx; 3dx] - } else if (openGrid.getGridClass() == openvdb::GRID_FOG_VOLUME) { - mOracle.setTolerance(0.01f);// range of FOG volumes: [0;1] - } else { - mOracle.setTolerance(0.0f); - } - } - - const size_t size = mArray0.size(); - mCodec.reset(new Codec[size]); - - DitherLUT lut(mDitherOn); - auto kernel = [&](const auto &r) { - const OracleT oracle = mOracle;// local copy since it's very lightweight - for (auto i=r.begin(); i!=r.end(); ++i) { - const float *data = mArray0[i].node->buffer().data(); - float min = std::numeric_limits::max(), max = -min; - for (int j=0; j<512; ++j) { - float v = data[j]; - if (vmax) max=v; - } - mCodec[i].min = min; - mCodec[i].max = max; - const float range = max - min; - uint16_t logBitWidth = 0;// 0,1,2,3,4 => 1,2,4,8,16 bits - while (range > 0.0f && logBitWidth < 4u) { - const uint32_t mask = (uint32_t(1) << (uint32_t(1) << logBitWidth)) - 1u; - const float encode = mask/range; - const float decode = range/mask; - int j = 0; - do { - const float exact = data[j];// exact value - const uint32_t code = uint32_t(encode*(exact - min) + lut(j)); - const float approx = code * decode + min;// approximate value - j += oracle(exact, approx) ? 1 : 513; - } while(j < 512); - if (j == 512) break; - ++logBitWidth; - } - mCodec[i].log2 = logBitWidth; - mCodec[i].size = NanoLeafT::DataType::memUsage(1u< -GridHandle OpenToNanoVDB:: - initHandle(const OpenGridT& openGrid, const BufferT& buffer) -{ - auto &openTree = openGrid.tree(); - auto &openRoot = openTree.root(); - - mArray0.clear(); - mArray1.clear(); - mArray2.clear(); - std::vector nodeCount = openTree.nodeCount(); - mArray0.reserve(nodeCount[0]); - mArray1.reserve(nodeCount[1]); - mArray2.reserve(nodeCount[2]); - - uint64_t offset[3] = {0}; - for (auto it2 = openRoot.cbeginChildOn(); it2; ++it2) { - mArray2.emplace_back(&(*it2), offset[2]); - offset[2] += NanoUpperT::memUsage(); - for (auto it1 = it2->cbeginChildOn(); it1; ++it1) { - mArray1.emplace_back(&(*it1), offset[1]); - offset[1] += NanoLowerT::memUsage(); - for (auto it0 = it1->cbeginChildOn(); it0; ++it0) { - mArray0.emplace_back(&(*it0), offset[0]); - offset[0] += sizeof(NanoLeafT); - } - } - } - - this->template compression(openGrid, offset[0]); + \warning this file has been replaced by CreateNanoGrid.h - this->preProcessMetadata(openGrid); - - mBufferOffsets[0] = 0;// grid is always placed at the beginning of the buffer! - mBufferOffsets[1] = NanoGridT::memUsage(); // grid ends and tree begins - mBufferOffsets[2] = NanoTreeT::memUsage(); // tree ends and root begins - mBufferOffsets[3] = NanoRootT::memUsage(openTree.root().getTableSize()); // root ends and upper internal nodes begins - mBufferOffsets[4] = offset[2];// upper ends and lower internal nodes - mBufferOffsets[5] = offset[1];// lower ends and leaf nodes begins - mBufferOffsets[6] = offset[0];// leafs end blind meta data begins - mBufferOffsets[7] = GridBlindMetaData::memUsage(mBlindMetaData.size()); // meta ends and blind data begins - mBufferOffsets[8] = 0;// blind data - for (auto& i : mBlindMetaData) mBufferOffsets[8] += i.size; // blind data - - // Compute the prefixed sum - for (int i = 2; i < 9; ++i) { - mBufferOffsets[i] += mBufferOffsets[i - 1]; - } - -#if 0 - std::cerr << "grid starts at " << mBufferOffsets[0] <<" byte" << std::endl; - std::cerr << "tree starts at " << mBufferOffsets[1] <<" byte" << std::endl; - std::cerr << "root starts at " << mBufferOffsets[2] <<" byte" << std::endl; - std::cerr << "node starts at " << mBufferOffsets[3] <<" byte" << " #" << mArray2.size() << std::endl; - std::cerr << "node starts at " << mBufferOffsets[4] <<" byte" << " #" << mArray1.size() << std::endl; - std::cerr << "leaf starts at " << mBufferOffsets[5] <<" byte" << " #" << mArray0.size() << std::endl; - std::cerr << "meta starts at " << mBufferOffsets[6] <<" byte" << std::endl; - std::cerr << "data starts at " << mBufferOffsets[7] <<" byte" << std::endl; - std::cerr << "buffer ends at " << mBufferOffsets[8] <<" byte" << std::endl; - std::cerr << "creating buffer of size " << (mBufferOffsets[8]>>20) << "MB" << std::endl; -#endif - - GridHandle handle(BufferT::create(mBufferOffsets[8], &buffer)); - mBufferPtr = handle.data(); - - //openvdb::util::CpuTimer timer("zero buffer"); -#if 1 - //std::memset(mBufferPtr, '8', mBufferOffsets[8]); -#else - forEach(0,mBufferOffsets[8],1024*1024,[&](const Range1D &r){ - //for (uint64_t *p = reinterpret_cast(mBufferPtr)+r.begin(), *q=p+r.size(); p!=q; ++p) *p=0; - std::memset(mBufferPtr+r.begin(), '8', r.size()); - }); - //uint8_t *begin = (mBufferPtr >> 3) << 3; - //std::memset((mBufferPtr >> 3) << 3, 0, mBufferPtr - p); - //forEach(0,mBufferOffsets[8],10*1024*1024,[&](const Range1D &r){std::memset(mBufferPtr+r.begin(), 0, r.size());}); -#endif - //timer.stop(); - - if (mVerbose) { - openvdb::util::printBytes(std::cout, mBufferOffsets[8], "Allocated", " for the NanoVDB grid\n"); - } - return handle;// is converted to r-value so return value is move constructed! -}// OpenToNanoVDB::initHandle - -//================================================================================================ - -template -NanoGrid* OpenToNanoVDB:: - processGrid(const OpenGridT& openGrid) -{ - auto *nanoGrid = reinterpret_cast(mBufferPtr + mBufferOffsets[0]); - if (!openGrid.transform().baseMap()->isLinear()) { - OPENVDB_THROW(openvdb::ValueError, "processGrid: OpenToNanoVDB only supports grids with affine transforms"); - } - auto affineMap = openGrid.transform().baseMap()->getAffineMap(); - const std::string gridName = openGrid.getName(); - auto *data = nanoGrid->data(); - - data->mMagic = NANOVDB_MAGIC_NUMBER;//8B - data->mChecksum = 0u;// 8B - data->mVersion = Version();//4B - data->mFlags = static_cast(GridFlags::IsBreadthFirst);//4B - data->mGridIndex = 0;//4B - data->mGridCount = 1;//4B - data->mGridSize = mBufferOffsets[8];//8B - std::memset(data->mGridName, '\0', GridData::MaxNameSize);// 256B overwrite mGridName - strncpy(data->mGridName, gridName.c_str(), GridData::MaxNameSize-1); - data->mWorldBBox = BBox(); - data->mBlindMetadataOffset = 0; - data->mBlindMetadataCount = 0; - - if (gridName.length() >= GridData::MaxNameSize) { - data->setLongGridNameOn();// grid name is long so store it as blind data - } - mDelta = NanoValueT(0); // dummy value - switch (openGrid.getGridClass()) { // set grid class - case openvdb::GRID_LEVEL_SET: - if (!is_floating_point::value) - OPENVDB_THROW(openvdb::ValueError, "processGrid: Level sets are expected to be floating point types"); - data->mGridClass = GridClass::LevelSet; - mDelta = NanoValueT(openGrid.voxelSize()[0]); // skip a node if max < -mDelta || min > mDelta - break; - case openvdb::GRID_FOG_VOLUME: - data->mGridClass = GridClass::FogVolume; - break; - case openvdb::GRID_STAGGERED: - data->mGridClass = GridClass::Staggered; - break; - default: - data->mGridClass = GridClass::Unknown; - } - - // mapping from the OpenVDB build type to the NanoVDB build type and GridType enum - if (std::is_same::value) { // resolved at compile time - data->mGridType = GridType::Float; - } else if (std::is_same::value) { - data->mGridType = GridType::Double; - } else if (std::is_same::value) { - data->mGridType = GridType::Int16; - } else if (std::is_same::value) { - data->mGridType = GridType::Int32; - } else if (std::is_same::value) { - data->mGridType = GridType::Int64; - } else if (std::is_same::value) { - data->mGridType = GridType::Vec3f; - } else if (std::is_same::value) { - data->mGridType = GridType::UInt32; - } else if (std::is_same::value) { - data->mGridType = GridType::UInt32; - data->mGridClass = GridClass::PointIndex; - } else if (std::is_same::value) { - data->mGridType = GridType::UInt32; - data->mGridClass = GridClass::PointData; - } else if (std::is_same::value) { - data->mGridType = GridType::Mask; - data->mGridClass = GridClass::Topology; - } else if (std::is_same::value) { - data->mGridType = GridType::Boolean; - } else if (std::is_same::value) { - data->mGridType = GridType::Fp4; - } else if (std::is_same::value) { - data->mGridType = GridType::Fp8; - } else if (std::is_same::value) { - data->mGridType = GridType::Fp16; - } else if (std::is_same::value) { - data->mGridType = GridType::FpN; - } else if (std::is_same::value) { - data->mGridType = GridType::Vec4f; - } else if (std::is_same::value) { - data->mGridType = GridType::Vec4d; - } else { - OPENVDB_THROW(openvdb::ValueError, "processGrid: Unsupported value type"); - } - { // set affine map - if (openGrid.hasUniformVoxels()) { - data->mVoxelSize = nanovdb::Vec3R(affineMap->voxelSize()[0]); - } else { - data->mVoxelSize = affineMap->voxelSize(); - } - const auto mat = affineMap->getMat4(); - // Only support non-tapered at the moment: - data->mMap.set(mat, mat.inverse(), 1.0); - } - data->mData0 = 0u; - data->mData1 = 0u; - data->mData2 = 0u; - - this->processTree(openGrid.tree());// calls processRoot - - if (auto size = mBlindMetaData.size()) { - auto *metaData = this->processMetadata(openGrid); - data->mBlindMetadataOffset = PtrDiff(metaData, nanoGrid); - data->mBlindMetadataCount = static_cast(size); - auto *blindData = reinterpret_cast(mBufferPtr + mBufferOffsets[7]); - metaData->setBlindData(blindData); - } - - return nanoGrid; -}// OpenToNanoVDB::processGrid - -//================================================================================================ - -template -NanoTree* OpenToNanoVDB:: - processTree(const OpenTreeT& openTree) -{ - auto *nanoTree = reinterpret_cast(mBufferPtr + mBufferOffsets[1]); - auto *data = nanoTree->data(); - - data->setRoot( this->processRoot( openTree.root()) ); - - NanoUpperT *nanoUpper = mArray2.empty() ? nullptr : reinterpret_cast(mBufferPtr + mBufferOffsets[3]); - data->setFirstNode(nanoUpper); - - NanoLowerT *nanoLower = mArray1.empty() ? nullptr : reinterpret_cast(mBufferPtr + mBufferOffsets[4]); - data->setFirstNode(nanoLower); - - NanoLeafT *nanoLeaf = mArray0.empty() ? nullptr : reinterpret_cast(mBufferPtr + mBufferOffsets[5]); - data->setFirstNode(nanoLeaf); - - data->mNodeCount[0] = static_cast(mArray0.size()); - data->mNodeCount[1] = static_cast(mArray1.size()); - data->mNodeCount[2] = static_cast(mArray2.size()); - -#if 1// count active tiles and voxels - - // Count number of active tiles in the lower internal nodes - data->mTileCount[0] = reduce(mArray1, uint32_t(0), [&](auto &r, uint32_t sum){ - for (auto i=r.begin(); i!=r.end(); ++i) sum += mArray1[i].node->getValueMask().countOn(); - return sum;}, std::plus()); - - // Count number of active tiles in the upper internal nodes - data->mTileCount[1] = reduce(mArray2, uint32_t(0), [&](auto &r, uint32_t sum){ - for (auto i=r.begin(); i!=r.end(); ++i) sum += mArray2[i].node->getValueMask().countOn(); - return sum;}, std::plus()); - - // Count number of active tile in the root node - uint32_t sum = 0; - for (auto it = openTree.root().cbeginValueOn(); it; ++it) ++sum; - data->mTileCount[2] = sum; - - data->mVoxelCount = reduce(mArray0, uint64_t(0), [&](auto &r, uint64_t sum){ - for (auto i=r.begin(); i!=r.end(); ++i) sum += mArray0[i].node->valueMask().countOn(); - return sum;}, std::plus()); - - data->mVoxelCount += data->mTileCount[0]*NanoLeafT::NUM_VALUES; - data->mVoxelCount += data->mTileCount[1]*NanoLowerT::NUM_VALUES; - data->mVoxelCount += data->mTileCount[2]*NanoUpperT::NUM_VALUES; - -#else - - data->mTileCount[0] = 0; - data->mTileCount[1] = 0; - data->mTileCount[2] = 0; - data->mVoxelCount = 0; - -#endif - - return nanoTree; -}// OpenToNanoVDB::processTree - -//================================================================================================ - -template -NanoRoot* OpenToNanoVDB:: - processRoot(const OpenRootT& openRoot) -{ - auto *nanoRoot = reinterpret_cast(mBufferPtr + mBufferOffsets[2]); - auto* data = nanoRoot->data(); - if (data->padding()>0) { - //std::cout << "Root has padding\n"; - std::memset(data, 0, NanoRootT::memUsage(openRoot.getTableSize())); - } else { - data->mTableSize = 0;// incremented below - } - data->mBackground = openRoot.background(); - data->mMinimum = data->mMaximum = data->mBackground; - data->mBBox.min() = openvdb::Coord::max(); // set to an empty bounding box - data->mBBox.max() = openvdb::Coord::min(); - - OpenValueT value = openvdb::zeroVal();// to avoid compiler warning - for (auto iter = openRoot.cbeginChildAll(); iter; ++iter) { - auto* tile = data->tile(data->mTableSize++); - if (const OpenUpperT *openChild = iter.probeChild( value )) { - tile->setChild(iter.getCoord(), this->decode(openChild), data); - } else { - tile->setValue(iter.getCoord(), iter.isValueOn(), value); - } - } - return nanoRoot; -} // OpenToNanoVDB::processRoot - -//================================================================================================ - -template -template -void OpenToNanoVDB:: - processNodes(std::vector>& openNodes) -{ - using NanoNodeT = typename NanoNode::Type; - //if (NanoNodeT::DataType::padding()>0u) std::cerr << "OpenToNanoVDB: internal node has padding\n"; - static_assert(NanoNodeT::LEVEL == 1 || NanoNodeT::LEVEL == 2, "Expected internal node"); - auto kernel = [&](const Range1D& r) { - uint8_t* ptr = mBufferPtr + mBufferOffsets[5 - NanoNodeT::LEVEL];// 3 or 4 - OpenValueT value = openvdb::zeroVal();// to avoid compiler warning - for (auto i = r.begin(); i != r.end(); ++i) { - auto *openNode = openNodes[i].node; - auto *nanoNode = PtrAdd(ptr, openNodes[i].offset); - auto* data = nanoNode->data(); - if (NanoNodeT::DataType::padding()>0u) std::memset(data, 0, NanoNodeT::DataType::memUsage()); - this->encode(openNode, nanoNode);// sets data->mBBoxMin - data->mValueMask = openNode->getValueMask(); // copy value mask - data->mChildMask = openNode->getChildMask(); // copy child mask - for (auto iter = openNode->cbeginChildAll(); iter; ++iter) { - if (const auto *openChild = iter.probeChild(value)) { - data->setChild(iter.pos(), this->decode(openChild)); - } else { - data->setValue(iter.pos(), value); - } - } - } - }; - forEach(openNodes, 1, kernel); -} // OpenToNanoVDB::processNodes - -//================================================================================================ - -template -template -inline typename std::enable_if::LeafT, typename T::OpenNodeT>::value && - !std::is_same::LeafT, typename T::OpenNodeT>::value && - !std::is_same::value && - !std::is_same::value && - !std::is_same::value && - !std::is_same::value>::type -OpenToNanoVDB::processLeafs(std::vector& openLeafs) -{ - //if (NanoLeafT::DataType::padding()>0u) std::cerr << "OpenToNanoVDB: leaf has padding\n"; - auto kernel = [&](const auto& r) { - uint8_t* ptr = mBufferPtr + mBufferOffsets[5]; - for (auto i = r.begin(); i != r.end(); ++i) { - auto *openLeaf = openLeafs[i].node; - auto *nanoLeaf = PtrAdd(ptr, openLeafs[i].offset); - auto* data = nanoLeaf->data(); - if (NanoLeafT::DataType::padding()>0u) {// resolved at compile time - std::memset(data, 0, NanoLeafT::DataType::memUsage()); - } else { - data->mFlags = data->mBBoxDif[2] = data->mBBoxDif[1] = data->mBBoxDif[0] = 0u; - data->mMaximum = data->mMinimum = typename NanoLeafT::DataType::ValueType(0); - data->mStdDevi = data->mAverage = typename NanoLeafT::DataType::FloatType(0); - } - this->encode(openLeaf, nanoLeaf);// sets data->mBBoxMin - data->mValueMask = openLeaf->valueMask(); // copy value mask - auto *src = reinterpret_cast(openLeaf->buffer().data()); - for (NanoValueT *dst = data->mValues, *end = dst + OpenLeafT::size(); dst != end; dst += 4, src += 4) { - dst[0] = src[0]; // copy *all* voxel values in sets of four, i.e. loop-unrolling - dst[1] = src[1]; - dst[2] = src[2]; - dst[3] = src[3]; - } - } - }; - forEach(openLeafs, 8, kernel); -} // OpenToNanoVDB::processLeafs - -//================================================================================================ - -template -template -inline typename std::enable_if::value || - std::is_same::value || - std::is_same::value>::type -OpenToNanoVDB::processLeafs(std::vector& openLeafs) -{ - static_assert(NanoLeafT::DataType::padding()==0u, "Expected no padding in LeafNode"); - using ArrayT = typename NanoLeafT::DataType::ArrayType; - using FloatT = typename std::conditional=16, double, float>::type;// 16 compression and higher requires double - DitherLUT lut(mDitherOn); - - auto kernel = [&](const auto& r) { - uint8_t* ptr = mBufferPtr + mBufferOffsets[5]; - for (auto i = r.begin(); i != r.end(); ++i) { - auto *openLeaf = openLeafs[i].node; - auto *nanoLeaf = PtrAdd(ptr, openLeafs[i].offset); - auto* data = nanoLeaf->data(); - data->mFlags = data->mBBoxDif[2] = data->mBBoxDif[1] = data->mBBoxDif[0] = 0u; - data->mDev = data->mAvg = data->mMax = data->mMin = 0u; - this->encode(openLeaf, nanoLeaf);// sets data->mBBoxMin - data->mValueMask = openLeaf->valueMask(); // copy value mask - auto *src = reinterpret_cast(openLeaf->buffer().data()); - // compute extrema values - float min = std::numeric_limits::max(), max = -min; - for (int i=0; i<512; ++i) { - const float v = src[i]; - if (v < min) min = v; - if (v > max) max = v; - } - data->init(min, max, NanoLeafT::DataType::bitWidth());// sets mMinimum and mQuantum - // perform quantization relative to the values in the current leaf node - const FloatT encode = FloatT((1 << NanoLeafT::DataType::bitWidth()) - 1)/(max-min); - auto *code = reinterpret_cast(data->mCode); - int offset = 0; - if (std::is_same::value) {// resolved at compile-time - for (int i=0; i<128; ++i) { - auto tmp = ArrayT(encode * (*src++ - min) + lut(offset++)); - *code++ = ArrayT(encode * (*src++ - min) + lut(offset++)) << 4 | tmp; - tmp = ArrayT(encode * (*src++ - min) + lut(offset++)); - *code++ = ArrayT(encode * (*src++ - min) + lut(offset++)) << 4 | tmp; - } - } else { - for (int i=0; i<128; ++i) { - *code++ = ArrayT(encode * (*src++ - min) + lut(offset++)); - *code++ = ArrayT(encode * (*src++ - min) + lut(offset++)); - *code++ = ArrayT(encode * (*src++ - min) + lut(offset++)); - *code++ = ArrayT(encode * (*src++ - min) + lut(offset++)); - } - } - } - }; - forEach(openLeafs, 8, kernel); -} // OpenToNanoVDB::processLeafs - -//================================================================================================ - -template -template -inline typename std::enable_if::value>::type -OpenToNanoVDB::processLeafs(std::vector& openLeafs) -{ - static_assert(is_same::value, "Expected OpenBuildT == float"); - static_assert(NanoLeafT::DataType::padding()==0u, "Expected no padding in LeafNode"); - DitherLUT lut(mDitherOn); - auto kernel = [&](const auto& r) { - uint8_t* ptr = mBufferPtr + mBufferOffsets[5]; - for (auto i = r.begin(); i != r.end(); ++i) { - const uint8_t logBitWidth = uint8_t(mCodec[i].log2); - auto *openLeaf = openLeafs[i].node; - auto *nanoLeaf = PtrAdd(ptr, openLeafs[i].offset); - auto* data = nanoLeaf->data(); - data->mBBoxDif[2] = data->mBBoxDif[1] = data->mBBoxDif[0] = 0u; - data->mDev = data->mAvg = data->mMax = data->mMin = 0u; - this->encode(openLeaf, nanoLeaf);// sets data->mBBoxMin - data->mFlags = logBitWidth << 5;// pack logBitWidth into 3 MSB of mFlag - data->mValueMask = openLeaf->valueMask(); // copy value mask - auto *src = reinterpret_cast(openLeaf->buffer().data()); - const float min = mCodec[i].min, max = mCodec[i].max; - data->init(min, max, uint8_t(1) << logBitWidth);// sets mMinimum and mQuantum - // perform quantization relative to the values in the current leaf node - int offset = 0; - switch (logBitWidth) { - case 0u: {// 1 bit - auto *dst = reinterpret_cast(data+1); - const float encode = 1.0f/(max - min); - for (int j=0; j<64; ++j) { - uint8_t a = 0; - for (int k=0; k<8; ++k) { - a |= uint8_t(encode * (*src++ - min) + lut(offset++)) << k; - } - *dst++ = a; - } - } - break; - case 1u: {// 2 bits - auto *dst = reinterpret_cast(data+1); - const float encode = 3.0f/(max - min); - for (int j=0; j<128; ++j) { - auto a = uint8_t(encode * (*src++ - min) + lut(offset++)); - a |= uint8_t(encode * (*src++ - min) + lut(offset++)) << 2; - a |= uint8_t(encode * (*src++ - min) + lut(offset++)) << 4; - *dst++ = uint8_t(encode * (*src++ - min) + lut(offset++)) << 6 | a; - } - } - break; - case 2u: {// 4 bits - auto *dst = reinterpret_cast(data+1); - const float encode = 15.0f/(max - min); - for (int j=0; j<128; ++j) { - auto a = uint8_t(encode * (*src++ - min) + lut(offset++)); - *dst++ = uint8_t(encode * (*src++ - min) + lut(offset++)) << 4 | a; - a = uint8_t(encode * (*src++ - min) + lut(offset++)); - *dst++ = uint8_t(encode * (*src++ - min) + lut(offset++)) << 4 | a; - } - } - break; - case 3u: {// 8 bits - auto *dst = reinterpret_cast(data+1); - const float encode = 255.0f/(max - min); - for (int j=0; j<128; ++j) { - *dst++ = uint8_t(encode * (*src++ - min) + lut(offset++)); - *dst++ = uint8_t(encode * (*src++ - min) + lut(offset++)); - *dst++ = uint8_t(encode * (*src++ - min) + lut(offset++)); - *dst++ = uint8_t(encode * (*src++ - min) + lut(offset++)); - } - } - break; - default: {// 16 bits - auto *dst = reinterpret_cast(data+1); - const double encode = 65535.0/(max - min);// note that double is required! - for (int j=0; j<128; ++j) { - *dst++ = uint16_t(encode * (*src++ - min) + lut(offset++)); - *dst++ = uint16_t(encode * (*src++ - min) + lut(offset++)); - *dst++ = uint16_t(encode * (*src++ - min) + lut(offset++)); - *dst++ = uint16_t(encode * (*src++ - min) + lut(offset++)); - } - } - }// end switch - } - };// kernel - forEach(openLeafs, 8, kernel); -} // OpenToNanoVDB::processLeafs - -//================================================================================================ - -template -template -inline typename std::enable_if::LeafT>::value>::type -OpenToNanoVDB::processLeafs(std::vector>& openLeafs) -{ - static_assert(NanoLeafT::DataType::padding()==0u, "Expected no padding in LeafNode"); - auto kernel = [&](const auto& r) { - uint8_t* ptr = mBufferPtr + mBufferOffsets[5]; - for (auto i = r.begin(); i != r.end(); ++i) { - auto *openLeaf = openLeafs[i].node; - auto *nanoLeaf = PtrAdd(ptr, openLeafs[i].offset); - this->encode(openLeaf, nanoLeaf);// sets data->mBBoxMin - auto* data = nanoLeaf->data(); - data->mFlags = data->mBBoxDif[2] = data->mBBoxDif[1] = data->mBBoxDif[0] = 0u; - data->mValueMask = openLeaf->valueMask(); // copy value mask - data->mValues = *reinterpret_cast*>(openLeaf->buffer().data()); // copy values - data->mPadding[1] = data->mPadding[0] = 0u; - } - }; - forEach(openLeafs, 8, kernel); -} // OpenToNanoVDB::processLeafs - -//================================================================================================ - -template -template -inline typename std::enable_if::LeafT>::value>::type -OpenToNanoVDB::processLeafs(std::vector>& openLeafs) -{ - static_assert(NanoLeafT::DataType::padding()==0u, "Expected no padding in LeafNode"); - auto kernel = [&](const auto& r) { - uint8_t* ptr = mBufferPtr + mBufferOffsets[5]; - for (auto i = r.begin(); i != r.end(); ++i) { - auto *openLeaf = openLeafs[i].node; - auto *nanoLeaf = PtrAdd(ptr, openLeafs[i].offset); - this->encode(openLeaf, nanoLeaf); - auto* data = nanoLeaf->data(); - data->mFlags = data->mBBoxDif[2] = data->mBBoxDif[1] = data->mBBoxDif[0] = 0u; - data->mValueMask = openLeaf->valueMask(); // copy value mask - data->mPadding[1] = data->mPadding[0] = 0u; - } - }; - forEach(openLeafs, 8, kernel); -} // OpenToNanoVDB::processLeafs - -//================================================================================================ - -template -uint64_t OpenToNanoVDB::pointCount() -{ - return reduce(mArray0, uint64_t(0), [&](auto &r, uint64_t sum) { - for (auto i=r.begin(); i!=r.end(); ++i) sum += mArray0[i].node->getLastValue(); - return sum;}, std::plus()); -}// OpenToNanoVDB::pointCount - -//================================================================================================ - -/// @brief Performs: nanoNode.origin = openNode.origin -/// openNode.origin = nanoNode offset -template -template -inline void OpenToNanoVDB:: -encode(const OpenNodeT *openNode, NanoNodeT *nanoNode) -{ - static_assert(is_same::Type>::value, "Type mismatch"); - openvdb::Coord &ijk = const_cast(openNode->origin()); - nanoNode->data()->setOrigin(ijk); - reinterpret_cast(ijk) = PtrDiff(nanoNode, mBufferPtr); -}// OpenToNanoVDB::encode - -//================================================================================================ - -/// @brief Performs: nanoNode offset = openNode.origin -/// openNode.origin = nanoNode.origin -/// return nanoNode offset -template -template -inline typename NanoNode::Type* OpenToNanoVDB:: -decode(const OpenNodeT *openNode) -{ - using NanoNodeT = typename NanoNode::Type; - openvdb::Coord &ijk = const_cast(openNode->origin()); - NanoNodeT *nanoNode = PtrAdd(mBufferPtr, reinterpret_cast(ijk)); - Coord tmp = nanoNode->origin(); - ijk[0] = tmp[0]; - ijk[1] = tmp[1]; - ijk[2] = tmp[2]; - return nanoNode; -}// OpenToNanoVDB::decode - -//================================================================================================ - -template -template -struct OpenToNanoVDB::NodePair { - using OpenNodeT = NodeT; - using NanoNodeT = typename NanoNode::Type; - NodePair(const NodeT *ptr, size_t n) : node(ptr), offset(n) {} - const NodeT *node;// pointer to OpenVDB node - uint64_t offset;// byte offset to matching NanoVDB node, relative to the first -};// OpenToNanoVDB::NodePair - -//================================================================================================ - -template -struct OpenToNanoVDB::BlindMetaData -{ - BlindMetaData(const std::string& n, const std::string& t, size_t i, size_t c, size_t s) - : name(n) - , typeName(t) - , index(i) - , count(c) - , size(AlignUp(c * s)) - { - } - const std::string name, typeName; - const size_t index, count, size; - bool operator<(const BlindMetaData& other) const { return index < other.index; } // required by std::set -}; // OpenToNanoVDB::BlindMetaData - -//================================================================================================ - -template -template -inline typename std::enable_if::value && - !std::is_same::value>::type -OpenToNanoVDB::preProcessMetadata(const T& openGrid) -{ - mBlindMetaData.clear(); - const size_t length = openGrid.getName().length(); - if (length >= GridData::MaxNameSize) { - mBlindMetaData.emplace("grid name", "uint8_t", 0, 1, length + 1);// Null-terminated byte strings - } -}// OpenToNanoVDB::preProcessMetadata - -//================================================================================================ - -template -template -inline typename std::enable_if::value>::type -OpenToNanoVDB::preProcessMetadata(const T& openGrid) -{ - mBlindMetaData.clear(); - if (const uint64_t pointCount = this->pointCount()) { - mBlindMetaData.emplace("index", "uint32_t", 0, pointCount, sizeof(uint32_t)); - } - const size_t length = openGrid.getName().length(); - if (length >= GridData::MaxNameSize) { - mBlindMetaData.emplace("grid name", "uint8_t", mBlindMetaData.size(), 1, length + 1);// Null-terminated byte strings - } -}// OpenToNanoVDB::preProcessMetadata - -//================================================================================================ - -template -template -inline typename std::enable_if::value>::type -OpenToNanoVDB::preProcessMetadata(const T& openGrid) -{ - mBlindMetaData.clear(); - size_t counter = 0; - if (const uint64_t pointCount = this->pointCount()) { - auto *openLeaf = openGrid.tree().cbeginLeaf().getLeaf(); - const auto& attributeSet = openLeaf->attributeSet(); - const auto& descriptor = attributeSet.descriptor(); - const auto& nameMap = descriptor.map(); - for (auto it = nameMap.begin(); it != nameMap.end(); ++it) { - const size_t index = it->second; - auto& attArray = openLeaf->constAttributeArray(index); - mBlindMetaData.emplace(it->first, descriptor.valueType(index), index, pointCount, attArray.valueTypeSize()); - } - counter += nameMap.size(); - } - const size_t length = openGrid.getName().length(); - if (length >= GridData::MaxNameSize) { - mBlindMetaData.emplace("grid name", "uint8_t", counter, 1, length + 1);// Null-terminated byte strings - } -}// OpenToNanoVDB::preProcessMetadata - -//================================================================================================ - -template -template -inline typename std::enable_if::value && - !std::is_same::value,GridBlindMetaData*>::type -OpenToNanoVDB:: - processMetadata(const T& openGrid) -{ - if (mBlindMetaData.empty()) { - return nullptr; - } - assert(mBlindMetaData.size() == 1);// only the grid name is expected - auto it = mBlindMetaData.cbegin(); - assert(it->name == "grid name" && it->typeName == "uint8_t" && it->index == 0); - assert(openGrid.getName().length() >= GridData::MaxNameSize); - auto *metaData = reinterpret_cast(mBufferPtr + mBufferOffsets[6]); - auto *blindData = reinterpret_cast(mBufferPtr + mBufferOffsets[7]); - // write the blind meta data - metaData->setBlindData(blindData); - metaData->mElementCount = it->count; - metaData->mFlags = 0; - metaData->mSemantic = GridBlindDataSemantic::Unknown; - metaData->mDataClass = GridBlindDataClass::GridName; - metaData->mDataType = GridType::Unknown; - // write the actual bind data - strcpy(blindData, openGrid.getName().c_str()); - return metaData; -}// OpenToNanoVDB::processMetadata - -//================================================================================================ - -template -template -inline typename std::enable_if::value,GridBlindMetaData*>::type -OpenToNanoVDB::processMetadata(const T& openGrid) -{ - if (mBlindMetaData.empty()) { - return nullptr; - } - assert(mBlindMetaData.size() == 1 || mBlindMetaData.size() == 2);// point index and maybe long grid name - auto *metaData = reinterpret_cast(mBufferPtr + mBufferOffsets[6]); - auto *blindData = reinterpret_cast(mBufferPtr + mBufferOffsets[7]); - - auto it = mBlindMetaData.cbegin(); - const uint32_t leafCount = static_cast(mArray0.size()); - - using LeafDataT = typename NanoLeafT::DataType; - uint8_t* ptr = mBufferPtr + mBufferOffsets[5]; - - auto *data0 = reinterpret_cast(ptr + mArray0[0].offset); - data0->mMinimum = 0; // start of prefix sum - data0->mMaximum = data0->mValues[NanoLeafT::SIZE - 1u]; - for (uint32_t i = 1; i < leafCount; ++i) { - auto *data1 = reinterpret_cast(ptr + mArray0[i].offset); - data1->mMinimum = data0->mMinimum + data0->mMaximum; - data1->mMaximum = data1->mValues[NanoLeafT::SIZE - 1u]; - data0 = data1; - } - - // write blind meta data for the point offsets - assert(it->count == data0->mMinimum + data0->mMaximum); - assert(it->name == "index" && it->typeName == "uint32_t" && it->index == 0); - metaData[0].setBlindData( blindData ); - metaData[0].mElementCount = it->count; - metaData[0].mFlags = 0; - metaData[0].mSemantic = GridBlindDataSemantic::Unknown; - metaData[0].mDataClass = GridBlindDataClass::IndexArray; - metaData[0].mDataType = GridType::UInt32; - if (it->name.length() >= GridBlindMetaData::MaxNameSize) { - std::stringstream ss; - ss << "Point attribute name \"" << it->name << "\" is more than " << (GridBlindMetaData::MaxNameSize-1) << " characters"; - OPENVDB_THROW(openvdb::ValueError, ss.str()); - } - std::memset(metaData[0].mName, '\0', GridBlindMetaData::MaxNameSize);//overwrite mName - memcpy(metaData[0].mName, it->name.c_str(), it->name.size() + 1); - - // write point offsets as blind data - forEach(mArray0, 16, [&](const auto& r) { - for (auto i = r.begin(); i != r.end(); ++i) { - auto *data = reinterpret_cast(ptr + mArray0[i].offset); - uint32_t* p = reinterpret_cast(blindData) + data->mMinimum; - for (uint32_t idx : mArray0[i].node->indices()) *p++ = idx; - } - }); - blindData += it->size;// add point offsets - - // write long grid name if it exists - ++it; - if (it != mBlindMetaData.end()) { - assert(it->name == "grid name" && it->typeName == "uint8_t" && it->index == 1); - assert(openGrid.getName().length() >= GridData::MaxNameSize); - metaData[1].setBlindData( blindData ); - metaData[1].mElementCount = it->count; - metaData[1].mFlags = 0; - metaData[1].mSemantic = GridBlindDataSemantic::Unknown; - metaData[1].mDataClass = GridBlindDataClass::GridName; - metaData[1].mDataType = GridType::Unknown; - strcpy(blindData, openGrid.getName().c_str()); - } - return metaData; -}// OpenToNanoVDB::processMetadata - -//================================================================================================ - -template -template -inline typename std::enable_if::value,GridBlindMetaData*>::type -OpenToNanoVDB::processMetadata(const T& openGrid) -{ - if (mBlindMetaData.empty()) { - return nullptr; - } - - auto *metaData = reinterpret_cast(mBufferPtr + mBufferOffsets[6]); - auto *blindData = reinterpret_cast(mBufferPtr + mBufferOffsets[7]); - - const uint32_t leafCount = static_cast(mArray0.size()); - - using LeafDataT = typename NanoLeafT::DataType; - uint8_t* ptr = mBufferPtr + mBufferOffsets[5]; - - auto *data0 = reinterpret_cast(ptr + mArray0[0].offset); - data0->mMinimum = 0; // start of prefix sum - data0->mMaximum = data0->mValues[NanoLeafT::SIZE - 1u]; - for (uint32_t i = 1; i < leafCount; ++i) { - auto *data1 = reinterpret_cast(ptr + mArray0[i].offset); - data1->mMinimum = data0->mMinimum + data0->mMaximum; - data1->mMaximum = data1->mValues[NanoLeafT::SIZE - 1u]; - data0 = data1; - } - - size_t i=0; - for (auto it = mBlindMetaData.cbegin(); it != mBlindMetaData.end(); ++it, ++i) { - metaData[i].setBlindData( blindData ); - metaData[i].mElementCount = it->count; - metaData[i].mFlags = 0; - if (it->name == "grid name") { - metaData[i].mSemantic = GridBlindDataSemantic::Unknown; - metaData[i].mDataClass = GridBlindDataClass::GridName; - metaData[i].mDataType = GridType::Unknown; - assert(openGrid.getName().length() >= GridData::MaxNameSize); - strcpy((char*)blindData, openGrid.getName().c_str()); - } else { - assert(it->count == data0->mMinimum + data0->mMaximum); - metaData[i].mDataClass = GridBlindDataClass::AttributeArray; - if (it->name.length()>= GridBlindMetaData::MaxNameSize) { - std::stringstream ss; - ss << "Point attribute name \"" << it->name << "\" is more than " << (GridBlindMetaData::MaxNameSize-1) << " characters"; - OPENVDB_THROW(openvdb::ValueError, ss.str()); - } - - std::memset(metaData[i].mName, '\0', GridBlindMetaData::MaxNameSize);//overwrite mName - memcpy(metaData[i].mName, it->name.c_str(), it->name.size() + 1); - if (it->typeName == "vec3s") { - metaData[i].mDataType = GridType::Vec3f; - this->copyPointAttribute(it->index, (openvdb::Vec3f*)blindData); - if (it->name == "P") { - metaData[i].mSemantic = GridBlindDataSemantic::PointPosition; - } else if (it->name == "V") { - metaData[i].mSemantic = GridBlindDataSemantic::PointVelocity; - } else if (it->name == "Cd") { - metaData[i].mSemantic = GridBlindDataSemantic::PointColor; - } else if (it->name == "N") { - metaData[i].mSemantic = GridBlindDataSemantic::PointNormal; - } else { - metaData[i].mSemantic = GridBlindDataSemantic::Unknown; - } - } else if (it->typeName == "int32") { - metaData[i].mDataType = GridType::Int32; - this->copyPointAttribute(it->index, (int32_t*)blindData); - if (it->name == "id") { - metaData[i].mSemantic = GridBlindDataSemantic::PointId; - } else { - metaData[i].mSemantic = GridBlindDataSemantic::Unknown; - } - } else if (it->typeName == "int64") { - metaData[i].mDataType = GridType::Int64; - this->copyPointAttribute(it->index, (int64_t*)blindData); - if (it->name == "id") { - metaData[i].mSemantic = GridBlindDataSemantic::PointId; - } else { - metaData[i].mSemantic = GridBlindDataSemantic::Unknown; - } - } else if (it->typeName == "float") { - metaData[i].mDataType = GridType::Float; - metaData[i].mSemantic = GridBlindDataSemantic::Unknown; - this->copyPointAttribute(it->index, (float*)blindData); - } else { - std::stringstream ss; - ss << "Unsupported point attribute type: \"" << it->typeName << "\""; - OPENVDB_THROW(openvdb::ValueError, ss.str()); - } - } - blindData += it->size; - } // loop over bind data - return metaData; -}// OpenToNanoVDB::processMetadata - -//================================================================================================ - - -template -template -inline void OpenToNanoVDB:: - copyPointAttribute(size_t attIdx, AttT *attPtr) -{ - static_assert(std::is_same::value, "Expected value to openvdb::PointData"); - using LeafDataT = typename NanoLeafT::DataType; - using HandleT = openvdb::points::AttributeHandle; - forEach(mArray0, 16, [&](const auto& r) { - uint8_t* ptr = mBufferPtr + mBufferOffsets[5]; - for (auto i = r.begin(); i != r.end(); ++i) { - auto* openLeaf = mArray0[i].node; - auto *nanoData = reinterpret_cast(ptr + mArray0[i].offset); - HandleT handle(openLeaf->constAttributeArray(attIdx)); - AttT* p = attPtr + nanoData->mMinimum; - for (auto iter = openLeaf->beginIndexOn(); iter; ++iter) { - *p++ = handle.get(*iter); - } - } - }); -}// OpenToNanoVDB::copyPointAttribute - -//================================================================================================ - -template -GridHandle -openToNanoVDB(const openvdb::Grid& grid, - StatsMode sMode, - ChecksumMode cMode, - int verbose) -{ - using OpenBuildT = typename OpenTreeT::BuildType; - OpenToNanoVDB s; - return s(grid, sMode, cMode, verbose); -}// openToNanoVDB - -//================================================================================================ - -template -GridHandle -openToNanoVDB(const openvdb::GridBase::Ptr& base, - StatsMode sMode, - ChecksumMode cMode, - int verbose) -{ - // We need to define these types because they are not defined in OpenVDB - using openvdb_Vec4fTree = typename openvdb::tree::Tree4::Type; - using openvdb_Vec4dTree = typename openvdb::tree::Tree4::Type; - using openvdb_Vec4fGrid = openvdb::Grid; - using openvdb_Vec4dGrid = openvdb::Grid; - - if (auto grid = openvdb::GridBase::grid(base)) { - return openToNanoVDB(*grid, sMode, cMode, verbose); - } else if (auto grid = openvdb::GridBase::grid(base)) { - return openToNanoVDB(*grid, sMode, cMode, verbose); - } else if (auto grid = openvdb::GridBase::grid(base)) { - return openToNanoVDB(*grid, sMode, cMode, verbose); - } else if (auto grid = openvdb::GridBase::grid(base)) { - return openToNanoVDB(*grid, sMode, cMode, verbose); - } else if (auto grid = openvdb::GridBase::grid>(base)) { - return openToNanoVDB(*grid, sMode, cMode, verbose); - } else if (auto grid = openvdb::GridBase::grid(base)) { - return openToNanoVDB(*grid, sMode, cMode, verbose); - } else if (auto grid = openvdb::GridBase::grid(base)) { - return openToNanoVDB(*grid, sMode, cMode, verbose); - } else if (auto grid = openvdb::GridBase::grid(base)) { - return openToNanoVDB(*grid, sMode, cMode, verbose); - } else if (auto grid = openvdb::GridBase::grid(base)) { - return openToNanoVDB(*grid, sMode, cMode, verbose); - } else if (auto grid = openvdb::GridBase::grid(base)) { - return openToNanoVDB(*grid, sMode, cMode, verbose); - } else if (auto grid = openvdb::GridBase::grid(base)) { - return openToNanoVDB(*grid, sMode, cMode, verbose); - } else if (auto grid = openvdb::GridBase::grid(base)) { - return openToNanoVDB(*grid, sMode, cMode, verbose); - } else if (auto grid = openvdb::GridBase::grid(base)) { - return openToNanoVDB(*grid, sMode, cMode, verbose); - } else { - OPENVDB_THROW(openvdb::RuntimeError, "Unrecognized OpenVDB grid type"); - } -}// openToNanoVDB - -} // namespace nanovdb +*/ -#endif // NANOVDB_OPENTONANOVDB_H_HAS_BEEN_INCLUDED +#include "CreateNanoGrid.h" \ No newline at end of file diff --git a/nanovdb/nanovdb/util/PrefixSum.h b/nanovdb/nanovdb/util/PrefixSum.h new file mode 100644 index 0000000000..87775c2d2a --- /dev/null +++ b/nanovdb/nanovdb/util/PrefixSum.h @@ -0,0 +1,79 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +/*! + \file PrefixSum.h + + \author Ken Museth + + \date March 12, 2023 + + \brief Multi-threaded implementations of inclusive prefix sum + + \note An exclusive prefix sum is simply an array starting with zero + followed by the elements in the inclusive prefix sum, minus its + last entry which is the sum of all the input elements. +*/ + +#ifndef NANOVDB_PREFIX_SUM_H_HAS_BEEN_INCLUDED +#define NANOVDB_PREFIX_SUM_H_HAS_BEEN_INCLUDED + +#include "Range.h"// for Range1D +#include +#include // for std::plus + +#ifdef NANOVDB_USE_TBB +#include +#endif + +namespace nanovdb { + +/// @brief Computes inclusive prefix sum of a vector +/// @tparam T Type of the elements in the input/out vector +/// @tparam OpT Type of operation performed on each element (defaults to sum) +/// @param vec input and output vector +/// @param threaded if true multi-threading is used +/// @note Inclusive prefix sum: for (i=1; i> +T prefixSum(std::vector &vec, bool threaded = true, OpT op = OpT()); + +/// @brief An inclusive scan includes in[i] when computing out[i] +/// @note Inclusive prefix operation: for (i=1; i +void inclusiveScan(T *array, size_t size, const T &identity, bool threaded, Op op) +{ +#ifndef NANOVDB_USE_TBB + threaded = false; + (void)identity;// avoids compiler warning +#endif + + if (threaded) { +#ifdef NANOVDB_USE_TBB + using RangeT = tbb::blocked_range; + tbb::parallel_scan(RangeT(0, size), identity, + [&](const RangeT &r, T sum, bool is_final_scan)->T { + T tmp = sum; + for (size_t i = r.begin(); i < r.end(); ++i) { + tmp = op(tmp, array[i]); + if (is_final_scan) array[i] = tmp; + } + return tmp; + },[&](const T &a, const T &b) {return op(a, b);} + ); +#endif + } else { // serial inclusive prefix operation + for (size_t i=1; i +T prefixSum(std::vector &vec, bool threaded, OpT op) +{ + inclusiveScan(vec.data(), vec.size(), T(0), threaded, op); + return vec.back();// sum of all input elements +}// prefixSum + +}// namespace nanovdb + +#endif // NANOVDB_PREFIX_SUM_H_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/Primitives.h b/nanovdb/nanovdb/util/Primitives.h index 892554978f..7c1f3a5856 100644 --- a/nanovdb/nanovdb/util/Primitives.h +++ b/nanovdb/nanovdb/util/Primitives.h @@ -16,7 +16,11 @@ #ifndef NANOVDB_PRIMITIVES_H_HAS_BEEN_INCLUDED #define NANOVDB_PRIMITIVES_H_HAS_BEEN_INCLUDED -#include "GridBuilder.h" +#define NANOVDB_PARALLEL_PRIMITIVES + +#include +#include "CreateNanoGrid.h" +#include namespace nanovdb { @@ -34,22 +38,47 @@ namespace nanovdb { /// @param ditherOn If true dithering will be applied when VoxelT = {Fp4,Fp8,Fp16,FpN} /// @param buffer Buffer used for memory allocation by the handle /// -/// @details The @c ValueT template parameter must be float (default) or double. -/// The @c VoxelT template parameter must be one of the following: +/// @details The @c BuildT template parameter must be one of the following: /// float (default), double, Fp4, Fp8, Fp16 or FpN. The @c tolerance -/// argument is only used when VoxelT is set to FpN. -template -GridHandle -createLevelSetSphere(ValueT radius = 100, - const Vec3& center = Vec3(0), +/// argument is only used when BuildT is set to FpN. +template +typename enable_if::value || + is_same::value, GridHandle>::type +createLevelSetSphere(double radius = 100.0, + const Vec3d& center = Vec3d(0), double voxelSize = 1.0, double halfWidth = 3.0, const Vec3d& origin = Vec3d(0), const std::string& name = "sphere_ls", StatsMode sMode = StatsMode::Default, ChecksumMode cMode = ChecksumMode::Default, + const BufferT& buffer = BufferT()); + +template +typename enable_if::value || + is_same::value || + is_same::value, GridHandle>::type +createLevelSetSphere(double radius = 100.0, + const Vec3d& center = Vec3d(0), + double voxelSize = 1.0, + double halfWidth = 3.0, + const Vec3d& origin = Vec3d(0), + const std::string& name = "sphere_ls", + StatsMode sMode = StatsMode::Default, + ChecksumMode cMode = ChecksumMode::Default, + bool ditherOn = false, + const BufferT& buffer = BufferT()); + +template +typename enable_if::value, GridHandle>::type +createLevelSetSphere(double radius = 100.0, + const Vec3d& center = Vec3d(0), + double voxelSize = 1.0, + double halfWidth = 3.0, + const Vec3d& origin = Vec3d(0), + const std::string& name = "sphere_ls_FpN", + StatsMode sMode = StatsMode::Default, + ChecksumMode cMode = ChecksumMode::Default, float tolerance = -1.0f, bool ditherOn = false, const BufferT& buffer = BufferT()); @@ -70,21 +99,30 @@ createLevelSetSphere(ValueT radius = 100, /// @param sMode Mode of computation for the statistics. /// @param cMode Mode of computation for the checksum. /// @param tolerance Global error tolerance use when VoxelT = FpN -/// @param ditherOn If true dithering will be applied when VoxelT = {Fp4,Fp8,Fp16,FpN} +/// @param ditherOn If true dithering will be applied when BuildT = {Fp4,Fp8,Fp16,FpN} /// @param buffer Buffer used for memory allocation by the handle /// -/// @details The @c ValueT template parameter must be float (default) or double. -/// The @c VoxelT template parameter must be one of the following: +/// @details The @c BuildT template parameter must be one of the following: /// float (default), double, Fp4, Fp8, Fp16 or FpN. The @c tolerance -/// argument is only used when VoxelT is set to FpN. -template -GridHandle -createFogVolumeSphere(ValueT radius = 100.0f, - const Vec3& center = Vec3(0.0f), - double voxelSize = 1.0f, - double halfWidth = 3.0f, +/// argument is only used when BuildT is set to FpN. +template +typename disable_if::value, GridHandle>::type +createFogVolumeSphere(double radius = 100.0, + const Vec3d& center = Vec3d(0.0), + double voxelSize = 1.0, + double halfWidth = 3.0, + const Vec3d& origin = Vec3d(0.0), + const std::string& name = "sphere_fog", + StatsMode sMode = StatsMode::Default, + ChecksumMode cMode = ChecksumMode::Default, + const BufferT& buffer = BufferT()); + +template +typename enable_if::value, GridHandle>::type +createFogVolumeSphere(double radius = 100.0, + const Vec3d& center = Vec3d(0.0), + double voxelSize = 1.0, + double halfWidth = 3.0, const Vec3d& origin = Vec3d(0.0), const std::string& name = "sphere_fog", StatsMode sMode = StatsMode::Default, @@ -107,14 +145,13 @@ createFogVolumeSphere(ValueT radius = 100.0f, /// @param mode Mode of computation for the checksum. /// @param buffer Buffer used for memory allocation by the handle /// -/// @details The @c ValueT template parameter must be float (default) or double. -template -inline GridHandle +/// @details The @c BuildT template parameter must be float (default) or double. +template +typename disable_if::value, GridHandle>::type createPointSphere(int pointsPerVoxel = 1, - ValueT radius = 100.0f, - const Vec3& center = Vec3(0.0f), - double voxelSize = 1.0f, + double radius = 100.0, + const Vec3d& center = Vec3d(0.0), + double voxelSize = 1.0, const Vec3d& origin = Vec3d(0.0), const std::string& name = "sphere_points", ChecksumMode mode = ChecksumMode::Default, @@ -137,17 +174,27 @@ createPointSphere(int pointsPerVoxel = 1, /// @param ditherOn If true dithering will be applied when VoxelT = {Fp4,Fp8,Fp16,FpN} /// @param buffer Buffer used for memory allocation by the handle /// -/// @details The @c ValueT template parameter must be float (default) or double. -/// The @c VoxelT template parameter must be one of the following: +/// @details The @c BuildT template parameter must be one of the following: /// float (default), double, Fp4, Fp8, Fp16 or FpN. The @c tolerance -/// argument is only used when VoxelT is set to FpN. -template -GridHandle -createLevelSetTorus(ValueT majorRadius = 100.0f, - ValueT minorRadius = 50.0f, - const Vec3& center = Vec3(0.0f), +/// argument is only used when BuildT is set to FpN. +template +typename disable_if::value, GridHandle>::type +createLevelSetTorus(double majorRadius = 100.0, + double minorRadius = 50.0, + const Vec3d& center = Vec3d(0.0), + double voxelSize = 1.0, + double halfWidth = 3.0, + const Vec3d& origin = Vec3d(0.0), + const std::string& name = "torus_ls", + StatsMode sMode = StatsMode::Default, + ChecksumMode cMode = ChecksumMode::Default, + const BufferT& buffer = BufferT()); + +template +typename enable_if::value, GridHandle>::type +createLevelSetTorus(double majorRadius = 100.0, + double minorRadius = 50.0, + const Vec3d& center = Vec3d(0.0), double voxelSize = 1.0, double halfWidth = 3.0, const Vec3d& origin = Vec3d(0.0), @@ -178,23 +225,33 @@ createLevelSetTorus(ValueT majorRadius = 100.0f, /// @param ditherOn If true dithering will be applied when VoxelT = {Fp4,Fp8,Fp16,FpN} /// @param buffer Buffer used for memory allocation by the handle /// -/// @details The @c ValueT template parameter must be float (default) or double. -/// The @c VoxelT template parameter must be one of the following: +/// @details The @c BuildT template parameter must be one of the following: /// float (default), double, Fp4, Fp8, Fp16 or FpN. The @c tolerance -/// argument is only used when VoxelT is set to FpN. -template -GridHandle -createFogVolumeTorus(ValueT majorRadius = 100.0f, - ValueT minorRadius = 50.0f, - const Vec3& center = Vec3(0.0f), +/// argument is only used when BuildT is set to FpN. +template +typename disable_if::value, GridHandle>::type +createFogVolumeTorus(double majorRadius = 100.0, + double minorRadius = 50.0, + const Vec3d& center = Vec3d(0.0), double voxelSize = 1.0, double halfWidth = 3.0, - const Vec3d& origin = Vec3d(0), + const Vec3d& origin = Vec3d(0.0), const std::string& name = "torus_fog", StatsMode sMode = StatsMode::Default, ChecksumMode cMode = ChecksumMode::Default, + const BufferT& buffer = BufferT()); + +template +typename enable_if::value, GridHandle>::type +createFogVolumeTorus(double majorRadius = 100.0, + double minorRadius = 50.0, + const Vec3d& center = Vec3d(0.0), + double voxelSize = 1.0, + double halfWidth = 3.0, + const Vec3d& origin = Vec3d(0.0), + const std::string& name = "torus_fog_FpN", + StatsMode sMode = StatsMode::Default, + ChecksumMode cMode = ChecksumMode::Default, float tolerance = -1.0f, bool ditherOn = false, const BufferT& buffer = BufferT()); @@ -214,14 +271,13 @@ createFogVolumeTorus(ValueT majorRadius = 100.0f, /// @param cMode Mode of computation for the checksum. /// @param buffer Buffer used for memory allocation by the handle // -/// @details The @c ValueT template parameter must be float (default) or double. -template -inline GridHandle +/// @details The @c BuildT template parameter must be float (default) or double. +template +typename disable_if::value, GridHandle>::type createPointTorus(int pointsPerVoxel = 1, // half-width of narrow band in voxel units - ValueT majorRadius = 100.0f, // major radius of torus in world units - ValueT minorRadius = 50.0f, // minor radius of torus in world units - const Vec3& center = Vec3(0.0f), //center of torus in world units + double majorRadius = 100.0, // major radius of torus in world units + double minorRadius = 50.0, // minor radius of torus in world units + const Vec3d& center = Vec3d(0.0), // center of torus in world units double voxelSize = 1.0, // size of a voxel in world units const Vec3d& origin = Vec3d(0.0f), // origin of grid in world units const std::string& name = "torus_points", // name of grid @@ -246,24 +302,35 @@ createPointTorus(int pointsPerVoxel = 1, // half-width of narrow /// @param ditherOn If true dithering will be applied when VoxelT = {Fp4,Fp8,Fp16,FpN} /// @param buffer Buffer used for memory allocation by the handle /// -//// @details The @c ValueT template parameter must be float (default) or double. -/// The @c VoxelT template parameter must be one of the following: +/// @details The @c BuildT template parameter must be one of the following: /// float (default), double, Fp4, Fp8, Fp16 or FpN. The @c tolerance -/// argument is only used when VoxelT is set to FpN. -template -GridHandle -createLevelSetBox(ValueT width = 40.0f, - ValueT height = 60.0f, - ValueT depth = 100.0f, - const Vec3& center = Vec3(0.0f), +/// argument is only used when BuildT is set to FpN. +template +typename disable_if::value, GridHandle>::type +createLevelSetBox(double width = 40.0, + double height = 60.0, + double depth = 100.0, + const Vec3d& center = Vec3d(0.0), double voxelSize = 1.0, double halfWidth = 3.0, const Vec3d& origin = Vec3d(0.0), const std::string& name = "box_ls", StatsMode sMode = StatsMode::Default, ChecksumMode cMode = ChecksumMode::Default, + const BufferT& buffer = BufferT()); + +template +typename enable_if::value, GridHandle>::type +createLevelSetBox(double width = 40.0, + double height = 60.0, + double depth = 100.0, + const Vec3d& center = Vec3d(0.0), + double voxelSize = 1.0, + double halfWidth = 3.0, + const Vec3d& origin = Vec3d(0.0), + const std::string& name = "box_ls_FpN", + StatsMode sMode = StatsMode::Default, + ChecksumMode cMode = ChecksumMode::Default, float tolerance = -1.0f, bool ditherOn = false, const BufferT& buffer = BufferT()); @@ -289,24 +356,35 @@ createLevelSetBox(ValueT width = 40.0f, /// @param ditherOn If true dithering will be applied when VoxelT = {Fp4,Fp8,Fp16,FpN} /// @param buffer Buffer used for memory allocation by the handle /// -/// @details The @c ValueT template parameter must be float (default) or double. -/// The @c VoxelT template parameter must be one of the following: +/// @details The @c BuildT template parameter must be one of the following: /// float (default), double, Fp4, Fp8, Fp16 or FpN. The @c tolerance -/// argument is only used when VoxelT is set to FpN. -template -GridHandle -createFogVolumeBox(ValueT width = 40.0f, - ValueT height = 60.0f, - ValueT depth = 100.0f, - const Vec3& center = Vec3(0.0f), +/// argument is only used when BuildT is set to FpN. +template +typename disable_if::value, GridHandle>::type +createFogVolumeBox(double width = 40.0, + double height = 60.0, + double depth = 100.0, + const Vec3d& center = Vec3d(0.0), double voxelSize = 1.0, double halfWidth = 3.0, const Vec3d& origin = Vec3d(0.0), const std::string& name = "box_fog", StatsMode sMode = StatsMode::Default, ChecksumMode cMode = ChecksumMode::Default, + const BufferT& buffer = BufferT()); + +template +typename enable_if::value, GridHandle>::type +createFogVolumeBox(double width = 40.0, + double height = 60.0, + double depth = 100.0, + const Vec3d& center = Vec3d(0.0), + double voxelSize = 1.0, + double halfWidth = 3.0, + const Vec3d& origin = Vec3d(0.0), + const std::string& name = "box_fog_FpN", + StatsMode sMode = StatsMode::Default, + ChecksumMode cMode = ChecksumMode::Default, float tolerance = -1.0f, bool ditherOn = false, const BufferT& buffer = BufferT()); @@ -327,22 +405,31 @@ createFogVolumeBox(ValueT width = 40.0f, /// @param ditherOn If true dithering will be applied when VoxelT = {Fp4,Fp8,Fp16,FpN} /// @param buffer Buffer used for memory allocation by the handle /// -/// @details The @c ValueT template parameter must be float (default) or double. -/// The @c VoxelT template parameter must be one of the following: +/// @details The @c BuildT template parameter must be one of the following: /// float (default), double, Fp4, Fp8, Fp16 or FpN. The @c tolerance -/// argument is only used when VoxelT is set to FpN. -template -GridHandle -createLevelSetOctahedron(ValueT scale = 100.0f, - const Vec3& center = Vec3(0.0f), +/// argument is only used when BuildT is set to FpN. +template +typename disable_if::value, GridHandle>::type +createLevelSetOctahedron(double scale = 100.0, + const Vec3d& center = Vec3d(0.0), double voxelSize = 1.0, double halfWidth = 3.0, const Vec3d& origin = Vec3d(0.0), const std::string& name = "octadedron_ls", StatsMode sMode = StatsMode::Default, ChecksumMode cMode = ChecksumMode::Default, + const BufferT& buffer = BufferT()); + +template +typename enable_if::value, GridHandle>::type +createLevelSetOctahedron(double scale = 100.0, + const Vec3d& center = Vec3d(0.0), + double voxelSize = 1.0, + double halfWidth = 3.0, + const Vec3d& origin = Vec3d(0.0), + const std::string& name = "octadedron_ls_FpN", + StatsMode sMode = StatsMode::Default, + ChecksumMode cMode = ChecksumMode::Default, float tolerance = -1.0f, bool ditherOn = false, const BufferT& buffer = BufferT()); @@ -366,22 +453,31 @@ createLevelSetOctahedron(ValueT scale = 100.0f, /// @param ditherOn If true dithering will be applied when VoxelT = {Fp4,Fp8,Fp16,FpN} /// @param buffer Buffer used for memory allocation by the handle /// -/// @details The @c ValueT template parameter must be float (default) or double. -/// The @c VoxelT template parameter must be one of the following: +/// @details The @c BuildT template parameter must be one of the following: /// float (default), double, Fp4, Fp8, Fp16 or FpN. The @c tolerance -/// argument is only used when VoxelT is set to FpN. -template -GridHandle -createFogVolumeOctahedron(ValueT scale = 100.0f, - const Vec3& center = Vec3(0.0f), +/// argument is only used when BuildT is set to FpN. +template +typename disable_if::value, GridHandle>::type +createFogVolumeOctahedron(double scale = 100.0, + const Vec3d& center = Vec3d(0.0), double voxelSize = 1.0, double halfWidth = 3.0, const Vec3d& origin = Vec3d(0.0), const std::string& name = "octadedron_fog", StatsMode sMode = StatsMode::Default, ChecksumMode cMode = ChecksumMode::Default, + const BufferT& buffer = BufferT()); + +template +typename enable_if::value, GridHandle>::type +createFogVolumeOctahedron(double scale = 100.0, + const Vec3d& center = Vec3d(0.0), + double voxelSize = 1.0, + double halfWidth = 3.0, + const Vec3d& origin = Vec3d(0.0), + const std::string& name = "octadedron_fog_FpN", + StatsMode sMode = StatsMode::Default, + ChecksumMode cMode = ChecksumMode::Default, float tolerance = -1.0f, bool ditherOn = false, const BufferT& buffer = BufferT()); @@ -405,25 +501,37 @@ createFogVolumeOctahedron(ValueT scale = 100.0f, /// @param ditherOn If true dithering will be applied when VoxelT = {Fp4,Fp8,Fp16,FpN} /// @param buffer Buffer used for memory allocation by the handle /// -/// @details The @c ValueT template parameter must be float (default) or double. -/// The @c VoxelT template parameter must be one of the following: +/// @details The @c BuildT template parameter must be one of the following: /// float (default), double, Fp4, Fp8, Fp16 or FpN. The @c tolerance -/// argument is only used when VoxelT is set to FpN. -template -GridHandle -createLevelSetBBox(ValueT width = 40.0f, - ValueT height = 60.0f, - ValueT depth = 100.0f, - ValueT thickness = 10.0f, - const Vec3& center = Vec3(0.0f), +/// argument is only used when BuildT is set to FpN. +template +typename disable_if::value, GridHandle>::type +createLevelSetBBox(double width = 40.0, + double height = 60.0, + double depth = 100.0, + double thickness = 10.0, + const Vec3d& center = Vec3d(0.0), double voxelSize = 1.0, double halfWidth = 3.0, const Vec3d& origin = Vec3d(0.0), const std::string& name = "bbox_ls", StatsMode sMode = StatsMode::Default, ChecksumMode cMode = ChecksumMode::Default, + const BufferT& buffer = BufferT()); + +template +typename enable_if::value, GridHandle>::type +createLevelSetBBox(double width = 40.0, + double height = 60.0, + double depth = 100.0, + double thickness = 10.0, + const Vec3d& center = Vec3d(0.0), + double voxelSize = 1.0, + double halfWidth = 3.0, + const Vec3d& origin = Vec3d(0.0), + const std::string& name = "bbox_ls_FpN", + StatsMode sMode = StatsMode::Default, + ChecksumMode cMode = ChecksumMode::Default, float tolerance = -1.0f, bool ditherOn = false, const BufferT& buffer = BufferT()); @@ -444,14 +552,13 @@ createLevelSetBBox(ValueT width = 40.0f, /// @param name Name of the grid /// @param mode Mode of computation for the checksum. /// @param buffer Buffer used for memory allocation by the handle -template -inline GridHandle +template +typename disable_if::value, GridHandle>::type createPointBox(int pointsPerVoxel = 1, // half-width of narrow band in voxel units - ValueT width = 40.0f, // width of box in world units - ValueT height = 60.0f, // height of box in world units - ValueT depth = 100.0f, // depth of box in world units - const Vec3& center = Vec3(0.0f), //center of box in world units + double width = 40.0, // width of box in world units + double height = 60.0, // height of box in world units + double depth = 100.0, // depth of box in world units + const Vec3d& center = Vec3d(0.0), // center of box in world units double voxelSize = 1.0, // size of a voxel in world units const Vec3d& origin = Vec3d(0.0), // origin of grid in world units const std::string& name = "box_points", // name of grid @@ -461,44 +568,45 @@ createPointBox(int pointsPerVoxel = 1, // half-width of narrow b //================================================================================================ /// @brief Given an input NanoVDB voxel grid this methods returns a GridHandle to another NanoVDB -/// PointDataGrid with points scattered in the active leaf voxels of in input grid. +/// PointDataGrid with points scattered in the active leaf voxels of in input grid. Note, the +/// coordinates of the points are encoded as blind data in world-space. /// /// @param srcGrid Const input grid used to determine the active voxels to scatter points into /// @param pointsPerVoxel Number of point per voxel on on the surface /// @param name Name of the grid /// @param mode Mode of computation for the checksum. /// @param buffer Buffer used for memory allocation by the handle -template +template inline GridHandle -createPointScatter(const NanoGrid& srcGrid, // origin of grid in world units - int pointsPerVoxel = 1, // half-width of narrow band in voxel units - const std::string& name = "point_scatter", // name of grid - ChecksumMode mode = ChecksumMode::Default, - const BufferT& buffer = BufferT()); +createPointScatter(const NanoGrid& srcGrid, // source grid used to scatter points into + int pointsPerVoxel = 1, // half-width of narrow band in voxel units + const std::string& name = "point_scatter", // name of grid + ChecksumMode mode = ChecksumMode::Default, + const BufferT& buffer = BufferT()); //================================================================================================ namespace { -/// @brief Returns a shared pointer to a GridBuilder with narrow-band SDF values for a sphere +/// @brief Returns a shared pointer to a build::Grid containing a narrow-band SDF values for a sphere /// /// @brief Note, this is not (yet) a valid level set SDF field since values inside sphere (and outside -/// the narrow band) are still undefined. Call GridBuilder::sdfToLevelSet() to set those -/// values or alternatively call GridBuilder::sdfToFog to generate a FOG volume. +/// the narrow band) are still undefined. Call builder::sdfToLevelSet() to set those +/// values or alternatively call builder::levelSetToFog to generate a FOG volume. /// -/// @details The @c VoxelT template parameter must be one of the following: -/// float (default), Fp4, Fp8, Fp16 or FpN. -template -std::shared_ptr> -initSphere(ValueT radius, // radius of sphere in world units - const Vec3& center, //center of sphere in world units +/// @details The @c BuildT template parameter must be one of the following: +/// float (default), double, Fp4, Fp8, Fp16 or FpN. +template +std::shared_ptr> +initSphere(double radius, // radius of sphere in world units + const Vec3d& center, // center of sphere in world units double voxelSize, // size of a voxel in world units double halfWidth, // half-width of narrow band in voxel units const Vec3d& origin) // origin of grid in world units { + using GridT = build::Grid; + using ValueT = typename BuildToValueMap::type; static_assert(is_floating_point::value, "initSphere: expect floating point"); - static_assert(is_floating_point::Type>::value, "initSphere: expect floating point"); if (!(radius > 0)) throw std::runtime_error("Sphere: radius must be positive!"); if (!(voxelSize > 0)) @@ -506,16 +614,14 @@ initSphere(ValueT radius, // radius of sphere in world units if (!(halfWidth > 0)) throw std::runtime_error("Sphere: halfWidth must be positive!"); - auto builder = std::make_shared>(ValueT(halfWidth * voxelSize)); - auto acc = builder->getAccessor(); + auto grid = std::make_shared(ValueT(halfWidth * voxelSize)); + grid->setTransform(voxelSize, origin); // Define radius of sphere with narrow-band in voxel units const ValueT r0 = radius / ValueT(voxelSize), rmax = r0 + ValueT(halfWidth); // Radius below the Nyquist frequency - if (r0 < ValueT(1.5f)) { - return builder; - } + if (r0 < ValueT(1.5f)) return grid; // Define center of sphere in voxel units const Vec3 c(ValueT(center[0] - origin[0]) / ValueT(voxelSize), @@ -527,40 +633,50 @@ initSphere(ValueT radius, // radius of sphere in world units const int jmin = Floor(c[1] - rmax), jmax = Ceil(c[1] + rmax); const int kmin = Floor(c[2] - rmax), kmax = Ceil(c[2] + rmax); - Coord ijk; - int & i = ijk[0], &j = ijk[1], &k = ijk[2], m = 1; - // Compute signed distances to sphere using leapfrogging in k - for (i = imin; i <= imax; ++i) { - const auto x2 = Pow2(ValueT(i) - c[0]); - for (j = jmin; j <= jmax; ++j) { - const auto x2y2 = Pow2(ValueT(j) - c[1]) + x2; - for (k = kmin; k <= kmax; k += m) { - m = 1; - const auto v = Sqrt(x2y2 + Pow2(ValueT(k) - c[2])) - r0; // Distance in voxel units - const auto d = v < 0 ? -v : v; - if (d < halfWidth) { // inside narrow band - acc.setValue(ijk, ValueT(voxelSize) * v); // distance in world units - } else { // outside narrow band - m += Floor(d - halfWidth); // leapfrog - } - } //end leapfrog over k - } //end loop over j - } //end loop over i - - return builder; + const Range<1,int> range(imin, imax+1, 32); + + auto kernel = [&](const Range<1,int> &r) { + auto acc = grid->getWriteAccessor(); + Coord ijk; + int &i = ijk[0], &j = ijk[1], &k = ijk[2], m = 1; + // Compute signed distances to sphere using leapfrogging in k + for (i = r.begin(); i < r.end(); ++i) { + const auto x2 = Pow2(ValueT(i) - c[0]); + for (j = jmin; j <= jmax; ++j) { + const auto x2y2 = Pow2(ValueT(j) - c[1]) + x2; + for (k = kmin; k <= kmax; k += m) { + m = 1; + const auto v = Sqrt(x2y2 + Pow2(ValueT(k) - c[2])) - r0; // Distance in voxel units + const auto d = v < 0 ? -v : v; + if (d < halfWidth) { // inside narrow band + acc.setValue(ijk, ValueT(voxelSize) * v); // distance in world units + } else { // outside narrow band + m += Floor(d - halfWidth); // leapfrog + } + } //end leapfrog over k + } //end loop over j + } //end loop over i + };// kernel +#ifdef NANOVDB_PARALLEL_PRIMITIVES + forEach(range, kernel); +#else + kernel(range); +#endif + return grid; } // initSphere -template -std::shared_ptr> -initTorus(ValueT radius1, // major radius of torus in world units - ValueT radius2, // minor radius of torus in world units - const Vec3& center, //center of torus in world units +template +std::shared_ptr> +initTorus(double radius1, // major radius of torus in world units + double radius2, // minor radius of torus in world units + const Vec3d& center, // center of torus in world units double voxelSize, // size of a voxel in world units double halfWidth, // half-width of narrow band in voxel units const Vec3d& origin) // origin of grid in world units { + using GridT = build::Grid; + using ValueT = typename BuildToValueMap::type; static_assert(is_floating_point::value, "initTorus: expect floating point"); - static_assert(is_floating_point::Type>::value, "initTorus: expect floating point"); if (!(radius2 > 0)) throw std::runtime_error("Torus: radius2 must be positive!"); if (!(radius1 > radius2)) @@ -570,15 +686,14 @@ initTorus(ValueT radius1, // major radius of torus in world units if (!(halfWidth > 0)) throw std::runtime_error("Torus: halfWidth must be positive!"); - auto builder = std::make_shared>(ValueT(halfWidth * voxelSize)); - auto acc = builder->getAccessor(); + auto grid = std::make_shared(ValueT(halfWidth * voxelSize)); + grid->setTransform(voxelSize, origin); // Define size of torus with narrow-band in voxel units const ValueT r1 = radius1 / ValueT(voxelSize), r2 = radius2 / ValueT(voxelSize), rmax1 = r1 + r2 + ValueT(halfWidth), rmax2 = r2 + ValueT(halfWidth); // Radius below the Nyquist frequency - if (r2 < ValueT(1.5)) - return builder; + if (r2 < ValueT(1.5)) return grid; // Define center of torus in voxel units const Vec3 c(ValueT(center[0] - origin[0]) / ValueT(voxelSize), @@ -590,41 +705,52 @@ initTorus(ValueT radius1, // major radius of torus in world units const int jmin = Floor(c[1] - rmax2), jmax = Ceil(c[1] + rmax2); const int kmin = Floor(c[2] - rmax1), kmax = Ceil(c[2] + rmax1); - Coord ijk; - int & i = ijk[0], &j = ijk[1], &k = ijk[2], m = 1; - // Compute signed distances to torus using leapfrogging in k - for (i = imin; i <= imax; ++i) { - const auto x2 = Pow2(ValueT(i) - c[0]); - for (k = kmin; k <= kmax; ++k) { - const auto x2z2 = Pow2(Sqrt(Pow2(ValueT(k) - c[2]) + x2) - r1); - for (j = jmin; j <= jmax; j += m) { - m = 1; - const auto v = Sqrt(x2z2 + Pow2(ValueT(j) - c[1])) - r2; // Distance in voxel units - const auto d = v < 0 ? -v : v; - if (d < halfWidth) { // inside narrow band - acc.setValue(ijk, ValueT(voxelSize) * v); // distance in world units - } else { // outside narrow band - m += Floor(d - halfWidth); // leapfrog - } - } //end leapfrog over k - } //end loop over j - } //end loop over i - - return builder; + const Range<1,int> range(imin, imax+1, 32); + auto kernel = [&](const Range<1,int> &r) { + auto acc = grid->getWriteAccessor(); + Coord ijk; + int &i = ijk[0], &j = ijk[1], &k = ijk[2], m = 1; + // Compute signed distances to torus using leapfrogging in k + for (i = r.begin(); i < r.end(); ++i) { + const auto x2 = Pow2(ValueT(i) - c[0]); + for (k = kmin; k <= kmax; ++k) { + const auto x2z2 = Pow2(Sqrt(Pow2(ValueT(k) - c[2]) + x2) - r1); + for (j = jmin; j <= jmax; j += m) { + m = 1; + const auto v = Sqrt(x2z2 + Pow2(ValueT(j) - c[1])) - r2; // Distance in voxel units + const auto d = v < 0 ? -v : v; + if (d < halfWidth) { // inside narrow band + acc.setValue(ijk, ValueT(voxelSize) * v); // distance in world units + } else { // outside narrow band + m += Floor(d - halfWidth); // leapfrog + } + } //end leapfrog over k + } //end loop over j + } //end loop over i + }; // kernel + +#ifdef NANOVDB_PARALLEL_PRIMITIVES + forEach(range, kernel); +#else + kernel(range); +#endif + + return grid; } // initTorus -template -std::shared_ptr> -initBox(ValueT width, // major radius of torus in world units - ValueT height, // minor radius of torus in world units - ValueT depth, - const Vec3& center, //center of box in world units - double voxelSize, // size of a voxel in world units - double halfWidth, // half-width of narrow band in voxel units - const Vec3d& origin) // origin of grid in world units +template +std::shared_ptr> +initBox(double width, // major radius of torus in world units + double height, // minor radius of torus in world units + double depth, + const Vec3d& center, // center of box in world units + double voxelSize, // size of a voxel in world units + double halfWidth, // half-width of narrow band in voxel units + const Vec3d& origin) // origin of grid in world units { + using GridT = build::Grid; + using ValueT = typename BuildToValueMap::type; static_assert(is_floating_point::value, "initBox: expect floating point"); - static_assert(is_floating_point::Type>::value, "initBox: expect floating point"); using Vec3T = Vec3; if (!(width > 0)) throw std::runtime_error("Box: width must be positive!"); @@ -638,15 +764,16 @@ initBox(ValueT width, // major radius of torus in world units if (!(halfWidth > 0)) throw std::runtime_error("Box: halfWidth must be positive!"); - auto builder = std::make_shared>(ValueT(halfWidth * voxelSize)); - auto acc = builder->getAccessor(); + auto grid = std::make_shared(ValueT(halfWidth * voxelSize)); + grid->setTransform(voxelSize, origin); // Define size of box with narrow-band in voxel units - const Vec3T r(width / (2 * ValueT(voxelSize)), height / (2 * ValueT(voxelSize)), depth / (2 * ValueT(voxelSize))); + const Vec3T r(width / (2 * ValueT(voxelSize)), + height / (2 * ValueT(voxelSize)), + depth / (2 * ValueT(voxelSize))); // Below the Nyquist frequency - if (r.min() < ValueT(1.5)) - return builder; + if (r.min() < ValueT(1.5)) return grid; // Define center of box in voxel units const Vec3T c(ValueT(center[0] - origin[0]) / ValueT(voxelSize), @@ -660,47 +787,56 @@ initBox(ValueT width, // major radius of torus in world units // Define bounds of the voxel coordinates const BBox b(c - r - Vec3T(ValueT(halfWidth)), c + r + Vec3T(ValueT(halfWidth))); const CoordBBox bbox(Coord(Floor(b[0][0]), Floor(b[0][1]), Floor(b[0][2])), - Coord(Ceil(b[1][0]), Ceil(b[1][1]), Ceil(b[1][2]))); + Coord( Ceil(b[1][0]), Ceil(b[1][1]), Ceil(b[1][2]))); + const Range<1,int> range(bbox[0][0], bbox[1][0]+1, 32); // Compute signed distances to box using leapfrogging in k - int m = 1; - for (Coord p = bbox[0]; p[0] <= bbox[1][0]; ++p[0]) { - const auto q1 = Abs(ValueT(p[0]) - c[0]) - r[0]; - const auto x2 = Pow2(Pos(q1)); - for (p[1] = bbox[0][1]; p[1] <= bbox[1][1]; ++p[1]) { - const auto q2 = Abs(ValueT(p[1]) - c[1]) - r[1]; - const auto q0 = Max(q1, q2); - const auto x2y2 = x2 + Pow2(Pos(q2)); - for (p[2] = bbox[0][2]; p[2] <= bbox[1][2]; p[2] += m) { - m = 1; - const auto q3 = Abs(ValueT(p[2]) - c[2]) - r[2]; - const auto v = Sqrt(x2y2 + Pow2(Pos(q3))) + Neg(Max(q0, q3)); // Distance in voxel units - const auto d = Abs(v); - if (d < halfWidth) { // inside narrow band - acc.setValue(p, ValueT(voxelSize) * v); // distance in world units - } else { // outside narrow band - m += Floor(d - halfWidth); // leapfrog - } - } //end leapfrog over k - } //end loop over j - } //end loop over i - - return builder; + auto kernel = [&](const Range<1,int> &ra) { + auto acc = grid->getWriteAccessor(); + int m = 1; + for (Coord p(ra.begin(),bbox[0][1],bbox[0][2]); p[0] < ra.end(); ++p[0]) { + const auto q1 = Abs(ValueT(p[0]) - c[0]) - r[0]; + const auto x2 = Pow2(Pos(q1)); + for (p[1] = bbox[0][1]; p[1] <= bbox[1][1]; ++p[1]) { + const auto q2 = Abs(ValueT(p[1]) - c[1]) - r[1]; + const auto q0 = Max(q1, q2); + const auto x2y2 = x2 + Pow2(Pos(q2)); + for (p[2] = bbox[0][2]; p[2] <= bbox[1][2]; p[2] += m) { + m = 1; + const auto q3 = Abs(ValueT(p[2]) - c[2]) - r[2]; + const auto v = Sqrt(x2y2 + Pow2(Pos(q3))) + Neg(Max(q0, q3)); // Distance in voxel units + const auto d = Abs(v); + if (d < halfWidth) { // inside narrow band + acc.setValue(p, ValueT(voxelSize) * v); // distance in world units + } else { // outside narrow band + m += Floor(d - halfWidth); // leapfrog + } + } //end leapfrog over k + } //end loop over j + } //end loop over i + }; // kernel +#ifdef NANOVDB_PARALLEL_PRIMITIVES + forEach(range, kernel); +#else + kernel(range); +#endif + return grid; } // initBox -template -std::shared_ptr> -initBBox(ValueT width, // width of the bbox in world units - ValueT height, // height of the bbox in world units - ValueT depth, // depth of the bbox in world units - ValueT thickness, // thickness of the wire in world units - const Vec3& center, //center of bbox in world units - double voxelSize, // size of a voxel in world units - double halfWidth, // half-width of narrow band in voxel units - const Vec3d& origin) // origin of grid in world units +template +std::shared_ptr> +initBBox(double width, // width of the bbox in world units + double height, // height of the bbox in world units + double depth, // depth of the bbox in world units + double thickness, // thickness of the wire in world units + const Vec3d& center, // center of bbox in world units + double voxelSize, // size of a voxel in world units + double halfWidth, // half-width of narrow band in voxel units + const Vec3d& origin) // origin of grid in world units { + using GridT = build::Grid; + using ValueT = typename BuildToValueMap::type; static_assert(is_floating_point::value, "initBBox: expect floating point"); - static_assert(is_floating_point::Type>::value, "initBBox: expect floating point"); using Vec3T = Vec3; if (!(width > 0)) throw std::runtime_error("BBox: width must be positive!"); @@ -713,16 +849,18 @@ initBBox(ValueT width, // width of the bbox in world units if (!(voxelSize > 0.0)) throw std::runtime_error("BBox: voxelSize must be positive!"); - auto builder = std::make_shared>(ValueT(halfWidth * voxelSize)); - auto acc = builder->getAccessor(); + + auto grid = std::make_shared(ValueT(halfWidth * voxelSize)); + grid->setTransform(voxelSize, origin); // Define size of bbox with narrow-band in voxel units - const Vec3T r(width / (2 * ValueT(voxelSize)), height / (2 * ValueT(voxelSize)), depth / (2 * ValueT(voxelSize))); + const Vec3T r(width / (2 * ValueT(voxelSize)), + height / (2 * ValueT(voxelSize)), + depth / (2 * ValueT(voxelSize))); const ValueT e = thickness / ValueT(voxelSize); // Below the Nyquist frequency - if (r.min() < ValueT(1.5) || e < ValueT(1.5)) - return builder; + if (r.min() < ValueT(1.5) || e < ValueT(1.5)) return grid; // Define center of bbox in voxel units const Vec3T c(ValueT(center[0] - origin[0]) / ValueT(voxelSize), @@ -736,71 +874,78 @@ initBBox(ValueT width, // width of the bbox in world units // Define bounds of the voxel coordinates const BBox b(c - r - Vec3T(e + ValueT(halfWidth)), c + r + Vec3T(e + ValueT(halfWidth))); const CoordBBox bbox(Coord(Floor(b[0][0]), Floor(b[0][1]), Floor(b[0][2])), - Coord(Ceil(b[1][0]), Ceil(b[1][1]), Ceil(b[1][2]))); + Coord( Ceil(b[1][0]), Ceil(b[1][1]), Ceil(b[1][2]))); + const Range<1,int> range(bbox[0][0], bbox[1][0]+1, 32); // Compute signed distances to bbox using leapfrogging in k - int m = 1; - for (Coord p = bbox[0]; p[0] <= bbox[1][0]; ++p[0]) { - const ValueT px = Abs(ValueT(p[0]) - c[0]) - r[0]; - const ValueT qx = Abs(ValueT(px) + e) - e; - const ValueT px2 = Pow2(Pos(px)); - const ValueT qx2 = Pow2(Pos(qx)); - for (p[1] = bbox[0][1]; p[1] <= bbox[1][1]; ++p[1]) { - const ValueT py = Abs(ValueT(p[1]) - c[1]) - r[1]; - const ValueT qy = Abs(ValueT(py) + e) - e; - const ValueT qy2 = Pow2(Pos(qy)); - ; - const ValueT px2qy2 = px2 + qy2; - const ValueT qx2py2 = qx2 + Pow2(Pos(py)); - const ValueT qx2qy2 = qx2 + qy2; - const ValueT a[3] = {Max(px, qy), Max(qx, py), Max(qx, qy)}; - for (p[2] = bbox[0][2]; p[2] <= bbox[1][2]; p[2] += m) { - m = 1; - const ValueT pz = Abs(ValueT(p[2]) - c[2]) - r[2]; - const ValueT qz = Abs(ValueT(pz) + e) - e; - const ValueT qz2 = Pow2(Pos(qz)); - const ValueT s1 = Sqrt(px2qy2 + qz2) + Neg(Max(a[0], qz)); - const ValueT s2 = Sqrt(qx2py2 + qz2) + Neg(Max(a[1], qz)); - const ValueT s3 = Sqrt(qx2qy2 + Pow2(Pos(pz))) + Neg(Max(a[2], pz)); - const ValueT v = Min(s1, Min(s2, s3)); // Distance in voxel units - const ValueT d = Abs(v); - if (d < halfWidth) { // inside narrow band - acc.setValue(p, ValueT(voxelSize) * v); // distance in world units - } else { // outside narrow band - m += Floor(d - halfWidth); // leapfrog - } - } //end leapfrog over k - } //end loop over j - } //end loop over i - - return builder; + auto kernel = [&](const Range<1,int> &ra) { + auto acc = grid->getWriteAccessor(); + int m = 1; + for (Coord p(ra.begin(),bbox[0][1],bbox[0][2]); p[0] < ra.end(); ++p[0]) { + const ValueT px = Abs(ValueT(p[0]) - c[0]) - r[0]; + const ValueT qx = Abs(ValueT(px) + e) - e; + const ValueT px2 = Pow2(Pos(px)); + const ValueT qx2 = Pow2(Pos(qx)); + for (p[1] = bbox[0][1]; p[1] <= bbox[1][1]; ++p[1]) { + const ValueT py = Abs(ValueT(p[1]) - c[1]) - r[1]; + const ValueT qy = Abs(ValueT(py) + e) - e; + const ValueT qy2 = Pow2(Pos(qy)); + const ValueT px2qy2 = px2 + qy2; + const ValueT qx2py2 = qx2 + Pow2(Pos(py)); + const ValueT qx2qy2 = qx2 + qy2; + const ValueT a[3] = {Max(px, qy), Max(qx, py), Max(qx, qy)}; + for (p[2] = bbox[0][2]; p[2] <= bbox[1][2]; p[2] += m) { + m = 1; + const ValueT pz = Abs(ValueT(p[2]) - c[2]) - r[2]; + const ValueT qz = Abs(ValueT(pz) + e) - e; + const ValueT qz2 = Pow2(Pos(qz)); + const ValueT s1 = Sqrt(px2qy2 + qz2) + Neg(Max(a[0], qz)); + const ValueT s2 = Sqrt(qx2py2 + qz2) + Neg(Max(a[1], qz)); + const ValueT s3 = Sqrt(qx2qy2 + Pow2(Pos(pz))) + Neg(Max(a[2], pz)); + const ValueT v = Min(s1, Min(s2, s3)); // Distance in voxel units + const ValueT d = Abs(v); + if (d < halfWidth) { // inside narrow band + acc.setValue(p, ValueT(voxelSize) * v); // distance in world units + } else { // outside narrow band + m += Floor(d - halfWidth); // leapfrog + } + } //end leapfrog over k + } //end loop over j + } //end loop over i + }; //kernel +#ifdef NANOVDB_PARALLEL_PRIMITIVES + forEach(range, kernel); +#else + kernel(range); +#endif + + return grid; } // initBBox -template -std::shared_ptr> -initOctahedron(ValueT scale, // scale of the octahedron in world units - const Vec3& center, //center of octahedron in world units - double voxelSize, // size of a voxel in world units - double halfWidth, // half-width of narrow band in voxel units - const Vec3d& origin) // origin of grid in world units +template +std::shared_ptr> +initOctahedron(double scale, // scale of the octahedron in world units + const Vec3d& center, // center of octahedron in world units + double voxelSize, // size of a voxel in world units + double halfWidth, // half-width of narrow band in voxel units + const Vec3d& origin) // origin of grid in world units { - static_assert(is_floating_point::value, "initOctahedron: expect floating point"); - static_assert(is_floating_point::Type>::value, "initOctahedron: expect floating point"); + using GridT = build::Grid; + using ValueT = typename BuildToValueMap::type; using Vec3T = Vec3; - if (!(scale > 0)) - throw std::runtime_error("Octahedron: width must be positive!"); - if (!(voxelSize > 0)) - throw std::runtime_error("Octahedron: voxelSize must be positive!"); + static_assert(is_floating_point::value, "initOctahedron: expect floating point"); + + if (!(scale > 0)) throw std::runtime_error("Octahedron: width must be positive!"); + if (!(voxelSize > 0)) throw std::runtime_error("Octahedron: voxelSize must be positive!"); - auto builder = std::make_shared>(halfWidth * voxelSize); - auto acc = builder->getAccessor(); + auto grid = std::make_shared(ValueT(halfWidth * voxelSize)); + grid->setTransform(voxelSize, origin); // Define size of octahedron with narrow-band in voxel units const ValueT s = scale / (2 * ValueT(voxelSize)); // Below the Nyquist frequency - if ( s < ValueT(1.5) ) - return builder; + if ( s < ValueT(1.5) ) return grid; // Define center of octahedron in voxel units const Vec3T c(ValueT(center[0] - origin[0]) / ValueT(voxelSize), @@ -821,50 +966,117 @@ initOctahedron(ValueT scale, // scale of the octahedron in world un // Define bounds of the voxel coordinates const BBox b(c - Vec3T(s + ValueT(halfWidth)), c + Vec3T(s + ValueT(halfWidth))); const CoordBBox bbox(Coord(Floor(b[0][0]), Floor(b[0][1]), Floor(b[0][2])), - Coord(Ceil(b[1][0]), Ceil(b[1][1]), Ceil(b[1][2]))); + Coord( Ceil(b[1][0]), Ceil(b[1][1]), Ceil(b[1][2]))); + const Range<1,int> range(bbox[0][0], bbox[1][0]+1, 32); // Compute signed distances to octahedron using leapfrogging in k - int m = 1; - static const ValueT a = Sqrt(ValueT(1)/ValueT(3)); - for (Coord p = bbox[0]; p[0] <= bbox[1][0]; ++p[0]) { - const ValueT px = Abs(ValueT(p[0]) - c[0]); - for (p[1] = bbox[0][1]; p[1] <= bbox[1][1]; ++p[1]) { - const ValueT py = Abs(ValueT(p[1]) - c[1]); - for (p[2] = bbox[0][2]; p[2] <= bbox[1][2]; p[2] += m) { - m = 1; - const ValueT pz = Abs(ValueT(p[2]) - c[2]); - ValueT d = px + py + pz - s; - ValueT v; - if (ValueT(3)*px < d) { - v = sdf(px, py, pz); - } else if (ValueT(3)*py < d) { - v = sdf(py, pz, px); - } else if (ValueT(3)*pz < d) { - v = sdf(pz, px, py); - } else { - v = a * d; - } - d = Abs(v); - if (d < halfWidth) { // inside narrow band - acc.setValue(p, ValueT(voxelSize) * v); // distance in world units - } else { // outside narrow band - m += Floor(d - halfWidth); // leapfrog - } - } //end leapfrog over k - } //end loop over j - } //end loop over i - - return builder; + auto kernel = [&](const Range<1,int> &ra) { + auto acc = grid->getWriteAccessor(); + int m = 1; + static const ValueT a = Sqrt(ValueT(1)/ValueT(3)); + for (Coord p(ra.begin(),bbox[0][1],bbox[0][2]); p[0] < ra.end(); ++p[0]) { + const ValueT px = Abs(ValueT(p[0]) - c[0]); + for (p[1] = bbox[0][1]; p[1] <= bbox[1][1]; ++p[1]) { + const ValueT py = Abs(ValueT(p[1]) - c[1]); + for (p[2] = bbox[0][2]; p[2] <= bbox[1][2]; p[2] += m) { + m = 1; + const ValueT pz = Abs(ValueT(p[2]) - c[2]); + ValueT d = px + py + pz - s; + ValueT v; + if (ValueT(3)*px < d) { + v = sdf(px, py, pz); + } else if (ValueT(3)*py < d) { + v = sdf(py, pz, px); + } else if (ValueT(3)*pz < d) { + v = sdf(pz, px, py); + } else { + v = a * d; + } + d = Abs(v); + if (d < halfWidth) { // inside narrow band + acc.setValue(p, ValueT(voxelSize) * v); // distance in world units + } else { // outside narrow band + m += Floor(d - halfWidth); // leapfrog + } + } //end leapfrog over k + } //end loop over j + } //end loop over i + };// kernel +#ifdef NANOVDB_PARALLEL_PRIMITIVES + forEach(range, kernel); +#else + kernel(range); +#endif + return grid; } // initOctahedron } // unnamed namespace //================================================================================================ -template -inline GridHandle -createLevelSetSphere(ValueT radius, // radius of sphere in world units - const Vec3& center, //center of sphere in world units +template +typename enable_if::value || + is_same::value, GridHandle>::type +createLevelSetSphere(double radius, // radius of sphere in world units + const Vec3d& center, // center of sphere in world units + double voxelSize, // size of a voxel in world units + double halfWidth, // half-width of narrow band in voxel units + const Vec3d& origin, // origin of grid in world units + const std::string& name, // name of grid + StatsMode sMode, // mode of computation for the statistics + ChecksumMode cMode, // mode of computation for the checksum + const BufferT& buffer) +{ + using GridT = build::Grid; + auto grid = initSphere(radius, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + auto handle = converter.template getHandle(buffer); + assert(handle); + return handle; +} // createLevelSetSphere + +//================================================================================================ + +template +typename enable_if::value || + is_same::value || + is_same::value, GridHandle>::type +createLevelSetSphere(double radius, // radius of sphere in world units + const Vec3d& center, // center of sphere in world units + double voxelSize, // size of a voxel in world units + double halfWidth, // half-width of narrow band in voxel units + const Vec3d& origin, // origin of grid in world units + const std::string& name, // name of grid + StatsMode sMode, // mode of computation for the statistics + ChecksumMode cMode, // mode of computation for the checksum + bool ditherOn, + const BufferT& buffer) +{ + using GridT = build::Grid; + auto grid = initSphere(radius, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + converter.enableDithering(ditherOn); + auto handle = converter.template getHandle(buffer); + assert(handle); + return handle; +} // createLevelSetSphere + +//================================================================================================ + +template +typename enable_if::value, GridHandle>::type +createLevelSetSphere(double radius, // radius of sphere in world units + const Vec3d& center, // center of sphere in world units double voxelSize, // size of a voxel in world units double halfWidth, // half-width of narrow band in voxel units const Vec3d& origin, // origin of grid in world units @@ -875,23 +1087,55 @@ createLevelSetSphere(ValueT radius, // radius of sphere in world un bool ditherOn, const BufferT& buffer) { - auto builder = initSphere(radius, center, voxelSize, halfWidth, origin); - builder->sdfToLevelSet(); - builder->setStats(sMode); - builder->setChecksum(cMode); - builder->enableDithering(ditherOn); + using GridT = build::Grid; + auto grid = initSphere(radius, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + converter.enableDithering(ditherOn); AbsDiff oracle(tolerance); - auto handle = builder->template getHandle(voxelSize, origin, name, oracle, buffer); + auto handle = converter.template getHandle(oracle, buffer); assert(handle); return handle; -} // createLevelSetSphere +} // createLevelSetSphere //================================================================================================ -template -inline GridHandle -createFogVolumeSphere(ValueT radius, // radius of sphere in world units - const Vec3& center, //center of sphere in world units +template +typename disable_if::value, GridHandle>::type +createFogVolumeSphere(double radius, // radius of sphere in world units + const Vec3d& center, // center of sphere in world units + double voxelSize, // size of a voxel in world units + double halfWidth, // half-width of narrow band in voxel units + const Vec3d& origin, // origin of grid in world units + const std::string& name, // name of grid + StatsMode sMode, // mode of computation for the statistics + ChecksumMode cMode, // mode of computation for the checksum + const BufferT& buffer) +{ + using GridT = build::Grid; + auto grid = initSphere(radius, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + build::levelSetToFog(mgr, false); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + auto handle = converter.template getHandle(buffer); + assert(handle); + return handle; +} // createFogVolumeSphere + +//================================================================================================ + +template +typename enable_if::value, GridHandle>::type +createFogVolumeSphere(double radius, // radius of sphere in world units + const Vec3d& center, // center of sphere in world units double voxelSize, // size of a voxel in world units double halfWidth, // half-width of narrow band in voxel units const Vec3d& origin, // origin of grid in world units @@ -902,24 +1146,29 @@ createFogVolumeSphere(ValueT radius, // radius of sphere in world u bool ditherOn, const BufferT& buffer) { - auto builder = initSphere(radius, center, voxelSize, halfWidth, origin); - builder->sdfToFog(); - builder->setStats(sMode); - builder->setChecksum(cMode); - builder->enableDithering(ditherOn); + using GridT = build::Grid; + auto grid = initSphere(radius, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + build::levelSetToFog(mgr, false); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + converter.enableDithering(ditherOn); AbsDiff oracle(tolerance); - auto handle = builder->template getHandle(voxelSize, origin, name, oracle, buffer); + auto handle = converter.template getHandle(oracle, buffer); assert(handle); return handle; -} // createFogVolumeSphere +} // createFogVolumeSphere //================================================================================================ -template -inline GridHandle -createPointSphere(int pointsPerVoxel, // half-width of narrow band in voxel units - ValueT radius, // radius of sphere in world units - const Vec3& center, //center of sphere in world units +template +typename disable_if::value, GridHandle>::type +createPointSphere(int pointsPerVoxel, // number of points to be scattered in each active voxel + double radius, // radius of sphere in world units + const Vec3d& center, // center of sphere in world units double voxelSize, // size of a voxel in world units const Vec3d& origin, // origin of grid in world units const std::string& name, // name of grid @@ -927,9 +1176,9 @@ createPointSphere(int pointsPerVoxel, // half-width of narrow ba const BufferT& buffer) { auto sphereHandle = createLevelSetSphere(radius, center, voxelSize, 0.5, origin, "dummy", - StatsMode::BBox, ChecksumMode::Disable, -1.0f, false, buffer); + StatsMode::BBox, ChecksumMode::Disable, buffer); assert(sphereHandle); - auto* sphereGrid = sphereHandle.template grid(); + auto* sphereGrid = sphereHandle.template grid(); assert(sphereGrid); auto pointHandle = createPointScatter(*sphereGrid, pointsPerVoxel, name, cMode, buffer); assert(pointHandle); @@ -938,11 +1187,39 @@ createPointSphere(int pointsPerVoxel, // half-width of narrow ba //================================================================================================ -template -inline GridHandle -createLevelSetTorus(ValueT majorRadius, // major radius of torus in world units - ValueT minorRadius, // minor radius of torus in world units - const Vec3& center, //center of torus in world units +template +typename disable_if::value, GridHandle>::type +createLevelSetTorus(double majorRadius, // major radius of torus in world units + double minorRadius, // minor radius of torus in world units + const Vec3d& center, // center of torus in world units + double voxelSize, // size of a voxel in world units + double halfWidth, // half-width of narrow band in voxel units + const Vec3d& origin, // origin of grid in world units + const std::string& name, // name of grid + StatsMode sMode, // mode of computation for the statistics + ChecksumMode cMode, // mode of computation for the checksum + const BufferT& buffer) +{ + using GridT = build::Grid; + auto grid = initTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + auto handle = converter.template getHandle(buffer); + assert(handle); + return handle; +} // createLevelSetTorus + +//================================================================================================ + +template +typename enable_if::value, GridHandle>::type +createLevelSetTorus(double majorRadius, // major radius of torus in world units + double minorRadius, // minor radius of torus in world units + const Vec3d& center, // center of torus in world units double voxelSize, // size of a voxel in world units double halfWidth, // half-width of narrow band in voxel units const Vec3d& origin, // origin of grid in world units @@ -953,24 +1230,57 @@ createLevelSetTorus(ValueT majorRadius, // major radius of torus in bool ditherOn, const BufferT& buffer) { - auto builder = initTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin); - builder->sdfToLevelSet(); - builder->setStats(sMode); - builder->setChecksum(cMode); - builder->enableDithering(ditherOn); + using GridT = build::Grid; + auto grid = initTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + converter.enableDithering(ditherOn); AbsDiff oracle(tolerance); - auto handle = builder->template getHandle(voxelSize, origin, name, oracle, buffer); + auto handle = converter.template getHandle(oracle, buffer); assert(handle); return handle; -} // createLevelSetTorus +} // createLevelSetTorus //================================================================================================ -template -inline GridHandle -createFogVolumeTorus(ValueT majorRadius, // major radius of torus in world units - ValueT minorRadius, // minor radius of torus in world units - const Vec3& center, //center of torus in world units +template +typename disable_if::value, GridHandle>::type +createFogVolumeTorus(double majorRadius, // major radius of torus in world units + double minorRadius, // minor radius of torus in world units + const Vec3d& center, // center of torus in world units + double voxelSize, // size of a voxel in world units + double halfWidth, // half-width of narrow band in voxel units + const Vec3d& origin, // origin of grid in world units + const std::string& name, // name of grid + StatsMode sMode, // mode of computation for the statistics + ChecksumMode cMode, // mode of computation for the checksum + const BufferT& buffer) +{ + using GridT = build::Grid; + auto grid = initTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + build::levelSetToFog(mgr, false); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + auto handle = converter.template getHandle(buffer); + assert(handle); + return handle; +} // createFogVolumeTorus + +//================================================================================================ + +template +typename enable_if::value, GridHandle>::type +createFogVolumeTorus(double majorRadius, // major radius of torus in world units + double minorRadius, // minor radius of torus in world units + const Vec3d& center, // center of torus in world units double voxelSize, // size of a voxel in world units double halfWidth, // half-width of narrow band in voxel units const Vec3d& origin, // origin of grid in world units @@ -981,25 +1291,30 @@ createFogVolumeTorus(ValueT majorRadius, // major radius of torus i bool ditherOn, const BufferT& buffer) { - auto builder = initTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin); - builder->sdfToFog(); - builder->setStats(sMode); - builder->setChecksum(cMode); - builder->enableDithering(ditherOn); + using GridT = build::Grid; + auto grid = initTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + build::levelSetToFog(mgr, false); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + converter.enableDithering(ditherOn); AbsDiff oracle(tolerance); - auto handle = builder->template getHandle(voxelSize, origin, name, oracle, buffer); + auto handle = converter.template getHandle(oracle, buffer); assert(handle); return handle; -} // createFogVolumeTorus +} // createFogVolumeTorus //================================================================================================ -template -inline GridHandle -createPointTorus(int pointsPerVoxel, // half-width of narrow band in voxel units - ValueT majorRadius, // major radius of torus in world units - ValueT minorRadius, // minor radius of torus in world units - const Vec3& center, //center of torus in world units +template +typename disable_if::value, GridHandle>::type +createPointTorus(int pointsPerVoxel, // number of points to be scattered in each active voxel + double majorRadius, // major radius of torus in world units + double minorRadius, // minor radius of torus in world units + const Vec3d& center, // center of torus in world units double voxelSize, // size of a voxel in world units const Vec3d& origin, // origin of grid in world units const std::string& name, // name of grid @@ -1007,23 +1322,52 @@ createPointTorus(int pointsPerVoxel, // half-width of narrow ban const BufferT& buffer) { auto torusHandle = createLevelSetTorus(majorRadius, minorRadius, center, voxelSize, 0.5f, origin, - "dummy", StatsMode::BBox, ChecksumMode::Disable, -1.0f, false, buffer); + "dummy", StatsMode::BBox, ChecksumMode::Disable, buffer); assert(torusHandle); - auto* torusGrid = torusHandle.template grid(); + auto* torusGrid = torusHandle.template grid(); assert(torusGrid); auto pointHandle = createPointScatter(*torusGrid, pointsPerVoxel, name, cMode, buffer); assert(pointHandle); return pointHandle; -} // createPointTorus +} // createPointTorus //================================================================================================ -template -inline GridHandle -createLevelSetBox(ValueT width, // width of box in world units - ValueT height, // height of box in world units - ValueT depth, // depth of box in world units - const Vec3& center, //center of box in world units +template +typename disable_if::value, GridHandle>::type +createLevelSetBox(double width, // width of box in world units + double height, // height of box in world units + double depth, // depth of box in world units + const Vec3d& center, // center of box in world units + double voxelSize, // size of a voxel in world units + double halfWidth, // half-width of narrow band in voxel units + const Vec3d& origin, // origin of grid in world units + const std::string& name, // name of grid + StatsMode sMode, // mode of computation for the statistics + ChecksumMode cMode, // mode of computation for the checksum + const BufferT& buffer) +{ + using GridT = build::Grid; + auto grid = initBox(width, height, depth, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + auto handle = converter.template getHandle(buffer); + assert(handle); + return handle; +} // createLevelSetBox + +//================================================================================================ + +template +typename enable_if::value, GridHandle>::type +createLevelSetBox(double width, // width of box in world units + double height, // height of box in world units + double depth, // depth of box in world units + const Vec3d& center, // center of box in world units double voxelSize, // size of a voxel in world units double halfWidth, // half-width of narrow band in voxel units const Vec3d& origin, // origin of grid in world units @@ -1034,23 +1378,54 @@ createLevelSetBox(ValueT width, // width of box in world units bool ditherOn, const BufferT& buffer) { - auto builder = initBox(width, height, depth, center, voxelSize, halfWidth, origin); - builder->sdfToLevelSet(); - builder->setStats(sMode); - builder->setChecksum(cMode); - builder->enableDithering(ditherOn); + using GridT = build::Grid; + auto grid = initBox(width, height, depth, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + converter.enableDithering(ditherOn); AbsDiff oracle(tolerance); - auto handle = builder->template getHandle(voxelSize, origin, name, oracle, buffer); + auto handle = converter.template getHandle(oracle, buffer); assert(handle); return handle; -} // createLevelSetBox +} // createLevelSetBox //================================================================================================ -template -inline GridHandle -createLevelSetOctahedron(ValueT scale, // scale of the octahedron in world units - const Vec3& center, //center of box in world units +template +typename disable_if::value, GridHandle>::type +createLevelSetOctahedron(double scale, // scale of the octahedron in world units + const Vec3d& center, // center of box in world units + double voxelSize, // size of a voxel in world units + double halfWidth, // half-width of narrow band in voxel units + const Vec3d& origin, // origin of grid in world units + const std::string& name, // name of grid + StatsMode sMode, // mode of computation for the statistics + ChecksumMode cMode, // mode of computation for the checksum + const BufferT& buffer) +{ + using GridT = build::Grid; + auto grid = initOctahedron(scale, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + auto handle = converter.template getHandle(buffer); + assert(handle); + return handle; +} // createLevelSetOctahedron + +//================================================================================================ + +template +typename enable_if::value, GridHandle>::type +createLevelSetOctahedron(double scale, // scale of the octahedron in world units + const Vec3d& center, // center of box in world units double voxelSize, // size of a voxel in world units double halfWidth, // half-width of narrow band in voxel units const Vec3d& origin, // origin of grid in world units @@ -1061,26 +1436,60 @@ createLevelSetOctahedron(ValueT scale, // scale of the octahedron i bool ditherOn, const BufferT& buffer) { - auto builder = initOctahedron(scale, center, voxelSize, halfWidth, origin); - builder->sdfToLevelSet(); - builder->setStats(sMode); - builder->setChecksum(cMode); - builder->enableDithering(ditherOn); + using GridT = build::Grid; + auto grid = initOctahedron(scale, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + converter.enableDithering(ditherOn); AbsDiff oracle(tolerance); - auto handle = builder->template getHandle(voxelSize, origin, name, oracle, buffer); + auto handle = converter.template getHandle(oracle, buffer); assert(handle); return handle; -} // createLevelSetOctahedron +} // createLevelSetOctahedron //================================================================================================ -template -inline GridHandle -createLevelSetBBox(ValueT width, // width of bbox in world units - ValueT height, // height of bbox in world units - ValueT depth, // depth of bbox in world units - ValueT thickness, // thickness of the wire in world units - const Vec3& center, //center of bbox in world units +template +typename disable_if::value, GridHandle>::type +createLevelSetBBox(double width, // width of bbox in world units + double height, // height of bbox in world units + double depth, // depth of bbox in world units + double thickness, // thickness of the wire in world units + const Vec3d& center, // center of bbox in world units + double voxelSize, // size of a voxel in world units + double halfWidth, // half-width of narrow band in voxel units + const Vec3d& origin, // origin of grid in world units + const std::string& name, // name of grid + StatsMode sMode, // mode of computation for the statistics + ChecksumMode cMode, // mode of computation for the checksum + const BufferT& buffer) +{ + using GridT = build::Grid; + auto grid = initBBox(width, height, depth, thickness, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + auto handle = converter.template getHandle(buffer); + assert(handle); + return handle; +} // createLevelSetBBox + +//================================================================================================ + +template +typename enable_if::value, GridHandle>::type +createLevelSetBBox(double width, // width of bbox in world units + double height, // height of bbox in world units + double depth, // depth of bbox in world units + double thickness, // thickness of the wire in world units + const Vec3d& center, // center of bbox in world units double voxelSize, // size of a voxel in world units double halfWidth, // half-width of narrow band in voxel units const Vec3d& origin, // origin of grid in world units @@ -1091,25 +1500,59 @@ createLevelSetBBox(ValueT width, // width of bbox in world units bool ditherOn, const BufferT& buffer) { - auto builder = initBBox(width, height, depth, thickness, center, voxelSize, halfWidth, origin); - builder->sdfToLevelSet(); - builder->setStats(sMode); - builder->setChecksum(cMode); - builder->enableDithering(ditherOn); + using GridT = build::Grid; + auto grid = initBBox(width, height, depth, thickness, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + converter.enableDithering(ditherOn); AbsDiff oracle(tolerance); - auto handle = builder->template getHandle(voxelSize, origin, name, oracle, buffer); + auto handle = converter.template getHandle(oracle, buffer); assert(handle); return handle; -} // createLevelSetBBox +} // createLevelSetBBox //================================================================================================ -template -inline GridHandle -createFogVolumeBox(ValueT width, // width of box in world units - ValueT height, // height of box in world units - ValueT depth, // depth of box in world units - const Vec3& center, //center of box in world units +template +typename disable_if::value, GridHandle>::type +createFogVolumeBox(double width, // width of box in world units + double height, // height of box in world units + double depth, // depth of box in world units + const Vec3d& center, // center of box in world units + double voxelSize, // size of a voxel in world units + double halfWidth, // half-width of narrow band in voxel units + const Vec3d& origin, // origin of grid in world units + const std::string& name, // name of grid + StatsMode sMode, // mode of computation for the statistics + ChecksumMode cMode, // mode of computation for the checksum + const BufferT& buffer) +{ + using GridT = build::Grid; + auto grid = initBox(width, height, depth, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + build::levelSetToFog(mgr, false); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + auto handle = converter.template getHandle(buffer); + assert(handle); + return handle; +} // createFogVolumeBox + +//================================================================================================ + +template +typename enable_if::value, GridHandle>::type +createFogVolumeBox(double width, // width of box in world units + double height, // height of box in world units + double depth, // depth of box in world units + const Vec3d& center, // center of box in world units double voxelSize, // size of a voxel in world units double halfWidth, // half-width of narrow band in voxel units const Vec3d& origin, // origin of grid in world units @@ -1120,23 +1563,56 @@ createFogVolumeBox(ValueT width, // width of box in world units bool ditherOn, const BufferT& buffer) { - auto builder = initBox(width, height, depth, center, voxelSize, halfWidth, origin); - builder->sdfToFog(); - builder->setStats(sMode); - builder->setChecksum(cMode); - builder->enableDithering(ditherOn); + using GridT = build::Grid; + auto grid = initBox(width, height, depth, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + build::levelSetToFog(mgr, false); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + converter.enableDithering(ditherOn); AbsDiff oracle(tolerance); - auto handle = builder->template getHandle(voxelSize, origin, name, oracle, buffer); + auto handle = converter.template getHandle(oracle, buffer); assert(handle); return handle; -} // createFogVolumeBox +} // createFogVolumeBox //================================================================================================ -template -inline GridHandle -createFogVolumeOctahedron(ValueT scale, // scale of octahedron in world units - const Vec3& center, //center of box in world units +template +typename disable_if::value, GridHandle>::type +createFogVolumeOctahedron(double scale, // scale of octahedron in world units + const Vec3d& center, // center of box in world units + double voxelSize, // size of a voxel in world units + double halfWidth, // half-width of narrow band in voxel units + const Vec3d& origin, // origin of grid in world units + const std::string& name, // name of grid + StatsMode sMode, // mode of computation for the statistics + ChecksumMode cMode, // mode of computation for the checksum + const BufferT& buffer) +{ + using GridT = build::Grid; + auto grid = initOctahedron(scale, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + build::levelSetToFog(mgr, false); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + auto handle = converter.template getHandle(buffer); + assert(handle); + return handle; +} // createFogVolumeOctahedron + +//================================================================================================ + +template +typename enable_if::value, GridHandle>::type +createFogVolumeOctahedron(double scale, // scale of octahedron in world units + const Vec3d& center, // center of box in world units double voxelSize, // size of a voxel in world units double halfWidth, // half-width of narrow band in voxel units const Vec3d& origin, // origin of grid in world units @@ -1147,26 +1623,31 @@ createFogVolumeOctahedron(ValueT scale, // scale of octahedron in w bool ditherOn, const BufferT& buffer) { - auto builder = initOctahedron(scale, center, voxelSize, halfWidth, origin); - builder->sdfToFog(); - builder->setStats(sMode); - builder->setChecksum(cMode); - builder->enableDithering(ditherOn); + using GridT = build::Grid; + auto grid = initOctahedron(scale, center, voxelSize, halfWidth, origin); + grid->mName = name; + build::NodeManager mgr(*grid); + build::sdfToLevelSet(mgr); + build::levelSetToFog(mgr, false); + CreateNanoGrid converter(*grid); + converter.setStats(sMode); + converter.setChecksum(cMode); + converter.enableDithering(ditherOn); AbsDiff oracle(tolerance); - auto handle = builder->template getHandle(voxelSize, origin, name, oracle, buffer); + auto handle = converter.template getHandle(oracle, buffer); assert(handle); return handle; -} // createFogVolumeOctahedron +} // createFogVolumeOctahedron //================================================================================================ -template -inline GridHandle -createPointBox(int pointsPerVoxel, // half-width of narrow band in voxel units - ValueT width, // width of box in world units - ValueT height, // height of box in world units - ValueT depth, // depth of box in world units - const Vec3& center, //center of box in world units +template +typename disable_if::value, GridHandle>::type +createPointBox(int pointsPerVoxel, // number of points to be scattered in each active voxel + double width, // width of box in world units + double height, // height of box in world units + double depth, // depth of box in world units + const Vec3d& center, // center of box in world units double voxelSize, // size of a voxel in world units const Vec3d& origin, // origin of grid in world units const std::string& name, // name of grid @@ -1174,26 +1655,26 @@ createPointBox(int pointsPerVoxel, // half-width of narrow band const BufferT& buffer) { auto boxHandle = createLevelSetBox(width, height, depth, center, voxelSize, 0.5, origin, "dummy", - StatsMode::BBox, ChecksumMode::Disable, -1.0f, false, buffer); + StatsMode::BBox, ChecksumMode::Disable, buffer); assert(boxHandle); - auto* boxGrid = boxHandle.template grid(); + auto* boxGrid = boxHandle.template grid(); assert(boxGrid); auto pointHandle = createPointScatter(*boxGrid, pointsPerVoxel, name, cMode, buffer); assert(pointHandle); return pointHandle; - -} // createPointBox +} // createPointBox //================================================================================================ -template +template inline GridHandle -createPointScatter(const NanoGrid& srcGrid, // origin of grid in world units - int pointsPerVoxel, // half-width of narrow band in voxel units - const std::string& name, // name of grid - ChecksumMode cMode, // mode of computation for the checksum - const BufferT& buffer) +createPointScatter(const NanoGrid& srcGrid, // origin of grid in world units + int pointsPerVoxel, // number of points to be scattered in each active voxel + const std::string& name, // name of grid + ChecksumMode cMode, // mode of computation for the checksum + const BufferT& buffer) { + using ValueT = typename BuildToValueMap::type; static_assert(is_floating_point::value, "createPointScatter: expect floating point"); using Vec3T = Vec3; if (pointsPerVoxel < 1) { @@ -1206,80 +1687,65 @@ createPointScatter(const NanoGrid& srcGrid, // origin of grid in world u throw std::runtime_error("createPointScatter: ActiveVoxelCount is required"); } const uint64_t pointCount = pointsPerVoxel * srcGrid.activeVoxelCount(); - const uint64_t pointSize = AlignUp(pointCount * sizeof(Vec3T)); if (pointCount == 0) { throw std::runtime_error("createPointScatter: No particles to scatter"); } std::vector xyz; xyz.reserve(pointCount); - GridBuilder builder(std::numeric_limits::max(), GridClass::PointData, pointSize); - auto dstAcc = builder.getAccessor(); + using DstGridT = build::Grid; + DstGridT dstGrid(std::numeric_limits::max(), name, GridClass::PointData); + dstGrid.mMap = srcGrid.map(); + auto dstAcc = dstGrid.getAccessor(); std::srand(1234); const ValueT s = 1 / (1 + ValueT(RAND_MAX)); // scale so s*rand() is in ] 0, 1 [ // return a point with random local voxel coordinates (-0.5 to +0.5) - auto randomPoint = [&s]() { - return s * Vec3T(rand(), rand(), rand()) - Vec3T(0.5); - }; + auto randomPoint = [&s](){return s * Vec3T(rand(), rand(), rand()) - Vec3T(0.5);}; const auto& srcTree = srcGrid.tree(); auto srcMgrHandle = createNodeManager(srcGrid); - auto *srcMgr = srcMgrHandle.template mgr(); + auto *srcMgr = srcMgrHandle.template mgr(); assert(srcMgr); for (uint32_t i = 0, end = srcTree.nodeCount(0); i < end; ++i) { - auto& srcLeaf = srcMgr->leaf(i);; + auto& srcLeaf = srcMgr->leaf(i); auto* dstLeaf = dstAcc.setValue(srcLeaf.origin(), pointsPerVoxel); // allocates leaf node dstLeaf->mValueMask = srcLeaf.valueMask(); for (uint32_t j = 0, m = 0; j < 512; ++j) { if (dstLeaf->mValueMask.isOn(j)) { - for (int n = 0; n < pointsPerVoxel; ++n, ++m) { - xyz.push_back(randomPoint()); - } - } + const Vec3f ijk = dstLeaf->offsetToGlobalCoord(j).asVec3s();// floating-point representatrion of index coorindates + for (int n = 0; n < pointsPerVoxel; ++n) xyz.push_back(srcGrid.indexToWorld(randomPoint() + ijk)); + m += pointsPerVoxel; + }// active voxels dstLeaf->mValues[j] = m; - } - } + }// loop over all voxels + }// loop over leaf nodes assert(pointCount == xyz.size()); - builder.setStats(StatsMode::MinMax); - builder.setChecksum(ChecksumMode::Disable); - const AbsDiff dummy; - auto handle = builder.template getHandle(srcGrid.map(), name, dummy, buffer); + CreateNanoGrid converter(dstGrid); + converter.setStats(StatsMode::MinMax); + converter.setChecksum(ChecksumMode::Disable); + + converter.addBlindData(name, + GridBlindDataSemantic::WorldCoords, + GridBlindDataClass::AttributeArray, + mapToGridType(), + pointCount, + sizeof(Vec3T)); + auto handle = converter.template getHandle(buffer); assert(handle); - auto* dstGrid = handle.template grid(); - assert(dstGrid && dstGrid->template isSequential<0>()); - auto& dstTree = dstGrid->tree(); - if (dstTree.nodeCount(0) == 0) { - throw std::runtime_error("Expect leaf nodes!"); - } - auto *leafData = dstTree.getFirstLeaf()->data(); + + auto* grid = handle.template grid(); + assert(grid && grid->template isSequential<0>()); + auto &tree = grid->tree(); + if (tree.nodeCount(0) == 0) throw std::runtime_error("Expect leaf nodes!"); + auto *leafData = tree.getFirstLeaf()->data(); leafData[0].mMinimum = 0; // start of prefix sum - for (uint32_t i = 1, n = dstTree.nodeCount(0); i < n; ++i) { + for (uint32_t i = 1, n = tree.nodeCount(0); i < n; ++i) { leafData[i].mMinimum = leafData[i - 1].mMinimum + leafData[i - 1].mMaximum; } - auto& meta = const_cast(dstGrid->blindMetaData(0u)); - - meta.mElementCount = xyz.size(); - meta.mFlags = 0; - meta.mDataClass = GridBlindDataClass::AttributeArray; - meta.mSemantic = GridBlindDataSemantic::PointPosition; - if (name.length() + 1 > GridBlindMetaData::MaxNameSize) { - std::stringstream ss; - ss << "Point attribute name \"" << name << "\" is more then " - << nanovdb::GridBlindMetaData::MaxNameSize << " characters"; - throw std::runtime_error(ss.str()); - } - memcpy(meta.mName, name.c_str(), name.size() + 1); - if (std::is_same::value) { // resolved at compiletime - meta.mDataType = GridType::Vec3f; - } else if (std::is_same::value) { - meta.mDataType = GridType::Vec3d; - } else { - throw std::runtime_error("Unsupported value type"); - } - if (const auto *p = dstGrid->blindData(0)) { - memcpy(const_cast(p), xyz.data(), xyz.size() * sizeof(Vec3T)); + if (Vec3T *blindData = grid->template getBlindData(0)) { + memcpy(blindData, xyz.data(), xyz.size() * sizeof(Vec3T)); } else { throw std::runtime_error("Blind data pointer was NULL"); } - updateChecksum(*dstGrid, cMode); + updateChecksum(*grid, cMode); return handle; } // createPointScatter diff --git a/nanovdb/nanovdb/util/Ray.h b/nanovdb/nanovdb/util/Ray.h index 2597e3f030..62d6ff51a0 100644 --- a/nanovdb/nanovdb/util/Ray.h +++ b/nanovdb/nanovdb/util/Ray.h @@ -474,22 +474,16 @@ class Ray // Vec3T(_bbox[1][0]+1e-4,_bbox[1][1]+1e-4,_bbox[1][2]+1e-4)); RealT t0 = (bbox[mSign[0]][0] - mEye[0]) * mInvDir[0]; RealT t2 = (bbox[1 - mSign[1]][1] - mEye[1]) * mInvDir[1]; - if (t0 > t2) - return false; + if (t0 > t2) return false; RealT t1 = (bbox[1 - mSign[0]][0] - mEye[0]) * mInvDir[0]; RealT t3 = (bbox[mSign[1]][1] - mEye[1]) * mInvDir[1]; - if (t3 > t1) - return false; - if (t3 > t0) - t0 = t3; - if (t2 < t1) - t1 = t2; + if (t3 > t1) return false; + if (t3 > t0) t0 = t3; + if (t2 < t1) t1 = t2; t3 = (bbox[mSign[2]][2] - mEye[2]) * mInvDir[2]; - if (t3 > t1) - return false; + if (t3 > t1) return false; t2 = (bbox[1 - mSign[2]][2] - mEye[2]) * mInvDir[2]; - if (t0 > t2) - return false; + if (t0 > t2) return false; //if (t3 > t0) t0 = t3; //if (mTimeSpan.t1 < t0) return false; //if (t2 < t1) t1 = t2; diff --git a/nanovdb/nanovdb/util/Stencils.h b/nanovdb/nanovdb/util/Stencils.h index e56d683069..88e943f4ff 100644 --- a/nanovdb/nanovdb/util/Stencils.h +++ b/nanovdb/nanovdb/util/Stencils.h @@ -15,7 +15,7 @@ #ifndef NANOVDB_STENCILS_HAS_BEEN_INCLUDED #define NANOVDB_STENCILS_HAS_BEEN_INCLUDED -#include "../NanoVDB.h"// for __hosedev__, Vec3, Min, Max, Pow2, Pow3, Pow4 +#include // for __hostdev__, Vec3, Min, Max, Pow2, Pow3, Pow4 namespace nanovdb { diff --git a/nanovdb/nanovdb/util/cuda/CudaAddBlindData.cuh b/nanovdb/nanovdb/util/cuda/CudaAddBlindData.cuh new file mode 100644 index 0000000000..c750412458 --- /dev/null +++ b/nanovdb/nanovdb/util/cuda/CudaAddBlindData.cuh @@ -0,0 +1,127 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +/*! + \file CudaAddBlindData.cuh + + \author Ken Museth + + \date August 3, 2023 + + \brief Defines function that appends blind device data to and existing device NanoGrid + + \warning The header file contains cuda device code so be sure + to only include it in .cu files (or other .cuh files) +*/ + +#ifndef NVIDIA_CUDA_ADD_BLIND_DATA_CUH_HAS_BEEN_INCLUDED +#define NVIDIA_CUDA_ADD_BLIND_DATA_CUH_HAS_BEEN_INCLUDED + +#include +#include "CudaDeviceBuffer.h" +#include +#include +#include +#include + +#include // for std::strcpy + +namespace nanovdb { + +/// @brief This function appends blind data to and existing NanoGrid +/// @tparam BuildT Build type of the grid +/// @tparam BlindDataT Type of the blind data +/// @tparam BufferT Type of the buffer used for allocation +/// @param d_grid Pointer to device grid +/// @param d_blindData Pointer to device blind data +/// @param valueCount number of values in the blind data +/// @param blindClass class of the blind data +/// @param semantics semantics of the blind data +/// @param name optional name of the blind data +/// @param pool optional pool used for allocation +/// @param stream optional CUDA stream (defaults to CUDA stream 0) +/// @return GridHandle with blind data appended +template +GridHandle +cudaAddBlindData(const NanoGrid *d_grid, + const BlindDataT *d_blindData, + uint64_t valueCount, + GridBlindDataClass blindClass = GridBlindDataClass::Unknown, + GridBlindDataSemantic semantics = GridBlindDataSemantic::Unknown, + const char *name = "", + const BufferT &pool = BufferT(), + cudaStream_t stream = 0) +{ + // In: |-----------|--------- |-----------| + // old grid old meta old data + // Out: |-----------|----------|----------|-----------|------------| + // old grid old meta new meta old data new data + + static_assert(BufferTraits::hasDeviceDual, "Expected BufferT to support device allocation"); + + // extract byte sizes of the grid, blind meta data and blind data + enum {GRID=0, META=1, DATA=2, CHECKSUM=3}; + uint64_t tmp[4], *d_tmp; + cudaCheck(cudaMallocAsync((void**)&d_tmp, 4*sizeof(uint64_t), stream)); + cudaLambdaKernel<<<1, 1, 0, stream>>>(1, [=] __device__(size_t) { + if (auto count = d_grid->blindDataCount()) { + d_tmp[GRID] = PtrDiff(&d_grid->blindMetaData(0), d_grid); + d_tmp[META] = count*sizeof(GridBlindMetaData); + d_tmp[DATA] = d_grid->gridSize() - d_tmp[GRID] - d_tmp[META]; + } else { + d_tmp[GRID] = d_grid->gridSize(); + d_tmp[META] = d_tmp[DATA] = 0u; + } + d_tmp[CHECKSUM] = d_grid->checksum(); + }); cudaCheckError(); + cudaCheck(cudaMemcpyAsync(&tmp, d_tmp, 4*sizeof(uint64_t), cudaMemcpyDeviceToHost, stream)); + + GridBlindMetaData metaData{int64_t(sizeof(GridBlindMetaData) + tmp[DATA]), valueCount, + sizeof(BlindDataT), semantics, blindClass, mapToGridType()}; + if (!metaData.isValid()) throw std::runtime_error("cudaAddBlindData: invalid combination of blind meta data"); + std::strcpy(metaData.mName, name); + auto buffer = BufferT::create(tmp[GRID] + tmp[META] + sizeof(GridBlindMetaData) + tmp[DATA] + metaData.blindDataSize(), &pool, false); + auto d_data = buffer.deviceData(); + + // 1: |-----------|----------| + // old grid old meta + cudaCheck(cudaMemcpyAsync(d_data, d_grid, tmp[GRID] + tmp[META], cudaMemcpyDeviceToDevice, stream)); + + // 2: |-----------|----------|----------| + // old grid old meta new meta + cudaCheck(cudaMemcpyAsync(d_data + tmp[GRID] + tmp[META], &metaData, sizeof(GridBlindMetaData), cudaMemcpyHostToDevice, stream)); + + // 3: |-----------|----------|----------|-----------| + // old grid old meta new meta old data + cudaCheck(cudaMemcpyAsync(d_data + tmp[GRID] + tmp[META] + sizeof(GridBlindMetaData), + (const char*)d_grid + tmp[GRID] + tmp[META], tmp[DATA], cudaMemcpyDeviceToDevice, stream)); + + // 4: |-----------|----------|----------|-----------|------------| + // old grid old meta new meta old data new data + const size_t dataSize = valueCount*sizeof(BlindDataT);// no padding + cudaCheck(cudaMemcpyAsync(d_data + tmp[GRID] + tmp[META] + sizeof(GridBlindMetaData) + tmp[DATA], + d_blindData, dataSize, cudaMemcpyDeviceToDevice, stream)); + if (auto padding = metaData.blindDataSize() - dataSize) {// zero out possible padding + cudaCheck(cudaMemsetAsync(d_data + tmp[GRID] + tmp[META] + sizeof(GridBlindMetaData) + tmp[DATA] + dataSize, 0, padding, stream)); + } + + // increment grid size and blind data counter in output grid + cudaLambdaKernel<<<1, 1, 0, stream>>>(1, [=] __device__(size_t) { + auto &grid = *reinterpret_cast*>(d_data); + grid.mBlindMetadataCount += 1; + grid.mBlindMetadataOffset = d_tmp[GRID]; + auto *meta = PtrAdd(d_data, grid.mBlindMetadataOffset);// points to first blind meta data + for (uint32_t i=0, n=grid.mBlindMetadataCount-1; imDataOffset += sizeof(GridBlindMetaData); + grid.mGridSize += sizeof(GridBlindMetaData) + meta->blindDataSize();// expansion with 32 byte alignment + }); cudaCheckError(); + cudaCheck(cudaFreeAsync(d_tmp, stream)); + + GridChecksum cs(tmp[CHECKSUM]); + cudaGridChecksum(reinterpret_cast(d_data), cs.mode()); + + return GridHandle(std::move(buffer)); +}// cudaAddBlindData + +}// nanovdb namespace + +#endif // NVIDIA_CUDA_ADD_BLIND_DATA_CUH_HAS_BEEN_INCLUDED \ No newline at end of file diff --git a/nanovdb/nanovdb/util/cuda/CudaDeviceBuffer.h b/nanovdb/nanovdb/util/cuda/CudaDeviceBuffer.h new file mode 100644 index 0000000000..4b9820771d --- /dev/null +++ b/nanovdb/nanovdb/util/cuda/CudaDeviceBuffer.h @@ -0,0 +1,194 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +/*! + \file CudaDeviceBuffer.h + + \author Ken Museth + + \date January 8, 2020 + + \brief Implements a simple dual (host/device) CUDA buffer. + + \note This file has no device-only (kernel) function calls, + which explains why it's a .h and not .cuh file. +*/ + +#ifndef NANOVDB_CUDA_DEVICE_BUFFER_H_HAS_BEEN_INCLUDED +#define NANOVDB_CUDA_DEVICE_BUFFER_H_HAS_BEEN_INCLUDED + +#include "../HostBuffer.h" // for BufferTraits +#include "CudaUtils.h"// for cudaMalloc/cudaMallocManaged/cudaFree + +namespace nanovdb { + +// ----------------------------> CudaDeviceBuffer <-------------------------------------- + +/// @brief Simple memory buffer using un-managed pinned host memory when compiled with NVCC. +/// Obviously this class is making explicit used of CUDA so replace it with your own memory +/// allocator if you are not using CUDA. +/// @note While CUDA's pinned host memory allows for asynchronous memory copy between host and device +/// it is significantly slower then cached (un-pinned) memory on the host. +class CudaDeviceBuffer +{ + + uint64_t mSize; // total number of bytes managed by this buffer (assumed to be identical for host and device) + uint8_t *mCpuData, *mGpuData; // raw pointers to the host and device buffers + +public: + /// @brief Static factory method that return an instance of this buffer + /// @param size byte size of buffer to be initialized + /// @param dummy this argument is currently ignored but required to match the API of the HostBuffer + /// @param host If true buffer is initialized only on the host/CPU, else on the device/GPU + /// @param stream optional stream argument (defaults to stream NULL) + /// @return An instance of this class using move semantics + static CudaDeviceBuffer create(uint64_t size, const CudaDeviceBuffer* dummy = nullptr, bool host = true, void* stream = nullptr); + + /// @brief Constructor + /// @param size byte size of buffer to be initialized + /// @param host If true buffer is initialized only on the host/CPU, else on the device/GPU + /// @param stream optional stream argument (defaults to stream NULL) + CudaDeviceBuffer(uint64_t size = 0, bool host = true, void* stream = nullptr) + : mSize(0) + , mCpuData(nullptr) + , mGpuData(nullptr) + { + if (size > 0) this->init(size, host, stream); + } + + /// @brief Disallow copy-construction + CudaDeviceBuffer(const CudaDeviceBuffer&) = delete; + + /// @brief Move copy-constructor + CudaDeviceBuffer(CudaDeviceBuffer&& other) noexcept + : mSize(other.mSize) + , mCpuData(other.mCpuData) + , mGpuData(other.mGpuData) + { + other.mSize = 0; + other.mCpuData = nullptr; + other.mGpuData = nullptr; + } + + /// @brief Disallow copy assignment operation + CudaDeviceBuffer& operator=(const CudaDeviceBuffer&) = delete; + + /// @brief Move copy assignment operation + CudaDeviceBuffer& operator=(CudaDeviceBuffer&& other) noexcept + { + this->clear(); + mSize = other.mSize; + mCpuData = other.mCpuData; + mGpuData = other.mGpuData; + other.mSize = 0; + other.mCpuData = nullptr; + other.mGpuData = nullptr; + return *this; + } + + /// @brief Destructor frees memory on both the host and device + ~CudaDeviceBuffer() { this->clear(); }; + + /// @brief Initialize buffer + /// @param size byte size of buffer to be initialized + /// @param host If true buffer is initialized only on the host/CPU, else on the device/GPU + /// @note All existing buffers are first cleared + /// @warning size is expected to be non-zero. Use clear() clear buffer! + void init(uint64_t size, bool host = true, void* stream = nullptr); + + /// @brief Retuns a raw pointer to the host/CPU buffer managed by this allocator. + /// @warning Note that the pointer can be NULL! + uint8_t* data() const { return mCpuData; } + + /// @brief Retuns a raw pointer to the device/GPU buffer managed by this allocator. + /// @warning Note that the pointer can be NULL! + uint8_t* deviceData() const { return mGpuData; } + + /// @brief Upload this buffer from the host to the device, i.e. CPU -> GPU. + /// @param stream optional CUDA stream (defaults to CUDA stream 0) + /// @param sync if false the memory copy is asynchronous + /// @note If the device/GPU buffer does not exist it is first allocated + /// @warning Assumes that the host/CPU buffer already exists + void deviceUpload(void* stream = nullptr, bool sync = true) const; + + /// @brief Upload this buffer from the device to the host, i.e. GPU -> CPU. + /// @param stream optional CUDA stream (defaults to CUDA stream 0) + /// @param sync if false the memory copy is asynchronous + /// @note If the host/CPU buffer does not exist it is first allocated + /// @warning Assumes that the device/GPU buffer already exists + void deviceDownload(void* stream = nullptr, bool sync = true) const; + + /// @brief Returns the size in bytes of the raw memory buffer managed by this allocator. + uint64_t size() const { return mSize; } + + //@{ + /// @brief Returns true if this allocator is empty, i.e. has no allocated memory + bool empty() const { return mSize == 0; } + bool isEmpty() const { return mSize == 0; } + //@} + + /// @brief De-allocate all memory managed by this allocator and set all pointers to NULL + void clear(void* stream = nullptr); + +}; // CudaDeviceBuffer class + +template<> +struct BufferTraits +{ + static constexpr bool hasDeviceDual = true; +}; + +// --------------------------> Implementations below <------------------------------------ + +inline CudaDeviceBuffer CudaDeviceBuffer::create(uint64_t size, const CudaDeviceBuffer*, bool host, void* stream) +{ + return CudaDeviceBuffer(size, host, stream); +} + +inline void CudaDeviceBuffer::init(uint64_t size, bool host, void* stream) +{ + if (mSize>0) this->clear(stream); + NANOVDB_ASSERT(size > 0); + if (host) { + cudaCheck(cudaMallocHost((void**)&mCpuData, size)); // un-managed pinned memory on the host (can be slow to access!). Always 32B aligned + checkPtr(mCpuData, "CudaDeviceBuffer::init: failed to allocate host buffer"); + } else { + cudaCheck(cudaMallocAsync((void**)&mGpuData, size, reinterpret_cast(stream))); // un-managed memory on the device, always 32B aligned! + checkPtr(mGpuData, "CudaDeviceBuffer::init: failed to allocate device buffer"); + } + mSize = size; +} // CudaDeviceBuffer::init + +inline void CudaDeviceBuffer::deviceUpload(void* stream, bool sync) const +{ + checkPtr(mCpuData, "uninitialized cpu data"); + if (mGpuData == nullptr) { + cudaCheck(cudaMallocAsync((void**)&mGpuData, mSize, reinterpret_cast(stream))); // un-managed memory on the device, always 32B aligned! + } + checkPtr(mGpuData, "uninitialized gpu data"); + cudaCheck(cudaMemcpyAsync(mGpuData, mCpuData, mSize, cudaMemcpyHostToDevice, reinterpret_cast(stream))); + if (sync) cudaCheck(cudaStreamSynchronize(reinterpret_cast(stream))); +} // CudaDeviceBuffer::gpuUpload + +inline void CudaDeviceBuffer::deviceDownload(void* stream, bool sync) const +{ + checkPtr(mGpuData, "uninitialized gpu data"); + if (mCpuData == nullptr) { + cudaCheck(cudaMallocHost((void**)&mCpuData, mSize)); // un-managed pinned memory on the host (can be slow to access!). Always 32B aligned + } + checkPtr(mCpuData, "uninitialized cpu data"); + cudaCheck(cudaMemcpyAsync(mCpuData, mGpuData, mSize, cudaMemcpyDeviceToHost, reinterpret_cast(stream))); + if (sync) cudaCheck(cudaStreamSynchronize(reinterpret_cast(stream))); +} // CudaDeviceBuffer::gpuDownload + +inline void CudaDeviceBuffer::clear(void *stream) +{ + if (mGpuData) cudaCheck(cudaFreeAsync(mGpuData, reinterpret_cast(stream))); + if (mCpuData) cudaCheck(cudaFreeHost(mCpuData)); + mCpuData = mGpuData = nullptr; + mSize = 0; +} // CudaDeviceBuffer::clear + +} // namespace nanovdb + +#endif // end of NANOVDB_CUDA_DEVICE_BUFFER_H_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/cuda/CudaGridChecksum.cuh b/nanovdb/nanovdb/util/cuda/CudaGridChecksum.cuh new file mode 100644 index 0000000000..e3ae9a941f --- /dev/null +++ b/nanovdb/nanovdb/util/cuda/CudaGridChecksum.cuh @@ -0,0 +1,244 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +/*! + \file CudaGridChecksum.cuh + + \author Ken Museth + + \date September 28, 2023 + + \brief Compute CRC32 checksum of NanoVDB grids + +*/ + +#ifndef NANOVDB_CUDA_GRID_CHECKSUM_CUH_HAS_BEEN_INCLUDED +#define NANOVDB_CUDA_GRID_CHECKSUM_CUH_HAS_BEEN_INCLUDED + +#include "CudaDeviceBuffer.h"// required for instantiation of move c-tor of GridHandle +#include "CudaNodeManager.cuh" +#include "../GridChecksum.h"// for +#include "../GridHandle.h" + +namespace nanovdb { + +namespace crc32 { + +/// @bried Cuda kernel to initiate lookup table for CRC32 computation +/// @tparam T Dummy template parameter used to avoid multiple instantiations. T should be uint32_t! +/// @param d_lut Device pointer to lookup table of size 256 +template +__global__ void initLutKernel(T *d_lut) +{ + static_assert(is_same::value,"Expected uint32_t"); + const uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; + if (tid < 256u) crc32::initLut(d_lut, tid); +} + +/// @brief Cuda kernel that computes CRC32 checksums of blocks of data using a look-up-table +/// @param d_data device pointer to raw data from wich to compute the CRC32 checksums +/// @param d_blockCRC device pointer to array of @c blockCount checksums for each block +/// @param blockCount number of blocks and checksums +/// @param blockSize size of each block in bytes +/// @param d_lut device pointer to CRC32 Lookup Table +template +__global__ void checksumKernel(const T *d_data, uint32_t* d_blockCRC, uint32_t blockCount, uint32_t blockSize, const uint32_t *d_lut) +{ + const uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; + if (tid < blockCount) d_blockCRC[tid] = crc32::checksum((const uint8_t*)d_data + tid * blockSize, blockSize, d_lut); +} + +/// @brief Cuda kernel that computes CRC32 checksums of blocks of data (without using a look-up-table) +/// @param d_data device pointer to raw data from wich to compute the CRC32 checksums +/// @param d_blockCRC device pointer to array of @c blockCount checksums for each block +/// @param blockCount number of blocks and checksums +/// @param blockSize size of each block in bytes +template +__global__ void checksumKernel(const T *d_data, uint32_t* d_blockCRC, uint32_t blockCount, uint32_t blockSize) +{ + const uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; + if (tid < blockCount) d_blockCRC[tid] = crc32::checksum((const uint8_t*)d_data + tid * blockSize, blockSize); +} + +/// @brief Host function to allocate and initiate a Look-Up-Table of size 256 for subsequent CRC32 computation on the device +/// @param stream optional cuda stream (defaults to zero) +/// @return returns a device point to a lookup-table for CRC32 computation +/// @warning It is the responsibility of the caller to delete the returned array +inline uint32_t* cudaCreateLut(cudaStream_t stream = 0) +{ + uint32_t *d_lut; + cudaCheck(cudaMallocAsync((void**)&d_lut, 256*sizeof(uint32_t), stream)); + initLutKernel<<<1, 256, 0, stream>>>(d_lut); + cudaCheckError(); + return d_lut; +} + +}// namespace crc + +#ifdef NANOVDB_CRC32_LOG2_BLOCK_SIZE// new approach computes CRC32 checksums for each 4 KB block + +/// @brief Update the checksum of a device grid +/// @param d_gridData device pointer to GridData +/// @param mode Mode of computation for the checksum. +/// @param stream optional cuda stream (defaults to zero) +/// @return The actual mode used for checksum computation. Eg. if @c d_gridData is NULL (or @c mode = ChecksumMode::Disable) +/// then ChecksumMode::Disable is always returned. Elseif the grid has no nodes or blind data ChecksumMode::Partial +/// is always returnd (even if @c mode = ChecksumMode::Full). +inline ChecksumMode cudaGridChecksum(GridData *d_gridData, ChecksumMode mode = ChecksumMode::Partial, cudaStream_t stream = 0) +{ + if (d_gridData == nullptr || mode == ChecksumMode::Disable) return ChecksumMode::Disable; + + static constexpr unsigned int mNumThreads = 128;// seems faster than the old value of 256! + auto numBlocks = [&](unsigned int n)->unsigned int{return (n + mNumThreads - 1) / mNumThreads;}; + uint8_t *d_begin = reinterpret_cast(d_gridData); + uint32_t *d_lut = crc32::cudaCreateLut(stream);// allocate and generate device LUT for CRC32 + uint64_t size[2], *d_size;// {total size of grid, partial size for first checksum} + cudaCheck(cudaMallocAsync((void**)&d_size, 2*sizeof(uint64_t), stream)); + + // Compute CRC32 checksum of GridData, TreeData, RootData (+tiles), but exclude GridData::mMagic and GridData::mChecksum + cudaLambdaKernel<<<1, 1, 0, stream>>>(1, [=] __device__(size_t) { + d_size[0] = d_gridData->mGridSize; + uint8_t *d_mid = d_gridData->template nodePtr<2>(); + if (d_mid == nullptr) {// no upper nodes + if (d_gridData->mBlindMetadataCount) { + d_mid = d_begin + d_gridData->mBlindMetadataOffset;// exclude blind data from partial checksum + } else { + d_mid = d_begin + d_gridData->mGridSize;// no nodes or blind data, so partial checksum is computed on the entire grid buffer + } + } + d_size[1] = d_mid - d_begin; + uint32_t *p = reinterpret_cast(&(d_gridData->mChecksum)); + p[0] = crc32::checksum(d_begin + 16u, d_mid, d_lut);// exclude GridData::mMagic and GridData::mChecksum + }); + cudaCheckError(); + cudaCheck(cudaMemcpyAsync(size, d_size, 2*sizeof(uint64_t), cudaMemcpyDeviceToHost, stream)); + cudaCheck(cudaFreeAsync(d_size, stream)); + + if (mode != ChecksumMode::Full || size[0] == size[1]) return ChecksumMode::Partial; + + // Compute CRC32 checksum of 4K block of everything remaining in the buffer, i.e. nodes and blind data + const uint8_t *d_mid = d_begin + size[1], *d_end = d_begin + size[0]; + uint32_t *d_checksums;// 4096 byte chunks + const uint64_t checksumCount = (d_end - d_mid) >> NANOVDB_CRC32_LOG2_BLOCK_SIZE;// 4 KB (4096 byte) + cudaCheck(cudaMallocAsync((void**)&d_checksums, checksumCount*sizeof(uint32_t), stream)); + cudaLambdaKernel<<>>(checksumCount, [=] __device__(size_t tid) { + uint32_t size = 1<>>(1, [=] __device__(size_t) { + uint32_t *p = reinterpret_cast(&(d_gridData->mChecksum)); + p[1] = crc32::checksum((const uint8_t*)d_checksums, checksumCount*sizeof(uint32_t), d_lut); + }); + cudaCheckError(); + cudaCheck(cudaFreeAsync(d_checksums, stream)); + cudaCheck(cudaFreeAsync(d_lut, stream)); + + return ChecksumMode::Full; +}// cudaGridChecksum + +template +inline ChecksumMode cudaGridChecksum(NanoGrid *d_grid, ChecksumMode mode = ChecksumMode::Partial, cudaStream_t stream = 0) +{ + return cudaGridChecksum(reinterpret_cast(d_grid), mode, stream); +} + +inline GridChecksum cudaGetGridChecksum(GridData *d_gridData, cudaStream_t stream = 0) +{ + uint64_t checksum, *d_checksum; + cudaCheck(cudaMallocAsync((void**)&d_checksum, sizeof(uint64_t), stream)); + cudaLambdaKernel<<<1, 1, 0, stream>>>(1, [=] __device__(size_t) {*d_checksum = d_gridData->mChecksum;}); + cudaCheckError(); + cudaCheck(cudaMemcpyAsync(&checksum, d_checksum, sizeof(uint64_t), cudaMemcpyDeviceToHost, stream)); + cudaCheck(cudaFreeAsync(d_checksum, stream)); + return GridChecksum(checksum);; +} + +inline ChecksumMode cudaUpdateGridChecksum(GridData *d_gridData, cudaStream_t stream = 0) +{ + return cudaGridChecksum(d_gridData, cudaGetGridChecksum(d_gridData, stream).mode(), stream); +} + +#else + +template +void cudaGridChecksum(NanoGrid *d_grid, ChecksumMode mode = ChecksumMode::Partial, cudaStream_t stream = 0) +{ + if (d_grid == nullptr || mode == ChecksumMode::Disable) return; + + static constexpr unsigned int mNumThreads = 128;// seems faster than the old value of 256! + auto numBlocks = [&](unsigned int n)->unsigned int{return (n + mNumThreads - 1) / mNumThreads;}; + + uint32_t *d_lut = crc32::cudaCreateLut(stream);// allocate and generate device LUT for CRC32 + uint64_t size[2], *d_size; + cudaCheck(cudaMallocAsync((void**)&d_size, 2*sizeof(uint64_t), stream)); + cudaLambdaKernel<<<1, 1, 0, stream>>>(1, [=] __device__(size_t) { + d_size[0] = d_grid->gridSize(); + d_size[1] = d_grid->memUsage() + d_grid->tree().memUsage() + d_grid->tree().root().memUsage(); + const uint8_t *begin = reinterpret_cast(d_grid); + uint32_t *p = reinterpret_cast(&(d_grid->mChecksum)); + p[0] = crc32::checksum(begin + 16u, begin + d_size[1], d_lut);// exclude mMagic and mChecksum + }); + cudaCheckError(); + cudaCheck(cudaMemcpyAsync(size, d_size, 2*sizeof(uint64_t), cudaMemcpyDeviceToHost, stream)); + cudaCheckError(); + + if (mode != ChecksumMode::Full) return; + + // Get node counts + uint32_t nodeCount[3], *d_nodeCount, *d_checksums, *d_ptr; + cudaCheck(cudaMallocAsync((void**)&d_nodeCount, 3*sizeof(uint32_t), stream)); + cudaLambdaKernel<<<1, 1, 0, stream>>>(1, [=] __device__(size_t) { + auto &tree = d_grid->tree(); + for (int i = 0; i < 3; ++i) d_nodeCount[i] = tree.nodeCount(i); + }); + cudaCheckError(); + cudaCheck(cudaMemcpyAsync(nodeCount, d_nodeCount, 3*sizeof(uint32_t), cudaMemcpyDeviceToHost, stream)); + cudaCheck(cudaFreeAsync(d_nodeCount, stream)); + cudaCheck(cudaMallocAsync((void**)&d_checksums, (nodeCount[0]+nodeCount[1]+nodeCount[2])*sizeof(uint32_t), stream)); + + auto nodeMgrHandle = cudaCreateNodeManager(d_grid, CudaDeviceBuffer(), stream); + auto *d_nodeMgr = nodeMgrHandle.template deviceMgr(); + NANOVDB_ASSERT(isValid(d_nodeMgr)); + d_ptr = d_checksums; + + // very slow due to large nodes + cudaLambdaKernel<<>>(nodeCount[2], [=] __device__(size_t tid) { + auto &node = d_nodeMgr->upper(uint32_t(tid)); + d_ptr[tid] = crc32::checksum((const uint8_t*)&node, node.memUsage(), d_lut); + }); + cudaCheckError(); + + d_ptr += nodeCount[2]; + cudaLambdaKernel<<>>(nodeCount[1], [=] __device__(size_t tid) { + auto &node = d_nodeMgr->lower(uint32_t(tid)); + d_ptr[tid] = crc32::checksum((const uint8_t*)&node, node.memUsage(), d_lut); + }); + cudaCheckError(); + + d_ptr += nodeCount[1]; + cudaLambdaKernel<<>>(nodeCount[0], [=] __device__(size_t tid) { + auto &node = d_nodeMgr->leaf(uint32_t(tid)); + d_ptr[tid] = crc32::checksum((const uint8_t*)&node, node.memUsage(), d_lut); + }); + cudaCheckError(); + + // to-do: process blind data + cudaLambdaKernel<<<1, 1, 0, stream>>>(1, [=] __device__(size_t) { + uint32_t *p = reinterpret_cast(&(d_grid->mChecksum)); + const uint8_t *begin = reinterpret_cast(d_checksums); + p[1] = crc32::checksum(begin, d_nodeMgr->tree().totalNodeCount()*sizeof(uint32_t), d_lut); + }); + cudaCheckError(); + + cudaCheck(cudaFreeAsync(d_size, stream)); + cudaCheck(cudaFreeAsync(d_checksums, stream)); + cudaCheck(cudaFreeAsync(d_lut, stream)); +}// cudaGridChecksum + +#endif + +}// namespace nanovdb + +#endif // NANOVDB_CUDA_GRID_CHECKSUM_CUH_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/cuda/CudaGridHandle.cuh b/nanovdb/nanovdb/util/cuda/CudaGridHandle.cuh new file mode 100644 index 0000000000..5446c56231 --- /dev/null +++ b/nanovdb/nanovdb/util/cuda/CudaGridHandle.cuh @@ -0,0 +1,134 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +/*! + \file CudaGridHandle.cuh + + \author Ken Museth, Doyub Kim + + \date August 3, 2023 + + \brief Contains cuda kernels for GridHandle + + \warning The header file contains cuda device code so be sure + to only include it in .cu files (or other .cuh files) +*/ + +#ifndef NANOVDB_CUDA_GRID_HANDLE_CUH_HAS_BEEN_INCLUDED +#define NANOVDB_CUDA_GRID_HANDLE_CUH_HAS_BEEN_INCLUDED + +#include "CudaDeviceBuffer.h"// required for instantiation of move c-tor of GridHandle +#include "CudaGridChecksum.cuh"// for cudaUpdateChecksum +#include "../GridHandle.h" + +namespace nanovdb { + +namespace {// anonymous namespace +__global__ void cudaCpyMetaData(const GridData *data, GridHandleMetaData *meta){cpyMetaData(data, meta);} +__global__ void cudaUpdateGridCount(GridData *data, uint32_t gridIndex, uint32_t gridCount, bool *d_dirty){ + NANOVDB_ASSERT(gridIndex < gridCount); + if (*d_dirty = data->mGridIndex != gridIndex || data->mGridCount != gridCount) { + data->mGridIndex = gridIndex; + data->mGridCount = gridCount; + if (data->mChecksum == GridChecksum::EMPTY) *d_dirty = false;// no need to update checksum if it didn't already exist + } +} +}// anonymous namespace + +template +template::hasDeviceDual, int>::type> +GridHandle::GridHandle(T&& buffer) +{ + static_assert(is_same::value, "Expected U==BufferT"); + mBuffer = std::move(buffer); + if (auto *data = reinterpret_cast(mBuffer.data())) { + if (!data->isValid()) throw std::runtime_error("GridHandle was constructed with an invalid host buffer"); + mMetaData.resize(data->mGridCount); + cpyMetaData(data, mMetaData.data()); + } else { + if (auto *d_data = reinterpret_cast(mBuffer.deviceData())) { + GridData tmp; + cudaCheck(cudaMemcpy(&tmp, d_data, sizeof(GridData), cudaMemcpyDeviceToHost)); + if (!tmp.isValid()) throw std::runtime_error("GridHandle was constructed with an invalid device buffer"); + GridHandleMetaData *d_metaData; + cudaMalloc((void**)&d_metaData, tmp.mGridCount*sizeof(GridHandleMetaData)); + cudaCpyMetaData<<<1,1>>>(d_data, d_metaData); + mMetaData.resize(tmp.mGridCount); + cudaCheck(cudaMemcpy(mMetaData.data(), d_metaData,tmp.mGridCount*sizeof(GridHandleMetaData), cudaMemcpyDeviceToHost)); + cudaCheck(cudaFree(d_metaData)); + } + } +}// GridHandle(T&& buffer) + +// Dummy function that ensures instantiation of the move-constructor above when BufferT=CudaDeviceBuffer +namespace {auto __dummy(){return GridHandle(std::move(CudaDeviceBuffer()));}} + +template class VectorT = std::vector> +inline typename enable_if::hasDeviceDual, VectorT>>::type +cudaSplitGridHandles(const GridHandle &handle, const BufferT* other = nullptr, cudaStream_t stream = 0) +{ + const uint8_t *ptr = handle.deviceData(); + if (ptr == nullptr) return VectorT>(); + VectorT> handles(handle.gridCount()); + bool dirty, *d_dirty;// use this to check if the checksum needs to be recomputed + cudaCheck(cudaMallocAsync((void**)&d_dirty, sizeof(bool), stream)); + for (uint32_t n=0; n(buffer.deviceData()); + const GridData *src = reinterpret_cast(ptr); + cudaCheck(cudaMemcpyAsync(dst, src, handle.gridSize(n), cudaMemcpyDeviceToDevice, stream)); + cudaUpdateGridCount<<<1, 1, 0, stream>>>(dst, 0u, 1u, d_dirty); + cudaCheckError(); + cudaCheck(cudaMemcpyAsync(&dirty, d_dirty, sizeof(bool), cudaMemcpyDeviceToHost, stream)); + if (dirty) cudaGridChecksum(dst, ChecksumMode::Partial); + handles[n] = GridHandle(std::move(buffer)); + ptr += handle.gridSize(n); + } + cudaCheck(cudaFreeAsync(d_dirty, stream)); + return std::move(handles); +}// cudaSplitGridHandles + +template class VectorT = std::vector> +inline typename enable_if::hasDeviceDual, VectorT>>::type +splitDeviceGrids(const GridHandle &handle, const BufferT* other = nullptr, cudaStream_t stream = 0) +{ return cudaSplitGridHandles(handle, other, stream); } + +template class VectorT> +inline typename enable_if::hasDeviceDual, GridHandle>::type +cudaMergeGridHandles(const VectorT> &handles, const BufferT* other = nullptr, cudaStream_t stream = 0) +{ + uint64_t size = 0u; + uint32_t counter = 0u, gridCount = 0u; + for (auto &h : handles) { + gridCount += h.gridCount(); + for (uint32_t n=0; n(dst); + cudaUpdateGridCount<<<1, 1, 0, stream>>>(data, counter++, gridCount, d_dirty); + cudaCheckError(); + cudaCheck(cudaMemcpyAsync(&dirty, d_dirty, sizeof(bool), cudaMemcpyDeviceToHost, stream)); + if (dirty) cudaGridChecksum(data, ChecksumMode::Partial); + dst += h.gridSize(n); + src += h.gridSize(n); + } + } + cudaCheck(cudaFreeAsync(d_dirty, stream)); + return GridHandle(std::move(buffer)); +}// cudaMergeGridHandles + +template class VectorT> +inline typename enable_if::hasDeviceDual, GridHandle>::type +mergeDeviceGrids(const VectorT> &handles, const BufferT* other = nullptr, cudaStream_t stream = 0) +{ return cudaMergeGridHandles(handles, other, stream); } + +} // namespace nanovdb + +#endif // NANOVDB_CUDA_GRID_HANDLE_CUH_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/cuda/CudaGridStats.cuh b/nanovdb/nanovdb/util/cuda/CudaGridStats.cuh new file mode 100644 index 0000000000..dcf5bfc850 --- /dev/null +++ b/nanovdb/nanovdb/util/cuda/CudaGridStats.cuh @@ -0,0 +1,250 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +/*! + \file CudaGridStats.cuh + + \author Ken Museth + + \date October 9, 2023 + + \brief Re-computes min/max/avg/var/bbox information for each node in a + pre-existing NanoVDB grid on the device. +*/ + +#ifndef NANOVDB_CUDAGRIDSTATS_CUH_HAS_BEEN_INCLUDED +#define NANOVDB_CUDAGRIDSTATS_CUH_HAS_BEEN_INCLUDED + +#include +#include + +namespace nanovdb { + +/// @brief Re-computes the min/max, stats and bbox information for an existing NanoVDB Grid +/// +/// @param grid Grid whose stats to update +/// @param mode Mode of computation for the statistics. +/// @param stream Optional cuda stream (defaults to zero) +template +void cudaGridStats(NanoGrid *d_grid, StatsMode mode = StatsMode::Default, cudaStream_t stream = 0); + +//================================================================================================ + +/// @brief Allows for the construction of NanoVDB grids without any dependecy +template::ValueType>> +class CudaGridStats +{ + using GridT = NanoGrid; + using TreeT = typename GridT::TreeType; + using ValueT = typename TreeT::ValueType; + using Node0 = typename TreeT::Node0; // leaf + using Node1 = typename TreeT::Node1; // lower + using Node2 = typename TreeT::Node2; // upper + using RootT = typename TreeT::Node3; // root + static_assert(is_same::value, "Mismatching type"); + + ValueT mDelta; // skip rendering of node if: node.max < -mDelta || node.min > mDelta + +public: + CudaGridStats(ValueT delta = ValueT(0)) : mDelta(delta) {} + + void operator()(GridT *d_grid, cudaStream_t stream = 0); + +}; // CudaGridStats + +//================================================================================================ + +namespace {// define cuda kernels in an unnamed namespace + +template +__global__ void processLeaf(NodeManager *d_nodeMgr, StatsT *d_stats) +{ + const uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; + if (tid >= d_nodeMgr->leafCount()) return; + auto &d_leaf = d_nodeMgr->leaf(tid); + + if (d_leaf.updateBBox()) {// updates active bounding box (also updates data->mFlags) and return true if non-empty + if constexpr(StatsT::hasStats()) { + StatsT stats; + for (auto it = d_leaf.cbeginValueOn(); it; ++it) stats.add(*it); + if constexpr(StatsT::hasAverage()) { + d_stats[tid] = stats; + *reinterpret_cast(&d_leaf.mMinimum) = tid; + } else { + stats.setStats(d_leaf); + } + } + } + d_leaf.mFlags &= ~uint8_t(1u);// enable rendering +}// processLeaf + +template +__global__ void processInternal(NodeManager *d_nodeMgr, StatsT *d_stats) +{ + using ChildT = typename NanoNode::type; + const uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; + if (tid >= d_nodeMgr->nodeCount(LEVEL)) return; + auto &d_node = d_nodeMgr->template node(tid); + auto &bbox = d_node.mBBox; + bbox = CoordBBox();// empty bbox + StatsT stats; + uint32_t childID = 0u; + + for (auto it = d_node.beginChild(); it; ++it) { + auto &child = *it; + bbox.expand( child.bbox() ); + if constexpr(StatsT::hasAverage()) { + childID = *reinterpret_cast(&child.mMinimum); + StatsT &s = d_stats[childID]; + s.setStats(child); + stats.add(s); + } else if constexpr(StatsT::hasMinMax()) { + stats.add(child.minimum()); + stats.add(child.maximum()); + } + } + for (auto it = d_node.cbeginValueOn(); it; ++it) { + const Coord ijk = it.getCoord(); + bbox[0].minComponent(ijk); + bbox[1].maxComponent(ijk + Coord(ChildT::DIM - 1)); + if constexpr(StatsT::hasStats()) stats.add(*it, ChildT::NUM_VALUES); + } + if constexpr(StatsT::hasAverage()) { + d_stats[childID] = stats; + *reinterpret_cast(&d_node.mMinimum) = childID; + } else if constexpr(StatsT::hasMinMax()) { + stats.setStats(d_node); + } + d_node.mFlags &= ~uint64_t(1u);// enable rendering +}// processInternal + +template +__global__ void processRootAndGrid(NodeManager *d_nodeMgr, StatsT *d_stats) +{ + using ChildT = NanoUpper; + using ValueT = typename ChildT::ValueType; + + // process root + auto &root = d_nodeMgr->root(); + root.mBBox = CoordBBox(); + if (root.isEmpty()) { + root.mMinimum = root.mMaximum = root.mBackground; + root.mAverage = root.mStdDevi = 0; + } else { + ValueT v; + StatsT s; + for (auto it = root.beginDense(); it; ++it) { + if (auto *child = it.probeChild(v)) { + root.mBBox.expand( child->bbox() ); + if constexpr(StatsT::hasAverage()) { + StatsT &stats = d_stats[*reinterpret_cast(&child->mMinimum)]; + stats.setStats(*child); + s.add(stats); + } else if constexpr(StatsT::hasMinMax()){ + s.add(child->minimum()); + s.add(child->maximum()); + } + } else if (it.isValueOn()) { + const Coord ijk = it.getCoord(); + root.mBBox[0].minComponent(ijk); + root.mBBox[1].maxComponent(ijk + Coord(ChildT::DIM - 1)); + if constexpr(StatsT::hasStats()) s.add(v, ChildT::NUM_VALUES); + } + } + s.setStats(root); + } + + // process Grid + auto& grid = d_nodeMgr->grid(); + const auto& indexBBox = root.bbox(); + if (indexBBox.empty()) { + grid.mWorldBBox = BBox(); + grid.setBBoxOn(false); + } else { + // Note that below max is offset by one since CoordBBox.max is inclusive + // while bbox.max is exclusive. However, min is inclusive in both + // CoordBBox and BBox. This also guarantees that a grid with a single + // active voxel, does not have an empty world bbox! E.g. if a grid with a + // unit index-to-world transformation only contains the active voxel (0,0,0) + // then indeBBox = (0,0,0) -> (0,0,0) and then worldBBox = (0.0, 0.0, 0.0) + // -> (1.0, 1.0, 1.0). This is a consequence of the different definitions + // of index and world bounding boxes inherited from OpenVDB! + const Coord min = indexBBox[0]; + const Coord max = indexBBox[1] + Coord(1); + + auto& wBBox = grid.mWorldBBox; + const auto& map = grid.map(); + wBBox[0] = wBBox[1] = map.applyMap(Vec3d(min[0], min[1], min[2])); + wBBox.expand(map.applyMap(Vec3d(min[0], min[1], max[2]))); + wBBox.expand(map.applyMap(Vec3d(min[0], max[1], min[2]))); + wBBox.expand(map.applyMap(Vec3d(max[0], min[1], min[2]))); + wBBox.expand(map.applyMap(Vec3d(max[0], max[1], min[2]))); + wBBox.expand(map.applyMap(Vec3d(max[0], min[1], max[2]))); + wBBox.expand(map.applyMap(Vec3d(min[0], max[1], max[2]))); + wBBox.expand(map.applyMap(Vec3d(max[0], max[1], max[2]))); + grid.setBBoxOn(true); + } + + // set bit flags + grid.setMinMaxOn(StatsT::hasMinMax()); + grid.setAverageOn(StatsT::hasAverage()); + grid.setStdDeviationOn(StatsT::hasStdDeviation()); +}// processRootAndGrid + +}// cuda kernels are defined in an unnamed namespace + +//================================================================================================ + +template +void CudaGridStats::operator()(NanoGrid *d_grid, cudaStream_t stream) +{ + static const uint32_t threadsPerBlock = 128; + auto blocksPerGrid = [&](uint32_t count)->uint32_t{return (count + (threadsPerBlock - 1)) / threadsPerBlock;}; + + auto nodeMgrHandle = cudaCreateNodeManager(d_grid, CudaDeviceBuffer(), stream); + auto *d_nodeMgr = nodeMgrHandle.template deviceMgr(); + + uint32_t nodeCount[3];// {leaf, lower, upper} + cudaCheck(cudaMemcpyAsync(nodeCount, (char*)d_grid + sizeof(GridData) + 4*sizeof(uint64_t), 3*sizeof(uint32_t), cudaMemcpyDeviceToHost, stream)); + cudaStreamSynchronize(stream);// finish all device tasks in stream + + StatsT *d_stats = nullptr; + + if constexpr(StatsT::hasAverage()) cudaCheck(cudaMallocAsync((void**)&d_stats, nodeCount[0]*sizeof(StatsT), stream)); + + processLeaf<<>>(d_nodeMgr, d_stats); + + processInternal<<>>(d_nodeMgr, d_stats); + + processInternal<<>>(d_nodeMgr, d_stats); + + processRootAndGrid<<<1, 1, 0, stream>>>(d_nodeMgr, d_stats); + + if constexpr(StatsT::hasAverage()) cudaCheck(cudaFreeAsync(d_stats, stream)); + +} // CudaGridStats::operator()( Grid ) + +//================================================================================================ + +template +void cudaGridStats(NanoGrid *d_grid, StatsMode mode, cudaStream_t stream) +{ + if (d_grid == nullptr && mode == StatsMode::Disable) { + return; + } else if (mode == StatsMode::BBox || is_same::value) { + CudaGridStats > stats; + stats(d_grid, stream); + } else if (mode == StatsMode::MinMax) { + CudaGridStats > stats; + stats(d_grid, stream); + } else if (mode == StatsMode::All) { + CudaGridStats > stats; + stats(d_grid, stream); + } else { + throw std::runtime_error("cudaGridStats: Unsupported statistics mode."); + } +}// cudaGridStats + +} // namespace nanovdb + +#endif // NANOVDB_CUDAGRIDSTATS_CUH_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/cuda/CudaIndexToGrid.cuh b/nanovdb/nanovdb/util/cuda/CudaIndexToGrid.cuh new file mode 100644 index 0000000000..8394ecefe1 --- /dev/null +++ b/nanovdb/nanovdb/util/cuda/CudaIndexToGrid.cuh @@ -0,0 +1,386 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +/*! + \file CudaIndexToGrid.cuh + + \author Ken Museth + + \date April 17, 2023 + + \brief Combines an IndexGrid and values into a regular Grid on the device + + \warning The header file contains cuda device code so be sure + to only include it in .cu files (or other .cuh files) +*/ + +#ifndef NVIDIA_CUDA_INDEX_TO_GRID_CUH_HAS_BEEN_INCLUDED +#define NVIDIA_CUDA_INDEX_TO_GRID_CUH_HAS_BEEN_INCLUDED + +#include +#include "CudaDeviceBuffer.h" +#include +#include +#include + +namespace nanovdb { + +/// @brief Freestanding function that combines an IndexGrid and values into a regular Grid +/// @tparam DstBuildT Build time of the destination/output Grid +/// @tparam SrcBuildT Build type of the source/input IndexGrid +/// @tparam BufferT Type of the buffer used for allocation of the destination Grid +/// @param d_srcGrid Device pointer to source/input IndexGrid, i.e. SrcBuildT={ValueIndex,ValueOnIndex,ValueIndexMask,ValueOnIndexMask} +/// @param d_srcValues Device pointer to an array of values +/// @param pool Memory pool used to create a buffer for the destination/output Grid +/// @param stream optional CUDA stream (defaults to CUDA stream 0 +/// @note If d_srcGrid has stats (min,max,avg,std-div), the d_srcValues is also assumed +/// to have the same information, all of which are then copied to the destination/output grid. +/// An exception to this rule is if the type of d_srcValues is different from the stats type +/// NanoRoot::FloatType, e.g. if DstBuildT=Vec3f then NanoRoot::FloatType=float, +/// in which case average and standard-deviation is undefined in the output grid. +/// @return +template +typename enable_if::is_index, GridHandle>::type +cudaIndexToGrid(const NanoGrid *d_srcGrid, const typename BuildToValueMap::type *d_srcValues, const BufferT &pool = BufferT(), cudaStream_t stream = 0); + + +template +typename enable_if::is_index, GridHandle>::type +cudaCreateNanoGrid(const NanoGrid *d_srcGrid, const typename BuildToValueMap::type *d_srcValues, const BufferT &pool = BufferT(), cudaStream_t stream = 0) +{ + return cudaIndexToGrid(d_srcGrid, d_srcValues, pool, stream); +} + +namespace {// anonymous namespace + +template +class CudaIndexToGrid +{ + using SrcGridT = NanoGrid; +public: + struct NodeAccessor; + + /// @brief Constructor from a source IndeGrid + /// @param srcGrid Device pointer to IndexGrid used as the source + CudaIndexToGrid(const SrcGridT *d_srcGrid, cudaStream_t stream = 0); + + ~CudaIndexToGrid() {cudaCheck(cudaFreeAsync(mDevNodeAcc, mStream));} + + /// @brief Toggle on and off verbose mode + /// @param on if true verbose is turned on + void setVerbose(bool on = true) {mVerbose = on; } + + /// @brief Set the name of the destination/output grid + /// @param name Name used for the destination grid + void setGridName(const std::string &name) {mGridName = name;} + + /// @brief Combines the IndexGrid with values to produce a regular Grid + /// @tparam DstBuildT Template parameter of the destination grid and value type + /// @tparam BufferT Template parameter of the memory allocator + /// @param srcValues pointer to values that will be inserted into the output grid + /// @param buffer optional buffer used for memory allocation + /// @return A new GridHandle with the grid of type @c DstBuildT + template + GridHandle getHandle(const typename BuildToValueMap::type *srcValues, const BufferT &buffer = BufferT()); + +private: + cudaStream_t mStream{0}; + GpuTimer mTimer; + std::string mGridName; + bool mVerbose{false}; + NodeAccessor mNodeAcc, *mDevNodeAcc; + + template + BufferT getBuffer(const BufferT &pool); +};// CudaIndexToGrid + +//================================================================================================ + +template +struct CudaIndexToGrid::NodeAccessor +{ + uint64_t grid, tree, root, node[3], meta, blind, size;// byte offsets, node: 0=leaf,1=lower, 2=upper + const SrcGridT *d_srcGrid;// device point to source IndexGrid + void *d_dstPtr;// device pointer to buffer with destination Grid + char *d_gridName; + uint32_t nodeCount[4];// 0=leaf, 1=lower, 2=upper, 3=root tiles + + __device__ const NanoGrid& srcGrid() const {return *d_srcGrid;} + __device__ const NanoTree& srcTree() const {return d_srcGrid->tree();} + __device__ const NanoRoot& srcRoot() const {return d_srcGrid->tree().root();} + template + __device__ const typename NanoNode::type& srcNode(int i) const { + return *(this->srcTree().template getFirstNode() + i); + } + + template + __device__ NanoGrid& dstGrid() const {return *PtrAdd>(d_dstPtr, grid);} + template + __device__ NanoTree& dstTree() const {return *PtrAdd>(d_dstPtr, tree);} + template + __device__ NanoRoot& dstRoot() const {return *PtrAdd>(d_dstPtr, root);} + template + __device__ typename NanoNode::type& dstNode(int i) const { + return *(PtrAdd::type>(d_dstPtr, node[LEVEL])+i); + } +};// CudaIndexToGrid::NodeAccessor + +//================================================================================================ + +template +__global__ void cudaProcessGridTreeRoot(typename CudaIndexToGrid::NodeAccessor *nodeAcc, + const typename BuildToValueMap::type *srcValues) +{ + using SrcValueT = typename BuildToValueMap::type; + using DstStatsT = typename NanoRoot::FloatType; + + auto &srcGrid = nodeAcc->srcGrid(); + auto &dstGrid = nodeAcc->template dstGrid(); + auto &srcTree = srcGrid.tree(); + auto &dstTree = nodeAcc->template dstTree(); + auto &srcRoot = srcTree.root(); + auto &dstRoot = nodeAcc->template dstRoot(); + + // process Grid + *dstGrid.data() = *srcGrid.data(); + dstGrid.mGridType = mapToGridType(); + dstGrid.mData1 = 0u; + // we will recompute GridData::mChecksum later + + // process Tree + *dstTree.data() = *srcTree.data(); + dstTree.setRoot(&dstRoot); + dstTree.setFirstNode(&nodeAcc->template dstNode(0)); + dstTree.setFirstNode(&nodeAcc->template dstNode(0)); + dstTree.setFirstNode(&nodeAcc->template dstNode(0)); + + // process Root + dstRoot.mBBox = srcRoot.mBBox; + dstRoot.mTableSize = srcRoot.mTableSize; + dstRoot.mBackground = srcValues[srcRoot.mBackground]; + if (srcGrid.hasMinMax()) { + dstRoot.mMinimum = srcValues[srcRoot.mMinimum]; + dstRoot.mMaximum = srcValues[srcRoot.mMaximum]; + } + if constexpr(is_same::value) {// e.g. {float,float} or {Vec3f,float} + if (srcGrid.hasAverage()) dstRoot.mAverage = srcValues[srcRoot.mAverage]; + if (srcGrid.hasStdDeviation()) dstRoot.mStdDevi = srcValues[srcRoot.mStdDevi]; + } +}// cudaProcessGridTreeRoot + +//================================================================================================ + +template +__global__ void cudaProcessRootTiles(typename CudaIndexToGrid::NodeAccessor *nodeAcc, + const typename BuildToValueMap::type *srcValues) +{ + const auto tid = blockIdx.x; + + // Process children and tiles + const auto &srcTile = *nodeAcc->srcRoot().tile(tid); + auto &dstTile = *nodeAcc->template dstRoot().tile(tid); + dstTile.key = srcTile.key; + if (srcTile.child) { + dstTile.child = sizeof(NanoRoot) + sizeof(NanoRoot::Tile)*((srcTile.child - sizeof(NanoRoot))/sizeof(NanoRoot::Tile)); + dstTile.value = srcValues[0];// set to background + dstTile.state = false; + } else { + dstTile.child = 0;// i.e. no child node + dstTile.value = srcValues[srcTile.value]; + dstTile.state = srcTile.state; + } +}// cudaProcessRootTiles + +//================================================================================================ + +template +__global__ void cudaProcessInternalNodes(typename CudaIndexToGrid::NodeAccessor *nodeAcc, + const typename BuildToValueMap::type *srcValues) +{ + using SrcNodeT = typename NanoNode::type; + using DstNodeT = typename NanoNode::type; + using SrcChildT = typename SrcNodeT::ChildNodeType; + using DstChildT = typename DstNodeT::ChildNodeType; + using SrcValueT = typename BuildToValueMap::type; + using DstStatsT = typename NanoRoot::FloatType; + + auto &srcNode = nodeAcc->template srcNode(blockIdx.x); + auto &dstNode = nodeAcc->template dstNode(blockIdx.x); + + if (threadIdx.x == 0 && threadIdx.y == 0) { + dstNode.mBBox = srcNode.mBBox; + dstNode.mFlags = srcNode.mFlags; + dstNode.mValueMask = srcNode.mValueMask; + dstNode.mChildMask = srcNode.mChildMask; + auto &srcGrid = nodeAcc->srcGrid(); + if (srcGrid.hasMinMax()) { + dstNode.mMinimum = srcValues[srcNode.mMinimum]; + dstNode.mMaximum = srcValues[srcNode.mMaximum]; + } + if constexpr(is_same::value) {// e.g. {float,float} or {Vec3f,float} + if (srcGrid.hasAverage()) dstNode.mAverage = srcValues[srcNode.mAverage]; + if (srcGrid.hasStdDeviation()) dstNode.mStdDevi = srcValues[srcNode.mStdDevi]; + } + } + const uint64_t nodeSkip = nodeAcc->nodeCount[LEVEL] - blockIdx.x, srcOff = sizeof(SrcNodeT)*nodeSkip, dstOff = sizeof(DstNodeT)*nodeSkip;// offset to first node of child type + const int off = blockDim.x*blockDim.y*threadIdx.x + blockDim.x*threadIdx.y; + for (int threadIdx_z=0; threadIdx_z +__global__ void cudaProcessLeafNodes(typename CudaIndexToGrid::NodeAccessor *nodeAcc, + const typename BuildToValueMap::type *srcValues) +{ + using SrcValueT = typename BuildToValueMap::type; + using DstStatsT = typename NanoRoot::FloatType; + static_assert(!BuildTraits::is_special, "Invalid destination type!"); + auto &srcLeaf = nodeAcc->template srcNode<0>(blockIdx.x); + auto &dstLeaf = nodeAcc->template dstNode(blockIdx.x); + if (threadIdx.x == 0 && threadIdx.y == 0) { + dstLeaf.mBBoxMin = srcLeaf.mBBoxMin; + for (int i=0; i<3; ++i) dstLeaf.mBBoxDif[i] = srcLeaf.mBBoxDif[i]; + dstLeaf.mFlags = srcLeaf.mFlags; + dstLeaf.mValueMask = srcLeaf.mValueMask; + /// + auto &srcGrid = nodeAcc->srcGrid(); + if (srcGrid.hasMinMax()) { + dstLeaf.mMinimum = srcValues[srcLeaf.getMin()]; + dstLeaf.mMaximum = srcValues[srcLeaf.getMax()]; + } + if constexpr(is_same::value) {// e.g. {float,float} or {Vec3f,float} + if (srcGrid.hasAverage()) dstLeaf.mAverage = srcValues[srcLeaf.getAvg()]; + if (srcGrid.hasStdDeviation()) dstLeaf.mStdDevi = srcValues[srcLeaf.getDev()]; + } + } + const int off = blockDim.x*blockDim.y*threadIdx.x + blockDim.x*threadIdx.y; + auto *dst = dstLeaf.mValues + off; + for (int threadIdx_z=0; threadIdx_z +__global__ void cudaCpyNodeCount(const NanoGrid *srcGrid, + typename CudaIndexToGrid::NodeAccessor *nodeAcc) +{ + assert(srcGrid->isSequential()); + nodeAcc->d_srcGrid = srcGrid; + for (int i=0; i<3; ++i) nodeAcc->nodeCount[i] = srcGrid->tree().nodeCount(i); + nodeAcc->nodeCount[3] = srcGrid->tree().root().tileCount(); +} + +}// anonymous namespace + +//================================================================================================ + +template +CudaIndexToGrid::CudaIndexToGrid(const SrcGridT *d_srcGrid, cudaStream_t stream) + : mStream(stream), mTimer(stream) +{ + NANOVDB_ASSERT(d_srcGrid); + cudaCheck(cudaMallocAsync((void**)&mDevNodeAcc, sizeof(NodeAccessor), mStream)); + cudaCpyNodeCount<<<1, 1, 0, mStream>>>(d_srcGrid, mDevNodeAcc); + cudaCheckError(); + cudaCheck(cudaMemcpyAsync(&mNodeAcc, mDevNodeAcc, sizeof(NodeAccessor), cudaMemcpyDeviceToHost, mStream));// mNodeAcc = *mDevNodeAcc +} + +//================================================================================================ + +template +template +GridHandle CudaIndexToGrid::getHandle(const typename BuildToValueMap::type *srcValues, + const BufferT &pool) +{ + if (mVerbose) mTimer.start("Initiate buffer"); + auto buffer = this->template getBuffer(pool); + + if (mVerbose) mTimer.restart("Process grid,tree,root"); + cudaProcessGridTreeRoot<<<1, 1, 0, mStream>>>(mDevNodeAcc, srcValues); + cudaCheckError(); + + if (mVerbose) mTimer.restart("Process root children and tiles"); + cudaProcessRootTiles<<>>(mDevNodeAcc, srcValues); + cudaCheckError(); + + cudaCheck(cudaFreeAsync(mNodeAcc.d_gridName, mStream)); + + if (mVerbose) mTimer.restart("Process upper internal nodes"); + cudaProcessInternalNodes<<>>(mDevNodeAcc, srcValues); + cudaCheckError(); + + if (mVerbose) mTimer.restart("Process lower internal nodes"); + cudaProcessInternalNodes<<>>(mDevNodeAcc, srcValues); + cudaCheckError(); + + if (mVerbose) mTimer.restart("Process leaf nodes"); + cudaProcessLeafNodes<<>>(mDevNodeAcc, srcValues); + if (mVerbose) mTimer.stop(); + cudaCheckError(); + + if (mVerbose) mTimer.restart("Compute checksums"); + cudaUpdateGridChecksum((GridData*)mNodeAcc.d_dstPtr, mStream); + if (mVerbose) mTimer.stop(); + + cudaStreamSynchronize(mStream);// finish all device tasks in mStream + return GridHandle(std::move(buffer)); +}// CudaIndexToGrid::getHandle + +//================================================================================================ + +template +template +inline BufferT CudaIndexToGrid::getBuffer(const BufferT &pool) +{ + mNodeAcc.grid = 0;// grid is always stored at the start of the buffer! + mNodeAcc.tree = NanoGrid::memUsage(); // grid ends and tree begins + mNodeAcc.root = mNodeAcc.tree + NanoTree::memUsage(); // tree ends and root node begins + mNodeAcc.node[2] = mNodeAcc.root + NanoRoot::memUsage(mNodeAcc.nodeCount[3]); // root node ends and upper internal nodes begin + mNodeAcc.node[1] = mNodeAcc.node[2] + NanoUpper::memUsage()*mNodeAcc.nodeCount[2]; // upper internal nodes ends and lower internal nodes begin + mNodeAcc.node[0] = mNodeAcc.node[1] + NanoLower::memUsage()*mNodeAcc.nodeCount[1]; // lower internal nodes ends and leaf nodes begin + mNodeAcc.meta = mNodeAcc.node[0] + NanoLeaf::DataType::memUsage()*mNodeAcc.nodeCount[0];// leaf nodes end and blind meta data begins + mNodeAcc.blind = mNodeAcc.meta + 0*sizeof(GridBlindMetaData); // meta data ends and blind data begins + mNodeAcc.size = mNodeAcc.blind;// end of buffer + auto buffer = BufferT::create(mNodeAcc.size, &pool, false, mStream); + mNodeAcc.d_dstPtr = buffer.deviceData(); + if (mNodeAcc.d_dstPtr == nullptr) throw std::runtime_error("Failed memory allocation on the device"); + + if (size_t size = mGridName.size()) { + cudaCheck(cudaMallocAsync((void**)&mNodeAcc.d_gridName, size, mStream)); + cudaCheck(cudaMemcpyAsync(mNodeAcc.d_gridName, mGridName.data(), size, cudaMemcpyHostToDevice, mStream)); + } else { + mNodeAcc.d_gridName = nullptr; + } + cudaCheck(cudaMemcpyAsync(mDevNodeAcc, &mNodeAcc, sizeof(NodeAccessor), cudaMemcpyHostToDevice, mStream));// copy NodeAccessor CPU -> GPU + return buffer; +} + +//================================================================================================ + +template +typename enable_if::is_index, GridHandle>::type +cudaIndexToGrid(const NanoGrid *d_srcGrid, const typename BuildToValueMap::type *d_srcValues, const BufferT &pool, cudaStream_t stream) +{ + CudaIndexToGrid converter(d_srcGrid, stream); + return converter.template getHandle(d_srcValues, pool); +} + +}// nanovdb namespace + +#endif // NVIDIA_CUDA_INDEX_TO_GRID_CUH_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/cuda/CudaNodeManager.cuh b/nanovdb/nanovdb/util/cuda/CudaNodeManager.cuh new file mode 100644 index 0000000000..3d35a4b902 --- /dev/null +++ b/nanovdb/nanovdb/util/cuda/CudaNodeManager.cuh @@ -0,0 +1,90 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +/*! + \file CudaNodeManager.cuh + + \author Ken Museth + + \date October 3, 2023 + + \brief Contains cuda kernels for NodeManager + + \warning The header file contains cuda device code so be sure + to only include it in .cu files (or other .cuh files) +*/ + +#ifndef NANOVDB_CUDA_NODE_MANAGER_CUH_HAS_BEEN_INCLUDED +#define NANOVDB_CUDA_NODE_MANAGER_CUH_HAS_BEEN_INCLUDED + +#include "CudaUtils.h"// for cudaLambdaKernel +#include "CudaDeviceBuffer.h" +#include "../NodeManager.h" + +namespace nanovdb { + +/// @brief Construct a NodeManager from a device grid pointer +/// +/// @param d_grid device grid pointer whose nodes will be accessed sequentially +/// @param buffer buffer from which to allocate the output handle +/// @param stream cuda stream +/// @return Handle that contains a device NodeManager +template +inline typename enable_if::hasDeviceDual, NodeManagerHandle>::type +cudaCreateNodeManager(const NanoGrid *d_grid, + const BufferT& pool = BufferT(), + cudaStream_t stream = 0) +{ + auto buffer = BufferT::create(sizeof(NodeManagerData), &pool, false, stream); + auto *d_data = (NodeManagerData*)buffer.deviceData(); + size_t size = 0u, *d_size; + cudaCheck(cudaMallocAsync((void**)&d_size, sizeof(size_t), stream)); + cudaLambdaKernel<<<1, 1, 0, stream>>>(1, [=] __device__(size_t) { +#ifdef NANOVDB_USE_NEW_MAGIC_NUMBERS + *d_data = NodeManagerData{NANOVDB_MAGIC_NODE, 0u, (void*)d_grid, {0u,0u,0u}}; +#else + *d_data = NodeManagerData{NANOVDB_MAGIC_NUMBER, 0u, (void*)d_grid, {0u,0u,0u}}; +#endif + *d_size = sizeof(NodeManagerData); + auto &tree = d_grid->tree(); + if (NodeManager::FIXED_SIZE && d_grid->isBreadthFirst()) { + d_data->mLinear = uint8_t(1u); + d_data->mOff[0] = PtrDiff(tree.template getFirstNode<0>(), d_grid); + d_data->mOff[1] = PtrDiff(tree.template getFirstNode<1>(), d_grid); + d_data->mOff[2] = PtrDiff(tree.template getFirstNode<2>(), d_grid); + } else { + *d_size += sizeof(uint64_t)*tree.totalNodeCount(); + } + }); + cudaCheckError(); + cudaCheck(cudaMemcpyAsync(&size, d_size, sizeof(size_t), cudaMemcpyDeviceToHost, stream)); + cudaCheck(cudaFreeAsync(d_size, stream)); + if (size > sizeof(NodeManagerData)) { + auto tmp = BufferT::create(size, &pool, false, stream);// only allocate buffer on the device + cudaCheck(cudaMemcpyAsync(tmp.deviceData(), buffer.deviceData(), sizeof(NodeManagerData), cudaMemcpyDeviceToDevice, stream)); + buffer = std::move(tmp); + d_data = reinterpret_cast(buffer.deviceData()); + cudaLambdaKernel<<<1, 1, 0, stream>>>(1, [=] __device__ (size_t) { + auto &tree = d_grid->tree(); + int64_t *ptr0 = d_data->mPtr[0] = reinterpret_cast(d_data + 1); + int64_t *ptr1 = d_data->mPtr[1] = d_data->mPtr[0] + tree.nodeCount(0); + int64_t *ptr2 = d_data->mPtr[2] = d_data->mPtr[1] + tree.nodeCount(1); + // Performs depth first traversal but breadth first insertion + for (auto it2 = tree.root().cbeginChild(); it2; ++it2) { + *ptr2++ = PtrDiff(&*it2, d_grid); + for (auto it1 = it2->cbeginChild(); it1; ++it1) { + *ptr1++ = PtrDiff(&*it1, d_grid); + for (auto it0 = it1->cbeginChild(); it0; ++it0) { + *ptr0++ = PtrDiff(&*it0, d_grid); + }// loop over child nodes of the lower internal node + }// loop over child nodes of the upper internal node + }// loop over child nodes of the root node + }); + } + + return NodeManagerHandle(mapToGridType(), std::move(buffer)); +}// cudaCreateNodeManager + +} // namespace nanovdb + +#endif // NANOVDB_CUDA_NODE_MANAGER_CUH_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/cuda/CudaPointsToGrid.cuh b/nanovdb/nanovdb/util/cuda/CudaPointsToGrid.cuh new file mode 100644 index 0000000000..a1d5150de5 --- /dev/null +++ b/nanovdb/nanovdb/util/cuda/CudaPointsToGrid.cuh @@ -0,0 +1,1179 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +/*! + \file CudaPointsToGrid.cuh + + \authors Greg Klar (initial version) and Ken Museth (final version) + + \brief Generates NanoVDB grids from a list of voxels or points on the device + + \warning The header file contains cuda device code so be sure + to only include it in .cu files (or other .cuh files) +*/ + +#ifndef NVIDIA_CUDA_POINTS_TO_GRID_CUH_HAS_BEEN_INCLUDED +#define NVIDIA_CUDA_POINTS_TO_GRID_CUH_HAS_BEEN_INCLUDED + +#include +#include +#include +#include + +#include +#include "CudaDeviceBuffer.h" +#include +#include +#include +#include + +/* + Note: 4.29 billion (=2^32) coordinates of type Vec3f have a memory footprint of 48 GB! +*/ + +namespace nanovdb { + +// Define the type used when the points are encoded as blind data in the output grid +enum class PointType : uint32_t { Disable = 0,// no point information e.g. when BuildT != Point + PointID = 1,// linear index of type uint32_t to points + World64 = 2,// Vec3d in world space + World32 = 3,// Vec3f in world space + Grid64 = 4,// Vec3d in grid space + Grid32 = 5,// Vec3f in grid space + Voxel32 = 6,// Vec3f in voxel space + Voxel16 = 7,// Vec3u16 in voxel space + Voxel8 = 8,// Vec3u8 in voxel space + Default = 9,// output matches input, i.e. Vec3d or Vec3f in world space + End =10 }; + +//================================================================================================ + +/// @brief Example class of a fancy pointer that can optionally be used as a template for writing +/// a custom fancy pointer that allows for particle coordinates to be arrange non-linearly +/// in memory. For instance with coordinates are interlaced with other dats, i.e. an array +/// of structs, a custom implementation of fancy_ptr::operator[](size_t i) can account for +/// strides that skip other interlaces data. +/// @tparam T Template type that specifies the type use for the coordinates of the points +template +class fancy_ptr +{ + const T* mPtr; +public: + /// @brief Default constructor. + /// @note This method is atcually not required by CudaPointsToGrid + /// @param ptr Pointer to array of elements + __hostdev__ explicit fancy_ptr(const T* ptr = nullptr) : mPtr(ptr) {} + /// @brief Index acces into the array pointed to by the stored pointer. + /// @note This method is required by CudaPointsToGrid! + /// @param i Unsigned index of the element to be returned + /// @return Const refernce to the element at the i'th poisiton + __hostdev__ inline const T& operator[](size_t i) const {return mPtr[i];} + /// @brief Dummy implementation required by pointer_traits. + /// @note Note that only the return type matters! + /// @details Unlike operator[] it is safe to assume that all pointer types have operator*, + /// which is why pointer_traits makes use of it to determine the element_type that + /// a pointer class is pointing to. E.g. operator[] is not always defined for std::shared_ptr! + __hostdev__ inline const T& operator*() const {return *mPtr;} +};// fancy_ptr + +/// @brief Simple stand-alone function that can be used to conveniently construct a fancy_ptr +/// @tparam T Template type that specifies the type use for the coordinates of the points +/// @param ptr Raw pointer to data +/// @return a new instance of a fancy_ptr +template +fancy_ptr make_fancy(const T* ptr = nullptr) {return fancy_ptr(ptr);} + +/// @brief Trait of points, like type of pointer and size of the pointer type +template +struct pointer_traits; + +template +struct pointer_traits { + using element_type = T; + static constexpr size_t element_size = sizeof(T); +}; + +template +struct pointer_traits { + using element_type = typename remove_reference())>::type;// assumes T::operator*() exists! + static constexpr size_t element_size = sizeof(element_type); +}; + +//================================================================================================ + +/// @brief Generates a NanoGrid from a list of point coordinates on the device. This method is +/// mainly used as a means to build a BVH acceleration structure for points, e.g. for efficient rendering. +/// @tparam PtrT Template type to a raw or fancy-pointer of point coordinates in world space. Dereferencing should return Vec3f or Vec3d. +/// @tparam BufferT Template type of buffer used for memory allocation on the device +/// @tparam AllocT Template type of optional device allocator for internal temporary memory +/// @param dWorldPoints Raw or fancy pointer to list of point coordinates in world space on the device +/// @param pointCount number of point in the list @c d_world +/// @param voxelSize Size of a voxel in world units used for the output grid +/// @param type Defined the way point information is represented in the output grid (see PointType enum above) +/// Should not be PointType::Disable! +/// @param buffer Instance of the device buffer used for memory allocation +/// @param stream optional CUDA stream (defaults to CUDA stream 0) +/// @return Returns a handle with a grid of type NanoGrid where point information, e.g. coordinates, +/// are represented as blind data defined by @c type. +template +GridHandle +cudaPointsToGrid(const PtrT dWorldPoints, + int pointCount, + double voxelSize = 1.0, + PointType type = PointType::Default, + const BufferT &buffer = BufferT(), + cudaStream_t stream = 0); + +//================================================================================================ + +template +GridHandle +cudaPointsToGrid(std::vector> pointSet, + const BufferT &buffer = BufferT(), + cudaStream_t stream = 0); + +//================================================================================================ + +/// @brief Generates a NanoGrid of any type from a list of voxel coordinates on the device. Unlike @c cudaPointsToGrid +/// this method only builds the grid but does not encode the coordinates as blind data. It is mainly useful as a +/// means to generate a grid that is know to contain the voxels given in the list. +/// @tparam BuildT Template type of the return grid +/// @tparam PtrT Template type to a raw or fancy-pointer of point coordinates in world space. Dereferencing should return Vec3f or Vec3d. +/// @tparam BufferT Template type of buffer used for memory allocation on the device +/// @tparam AllocT Template type of optional device allocator for internal temporary memory +/// @param dGridVoxels Raw or fancy pointer to list of voxel coordinates in grid (or index) space on the device +/// @param pointCount number of voxel in the list @c dGridVoxels +/// @param voxelSize Size of a voxel in world units used for the output grid +/// @param buffer Instance of the device buffer used for memory allocation +/// @return Returns a handle with the grid of type NanoGrid +template +GridHandle +cudaVoxelsToGrid(const PtrT dGridVoxels, + size_t voxelCount, + double voxelSize = 1.0, + const BufferT &buffer = BufferT(), + cudaStream_t stream = 0); + +//================================================================================================ + +template +GridHandle +cudaVoxelsToGrid(std::vector> pointSet, + const BufferT &buffer = BufferT(), + cudaStream_t stream = 0); + +//================================================================================================ + +template +__hostdev__ inline static void worldToVoxel(Vec3u8 &voxel, const Vec3T &world, const Map &map) +{ + const Vec3d ijk = map.applyInverseMap(world);// world -> index + static constexpr double encode = double((1<<8) - 1); + voxel[0] = uint8_t( encode*(ijk[0] - Floor(ijk[0] + 0.5) + 0.5) ); + voxel[1] = uint8_t( encode*(ijk[1] - Floor(ijk[1] + 0.5) + 0.5) ); + voxel[2] = uint8_t( encode*(ijk[2] - Floor(ijk[2] + 0.5) + 0.5) ); +} + +template +__hostdev__ inline static void worldToVoxel(Vec3u16 &voxel, const Vec3T &world, const Map &map) +{ + const Vec3d ijk = map.applyInverseMap(world);// world -> index + static constexpr double encode = double((1<<16) - 1); + voxel[0] = uint16_t( encode*(ijk[0] - Floor(ijk[0] + 0.5) + 0.5) ); + voxel[1] = uint16_t( encode*(ijk[1] - Floor(ijk[1] + 0.5) + 0.5) ); + voxel[2] = uint16_t( encode*(ijk[2] - Floor(ijk[2] + 0.5) + 0.5) ); +} + +template +__hostdev__ inline static void worldToVoxel(Vec3f &voxel, const Vec3T &world, const Map &map) +{ + const Vec3d ijk = map.applyInverseMap(world);// world -> index + voxel[0] = float( ijk[0] - Floor(ijk[0] + 0.5) ); + voxel[1] = float( ijk[1] - Floor(ijk[1] + 0.5) ); + voxel[2] = float( ijk[2] - Floor(ijk[2] + 0.5) ); +} + +//================================================================================================ + +template +__hostdev__ inline static Vec3T voxelToWorld(const Vec3u8 &voxel, const Coord &ijk, const Map &map) +{ + static constexpr double decode = 1.0/double((1<<8) - 1); + if constexpr(is_same::value) { + return map.applyMap( Vec3d(ijk[0] + decode*voxel[0] - 0.5, ijk[1] + decode*voxel[1] - 0.5, ijk[2] + decode*voxel[2] - 0.5)); + } else { + return map.applyMapF(Vec3f(ijk[0] + decode*voxel[0] - 0.5f, ijk[1] + decode*voxel[1] - 0.5f, ijk[2] + decode*voxel[2] - 0.5f)); + } +} + +template +__hostdev__ inline static Vec3T voxelToWorld(const Vec3u16 &voxel, const Coord &ijk, const Map &map) +{ + static constexpr double decode = 1.0/double((1<<16) - 1); + if constexpr(is_same::value) { + return map.applyMap( Vec3d(ijk[0] + decode*voxel[0] - 0.5, ijk[1] + decode*voxel[1] - 0.5, ijk[2] + decode*voxel[2] - 0.5)); + } else { + return map.applyMapF(Vec3f(ijk[0] + decode*voxel[0] - 0.5f, ijk[1] + decode*voxel[1] - 0.5f, ijk[2] + decode*voxel[2] - 0.5f)); + } +} + +template +__hostdev__ inline static Vec3T voxelToWorld(const Vec3f &voxel, const Coord &ijk, const Map &map) +{ + if constexpr(is_same::value) { + return map.applyMap( Vec3d(ijk[0] + voxel[0], ijk[1] + voxel[1], ijk[2] + voxel[2])); + } else { + return map.applyMapF(Vec3f(ijk[0] + voxel[0], ijk[1] + voxel[1], ijk[2] + voxel[2])); + } +} + +//================================================================================================ + +namespace {// anonymous namespace + +template +class CudaPointsToGrid +{ +public: + + struct Data { + Map map; + void *d_bufferPtr; + uint64_t *d_keys, *d_tile_keys, *d_lower_keys, *d_leaf_keys;// device pointer to 64 bit keys + uint64_t grid, tree, root, upper, lower, leaf, meta, blind, size;// byte offsets to nodes in buffer + uint32_t *d_indx;// device pointer to point indices (or IDs) + uint32_t nodeCount[3], *pointsPerLeafPrefix, *pointsPerLeaf;// 0=leaf,1=lower, 2=upper + uint32_t voxelCount, *pointsPerVoxelPrefix, *pointsPerVoxel; + BitFlags<16> flags; + __hostdev__ NanoGrid& getGrid() const {return *PtrAdd>(d_bufferPtr, grid);} + __hostdev__ NanoTree& getTree() const {return *PtrAdd>(d_bufferPtr, tree);} + __hostdev__ NanoRoot& getRoot() const {return *PtrAdd>(d_bufferPtr, root);} + __hostdev__ NanoUpper& getUpper(int i) const {return *(PtrAdd>(d_bufferPtr, upper)+i);} + __hostdev__ NanoLower& getLower(int i) const {return *(PtrAdd>(d_bufferPtr, lower)+i);} + __hostdev__ NanoLeaf& getLeaf(int i) const {return *(PtrAdd>(d_bufferPtr, leaf)+i);} + __hostdev__ GridBlindMetaData& getMeta() const { return *PtrAdd(d_bufferPtr, meta);}; + template + __hostdev__ Vec3T& getPoint(int i) const {return *(PtrAdd(d_bufferPtr, blind)+i);} + };// Data + + /// @brief Constructor from a Map + /// @param map Map to be used for the output device grid + /// @param stream optional CUDA stream (defaults to CUDA stream 0) + CudaPointsToGrid(const Map &map, cudaStream_t stream = 0) + : mStream(stream) + , mPointType(is_same::value ? PointType::Default : PointType::Disable) + { + mData.map = map; + mData.flags.initMask({GridFlags::HasBBox, GridFlags::IsBreadthFirst}); + cudaCheck(cudaMallocAsync((void**)&mDeviceData, sizeof(Data), mStream)); + } + + /// @brief Default constructor + /// @param scale Voxel size in world units + /// @param trans Translation of origin in world units + /// @param stream optional CUDA stream (defaults to CUDA stream 0) + CudaPointsToGrid(const double scale = 1.0, const Vec3d &trans = Vec3d(0.0), cudaStream_t stream = 0) + : CudaPointsToGrid(Map(scale, trans), stream) {} + + /// @brief Destructor + ~CudaPointsToGrid() {cudaCheck(cudaFreeAsync(mDeviceData, mStream));} + + /// @brief Toggle on and off verbose mode + /// @param level Verbose level: 0=quiet, 1=timing, 2=benchmarking + void setVerbose(int level = 1) {mVerbose = level; mData.flags.setBit(7u, level); } + + /// @brief Set the mode for checksum computation, which is disabled by default + /// @param mode Mode of checksum computation + void setChecksum(ChecksumMode mode = ChecksumMode::Disable){mChecksum = mode;} + + /// @brief Toggle on and off the computation of a bounding-box + /// @param on If true bbox will be computed + void includeBBox(bool on = true) { mData.flags.setMask(GridFlags::HasBBox, on); } + + /// @brief Set the name of the output grid + /// @param name name of the output grid + void setGridName(const std::string &name) {mGridName = name;} + + // only available when BuildT == Point + template typename enable_if::value>::type + setPointType(PointType type) { mPointType = type; } + + /// @brief Creates a handle to a grid with the specified build type from a list of points in index or world space + /// @tparam BuildT Build type of the output grid, i.e NanoGrid + /// @tparam PtrT Template type to a raw or fancy-pointer of point coordinates in world or index space. + /// @tparam BufferT Buffer type used for allocation of the grid handle + /// @param points device point to an array of points in world space + /// @param pointCount number of input points or voxels + /// @param gridName optional name of the output grid + /// @param buffer optional buffer (currently ignored) + /// @return returns a handle with a grid of type NanoGrid + template + GridHandle getHandle(const PtrT points, + size_t pointCount, + const BufferT &buffer = BufferT()); + + template + void countNodes(const PtrT points, size_t pointCount); + + template + void processGridTreeRoot(const PtrT points, size_t pointCount); + + void processUpperNodes(); + + void processLowerNodes(); + + template + void processLeafNodes(const PtrT points); + + template + void processPoints(const PtrT points, size_t pointCount); + + void processBBox(); + + // the following methods are only defined when BuildT == Point + template typename enable_if::value, uint32_t>::type + maxPointsPerVoxel() const {return mMaxPointsPerVoxel;} + template typename enable_if::value, uint32_t>::type + maxPointsPerLeaf() const {return mMaxPointsPerLeaf;} + +private: + static constexpr unsigned int mNumThreads = 128;// seems faster than the old value of 256! + static unsigned int numBlocks(unsigned int n) {return (n + mNumThreads - 1) / mNumThreads;} + + cudaStream_t mStream{0}; + GpuTimer mTimer; + PointType mPointType; + std::string mGridName; + int mVerbose{0}; + Data mData, *mDeviceData; + uint32_t mMaxPointsPerVoxel{0u}, mMaxPointsPerLeaf{0u}; + ChecksumMode mChecksum{ChecksumMode::Disable}; + + // wrapper of cub::CachingDeviceAllocator with a shared scratch space + struct Allocator { + AllocT mAllocator; + void* d_scratch; + size_t scratchSize, actualScratchSize; + Allocator() : d_scratch(nullptr), scratchSize(0), actualScratchSize(0) {} + ~Allocator() { + if (scratchSize > 0) this->free(d_scratch);// a bug in cub makes this necessary + mAllocator.FreeAllCached(); + } + template + T* alloc(size_t count, cudaStream_t stream) { + T* d_ptr = nullptr; + cudaCheck(mAllocator.DeviceAllocate((void**)&d_ptr, sizeof(T)*count, stream)); + return d_ptr; + } + void free(void *d_ptr) {if (d_ptr) cudaCheck(mAllocator.DeviceFree(d_ptr));} + template + void free(void *d_ptr, T... other) { + if (d_ptr) cudaCheck(mAllocator.DeviceFree(d_ptr)); + this->free(other...); + } + void adjustScratch(cudaStream_t stream){ + if (scratchSize > actualScratchSize) { + if (actualScratchSize>0) cudaCheck(mAllocator.DeviceFree(d_scratch)); + cudaCheck(mAllocator.DeviceAllocate((void**)&d_scratch, scratchSize, stream)); + actualScratchSize = scratchSize; + } + } + } mMemPool; + + template + BufferT getBuffer(const PtrT points, size_t pointCount, const BufferT &buffer); +};// CudaPointsToGrid + + +namespace kernels { +/// @details Used by CudaPointsToGrid::processLeafNodes before the computation +/// of prefix-sum for index grid. +/// Moving this away from an implementation using the cudaLambdaKernel wrapper +/// to fix the following on Windows platform: +/// error : For this host platform/dialect, an extended lambda cannot be defined inside the 'if' +/// or 'else' block of a constexpr if statement. +/// function in a lambda through cudaLambdaKernel wrapper defined in CudaUtils.h. +template +__global__ void fillValueIndexKernel(const size_t numItems, uint64_t* devValueIndex, typename CudaPointsToGrid::Data* d_data) { + const int tid = blockIdx.x * blockDim.x + threadIdx.x; + if (tid >= numItems) + return; + + devValueIndex[tid] = static_cast(d_data->getLeaf(tid).mValueMask.countOn()); +} + +/// @details Used by CudaPointsToGrid::processLeafNodes for the computation +/// of prefix-sum for index grid. +/// Moving this away from an implementation using the cudaLambdaKernel wrapper +/// to fix the following on Windows platform: +/// error : For this host platform/dialect, an extended lambda cannot be defined inside the 'if' +/// or 'else' block of a constexpr if statement. +template +__global__ void leafPrefixSumKernel(const size_t numItems, uint64_t* devValueIndexPrefix, typename CudaPointsToGrid::Data* d_data) { + const int tid = blockIdx.x * blockDim.x + threadIdx.x; + if (tid >= numItems) + return; + + auto &leaf = d_data->getLeaf(tid); + leaf.mOffset = 1u;// will be re-set below + const uint64_t *w = leaf.mValueMask.words(); + uint64_t &prefixSum = leaf.mPrefixSum, sum = CountOn(*w++); + prefixSum = sum; + for (int n = 9; n < 55; n += 9) {// n=i*9 where i=1,2,..6 + sum += CountOn(*w++); + prefixSum |= sum << n;// each pre-fixed sum is encoded in 9 bits + } + if (tid==0) { + d_data->getGrid().mData1 = 1u + devValueIndexPrefix[d_data->nodeCount[0]-1];// set total count + d_data->getTree().mVoxelCount = devValueIndexPrefix[d_data->nodeCount[0]-1]; + } else { + leaf.mOffset = 1u + devValueIndexPrefix[tid-1];// background is index 0 + } +} + +/// @details Used by CudaPointsToGrid::processLeafNodes to make sure leaf.mMask - leaf.mValueMask. +/// Moving this away from an implementation using the cudaLambdaKernel wrapper +/// to fix the following on Windows platform: +/// error : For this host platform/dialect, an extended lambda cannot be defined inside the 'if' +/// or 'else' block of a constexpr if statement. +template +__global__ void setMaskEqValMaskKernel(const size_t numItems, typename CudaPointsToGrid::Data* d_data) { + const int tid = blockIdx.x * blockDim.x + threadIdx.x; + if (tid >= numItems) + return; + + auto &leaf = d_data->getLeaf(tid); + leaf.mMask = leaf.mValueMask; +} +} // namespace kernels + + +//================================================================================================ + +// Define utility macro used to call cub functions that use dynamic temporary storage +#ifndef CALL_CUBS +#ifdef _WIN32 +#define CALL_CUBS(func, ...) \ + cudaCheck(cub::func(nullptr, mMemPool.scratchSize, __VA_ARGS__, mStream)); \ + mMemPool.adjustScratch(mStream); \ + cudaCheck(cub::func(mMemPool.d_scratch, mMemPool.scratchSize, __VA_ARGS__, mStream)); +#else// fdef _WIN32 +#define CALL_CUBS(func, args...) \ + cudaCheck(cub::func(nullptr, mMemPool.scratchSize, args, mStream)); \ + mMemPool.adjustScratch(mStream); \ + cudaCheck(cub::func(mMemPool.d_scratch, mMemPool.scratchSize, args, mStream)); +#endif// ifdef _WIN32 +#endif// ifndef CALL_CUBS + +}// anonymous namespace + +//================================================================================================ + +template +template +inline GridHandle +CudaPointsToGrid::getHandle(const PtrT points, + size_t pointCount, + const BufferT &pool) +{ + if (mVerbose==1) mTimer.start("\nCounting nodes"); + this->countNodes(points, pointCount); + + if (mVerbose==1) mTimer.restart("Initiate buffer"); + auto buffer = this->getBuffer(points, pointCount, pool); + + if (mVerbose==1) mTimer.restart("Process grid,tree,root"); + this->processGridTreeRoot(points, pointCount); + + if (mVerbose==1) mTimer.restart("Process upper nodes"); + this->processUpperNodes(); + + if (mVerbose==1) mTimer.restart("Process lower nodes"); + this->processLowerNodes(); + + if (mVerbose==1) mTimer.restart("Process leaf nodes"); + this->processLeafNodes(points); + + if (mVerbose==1) mTimer.restart("Process points"); + this->processPoints(points, pointCount); + + if (mVerbose==1) mTimer.restart("Process bbox"); + this->processBBox(); + if (mVerbose==1) mTimer.stop(); + + if (mChecksum != ChecksumMode::Disable) { + if (mVerbose==1) mTimer.restart("Computation of checksum"); + cudaGridChecksum((GridData*)buffer.deviceData(), mChecksum); + if (mVerbose==1) mTimer.stop(); + } + + cudaStreamSynchronize(mStream);// finish all device tasks in mStream + + return GridHandle(std::move(buffer)); +}// CudaPointsToGrid::getHandle + +//================================================================================================ + +// --- CUB helpers --- +template +struct ShiftRight +{ + __hostdev__ inline OutT operator()(const InT& v) const {return static_cast(v >> BitCount);} +}; + +template +struct ShiftRightIterator : public cub::TransformInputIterator, InT*> +{ + using BASE = cub::TransformInputIterator, InT*>; + __hostdev__ inline ShiftRightIterator(uint64_t* input_itr) : BASE(input_itr, ShiftRight()) {} +}; + +//================================================================================================ + +template +template +void CudaPointsToGrid::countNodes(const PtrT points, size_t pointCount) +{ + using Vec3T = typename remove_const::element_type>::type; + if constexpr(is_same::value) { + static_assert(is_same::value, "Point (vs voxels) coordinates should be represented as Vec3f or Vec3d"); + } else { + static_assert(is_same::value, "Voxel coordinates should be represented as Coord, Vec3f or Vec3d"); + } + + mData.d_keys = mMemPool.template alloc(pointCount, mStream); + mData.d_indx = mMemPool.template alloc(pointCount, mStream);// uint32_t can index 4.29 billion Coords, corresponding to 48 GB + cudaCheck(cudaMemcpyAsync(mDeviceData, &mData, sizeof(Data), cudaMemcpyHostToDevice, mStream));// copy mData from CPU -> GPU + + if (mVerbose==2) mTimer.start("\nAllocating arrays for keys and indices"); + auto *d_keys = mMemPool.template alloc(pointCount, mStream); + auto *d_indx = mMemPool.template alloc(pointCount, mStream); + + if (mVerbose==2) mTimer.restart("Generate tile keys"); + cudaLambdaKernel<<>>(pointCount, [=] __device__(size_t tid, const Data *d_data, const PtrT points) { + auto coordToKey = [](const Coord &ijk)->uint64_t{ + // int32_t has a range of -2^31 to 2^31 - 1 + // uint32_t has a range of 0 to 2^32 - 1 + static constexpr int64_t offset = 1 << 31; + return (uint64_t(uint32_t(int64_t(ijk[2]) + offset) >> 12) ) | // z is the lower 21 bits + (uint64_t(uint32_t(int64_t(ijk[1]) + offset) >> 12) << 21) | // y is the middle 21 bits + (uint64_t(uint32_t(int64_t(ijk[0]) + offset) >> 12) << 42); // x is the upper 21 bits + }; + d_indx[tid] = uint32_t(tid); + uint64_t &key = d_keys[tid]; + if constexpr(is_same::value) {// points are in world space + if constexpr(is_same::value) { + key = coordToKey(d_data->map.applyInverseMapF(points[tid]).round()); + //key = NanoRoot::CoordToKey(d_data->map.applyInverseMapF(points[tid]).round()); + } else {// points are Vec3d + //key = NanoRoot::CoordToKey(d_data->map.applyInverseMap(points[tid]).round()); + key = coordToKey(d_data->map.applyInverseMap(points[tid]).round()); + } + } else if constexpr(is_same::value) {// points Coord are in index space + //key = NanoRoot::CoordToKey(points[tid]); + key = coordToKey(points[tid]); + } else {// points are Vec3f or Vec3d in index space + //key = NanoRoot::CoordToKey(points[tid].round()); + key = coordToKey(points[tid].round()); + } + }, mDeviceData, points); + cudaCheckError(); + if (mVerbose==2) mTimer.restart("DeviceRadixSort of "+std::to_string(pointCount)+" tile keys"); + CALL_CUBS(DeviceRadixSort::SortPairs, d_keys, mData.d_keys, d_indx, mData.d_indx, pointCount, 0, 62);// 21 bits per coord + std::swap(d_indx, mData.d_indx);// sorted indices are now in d_indx + + if (mVerbose==2) mTimer.restart("Allocate runs"); + auto *d_points_per_tile = mMemPool.template alloc(pointCount, mStream); + uint32_t *d_node_count = mMemPool.template alloc(3, mStream); + + if (mVerbose==2) mTimer.restart("DeviceRunLengthEncode tile keys"); + CALL_CUBS(DeviceRunLengthEncode::Encode, mData.d_keys, d_keys, d_points_per_tile, d_node_count+2, pointCount); + cudaCheck(cudaMemcpyAsync(mData.nodeCount+2, d_node_count+2, sizeof(uint32_t), cudaMemcpyDeviceToHost, mStream)); + mData.d_tile_keys = mMemPool.template alloc(mData.nodeCount[2], mStream); + cudaCheck(cudaMemcpyAsync(mData.d_tile_keys, d_keys, mData.nodeCount[2]*sizeof(uint64_t), cudaMemcpyDeviceToDevice, mStream)); + + if (mVerbose) mTimer.restart("DeviceRadixSort of " + std::to_string(pointCount) + " voxel keys in " + std::to_string(mData.nodeCount[2]) + " tiles"); + uint32_t *points_per_tile = new uint32_t[mData.nodeCount[2]]; + cudaCheck(cudaMemcpyAsync(points_per_tile, d_points_per_tile, mData.nodeCount[2]*sizeof(uint32_t), cudaMemcpyDeviceToHost, mStream)); + mMemPool.free(d_points_per_tile); + + for (uint32_t id = 0, offset = 0; id < mData.nodeCount[2]; ++id) { + const uint32_t count = points_per_tile[id]; + cudaLambdaKernel<<>>(count, [=] __device__(size_t tid, const Data *d_data) { + auto voxelKey = [] __device__ (uint64_t tileID, const Coord &ijk){ + return tileID << 36 | // upper offset: 64-15-12-9=28, i.e. last 28 bits + uint64_t(NanoUpper::CoordToOffset(ijk)) << 21 | // lower offset: 32^3 = 2^15, i.e. next 15 bits + uint64_t(NanoLower::CoordToOffset(ijk)) << 9 | // leaf offset: 16^3 = 2^12, i.e. next 12 bits + uint64_t(NanoLeaf< BuildT>::CoordToOffset(ijk)); // voxel offset: 8^3 = 2^9, i.e. first 9 bits + }; + tid += offset; + Vec3T p = points[d_indx[tid]]; + if constexpr(is_same::value) p = is_same::value ? d_data->map.applyInverseMapF(p) : d_data->map.applyInverseMap(p); + d_keys[tid] = voxelKey(id, p.round()); + }, mDeviceData); cudaCheckError(); + CALL_CUBS(DeviceRadixSort::SortPairs, d_keys + offset, mData.d_keys + offset, d_indx + offset, mData.d_indx + offset, count, 0, 36);// 9+12+15=36 + offset += count; + } + mMemPool.free(d_indx); + delete [] points_per_tile; + + if (mVerbose==2) mTimer.restart("Count points per voxel"); + + mData.pointsPerVoxel = mMemPool.template alloc(pointCount, mStream); + uint32_t *d_voxel_count = mMemPool.template alloc(1, mStream); + CALL_CUBS(DeviceRunLengthEncode::Encode, mData.d_keys, d_keys, mData.pointsPerVoxel, d_voxel_count, pointCount); + cudaCheck(cudaMemcpyAsync(&mData.voxelCount, d_voxel_count, sizeof(uint32_t), cudaMemcpyDeviceToHost, mStream)); + mMemPool.free(d_voxel_count); + + if constexpr(is_same::value) { + if (mVerbose==2) mTimer.restart("Count max points per voxel"); + uint32_t *d_maxPointsPerVoxel = mMemPool.template alloc(1, mStream); + CALL_CUBS(DeviceReduce::Max, mData.pointsPerVoxel, d_maxPointsPerVoxel, mData.voxelCount); + cudaCheck(cudaMemcpyAsync(&mMaxPointsPerVoxel, d_maxPointsPerVoxel, sizeof(uint32_t), cudaMemcpyDeviceToHost, mStream)); + mMemPool.free(d_maxPointsPerVoxel); + } + + //printf("\n Active voxel count = %u, max points per voxel = %u\n", mData.voxelCount, mMaxPointsPerVoxel); + if (mVerbose==2) mTimer.restart("Compute prefix sum of points per voxel"); + mData.pointsPerVoxelPrefix = mMemPool.template alloc(mData.voxelCount, mStream); + CALL_CUBS(DeviceScan::ExclusiveSum, mData.pointsPerVoxel, mData.pointsPerVoxelPrefix, mData.voxelCount); + + mData.pointsPerLeaf = mMemPool.template alloc(pointCount, mStream); + CALL_CUBS(DeviceRunLengthEncode::Encode, ShiftRightIterator<9>(mData.d_keys), d_keys, mData.pointsPerLeaf, d_node_count, pointCount); + cudaCheck(cudaMemcpyAsync(mData.nodeCount, d_node_count, sizeof(uint32_t), cudaMemcpyDeviceToHost, mStream)); + + if constexpr(is_same::value) { + uint32_t *d_maxPointsPerLeaf = mMemPool.template alloc(1, mStream); + CALL_CUBS(DeviceReduce::Max, mData.pointsPerLeaf, d_maxPointsPerLeaf, mData.nodeCount[0]); + cudaCheck(cudaMemcpyAsync(&mMaxPointsPerLeaf, d_maxPointsPerLeaf, sizeof(uint32_t), cudaMemcpyDeviceToHost, mStream)); + //printf("\n Leaf count = %u, max points per leaf = %u\n", mData.nodeCount[0], mMaxPointsPerLeaf); + if (mMaxPointsPerLeaf > std::numeric_limits::max()) { + throw std::runtime_error("Too many points per leaf: "+std::to_string(mMaxPointsPerLeaf)); + } + mMemPool.free(d_maxPointsPerLeaf); + } + + mData.pointsPerLeafPrefix = mMemPool.template alloc(mData.nodeCount[0], mStream); + CALL_CUBS(DeviceScan::ExclusiveSum, mData.pointsPerLeaf, mData.pointsPerLeafPrefix, mData.nodeCount[0]); + + mData.d_leaf_keys = mMemPool.template alloc(mData.nodeCount[0], mStream); + cudaCheck(cudaMemcpyAsync(mData.d_leaf_keys, d_keys, mData.nodeCount[0]*sizeof(uint64_t), cudaMemcpyDeviceToDevice, mStream)); + + CALL_CUBS(DeviceSelect::Unique, ShiftRightIterator<12>(mData.d_leaf_keys), d_keys, d_node_count+1, mData.nodeCount[0]);// count lower nodes + cudaCheck(cudaMemcpyAsync(mData.nodeCount+1, d_node_count+1, sizeof(uint32_t), cudaMemcpyDeviceToHost, mStream)); + mData.d_lower_keys = mMemPool.template alloc(mData.nodeCount[1], mStream); + cudaCheck(cudaMemcpyAsync(mData.d_lower_keys, d_keys, mData.nodeCount[1]*sizeof(uint64_t), cudaMemcpyDeviceToDevice, mStream)); + + mMemPool.free(d_keys, d_node_count); + if (mVerbose==2) mTimer.stop(); + + //printf("Leaf count = %u, lower count = %u, upper count = %u\n", mData.nodeCount[0], mData.nodeCount[1], mData.nodeCount[2]); +}// CudaPointsToGrid::countNodes + +//================================================================================================ + +template +template +inline BufferT CudaPointsToGrid::getBuffer(const PtrT, size_t pointCount, const BufferT &pool) +{ + auto sizeofPoint = [&]()->size_t{ + switch (mPointType){ + case PointType::PointID: return sizeof(uint32_t); + case PointType::World64: return sizeof(Vec3d); + case PointType::World32: return sizeof(Vec3f); + case PointType::Grid64: return sizeof(Vec3d); + case PointType::Grid32: return sizeof(Vec3f); + case PointType::Voxel32: return sizeof(Vec3f); + case PointType::Voxel16: return sizeof(Vec3u16); + case PointType::Voxel8: return sizeof(Vec3u8); + case PointType::Default: return pointer_traits::element_size; + default: return size_t(0);// PointType::Disable + } + }; + + mData.grid = 0;// grid is always stored at the start of the buffer! + mData.tree = NanoGrid::memUsage(); // grid ends and tree begins + mData.root = mData.tree + NanoTree::memUsage(); // tree ends and root node begins + mData.upper = mData.root + NanoRoot::memUsage(mData.nodeCount[2]); // root node ends and upper internal nodes begin + mData.lower = mData.upper + NanoUpper::memUsage()*mData.nodeCount[2]; // upper internal nodes ends and lower internal nodes begin + mData.leaf = mData.lower + NanoLower::memUsage()*mData.nodeCount[1]; // lower internal nodes ends and leaf nodes begin + mData.meta = mData.leaf + NanoLeaf::DataType::memUsage()*mData.nodeCount[0];// leaf nodes end and blind meta data begins + mData.blind = mData.meta + sizeof(GridBlindMetaData)*int( mPointType!=PointType::Disable ); // meta data ends and blind data begins + mData.size = mData.blind + pointCount*sizeofPoint();// end of buffer + + auto buffer = BufferT::create(mData.size, &pool, false);// only allocate buffer on the device + mData.d_bufferPtr = buffer.deviceData(); + if (mData.d_bufferPtr == nullptr) throw std::runtime_error("Failed to allocate grid buffer on the device"); + cudaCheck(cudaMemcpyAsync(mDeviceData, &mData, sizeof(Data), cudaMemcpyHostToDevice, mStream));// copy Data CPU -> GPU + return buffer; +}// CudaPointsToGrid::getBuffer + +//================================================================================================ + +template +template +inline void CudaPointsToGrid::processGridTreeRoot(const PtrT points, size_t pointCount) +{ + using Vec3T = typename remove_const::element_type>::type; + cudaLambdaKernel<<<1, 1, 0, mStream>>>(1, [=] __device__(size_t, Data *d_data, PointType pointType) { + // process Root + auto &root = d_data->getRoot(); + root.mBBox = CoordBBox(); // init to empty + root.mTableSize = d_data->nodeCount[2]; + root.mBackground = NanoRoot::ValueType(0);// background_value + root.mMinimum = root.mMaximum = NanoRoot::ValueType(0); + root.mAverage = root.mStdDevi = NanoRoot::FloatType(0); + + // process Tree + auto &tree = d_data->getTree(); + tree.setRoot(&root); + tree.setFirstNode(&d_data->getUpper(0)); + tree.setFirstNode(&d_data->getLower(0)); + tree.setFirstNode(&d_data->getLeaf(0)); + tree.mNodeCount[2] = tree.mTileCount[2] = d_data->nodeCount[2]; + tree.mNodeCount[1] = tree.mTileCount[1] = d_data->nodeCount[1]; + tree.mNodeCount[0] = tree.mTileCount[0] = d_data->nodeCount[0]; + tree.mVoxelCount = d_data->voxelCount; + + // process Grid + auto &grid = d_data->getGrid(); + grid.init({GridFlags::HasBBox, GridFlags::IsBreadthFirst}, d_data->size, d_data->map, mapToGridType()); + grid.mChecksum = ~uint64_t(0);// set all bits on which means it's disabled + grid.mBlindMetadataCount = is_same::value;// ? 1u : 0u; + grid.mBlindMetadataOffset = d_data->meta; + if (pointType != PointType::Disable) { + const auto lastLeaf = tree.mNodeCount[0] - 1; + grid.mData1 = d_data->pointsPerLeafPrefix[lastLeaf] + d_data->pointsPerLeaf[lastLeaf]; + auto &meta = d_data->getMeta(); + meta.mDataOffset = sizeof(GridBlindMetaData);// blind data is placed right after this meta data + meta.mValueCount = pointCount; + // Blind meta data + switch (pointType){ + case PointType::PointID: + grid.mGridClass = GridClass::PointIndex; + meta.mSemantic = GridBlindDataSemantic::PointId; + meta.mDataClass = GridBlindDataClass::IndexArray; + meta.mDataType = mapToGridType(); + meta.mValueSize = sizeof(uint32_t); + cudaStrcpy(meta.mName, "PointID: uint32_t indices to points"); + break; + case PointType::World64: + grid.mGridClass = GridClass::PointData; + meta.mSemantic = GridBlindDataSemantic::WorldCoords; + meta.mDataClass = GridBlindDataClass::AttributeArray; + meta.mDataType = mapToGridType(); + meta.mValueSize = sizeof(Vec3d); + cudaStrcpy(meta.mName, "World64: Vec3 point coordinates in world space"); + break; + case PointType::World32: + grid.mGridClass = GridClass::PointData; + meta.mSemantic = GridBlindDataSemantic::WorldCoords; + meta.mDataClass = GridBlindDataClass::AttributeArray; + meta.mDataType = mapToGridType(); + meta.mValueSize = sizeof(Vec3f); + cudaStrcpy(meta.mName, "World32: Vec3 point coordinates in world space"); + break; + case PointType::Grid64: + grid.mGridClass = GridClass::PointData; + meta.mSemantic = GridBlindDataSemantic::GridCoords; + meta.mDataClass = GridBlindDataClass::AttributeArray; + meta.mDataType = mapToGridType(); + meta.mValueSize = sizeof(Vec3d); + cudaStrcpy(meta.mName, "Grid64: Vec3 point coordinates in grid space"); + break; + case PointType::Grid32: + grid.mGridClass = GridClass::PointData; + meta.mSemantic = GridBlindDataSemantic::GridCoords; + meta.mDataClass = GridBlindDataClass::AttributeArray; + meta.mDataType = mapToGridType(); + meta.mValueSize = sizeof(Vec3f); + cudaStrcpy(meta.mName, "Grid32: Vec3 point coordinates in grid space"); + break; + case PointType::Voxel32: + grid.mGridClass = GridClass::PointData; + meta.mSemantic = GridBlindDataSemantic::VoxelCoords; + meta.mDataClass = GridBlindDataClass::AttributeArray; + meta.mDataType = mapToGridType(); + meta.mValueSize = sizeof(Vec3f); + cudaStrcpy(meta.mName, "Voxel32: Vec3 point coordinates in voxel space"); + break; + case PointType::Voxel16: + grid.mGridClass = GridClass::PointData; + meta.mSemantic = GridBlindDataSemantic::VoxelCoords; + meta.mDataClass = GridBlindDataClass::AttributeArray; + meta.mDataType = mapToGridType(); + meta.mValueSize = sizeof(Vec3u16); + cudaStrcpy(meta.mName, "Voxel16: Vec3 point coordinates in voxel space"); + break; + case PointType::Voxel8: + grid.mGridClass = GridClass::PointData; + meta.mSemantic = GridBlindDataSemantic::VoxelCoords; + meta.mDataClass = GridBlindDataClass::AttributeArray; + meta.mDataType = mapToGridType(); + meta.mValueSize = sizeof(Vec3u8); + cudaStrcpy(meta.mName, "Voxel8: Vec3 point coordinates in voxel space"); + break; + case PointType::Default: + grid.mGridClass = GridClass::PointData; + meta.mSemantic = GridBlindDataSemantic::WorldCoords; + meta.mDataClass = GridBlindDataClass::AttributeArray; + meta.mDataType = mapToGridType(); + meta.mValueSize = sizeof(Vec3T); + if constexpr(is_same::value) { + cudaStrcpy(meta.mName, "World32: Vec3 point coordinates in world space"); + } else if constexpr(is_same::value){ + cudaStrcpy(meta.mName, "World64: Vec3 point coordinates in world space"); + } else { + printf("Error in CudaPointsToGrid::processGridTreeRoot: expected Vec3T = Vec3f or Vec3d\n"); + } + break; + default: + printf("Error in CudaPointsToGrid::processGridTreeRoot: invalid pointType\n"); + } + } else if constexpr(BuildTraits::is_offindex) { + grid.mData1 = 1u + 512u*d_data->nodeCount[0]; + grid.mGridClass = GridClass::IndexGrid; + } + }, mDeviceData, mPointType);// cudaLambdaKernel + cudaCheckError(); + + char *dst = mData.getGrid().mGridName; + if (const char *src = mGridName.data()) { + cudaCheck(cudaMemcpyAsync(dst, src, GridData::MaxNameSize, cudaMemcpyHostToDevice, mStream)); + } else { + cudaCheck(cudaMemsetAsync(dst, 0, GridData::MaxNameSize, mStream)); + } +}// CudaPointsToGrid::processGridTreeRoot + +//================================================================================================ + +template +inline void CudaPointsToGrid::processUpperNodes() +{ + cudaLambdaKernel<<>>(mData.nodeCount[2], [=] __device__(size_t tid, Data *d_data) { + auto &root = d_data->getRoot(); + auto &upper = d_data->getUpper(tid); +#if 1 + auto keyToCoord = [](uint64_t key)->nanovdb::Coord{ + static constexpr int64_t offset = 1 << 31;// max values of uint32_t is 2^31 - 1 + static constexpr uint64_t MASK = (1u << 21) - 1; // used to mask out 21 lower bits + return nanovdb::Coord(int(int64_t(((key >> 42) & MASK) << 12) - offset), // x are the upper 21 bits + int(int64_t(((key >> 21) & MASK) << 12) - offset), // y are the middle 21 bits + int(int64_t(( key & MASK) << 12) - offset)); // z are the lower 21 bits + }; + const Coord ijk = keyToCoord(d_data->d_tile_keys[tid]); +#else + const Coord ijk = NanoRoot::KeyToCoord(d_data->d_tile_keys[tid]); +#endif + root.tile(tid)->setChild(ijk, &upper, &root); + upper.mBBox[0] = ijk; + upper.mFlags = 0; + upper.mValueMask.setOff(); + upper.mChildMask.setOff(); + upper.mMinimum = upper.mMaximum = NanoLower::ValueType(0); + upper.mAverage = upper.mStdDevi = NanoLower::FloatType(0); + }, mDeviceData); + cudaCheckError(); + + mMemPool.free(mData.d_tile_keys); + + const uint64_t valueCount = mData.nodeCount[2] << 15; + cudaLambdaKernel<<>>(valueCount, [=] __device__(size_t tid, Data *d_data) { + auto &upper = d_data->getUpper(tid >> 15); + upper.mTable[tid & 32767u].value = NanoUpper::ValueType(0);// background + }, mDeviceData); + cudaCheckError(); +}// CudaPointsToGrid::processUpperNodes + +//================================================================================================ + +template +inline void CudaPointsToGrid::processLowerNodes() +{ + cudaLambdaKernel<<>>(mData.nodeCount[1], [=] __device__(size_t tid, Data *d_data) { + auto &root = d_data->getRoot(); + const uint64_t lowerKey = d_data->d_lower_keys[tid]; + auto &upper = d_data->getUpper(lowerKey >> 15); + const uint32_t upperOffset = lowerKey & 32767u;// (1 << 15) - 1 = 32767 + upper.mChildMask.setOnAtomic(upperOffset); + auto &lower = d_data->getLower(tid); + upper.setChild(upperOffset, &lower); + lower.mBBox[0] = upper.offsetToGlobalCoord(upperOffset); + lower.mFlags = 0; + lower.mValueMask.setOff(); + lower.mChildMask.setOff(); + lower.mMinimum = lower.mMaximum = NanoLower::ValueType(0);// background; + lower.mAverage = lower.mStdDevi = NanoLower::FloatType(0); + }, mDeviceData); + cudaCheckError(); + + const uint64_t valueCount = mData.nodeCount[1] << 12; + cudaLambdaKernel<<>>(valueCount, [=] __device__(size_t tid, Data *d_data) { + auto &lower = d_data->getLower(tid >> 12); + lower.mTable[tid & 4095u].value = NanoLower::ValueType(0);// background + }, mDeviceData); + cudaCheckError(); +}// CudaPointsToGrid::processLowerNodes + +//================================================================================================ + +template +template +inline void CudaPointsToGrid::processLeafNodes(const PtrT points) +{ + const uint8_t flags = static_cast(mData.flags.data());// mIncludeStats ? 16u : 0u;// 4th bit indicates stats + + if (mVerbose==2) mTimer.start("process leaf meta data"); + // loop over leaf nodes and add it to its parent node + cudaLambdaKernel<<>>(mData.nodeCount[0], [=] __device__(size_t tid, Data *d_data) { + const uint64_t leafKey = d_data->d_leaf_keys[tid], tile_id = leafKey >> 27; + auto &upper = d_data->getUpper(tile_id); + const uint32_t lowerOffset = leafKey & 4095u, upperOffset = (leafKey >> 12) & 32767u; + auto &lower = *upper.getChild(upperOffset); + lower.mChildMask.setOnAtomic(lowerOffset); + auto &leaf = d_data->getLeaf(tid); + lower.setChild(lowerOffset, &leaf); + leaf.mBBoxMin = lower.offsetToGlobalCoord(lowerOffset); + leaf.mFlags = flags; + auto &valueMask = leaf.mValueMask; + valueMask.setOff();// initiate all bits to off + + if constexpr(is_same::value) { + leaf.mOffset = d_data->pointsPerLeafPrefix[tid]; + leaf.mPointCount = d_data->pointsPerLeaf[tid]; + } else if constexpr(BuildTraits::is_offindex) { + leaf.mOffset = tid*512u + 1u;// background is index 0 + leaf.mPrefixSum = 0u; + } else if constexpr(!BuildTraits::is_special) { + leaf.mAverage = leaf.mStdDevi = NanoLeaf::FloatType(0); + leaf.mMinimum = leaf.mMaximum = NanoLeaf::ValueType(0); + } + }, mDeviceData); cudaCheckError(); + + if (mVerbose==2) mTimer.restart("set active voxel state and values"); + // loop over all active voxels and set LeafNode::mValueMask and LeafNode::mValues + cudaLambdaKernel<<>>(mData.voxelCount, [=] __device__(size_t tid, Data *d_data) { + const uint32_t pointID = d_data->pointsPerVoxelPrefix[tid]; + const uint64_t voxelKey = d_data->d_keys[pointID]; + auto &upper = d_data->getUpper(voxelKey >> 36); + auto &lower = *upper.getChild((voxelKey >> 21) & 32767u); + auto &leaf = *lower.getChild((voxelKey >> 9) & 4095u); + const uint32_t n = voxelKey & 511u; + leaf.mValueMask.setOnAtomic(n);// <--- slow! + if constexpr(is_same::value) { + leaf.mValues[n] = uint16_t(pointID + d_data->pointsPerVoxel[tid] - leaf.offset()); + } else if constexpr(!BuildTraits::is_special) { + leaf.mValues[n] = NanoLeaf::ValueType(1);// set value of active voxels that are not points (or index) + } + }, mDeviceData); cudaCheckError(); + + mMemPool.free(mData.d_keys, mData.pointsPerVoxel, mData.pointsPerVoxelPrefix, mData.pointsPerLeafPrefix, mData.pointsPerLeaf); + + if (mVerbose==2) mTimer.restart("set inactive voxel values"); + const uint64_t denseVoxelCount = mData.nodeCount[0] << 9; + cudaLambdaKernel<<>>(denseVoxelCount, [=] __device__(size_t tid, Data *d_data) { + auto &leaf = d_data->getLeaf(tid >> 9u); + const uint32_t n = tid & 511u; + if (leaf.mValueMask.isOn(n)) return; + if constexpr(is_same::value) { + const uint32_t m = leaf.mValueMask.findPrev(n - 1); + leaf.mValues[n] = m < 512u ? leaf.mValues[m] : 0u; + } else if constexpr(!BuildTraits::is_special) { + leaf.mValues[n] = NanoLeaf::ValueType(0);// value of inactive voxels + } + }, mDeviceData); cudaCheckError(); + + if constexpr(BuildTraits::is_onindex) { + if (mVerbose==2) mTimer.restart("prefix-sum for index grid"); + uint64_t *devValueIndex = mMemPool.template alloc(mData.nodeCount[0], mStream); + auto devValueIndexPrefix = mMemPool.template alloc(mData.nodeCount[0], mStream); + kernels::fillValueIndexKernel<<>>(mData.nodeCount[0], devValueIndex, mDeviceData); + cudaCheckError(); + CALL_CUBS(DeviceScan::InclusiveSum, devValueIndex, devValueIndexPrefix, mData.nodeCount[0]); + mMemPool.free(devValueIndex); + kernels::leafPrefixSumKernel<<>>(mData.nodeCount[0], devValueIndexPrefix, mDeviceData); + cudaCheckError(); + mMemPool.free(devValueIndexPrefix); + } + + if constexpr(BuildTraits::is_indexmask) { + if (mVerbose==2) mTimer.restart("leaf.mMask = leaf.mValueMask"); + kernels::setMaskEqValMaskKernel<<>>(mData.nodeCount[0], mDeviceData); + cudaCheckError(); + } + if (mVerbose==2) mTimer.stop(); +}// CudaPointsToGrid::processLeafNodes + +//================================================================================================ + +template +template +inline void CudaPointsToGrid::processPoints(const PtrT, size_t) +{ + mMemPool.free(mData.d_indx, mStream); +} + +//================================================================================================ + +// Template specialization with BuildT = Point +template <> +template +inline void CudaPointsToGrid::processPoints(const PtrT points, size_t pointCount) +{ + switch (mPointType){ + case PointType::Disable: + throw std::runtime_error("CudaPointsToGrid::processPoints: mPointType == PointType::Disable\n"); + case PointType::PointID: + cudaLambdaKernel<<>>(pointCount, [=] __device__(size_t tid, Data *d_data) { + d_data->template getPoint(tid) = d_data->d_indx[tid]; + }, mDeviceData); cudaCheckError(); + break; + case PointType::World64: + cudaLambdaKernel<<>>(pointCount, [=] __device__(size_t tid, Data *d_data) { + d_data->template getPoint(tid) = points[d_data->d_indx[tid]]; + }, mDeviceData); cudaCheckError(); + break; + case PointType::World32: + cudaLambdaKernel<<>>(pointCount, [=] __device__(size_t tid, Data *d_data) { + d_data->template getPoint(tid) = points[d_data->d_indx[tid]]; + }, mDeviceData); cudaCheckError(); + break; + case PointType::Grid64: + cudaLambdaKernel<<>>(pointCount, [=] __device__(size_t tid, Data *d_data) { + d_data->template getPoint(tid) = d_data->map.applyInverseMap(points[d_data->d_indx[tid]]); + }, mDeviceData); cudaCheckError(); + break; + case PointType::Grid32: + cudaLambdaKernel<<>>(pointCount, [=] __device__(size_t tid, Data *d_data) { + d_data->template getPoint(tid) = d_data->map.applyInverseMapF(points[d_data->d_indx[tid]]); + }, mDeviceData); cudaCheckError(); + break; + case PointType::Voxel32: + cudaLambdaKernel<<>>(pointCount, [=] __device__(size_t tid, Data *d_data) { + worldToVoxel(d_data->template getPoint(tid), points[d_data->d_indx[tid]], d_data->map); + }, mDeviceData); cudaCheckError(); + break; + case PointType::Voxel16: + cudaLambdaKernel<<>>(pointCount, [=] __device__(size_t tid, Data *d_data) { + worldToVoxel(d_data->template getPoint(tid), points[d_data->d_indx[tid]], d_data->map); + }, mDeviceData); cudaCheckError(); + break; + case PointType::Voxel8: + cudaLambdaKernel<<>>(pointCount, [=] __device__(size_t tid, Data *d_data) { + worldToVoxel(d_data->template getPoint(tid), points[d_data->d_indx[tid]], d_data->map); + }, mDeviceData); cudaCheckError(); + break; + case PointType::Default: + cudaLambdaKernel<<>>(pointCount, [=] __device__(size_t tid, Data *d_data) { + d_data->template getPoint::element_type>(tid) = points[d_data->d_indx[tid]]; + }, mDeviceData); cudaCheckError(); + break; + default: + printf("Internal error in CudaPointsToGrid::processPoints\n"); + } + mMemPool.free(mData.d_indx); +}// CudaPointsToGrid::processPoints + +//================================================================================================ + +template +inline void CudaPointsToGrid::processBBox() +{ + if (mData.flags.isMaskOff(GridFlags::HasBBox)) { + mMemPool.free(mData.d_leaf_keys, mData.d_lower_keys); + return; + } + + // reset bbox in lower nodes + cudaLambdaKernel<<>>(mData.nodeCount[1], [=] __device__(size_t tid, Data *d_data) { + d_data->getLower(tid).mBBox = CoordBBox(); + }, mDeviceData); + cudaCheckError(); + + // update and propagate bbox from leaf -> lower/parent nodes + cudaLambdaKernel<<>>(mData.nodeCount[0], [=] __device__(size_t tid, Data *d_data) { + const uint64_t leafKey = d_data->d_leaf_keys[tid]; + auto &upper = d_data->getUpper(leafKey >> 27); + auto &lower = *upper.getChild((leafKey >> 12) & 32767u); + auto &leaf = d_data->getLeaf(tid); + leaf.updateBBox(); + lower.mBBox.expandAtomic(leaf.bbox()); + }, mDeviceData); + mMemPool.free(mData.d_leaf_keys); + cudaCheckError(); + + // reset bbox in upper nodes + cudaLambdaKernel<<>>(mData.nodeCount[2], [=] __device__(size_t tid, Data *d_data) { + d_data->getUpper(tid).mBBox = CoordBBox(); + }, mDeviceData); + cudaCheckError(); + + // propagate bbox from lower -> upper/parent node + cudaLambdaKernel<<>>(mData.nodeCount[1], [=] __device__(size_t tid, Data *d_data) { + const uint64_t lowerKey = d_data->d_lower_keys[tid]; + auto &upper = d_data->getUpper(lowerKey >> 15); + auto &lower = d_data->getLower(tid); + upper.mBBox.expandAtomic(lower.bbox()); + }, mDeviceData); + mMemPool.free(mData.d_lower_keys); + cudaCheckError() + + // propagate bbox from upper -> root/parent node + cudaLambdaKernel<<>>(mData.nodeCount[2], [=] __device__(size_t tid, Data *d_data) { + d_data->getRoot().mBBox.expandAtomic(d_data->getUpper(tid).bbox()); + }, mDeviceData); + cudaCheckError(); + + // update the world-bbox in the root node + cudaLambdaKernel<<<1, 1, 0, mStream>>>(1, [=] __device__(size_t, Data *d_data) { + d_data->getGrid().mWorldBBox = d_data->getRoot().mBBox.transform(d_data->map); + }, mDeviceData); + cudaCheckError(); +}// CudaPointsToGrid::processBBox + +//================================================================================================ + +template +GridHandle// Grid with PointType coordinates as blind data +cudaPointsToGrid(const PtrT d_xyz, int pointCount, double voxelSize, PointType type, const BufferT &buffer, cudaStream_t stream) +{ + CudaPointsToGrid converter(voxelSize, Vec3d(0.0), stream); + converter.setPointType(type); + return converter.getHandle(d_xyz, pointCount, buffer); +} + +//================================================================================================ + +template +GridHandle// Grid +cudaVoxelsToGrid(const PtrT d_ijk, size_t voxelCount, double voxelSize, const BufferT &buffer, cudaStream_t stream) +{ + CudaPointsToGrid converter(voxelSize, Vec3d(0.0), stream); + return converter.getHandle(d_ijk, voxelCount, buffer); +} + +//================================================================================================ + +template +GridHandle +cudaPointsToGrid(std::vector> vec, const BufferT &buffer, cudaStream_t stream) +{ + std::vector> handles; + for (auto &p : vec) handles.push_back(cudaPointsToGrid(std::get<0>(p), std::get<1>(p), std::get<2>(p), std::get<3>(p), buffer, stream)); + return mergeDeviceGrids(handles, stream); +} + +//================================================================================================ + +template +GridHandle +cudaVoxelsToGrid(std::vector> vec, const BufferT &buffer, cudaStream_t stream) +{ + std::vector> handles; + for (auto &p : vec) handles.push_back(cudaVoxelsToGrid(std::get<0>(p), std::get<1>(p), std::get<2>(p), buffer, stream)); + return mergeDeviceGrids(handles, stream); +} + +}// nanovdb namespace + +#endif // NVIDIA_CUDA_POINTS_TO_GRID_CUH_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/cuda/CudaSignedFloodFill.cuh b/nanovdb/nanovdb/util/cuda/CudaSignedFloodFill.cuh new file mode 100644 index 0000000000..2f4bf203d6 --- /dev/null +++ b/nanovdb/nanovdb/util/cuda/CudaSignedFloodFill.cuh @@ -0,0 +1,201 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +/*! + \file CudaSignedFloodFill.cuh + + \author Ken Museth + + \date May 3, 2023 + + \brief Performs signed flood-fill operation on the hierarchical tree structure on the device + + \todo This tools needs to handle the (extremely) rare case when root node + needs to be modified during the signed flood fill operation. This happens + when the root-table needs to be expanded with tile values (of size 4096^3) + that are completely inside the implicit surface. + + \warning The header file contains cuda device code so be sure + to only include it in .cu files (or other .cuh files) +*/ + +#ifndef NANOVDB_CUDA_SIGNED_FLOOD_FILL_CUH_HAS_BEEN_INCLUDED +#define NANOVDB_CUDA_SIGNED_FLOOD_FILL_CUH_HAS_BEEN_INCLUDED + +#include +#include +#include +#include +#include + +namespace nanovdb { + +/// @brief Performs signed flood-fill operation on the hierarchical tree structure on the device +/// @tparam BuildT Build type of the grid to be flood-filled +/// @param d_grid Non-const device pointer to the grid that will be flood-filled +/// @param verbose If true timing information will be printed to the terminal +/// @param stream optional cuda stream +template +typename enable_if::is_float, void>::type +cudaSignedFloodFill(NanoGrid *d_grid, bool verbose = false, cudaStream_t stream = 0); + +namespace {// anonymous namespace + +template +class CudaSignedFloodFill +{ +public: + CudaSignedFloodFill(bool verbose = false, cudaStream_t stream = 0) + : mStream(stream), mVerbose(verbose) {} + + /// @brief Toggle on and off verbose mode + /// @param on if true verbose is turned on + void setVerbose(bool on = true) {mVerbose = on;} + + void operator()(NanoGrid *d_grid); + +private: + cudaStream_t mStream{0}; + GpuTimer mTimer; + bool mVerbose{false}; + +};// CudaSignedFloodFill + +//================================================================================================ + +template +__global__ void cudaProcessRootNode(NanoTree *tree) +{ + // auto &root = tree->root(); + /* + using ChildT = typename RootT::ChildNodeType; + // Insert the child nodes into a map sorted according to their origin + std::map nodeKeys; + typename RootT::ChildOnIter it = root.beginChildOn(); + for (; it; ++it) nodeKeys.insert(std::pair(it.getCoord(), &(*it))); + static const Index DIM = RootT::ChildNodeType::DIM; + + // We employ a simple z-scanline algorithm that inserts inactive tiles with + // the inside value if they are sandwiched between inside child nodes only! + typename std::map::const_iterator b = nodeKeys.begin(), e = nodeKeys.end(); + if ( b == e ) return; + for (typename std::map::const_iterator a = b++; b != e; ++a, ++b) { + Coord d = b->first - a->first; // delta of neighboring coordinates + if (d[0]!=0 || d[1]!=0 || d[2]==Int32(DIM)) continue;// not same z-scanline or neighbors + const ValueT fill[] = { a->second->getLastValue(), b->second->getFirstValue() }; + if (!(fill[0] < 0) || !(fill[1] < 0)) continue; // scanline isn't inside + Coord c = a->first + Coord(0u, 0u, DIM); + for (; c[2] != b->first[2]; c[2] += DIM) root.addTile(c, mInside, false); + } + */ + //root.setBackground(mOutside, /*updateChildNodes=*/false); +}// cudaProcessRootNode + +//================================================================================================ + +template +__global__ void cudaProcessInternalNodes(NanoTree *tree, size_t count) +{ + using NodeT = typename NanoNode::type; + const int tid = blockIdx.x * blockDim.x + threadIdx.x; + if (tid >= count) return; + const uint32_t nValue = tid & (NodeT::SIZE - 1u); + auto &node = *(tree->template getFirstNode() + (tid >> (3*NodeT::LOG2DIM))); + const auto &mask = node.childMask(); + if (mask.isOn(nValue)) return;// ignore if child + auto value = tree->background();// initiate to outside value + auto n = mask.template findNext(nValue); + if (n < NodeT::SIZE) { + if (node.getChild(n)->getFirstValue() < 0) value = -value; + } else if ((n = mask.template findPrev(nValue)) < NodeT::SIZE) { + if (node.getChild(n)->getLastValue() < 0) value = -value; + } else if (node.getValue(0)<0) { + value = -value; + } + node.setValue(nValue, value); +}// cudaProcessInternalNodes + +//================================================================================================ + +template +__global__ void cudaProcessLeafNodes(NanoTree *tree, size_t count) +{ + using LeafT = NanoLeaf; + const size_t tid = blockIdx.x * blockDim.x + threadIdx.x; + if (tid >= count) return; + const uint32_t nVoxel = tid & (LeafT::SIZE - 1u); + auto *leaf = tree->getFirstLeaf() + (tid >> (3*LeafT::LOG2DIM)); + const auto &mask = leaf->valueMask(); + if (mask.isOn(nVoxel)) return; + auto *buffer = leaf->mValues; + auto n = mask.template findNext(nVoxel); + if (n == LeafT::SIZE && (n = mask.template findPrev(nVoxel)) == LeafT::SIZE) n = 0u; + buffer[nVoxel] = buffer[n]<0 ? -tree->background() : tree->background(); +}// cudaProcessLeafNodes + +//================================================================================================ + +template +__global__ void cudaCpyNodeCount(NanoGrid *d_grid, uint64_t *d_count) +{ + NANOVDB_ASSERT(d_grid->isSequential()); + for (int i=0; i<3; ++i) *d_count++ = d_grid->tree().nodeCount(i); + *d_count = d_grid->tree().root().tileCount(); +} + +}// anonymous namespace + +//================================================================================================ + +template +void CudaSignedFloodFill::operator()(NanoGrid *d_grid) +{ + static_assert(BuildTraits::is_float, "CudaSignedFloodFill only works on float grids"); + NANOVDB_ASSERT(d_grid); + uint64_t count[4], *d_count = nullptr; + cudaCheck(cudaMallocAsync((void**)&d_count, 4*sizeof(uint64_t), mStream)); + cudaCpyNodeCount<<<1, 1, 0, mStream>>>(d_grid, d_count); + cudaCheckError(); + cudaCheck(cudaMemcpyAsync(&count, d_count, 4*sizeof(uint64_t), cudaMemcpyDeviceToHost, mStream)); + cudaCheck(cudaFreeAsync(d_count, mStream)); + + static const int threadsPerBlock = 128; + auto blocksPerGrid = [&](size_t count)->uint32_t{return (count + (threadsPerBlock - 1)) / threadsPerBlock;}; + auto *tree = reinterpret_cast*>(d_grid + 1); + + if (mVerbose) mTimer.start("\nProcess leaf nodes"); + cudaProcessLeafNodes<<>>(tree, count[0]<<9); + cudaCheckError(); + + if (mVerbose) mTimer.restart("Process lower internal nodes"); + cudaProcessInternalNodes<<>>(tree, count[1]<<12); + cudaCheckError(); + + if (mVerbose) mTimer.restart("Process upper internal nodes"); + cudaProcessInternalNodes<<>>(tree, count[2]<<15); + cudaCheckError(); + + //if (mVerbose) mTimer.restart("Process root node"); + //cudaProcessRootNode<<<1, 1, 0, mStream>>>(tree); + if (mVerbose) mTimer.stop(); + cudaCheckError(); +}// CudaSignedFloodFill::operator() + +//================================================================================================ + +template +typename enable_if::is_float, void>::type +cudaSignedFloodFill(NanoGrid *d_grid, bool verbose, cudaStream_t stream) +{ + CudaSignedFloodFill sff(verbose, stream); + sff(d_grid); + auto *d_gridData = d_grid->data(); + GridChecksum cs = cudaGetGridChecksum(d_gridData, stream); + if (cs.mode() == ChecksumMode::Full) {// ChecksumMode::Partial checksum is unaffected + cudaGridChecksum(d_gridData, ChecksumMode::Full, stream); + } +} + +}// nanovdb namespace + +#endif // NANOVDB_CUDA_SIGNED_FLOOD_FILL_CUH_HAS_BEEN_INCLUDED diff --git a/nanovdb/nanovdb/util/cuda/CudaUtils.h b/nanovdb/nanovdb/util/cuda/CudaUtils.h new file mode 100644 index 0000000000..40001748ee --- /dev/null +++ b/nanovdb/nanovdb/util/cuda/CudaUtils.h @@ -0,0 +1,136 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +#ifndef NANOVDB_CUDA_UTILS_H_HAS_BEEN_INCLUDED +#define NANOVDB_CUDA_UTILS_H_HAS_BEEN_INCLUDED + +#include +#include + +//#if defined(DEBUG) || defined(_DEBUG) + static inline void gpuAssert(cudaError_t code, const char* file, int line, bool abort = true) + { + if (code != cudaSuccess) { + fprintf(stderr, "CUDA error %u: %s (%s:%d)\n", unsigned(code), cudaGetErrorString(code), file, line); + //fprintf(stderr, "CUDA Runtime Error: %s %s %d\n", cudaGetErrorString(code), file, line); + if (abort) exit(code); + } + } + static inline void ptrAssert(const void* ptr, const char* msg, const char* file, int line, bool abort = true) + { + if (ptr == nullptr) { + fprintf(stderr, "NULL pointer error: %s %s %d\n", msg, file, line); + if (abort) exit(1); + } else if (uint64_t(ptr) % NANOVDB_DATA_ALIGNMENT) { + fprintf(stderr, "Pointer misalignment error: %s %s %d\n", msg, file, line); + if (abort) exit(1); + } + } +//#else +// static inline void gpuAssert(cudaError_t, const char*, int, bool = true){} +// static inline void ptrAssert(void*, const char*, const char*, int, bool = true){} +//#endif + +// Convenience function for checking CUDA runtime API results +// can be wrapped around any runtime API call. No-op in release builds. +#define cudaCheck(ans) \ + { \ + gpuAssert((ans), __FILE__, __LINE__); \ + } + +#define checkPtr(ptr, msg) \ + { \ + ptrAssert((ptr), (msg), __FILE__, __LINE__); \ + } + +#define cudaSync() \ + { \ + cudaCheck(cudaDeviceSynchronize()); \ + } + +#define cudaCheckError() \ + { \ + cudaCheck(cudaGetLastError()); \ + } + +#if CUDART_VERSION < 11020 // 11.2 introduced cudaMallocAsync and cudaFreeAsync + +/// @brief Dummy implementation of cudaMallocAsync that calls cudaMalloc +/// @param d_ptr Device pointer to allocated device memory +/// @param size Number of bytes to allocate +/// @param dummy The stream establishing the stream ordering contract and the memory pool to allocate from (ignored) +/// @return Cuda error code +inline cudaError_t cudaMallocAsync(void** d_ptr, size_t size, cudaStream_t){return cudaMalloc(d_ptr, size);} + +/// @brief Dummy implementation of cudaFreeAsync that calls cudaFree +/// @param d_ptr Device pointer that will be freed +/// @param dummy The stream establishing the stream ordering promise (ignored) +/// @return Cuda error code +inline cudaError_t cudaFreeAsync(void* d_ptr, cudaStream_t){return cudaFree(d_ptr);} + +#endif + +#if defined(__CUDACC__)// the following functions only run on the GPU! + +// --- Wrapper for launching lambda kernels +template +__global__ void cudaLambdaKernel(const size_t numItems, Func func, Args... args) +{ + const int tid = blockIdx.x * blockDim.x + threadIdx.x; + if (tid >= numItems) return; + func(tid, args...); +} + +/// @brief Copy characters from @c src to @c dst on the device. +/// @param dst pointer to the character array to write to. +/// @param src pointer to the null-terminated character string to copy from. +/// @return pointer to the character array being written to. +/// @note Emulates the behaviour of std::strcpy. +__device__ inline char* cudaStrcpy(char *dst, const char *src) +{ + char *p = dst; + do {*p++ = *src;} while(*src++); + return dst; +} + +/// @brief Appends a copy of the character string pointed to by @c src to +/// the end of the character string pointed to by @c dst on the device. +/// @param dst pointer to the null-terminated byte string to append to. +/// @param src pointer to the null-terminated byte string to copy from. +/// @return pointer to the character array being appended to. +/// @note Emulates the behaviour of std::strcat. +__device__ inline char* cudaStrcat(char *dst, const char *src) +{ + char *p = dst; + while (*p) ++p; + cudaStrcpy(p, src); + return dst; +} + +/// @brief Compares two null-terminated byte strings lexicographically on the device. +/// @param lhs pointer to the null-terminated byte strings to compare +/// @param rhs pointer to the null-terminated byte strings to compare +/// @return Negative value if @c lhs appears before @c rhs in lexicographical order. +/// Zero if @c lhs and @c rhs compare equal. Positive value if @c lhs appears +/// after @c rhs in lexicographical order. +__device__ inline int cudaStrcmp(const char *lhs, const char *rhs) +{ + while(*lhs && (*lhs == *rhs)){ + lhs++; + rhs++; + } + return *(const unsigned char*)lhs - *(const unsigned char*)rhs;// zero if lhs == rhs +} + +/// @brief Test if two null-terminated byte strings are the same +/// @param lhs pointer to the null-terminated byte strings to compare +/// @param rhs pointer to the null-terminated byte strings to compare +/// @return true if the two c-strings are identical +__device__ inline bool cudaStrEq(const char *lhs, const char *rhs) +{ + return cudaStrcmp(lhs, rhs) == 0; +} + +#endif// __CUDACC__ + +#endif// NANOVDB_CUDA_UTILS_H_HAS_BEEN_INCLUDED \ No newline at end of file diff --git a/nanovdb/nanovdb/util/cuda/GpuTimer.h b/nanovdb/nanovdb/util/cuda/GpuTimer.h new file mode 100644 index 0000000000..6c6e217403 --- /dev/null +++ b/nanovdb/nanovdb/util/cuda/GpuTimer.h @@ -0,0 +1,110 @@ +// Copyright Contributors to the OpenVDB Project +// SPDX-License-Identifier: MPL-2.0 + +/// @file GpuTimer.h +/// +/// @author Ken Museth +/// +/// @brief A simple GPU timing class + +#ifndef NANOVDB_GPU_TIMER_H_HAS_BEEN_INCLUDED +#define NANOVDB_GPU_TIMER_H_HAS_BEEN_INCLUDED + +#include // for std::cerr +#include +#include + +namespace nanovdb { + +class GpuTimer +{ + cudaStream_t mStream{0}; + cudaEvent_t mStart, mStop; + +public: + /// @brief Default constructor + /// @param stream CUDA stream to be timed (defaults to stream 0) + /// @note Starts the timer + GpuTimer(cudaStream_t stream = 0) : mStream(stream) + { + cudaEventCreate(&mStart); + cudaEventCreate(&mStop); + cudaEventRecord(mStart, mStream); + } + + /// @brief Construct and start the timer + /// @param msg string message to be printed when timer is started + /// @param stream CUDA stream to be timed (defaults to stream 0) + /// @param os output stream for the message above + GpuTimer(const std::string &msg, cudaStream_t stream = 0, std::ostream& os = std::cerr) + : mStream(stream) + { + os << msg << " ... " << std::flush; + cudaEventCreate(&mStart); + cudaEventCreate(&mStop); + cudaEventRecord(mStart, mStream); + } + + /// @brief Destructor + ~GpuTimer() + { + cudaEventDestroy(mStart); + cudaEventDestroy(mStop); + } + + /// @brief Start the timer + /// @param stream CUDA stream to be timed (defaults to stream 0) + /// @param os output stream for the message above + void start() {cudaEventRecord(mStart, mStream);} + + /// @brief Start the timer + /// @param msg string message to be printed when timer is started + + /// @param os output stream for the message above + void start(const std::string &msg, std::ostream& os = std::cerr) + { + os << msg << " ... " << std::flush; + this->start(); + } + + /// @brief Start the timer + /// @param msg string message to be printed when timer is started + /// @param os output stream for the message above + void start(const char* msg, std::ostream& os = std::cerr) + { + os << msg << " ... " << std::flush; + this->start(); + } + + /// @brief elapsed time (since start) in miliseconds + /// @return elapsed time (since start) in miliseconds + float elapsed() + { + cudaEventRecord(mStop, mStream); + cudaEventSynchronize(mStop); + float diff = 0.0f; + cudaEventElapsedTime(&diff, mStart, mStop); + return diff; + } + + /// @brief stop the timer + /// @param os output stream for the message above + void stop(std::ostream& os = std::cerr) + { + float diff = this->elapsed(); + os << "completed in " << diff << " milliseconds" << std::endl; + } + + /// @brief stop and start the timer + /// @param msg string message to be printed when timer is started + /// @warning Remember to call start before restart + void restart(const std::string &msg, std::ostream& os = std::cerr) + { + this->stop(); + this->start(msg, os); + } +};// GpuTimer + +} // namespace nanovdb + +#endif // NANOVDB_GPU_TIMER_H_HAS_BEEN_INCLUDED diff --git a/openvdb/openvdb/points/AttributeArray.h b/openvdb/openvdb/points/AttributeArray.h index cbbab2d955..d767801fb0 100644 --- a/openvdb/openvdb/points/AttributeArray.h +++ b/openvdb/openvdb/points/AttributeArray.h @@ -1862,7 +1862,7 @@ TypedAttributeArray::writeMetadata(std::ostream& os, bool ou uint8_t flags(mFlags); uint8_t serializationFlags(0); Index size(mSize); - Index stride(mStrideOrTotalSize); + Index strideOrTotalSize(mStrideOrTotalSize); bool strideOfOne(this->stride() == 1); bool bloscCompression = io::getDataCompression(os) & io::COMPRESS_BLOSC; @@ -1904,7 +1904,7 @@ TypedAttributeArray::writeMetadata(std::ostream& os, bool ou os.write(reinterpret_cast(&size), sizeof(Index)); // write strided - if (!strideOfOne) os.write(reinterpret_cast(&stride), sizeof(Index)); + if (!strideOfOne) os.write(reinterpret_cast(&strideOrTotalSize), sizeof(Index)); } diff --git a/openvdb_cmd/vdb_tool/include/Tool.h b/openvdb_cmd/vdb_tool/include/Tool.h index c1887a5c3d..b397811a6d 100644 --- a/openvdb_cmd/vdb_tool/include/Tool.h +++ b/openvdb_cmd/vdb_tool/include/Tool.h @@ -52,7 +52,7 @@ #ifdef VDB_TOOL_USE_NANO #include #include -#include +#include #include #endif @@ -1224,6 +1224,7 @@ void Tool::writeNVDB(const std::string &fileName) const float tolerance = mParser.get("tolerance");// negative values means derive it from the grid class (eg ls or fog) const std::string stats = mParser.get("stats"); const std::string checksum = mParser.get("checksum"); + const int verbose = mParser.verbose ? 1 : 0; nanovdb::io::Codec codec = nanovdb::io::Codec::NONE;// compression codec for the file if (codec_str == "zip") { @@ -1284,35 +1285,24 @@ void Tool::writeNVDB(const std::string &fileName) auto openToNano = [&](const GridBase::Ptr& base) { if (auto floatGrid = GridBase::grid(base)) { + using SrcGridT = openvdb::FloatGrid; switch (qMode){ - case nanovdb::GridType::Fp4: { - nanovdb::OpenToNanoVDB s; - s.enableDithering(dither); - return s(*floatGrid, sMode, cMode, mParser.verbose ? 1 : 0); - } case nanovdb::GridType::Fp8: { - nanovdb::OpenToNanoVDB s; - s.enableDithering(dither); - return s(*floatGrid, sMode, cMode, mParser.verbose ? 1 : 0); - } case nanovdb::GridType::Fp16: { - nanovdb::OpenToNanoVDB s; - s.enableDithering(dither); - return s(*floatGrid, sMode, cMode, mParser.verbose ? 1 : 0); - } case nanovdb::GridType::FpN: { + case nanovdb::GridType::Fp4: + return nanovdb::createNanoGrid(*floatGrid, sMode, cMode, dither, verbose); + case nanovdb::GridType::Fp8: + return nanovdb::createNanoGrid(*floatGrid, sMode, cMode, dither, verbose); + case nanovdb::GridType::Fp16: + return nanovdb::createNanoGrid(*floatGrid, sMode, cMode, dither, verbose); + case nanovdb::GridType::FpN: if (absolute) { - nanovdb::OpenToNanoVDB s; - s.enableDithering(dither); - s.oracle() = nanovdb::AbsDiff(tolerance); - return s(*floatGrid, sMode, cMode, mParser.verbose ? 1 : 0); + return nanovdb::createNanoGrid(*floatGrid, sMode, cMode, dither, verbose, nanovdb::AbsDiff(tolerance)); } else { - nanovdb::OpenToNanoVDB s; - s.enableDithering(dither); - s.oracle() = nanovdb::RelDiff(tolerance); - return s(*floatGrid, sMode, cMode, mParser.verbose ? 1 : 0); + return nanovdb::createNanoGrid(*floatGrid, sMode, cMode, dither, verbose, nanovdb::RelDiff(tolerance)); } - } default: break;// 32 bit float grids are handled below + default: break;// 32 bit float grids are handled below }// end of switch } - return nanovdb::openToNanoVDB(base, sMode, cMode, mParser.verbose ? 1 : 0);// float and other grids + return nanovdb::openToNanoVDB(base, sMode, cMode, verbose);// float and other grids };// openToNano if (fileName=="stdout.nvdb") { diff --git a/pendingchanges/nanovdb.txt b/pendingchanges/nanovdb.txt new file mode 100644 index 0000000000..239c3f5e93 --- /dev/null +++ b/pendingchanges/nanovdb.txt @@ -0,0 +1,31 @@ + + +NanoVDB: + - Minor version changed from 4 to 6 (major version is unchanged since the ABI is preserved) + - Transition from C++11 to C++17 in NanoVDB.h and its tools + - New (backwards compatible ) file format that allows serialized grids to with streamed directly to file without headers + - Several new ways to construct and modify NanoVDB grids on the GPU + - New device function to convert points into a compact grid: nanovdb::cudaPointsToGrid + - Improved and renamed device function that converts voxels into a grid: nanovdb::cudaVoxelsToGrid + - Introduced a new extendable API for acceleration of custom random-access methods, e.g. getValue(ijk) + - Index grids in 4 flavors (Index, OnIndex, IndexMask, and OnIndexMask) + - Introduced new (dummy) build-type nanovdb::Points and nanovdb::GridType::PointIndex + - Introduced new types nanovdb::GridType::Vec3u16 and nanovdb::GridType::Vec3u8 used for compressed representations of point coordinates as blind data + - CreateNanoGrid.h is replacing GridBuilder.h, IndexGridBuilder.h and OpenToNanoVDB.h + - Moved CudaDeviceBuffer.h to cuda/CudaDeviceBuffer.h + - Added cuda/CudaUtils.h and cuda/GpuTimer.h with cuda utility functions + - Added cuda/CudaPointToGrids.cuh that constructs device grids from lists of points or voxels + - Added cuda/CudaIndexToGrid.cuh that converts IndexGrids and values into regular Grids + - Added cuda/CudaSignedFloodFill.cuh that performs signed-flood filing on SDF on the GPU + - Added cuda/CudaAddBlindData.cuh that adds bind data to an existing grid on the GPU + - Added cuda/CudaGridChecksum.cuh that computes CRC32 checksums of grids on the GPU + - Added cuda/CudaGridHandle.cuh that handles grids on the GPU + - Added cuda/CudaNodeManager.cuh that constructs a NodeManager on the GPU + - Added cuda/CudaGridStats.cuh that computes grid statistics on the GPU + - The move constructor in GridHandle now requires the GridBuffer to actually contain a valid grid + - Added new types: Ve4f, Ve4d, ValueIndex, ValueOnIndex, ValueIndexMask, and ValueOnIndexMask + - Major improvements to GridBuilder.h, which allows user to construct grids with random access on the host + - Numerous improvements in NanoVDB.h: e.g. Customizable get/set methods on ValueAccessor, BitFlags, transform(Map), expandAtomic(BBox), expandAtomic(Coord), intersectAtomic(BBox), pi(), BuildTraits, more documentation, Mask:: DenseIterator, Mask:: setOnAtomic,Mask:: setOffAtomic, Map constructors, DataType are now public vs private in all node types, GridMetaData can now be copied + - PNanoVDB.h is now in sync with NanoVDB.h + - Added PrefixSum.h for concurrent computation of prefix sum on the host + - Primitives.h can now create grids on the CPU with SDF, FOG and point of torus \ No newline at end of file From fc4a5592d65207f5f64e2e94c35cbc43d2e6f42c Mon Sep 17 00:00:00 2001 From: Ken Museth Date: Fri, 27 Oct 2023 18:34:03 -0700 Subject: [PATCH 17/22] Cleanup several aspects of the Python bindings (copy of PR #1696) (#1696) Signed-off-by: Ken Museth --- openvdb/openvdb/python/pyGrid.h | 50 +-- openvdb/openvdb/python/pyGridBase.cc | 8 +- openvdb/openvdb/python/pyOpenVDBModule.cc | 72 --- openvdb/openvdb/python/pyTransform.cc | 19 +- openvdb/openvdb/python/pyTypeCasters.h | 516 ++++++++++++++++++---- openvdb/openvdb/python/pyutil.h | 12 +- 6 files changed, 462 insertions(+), 215 deletions(-) diff --git a/openvdb/openvdb/python/pyGrid.h b/openvdb/openvdb/python/pyGrid.h index 55d96c2622..9249eaf6d2 100644 --- a/openvdb/openvdb/python/pyGrid.h +++ b/openvdb/openvdb/python/pyGrid.h @@ -68,7 +68,7 @@ inline bool sharesWith(const GridType& grid, py::object other) { if (py::isinstance(other)) { - typename GridType::ConstPtr otherGrid = other.cast(); + typename GridType::ConstPtr otherGrid = py::cast(other); return (&otherGrid->tree() == &grid.tree()); } return false; @@ -893,7 +893,7 @@ applyMap(const char* methodName, GridType& grid, py::object funcObj) // Verify that the result is of type GridType::ValueType. try { - result.cast(); + py::cast(result); } catch (py::cast_error&) { std::ostringstream os; os << "expected callable argument to "; @@ -904,7 +904,7 @@ applyMap(const char* methodName, GridType& grid, py::object funcObj) throw py::type_error(os.str()); } - it.setValue(result.cast()); + it.setValue(py::cast(result)); } } @@ -955,7 +955,7 @@ struct TreeCombineOp throw py::type_error(os.str()); } - result = resultObj.cast(); + result = py::cast(resultObj); } py::function op; }; @@ -1177,7 +1177,7 @@ class IterValueProxy py::object getItem(py::object keyObj) const { if (py::isinstance(keyObj)) { - const std::string key = keyObj.cast(); + const std::string key = py::cast(keyObj); if (key == "value") return py::cast(this->getValue()); else if (key == "active") return py::cast(this->getActive()); else if (key == "depth") return py::cast(this->getDepth()); @@ -1185,7 +1185,7 @@ class IterValueProxy else if (key == "max") return py::cast(this->getBBoxMax()); else if (key == "count") return py::cast(this->getVoxelCount()); } - throw py::key_error(keyObj.attr("__repr__")().cast()); + throw py::key_error(py::cast(keyObj.attr("__repr__")())); return py::object(); } @@ -1195,20 +1195,20 @@ class IterValueProxy void setItem(py::object keyObj, py::object valObj) { if (py::isinstance(keyObj)) { - const std::string key = keyObj.cast(); + const std::string key = py::cast(keyObj); if (key == "value") { - this->setValue(valObj.cast()); return; + this->setValue(py::cast(valObj)); return; } else if (key == "active") { - this->setActive(valObj.cast()); return; + this->setActive(py::cast(valObj)); return; } else if (this->hasKey(key)) { std::ostringstream os; os << "can't set attribute '"; - os << keyObj.attr("__repr__")().cast(); + os << py::cast(keyObj.attr("__repr__")()); os << "'"; throw py::attribute_error(os.str()); } } - throw py::key_error(keyObj.attr("__repr__")().cast()); + throw py::key_error(py::cast(keyObj.attr("__repr__")())); } bool operator==(const IterValueProxy& other) const @@ -1235,7 +1235,7 @@ class IterValueProxy } // print ", ".join(valuesAsStrings) py::object joined = py::str(", ").attr("join")(valuesAsStrings); - std::string s = joined.cast(); + std::string s = py::cast(joined); os << "{" << s << "}"; return os; } @@ -1379,13 +1379,9 @@ struct PickleSuite } // Construct a state tuple for the serialized Grid. -#if PY_MAJOR_VERSION >= 3 // Convert the byte string to a "bytes" sequence. const std::string s = ostr.str(); py::bytes bytesObj(s); -#else - py::str bytesObj(ostr.str()); -#endif return py::make_tuple(bytesObj); } @@ -1397,25 +1393,16 @@ struct PickleSuite std::string serialized; if (!badState) { // Extract the sequence containing the serialized Grid. -#if PY_MAJOR_VERSION >= 3 if (py::isinstance(state[0])) - serialized = state[0].cast(); -#else - if (py::isinstance(state[0])) - serialized = state[0].cast(); -#endif + serialized = py::cast(state[0]); else badState = true; } if (badState) { std::ostringstream os; -#if PY_MAJOR_VERSION >= 3 os << "expected (dict, bytes) tuple in call to __setstate__; found "; -#else - os << "expected (dict, str) tuple in call to __setstate__; found "; -#endif - os << state.attr("__repr__")().cast(); + os << py::cast(state.attr("__repr__")()); throw py::value_error(os.str()); } @@ -1457,14 +1444,15 @@ exportGrid(py::module_ m) using ValueAllIterT = typename GridType::ValueAllIter; const std::string pyGridTypeName = Traits::name(); - const std::string defaultCtorDescr = "Initialize with a background value of " - + pyutil::str(pyGrid::getZeroValue()) + "."; + std::stringstream docstream; + docstream << "Initialize with a background value of " << pyGrid::getZeroValue() << "."; + std::string docstring = docstream.str(); // Define the Grid wrapper class and make it the current scope. py::class_(m, /*classname=*/pyGridTypeName.c_str(), /*docstring=*/(Traits::descr()).c_str()) - .def(py::init<>(), defaultCtorDescr.c_str()) + .def(py::init<>(), docstring.c_str()) .def(py::init(), py::arg("background"), "Initialize with the given background value.") @@ -1711,7 +1699,7 @@ exportGrid(py::module_ m) IterWrap::wrap(m); // Add the Python type object for this grid type to the module-level list. - m.attr("GridTypes").cast().append(m.attr(pyGridTypeName.c_str())); + py::cast(m.attr("GridTypes")).append(m.attr(pyGridTypeName.c_str())); } } // namespace pyGrid diff --git a/openvdb/openvdb/python/pyGridBase.cc b/openvdb/openvdb/python/pyGridBase.cc index 19fa054dc6..0e10a58381 100644 --- a/openvdb/openvdb/python/pyGridBase.cc +++ b/openvdb/openvdb/python/pyGridBase.cc @@ -100,12 +100,8 @@ exportGridBase(py::module_ m) auto getMetadataKeys = [](GridBase::ConstPtr grid) { -#if PY_MAJOR_VERSION >= 3 // Return an iterator over the "keys" view of a dict. return py::make_key_iterator(static_cast(*grid).beginMeta(), static_cast(*grid).endMeta()); -#else - return py::dict(py::cast(static_cast(*grid))).iterkeys(); -#endif }; @@ -118,7 +114,7 @@ exportGridBase(py::module_ m) MetaMap metamap; metamap.insertMeta(name, *metadata); // todo: Add/refactor out type_casters for each TypedMetadata from MetaMap's type_caster - return py::dict(py::cast(metamap))[py::str(name)].cast(); + return py::cast(py::dict(py::cast(metamap))[py::str(name)]); }; @@ -135,7 +131,7 @@ exportGridBase(py::module_ m) // todo: Add/refactor out type_casters for each TypedMetadata from MetaMap's type_caster py::dict dictObj; dictObj[py::str(name)] = value; - MetaMap metamap = dictObj.cast(); + MetaMap metamap = py::cast(dictObj); if (Metadata::Ptr metadata = metamap[name]) { grid->removeMeta(name); diff --git a/openvdb/openvdb/python/pyOpenVDBModule.cc b/openvdb/openvdb/python/pyOpenVDBModule.cc index 37dd42287f..ed6754ac56 100644 --- a/openvdb/openvdb/python/pyOpenVDBModule.cc +++ b/openvdb/openvdb/python/pyOpenVDBModule.cc @@ -371,78 +371,6 @@ PYBIND11_MODULE(PY_OPENVDB_MODULE_NAME, m) #undef PYOPENVDB_TRANSLATE_EXCEPTION - // Basic bindings for these Vec types are required to support them as - // default arguments to functions. - py::class_(m, "Coord") - .def(py::init<>()) - .def(py::init()) - .def(py::init()) - .def(py::self == py::self) - .def(py::self != py::self); - - py::class_(m, "Vec2i") - .def(py::init<>()) - .def(py::init()) - .def(py::init()) - .def(py::self == py::self) - .def(py::self != py::self); - - py::class_(m, "Vec2f") - .def(py::init<>()) - .def(py::init()) - .def(py::init()) - .def(py::self == py::self) - .def(py::self != py::self); - - py::class_(m, "Vec2d") - .def(py::init<>()) - .def(py::init()) - .def(py::init()) - .def(py::self == py::self) - .def(py::self != py::self); - - py::class_(m, "Vec3i") - .def(py::init<>()) - .def(py::init()) - .def(py::init()) - .def(py::self == py::self) - .def(py::self != py::self); - - py::class_(m, "Vec3f") - .def(py::init<>()) - .def(py::init()) - .def(py::init()) - .def(py::self == py::self) - .def(py::self != py::self); - - py::class_(m, "Vec3d") - .def(py::init<>()) - .def(py::init()) - .def(py::init()) - .def(py::self == py::self) - .def(py::self != py::self); - - py::class_(m, "Vec4i") - .def(py::init<>()) - .def(py::init()) - .def(py::init()) - .def(py::self == py::self) - .def(py::self != py::self); - - py::class_(m, "Vec4f") - .def(py::init<>()) - .def(py::init()) - .def(py::init()) - .def(py::self == py::self) - .def(py::self != py::self); - - py::class_(m, "Vec4d") - .def(py::init<>()) - .def(py::init()) - .def(py::init()) - .def(py::self == py::self) - .def(py::self != py::self); - py::class_(m, "PointDataIndex32") .def(py::init(), py::arg("i") = openvdb::Index32(0)); diff --git a/openvdb/openvdb/python/pyTransform.cc b/openvdb/openvdb/python/pyTransform.cc index c335e60dcf..b5116dfaef 100644 --- a/openvdb/openvdb/python/pyTransform.cc +++ b/openvdb/openvdb/python/pyTransform.cc @@ -99,12 +99,8 @@ struct PickleSuite // Construct a state tuple comprising the version numbers of // the serialization format and the serialized Transform. -#if PY_MAJOR_VERSION >= 3 // Convert the byte string to a "bytes" sequence. py::bytes bytesObj(ostr.str()); -#else - py::str bytesObj(ostr.str()); -#endif return py::make_tuple( uint32_t(OPENVDB_LIBRARY_MAJOR_VERSION), uint32_t(OPENVDB_LIBRARY_MINOR_VERSION), @@ -125,7 +121,7 @@ struct PickleSuite uint32_t version[3] = { 0, 0, 0 }; for (int i = 0; i < 3 && !badState; ++i) { if (py::isinstance(state[idx[i]])) - version[i] = state[idx[i]].cast(); + version[i] = py::cast(state[idx[i]]); else badState = true; } libVersion.first = version[0]; @@ -137,24 +133,15 @@ struct PickleSuite if (!badState) { // Extract the sequence containing the serialized Transform. py::object bytesObj = state[int(STATE_XFORM)]; -#if PY_MAJOR_VERSION >= 3 if (py::isinstance(bytesObj)) - serialized = bytesObj.cast(); -#else - if (py::isinstance(bytesObj)) - serialized = bytesObj.cast(); -#endif + serialized = py::cast(bytesObj); else badState = true; } if (badState) { std::ostringstream os; -#if PY_MAJOR_VERSION >= 3 os << "expected (int, int, int, bytes) tuple in call to __setstate__; found "; -#else - os << "expected (int, int, int, str) tuple in call to __setstate__; found "; -#endif - os << state.attr("__repr__")().cast(); + os << py::cast(state.attr("__repr__")()); throw py::value_error(os.str()); } diff --git a/openvdb/openvdb/python/pyTypeCasters.h b/openvdb/openvdb/python/pyTypeCasters.h index ea176b4f96..b30af05c6a 100644 --- a/openvdb/openvdb/python/pyTypeCasters.h +++ b/openvdb/openvdb/python/pyTypeCasters.h @@ -633,11 +633,7 @@ namespace pybind11 { namespace detail { PyObject* source = src.ptr(); PyObject* number = PyNumber_Long(source); if (number) { -#if PY_MAJOR_VERSION >= 3 value = static_cast(PyLong_AsLong(number)); -#else - value = static_cast(PyInt_AsLong(number)); -#endif } Py_XDECREF(number); @@ -661,11 +657,7 @@ namespace pybind11 { namespace detail { PyObject* source = src.ptr(); PyObject* number = PyNumber_Long(source); if (number) { -#if PY_MAJOR_VERSION >= 3 value = static_cast(PyLong_AsLong(number)); -#else - value = static_cast(PyInt_AsLong(number)); -#endif } Py_XDECREF(number); @@ -681,44 +673,393 @@ namespace pybind11 { namespace detail { } }; - template <> struct type_caster { + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(openvdb::PointDataIndex32, const_name("openvdb::PointDataIndex32")); + + bool load(handle src, bool) { + PyObject* source = src.ptr(); + PyObject* number = PyNumber_Long(source); + if (number) { + value = static_cast(PyLong_AsLong(number)); + } + Py_XDECREF(number); + + if (PyErr_Occurred()) + return false; + + return true; + } + + static handle cast(openvdb::PointDataIndex32 src, return_value_policy, handle) { + py::int_ integer(static_cast(src)); + return integer.release(); + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(openvdb::PointDataIndex64, const_name("openvdb::PointDataIndex64")); + + bool load(handle src, bool) { + PyObject* source = src.ptr(); + PyObject* number = PyNumber_Long(source); + if (number) { + value = static_cast(PyLong_AsLong(number)); + } + Py_XDECREF(number); + + if (PyErr_Occurred()) + return false; + + return true; + } + + static handle cast(openvdb::PointDataIndex64 src, return_value_policy, handle) { + py::int_ integer(static_cast(src)); + return integer.release(); + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(openvdb::StringMetadata, _("openvdb::StringMetadata")); + + bool load(handle src, bool) { + if (!py::isinstance(src)) { + return false; + } + + value.setValue(py::cast(src)); + return true; + } + + static handle cast(openvdb::StringMetadata src, return_value_policy, handle) { + return py::str(src.value()).release(); + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(openvdb::BoolMetadata, _("openvdb::BoolMetadata")); + + bool load(handle src, bool) { + if (!py::isinstance(src)) { + return false; + } + + value.setValue(py::cast(src)); + return true; + } + + static handle cast(openvdb::BoolMetadata src, return_value_policy, handle) { + return py::bool_(src.value()).release(); + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(openvdb::Int32Metadata, _("openvdb::Int32Metadata")); + + bool load(handle src, bool) { + if (!py::isinstance(src)) { + return false; + } + + value.setValue(py::cast(src)); + return true; + } + + static handle cast(openvdb::Int32Metadata src, return_value_policy, handle) { + return py::int_(src.value()).release(); + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(openvdb::Int64Metadata, _("openvdb::Int64Metadata")); + + bool load(handle src, bool) { + if (!py::isinstance(src)) { + return false; + } + + value.setValue(py::cast(src)); + return true; + } + + static handle cast(openvdb::Int64Metadata src, return_value_policy, handle) { + return py::int_(src.value()).release(); + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(openvdb::FloatMetadata, _("openvdb::FloatMetadata")); + + bool load(handle src, bool) { + if (!py::isinstance(src)) { + return false; + } + + value.setValue(py::cast(src)); + return true; + } + + static handle cast(openvdb::FloatMetadata src, return_value_policy, handle) { + return py::float_(src.value()).release(); + } + }; + + template <> struct type_caster { public: - PYBIND11_TYPE_CASTER(openvdb::MetaMap, _("openvdb::Metamap")); + PYBIND11_TYPE_CASTER(openvdb::DoubleMetadata, _("openvdb::DoubleMetadata")); bool load(handle src, bool) { + if (!py::isinstance(src)) { + return false; + } + + value.setValue(py::cast(src)); + return true; + } + + static handle cast(openvdb::DoubleMetadata src, return_value_policy, handle) { + return py::float_(src.value()).release(); + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(openvdb::Vec2IMetadata, _("openvdb::Vec2IMetadata")); + + bool load(handle src, bool convert) { + if (!py::isinstance(src)) { + return false; + } + + make_caster conv; + if (!conv.load(src, convert)) + return false; + + value.setValue(cast_op(std::move(conv))); + return true; + } + + static handle cast(openvdb::Vec2IMetadata src, return_value_policy policy, handle parent) { + return make_caster::cast(src.value(), policy, parent); + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(openvdb::Vec3IMetadata, _("openvdb::Vec3IMetadata")); + + bool load(handle src, bool convert) { + if (!py::isinstance(src)) { + return false; + } + + make_caster conv; + if (!conv.load(src, convert)) + return false; + + value.setValue(cast_op(std::move(conv))); + return true; + } + + static handle cast(openvdb::Vec3IMetadata src, return_value_policy policy, handle parent) { + return make_caster::cast(src.value(), policy, parent); + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(openvdb::Vec4IMetadata, _("openvdb::Vec4IMetadata")); + + bool load(handle src, bool convert) { + if (!py::isinstance(src)) { + return false; + } + + make_caster conv; + if (!conv.load(src, convert)) + return false; + + value.setValue(cast_op(std::move(conv))); + return true; + } + + static handle cast(openvdb::Vec4IMetadata src, return_value_policy policy, handle parent) { + return make_caster::cast(src.value(), policy, parent); + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(openvdb::Vec2DMetadata, _("openvdb::Vec2DMetadata")); + + bool load(handle src, bool convert) { + if (!py::isinstance(src)) { + return false; + } + + make_caster conv; + if (!conv.load(src, convert)) + return false; + + value.setValue(cast_op(std::move(conv))); + return true; + } + + static handle cast(openvdb::Vec2DMetadata src, return_value_policy policy, handle parent) { + return make_caster::cast(src.value(), policy, parent); + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(openvdb::Vec3DMetadata, _("openvdb::Vec3DMetadata")); + + bool load(handle src, bool convert) { + if (!py::isinstance(src)) { + return false; + } + + make_caster conv; + if (!conv.load(src, convert)) + return false; + + value.setValue(cast_op(std::move(conv))); + return true; + } + + static handle cast(openvdb::Vec3DMetadata src, return_value_policy policy, handle parent) { + return make_caster::cast(src.value(), policy, parent); + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(openvdb::Vec4DMetadata, _("openvdb::Vec4DMetadata")); + + bool load(handle src, bool convert) { + if (!py::isinstance(src)) { + return false; + } + + make_caster conv; + if (!conv.load(src, convert)) + return false; + + value.setValue(cast_op(std::move(conv))); + return true; + } + + static handle cast(openvdb::Vec4DMetadata src, return_value_policy policy, handle parent) { + return make_caster::cast(src.value(), policy, parent); + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(openvdb::Mat4SMetadata, _("openvdb::Mat4SMetadata")); + + bool load(handle src, bool convert) { + if (!py::isinstance(src)) { + return false; + } + + make_caster conv; + if (!conv.load(src, convert)) + return false; + + value.setValue(cast_op(std::move(conv))); + return true; + } + + static handle cast(openvdb::Mat4SMetadata src, return_value_policy policy, handle parent) { + return make_caster::cast(src.value(), policy, parent); + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(openvdb::Mat4DMetadata, _("openvdb::Mat4DMetadata")); + + bool load(handle src, bool convert) { + if (!py::isinstance(src)) { + return false; + } + + make_caster conv; + if (!conv.load(src, convert)) + return false; + + value.setValue(cast_op(std::move(conv))); + return true; + } + + static handle cast(openvdb::Mat4DMetadata src, return_value_policy policy, handle parent) { + return make_caster::cast(src.value(), policy, parent); + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(openvdb::MetaMap, _("openvdb::MetaMap")); + + bool load(handle src, bool convert) { py::dict dictionary = py::reinterpret_borrow(src); - for (auto item : dictionary) { + for (auto [key, val] : dictionary) { std::string name; - py::object key = py::reinterpret_borrow(item.first); if (py::isinstance(key)) { - name = key.cast(); + name = py::cast(key); } else { throw py::type_error("Expected string as metadata name"); } - openvdb::Metadata::Ptr metadata; // Note: the order of the following tests is significant, as it // avoids unnecessary type promotion (e.g., of ints to doubles). // Python does not natively support single precision and therefore // all floating point metadata is promoted to doubles. - py::object val = py::reinterpret_borrow(item.second); + bool success = false; if (py::isinstance(val)) { - metadata.reset(new openvdb::StringMetadata(val.cast())); + make_caster conv; + if(conv.load(val, convert)) { + value.insertMeta(name, cast_op(std::move(conv))); + success = true; + } } else if (py::isinstance(val)) { - metadata.reset(new openvdb::BoolMetadata(val.cast())); + make_caster conv; + if(conv.load(val, convert)) { + value.insertMeta(name, cast_op(std::move(conv))); + success = true; + } } else if (py::isinstance(val)) { - const openvdb::Int64 n = val.cast(); - if (n <= std::numeric_limits::max() - && n >= std::numeric_limits::min()) { - metadata.reset(new openvdb::Int32Metadata(static_cast(n))); + const openvdb::Int64 n = py::cast(val); + if (n <= std::numeric_limits::max() && n >= std::numeric_limits::min()) { + make_caster conv; + if(conv.load(val, convert)) { + value.insertMeta(name, cast_op(std::move(conv))); + success = true; + } } else { - metadata.reset(new openvdb::Int64Metadata(n)); + make_caster conv; + if(conv.load(val, convert)) { + value.insertMeta(name, cast_op(std::move(conv))); + success = true; + } } } else if (py::isinstance(val)) { - metadata.reset(new openvdb::DoubleMetadata(val.cast())); + make_caster conv; + if(conv.load(val, convert)) { + value.insertMeta(name, cast_op(std::move(conv))); + success = true; + } } else if (py::isinstance(val)) { - py::tuple t = val.cast(); + py::tuple t = py::cast(val); size_t size = t.size(); bool isIntegerTuple = true; for (size_t i = 0; i < size; ++i) { @@ -727,110 +1068,127 @@ namespace pybind11 { namespace detail { if (isIntegerTuple) { switch(size) { - case 2: - metadata.reset(new openvdb::Vec2IMetadata(t.cast())); + case 2: { + make_caster conv; + if (conv.load(val, convert)) { + value.insertMeta(name, cast_op(std::move(conv))); + success = true; + } break; - case 3: - metadata.reset(new openvdb::Vec3IMetadata(t.cast())); + } + case 3: { + make_caster conv; + if (conv.load(val, convert)) { + value.insertMeta(name, cast_op(std::move(conv))); + success = true; + } break; - case 4: - metadata.reset(new openvdb::Vec4IMetadata(t.cast())); + } + case 4: { + make_caster conv; + if (conv.load(val, convert)) { + value.insertMeta(name, cast_op(std::move(conv))); + success = true; + } break; + } default: break; } } else { switch(size) { - case 2: - metadata.reset(new openvdb::Vec2DMetadata(t.cast())); + case 2: { + make_caster conv; + if (conv.load(val, convert)) { + value.insertMeta(name, cast_op(std::move(conv))); + success = true; + } break; - case 3: - metadata.reset(new openvdb::Vec3DMetadata(t.cast())); + } + case 3: { + make_caster conv; + if (conv.load(val, convert)) { + value.insertMeta(name, cast_op(std::move(conv))); + success = true; + } break; - case 4: - metadata.reset(new openvdb::Vec4DMetadata(t.cast())); + } + case 4: { + make_caster conv; + if (conv.load(val, convert)) { + value.insertMeta(name, cast_op(std::move(conv))); + success = true; + } break; + } default: break; } } } else if (py::isinstance(val)) { - py::list list = val.cast(); - bool valid = (list.size() == 4); - if (valid) { - for (size_t i = 0; i < list.size(); ++i) { - valid &= py::isinstance(list[i]); - if (valid) { - py::list sublist = list[i].cast(); - valid &= (sublist.size() == 4); - if (valid) { - for (size_t j = 0; j < sublist.size(); ++j) { - valid &= (py::isinstance(sublist[j]) || py::isinstance(sublist[j])); - } - } - } - } - } - if (valid) { - metadata.reset(new openvdb::Mat4DMetadata(val.cast())); + make_caster conv; + if (conv.load(val, convert)) { + value.insertMeta(name, cast_op(std::move(conv))); + success = true; } } else if (py::isinstance(val)) { - metadata = val.cast(); + openvdb::Metadata::Ptr metadata = py::cast(val); + if (metadata) { + value.insertMeta(name, *metadata); + } + } - if (metadata) { - value.insertMeta(name, *metadata); - } else { - const std::string valAsStr = (val.attr("__str__")()).cast(); - const std::string valType = val.attr("__class__").attr("__name__").cast(); + if (!success) { + const std::string valAsStr = py::cast(val.attr("__str__")()); + const std::string valType = py::cast(val.attr("__class__").attr("__name__")); throw py::type_error(std::string("metadata value " + valAsStr + " of type " + valType + " is not allowed")); } - } return true; } - static handle cast(openvdb::MetaMap src, return_value_policy, handle) { + static handle cast(openvdb::MetaMap src, return_value_policy policy, handle parent) { py::dict dict; for (openvdb::MetaMap::ConstMetaIterator it = src.beginMeta(); it != src.endMeta(); ++it) { if (openvdb::Metadata::Ptr meta = it->second) { py::object obj(py::cast(meta)); const std::string typeName = meta->typeName(); if (typeName == openvdb::StringMetadata::staticTypeName()) { - obj = py::str(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } else if (typeName == openvdb::DoubleMetadata::staticTypeName()) { - obj = py::float_(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } else if (typeName == openvdb::FloatMetadata::staticTypeName()) { - obj = py::float_(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } else if (typeName == openvdb::Int32Metadata::staticTypeName()) { - obj = py::int_(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } else if (typeName == openvdb::Int64Metadata::staticTypeName()) { - obj = py::int_(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } else if (typeName == openvdb::BoolMetadata::staticTypeName()) { - obj = py::bool_(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } else if (typeName == openvdb::Vec2DMetadata::staticTypeName()) { - obj = py::cast(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } else if (typeName == openvdb::Vec2IMetadata::staticTypeName()) { - obj = py::cast(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } else if (typeName == openvdb::Vec2SMetadata::staticTypeName()) { - obj = py::cast(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } else if (typeName == openvdb::Vec3DMetadata::staticTypeName()) { - obj = py::cast(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } else if (typeName == openvdb::Vec3IMetadata::staticTypeName()) { - obj = py::cast(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } else if (typeName == openvdb::Vec3SMetadata::staticTypeName()) { - obj = py::cast(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } else if (typeName == openvdb::Vec4DMetadata::staticTypeName()) { - obj = py::cast(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } else if (typeName == openvdb::Vec4IMetadata::staticTypeName()) { - obj = py::cast(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } else if (typeName == openvdb::Vec4SMetadata::staticTypeName()) { - obj = py::cast(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } else if (typeName == openvdb::Mat4SMetadata::staticTypeName()) { - obj = py::cast(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } else if (typeName == openvdb::Mat4DMetadata::staticTypeName()) { - obj = py::cast(static_cast(*meta).value()); + obj = reinterpret_steal(make_caster::cast(static_cast(*meta), policy, parent)); } dict[py::str(it->first)] = obj; } diff --git a/openvdb/openvdb/python/pyutil.h b/openvdb/openvdb/python/pyutil.h index 52216666cc..1727e0aace 100644 --- a/openvdb/openvdb/python/pyutil.h +++ b/openvdb/openvdb/python/pyutil.h @@ -163,21 +163,11 @@ struct StringEnum //////////////////////////////////////// - -/// Return str(val) for the given value. -template -inline std::string -str(const T& val) -{ - return py::str(py::cast(val)); -} - - /// Return the name of the given Python object's class. inline std::string className(py::handle h) { - return h.attr("__class__").attr("__name__").cast(); + return py::cast(h.attr("__class__").attr("__name__")); } } // namespace pyutil From 04f4ee3915c4149f1611524ba803b6a6c07ac743 Mon Sep 17 00:00:00 2001 From: Dan Bailey Date: Mon, 30 Oct 2023 21:18:35 -0700 Subject: [PATCH 18/22] Update change notes Signed-off-by: Dan Bailey --- CHANGES | 87 +++++++++++++++++++++++++++- doc/changes.txt | 116 +++++++++++++++++++++++++++++++++++++ pendingchanges/nanovdb.txt | 31 ---------- pendingchanges/vdb11.txt | 10 ---- 4 files changed, 202 insertions(+), 42 deletions(-) delete mode 100644 pendingchanges/nanovdb.txt delete mode 100644 pendingchanges/vdb11.txt diff --git a/CHANGES b/CHANGES index 7fd5c5b343..9486a712c3 100644 --- a/CHANGES +++ b/CHANGES @@ -1,13 +1,98 @@ OpenVDB Version History ======================= -Version 11.0.0 - In Progress +Version 11.0.0 - October 31, 2023 This version introduces ABI changes relative to older major releases, so to preserve ABI compatibility it might be necessary to define the macro OPENVDB_ABI_VERSION_NUMBER=N, where, for example, N is 9 for Houdini 19.5 and 10 for Houdini 20.0. + OpenEXR 2 and Python 2 are no longer supported. + Boost is now an optional OpenVDB dependency. + + OpenVDB: + Improvements: + - Removed last traces of Boost when OPENVDB_USE_DELAYED_LOADING is OFF + [Contributed by Brian McKinnon] + + NanoVDB: + API Changes: + - Minor version changed from 4 to 6 (major version is unchanged since the ABI is + preserved) + - nanovdb::Vec3R is deprecated. Use nanovdb::Vec3d instead. + - nanoToOpenVDB now accepts the index of the NanoVDB grid contained inside of a + GridHandle to be converted to OpenVDB. + - DataTypes are now public in all node types. + - GridMetaData can be copied. + - Major improvements to GridBuilder.h, which allows users to construct grids + with random access on the host + - The move constructor now requires the GridBuffer to actually contain a + valid grid + - Moved CudaDeviceBuffer.h to cuda/CudaDeviceBuffer.h + - Added nanovdb::pi() + - Introduced a new extendable API for acceleration of custom random-access + get/set methods on ValueAccessor, e.g. getValue(ijk), isActive + (ijk), probeValue(ijk, val). + - Added nanovdb::BitFlags. + - Added minComponentAtomic and maxComponentAtomic methods on the GPU to + nanovdb::Vec2 and nanovdb::Vec3. Added expandAtomic and intersectAtomic on + nanovdb::BBox, and expandAtomic in nanovdb::Coord. + - Added nanovdb::Map constructors. + - Mask:: DenseIterator, Mask:: setOnAtomic,Mask:: setOffAtomic. + - Improved and renamed device function that converts voxels into a grid - + nanovdb::cudaVoxelsToGrid + + New Features: + - Added a new grid class called IndexGrid in 4 flavors (Index, OnIndex, + IndexMask, OnIndexMask). + - Several new ways to construct and modify NanoVDB grids on the GPU. See + CreateNanoGrid.h and CudaPointsToGrid.cuh. + - New device function to convert points into a compact grid - + nanovdb::cudaPointsToGrid + - Added cuda/CudaUtils.h and cuda/GpuTimer.h with cuda utility functions + - Added cuda/CudaPointToGrids.cuh that constructs device grids from points or + voxels + - Added cuda/CudaIndexToGrid.cuh that converts IndexGrids and values into + regular Grids + - Added cuda/CudaSignedFloodFill.cuh that performs signed-flood filing on SDF on + the GPU + - Added cuda/CudaAddBlindData.cuh that adds blind data to an existing grid on the + GPU + - Added cuda/CudaGridChecksum.cuh that computes CRC32 checksums of grids on the + GPU + - Added cuda/CudaGridHandle.cuh that handles grids on the GPU + - Added cuda/CudaNodeManager.cuh that constructs a NodeManager on the GPU + - Introduced new (dummy) build-type nanovdb::Points and + nanovdb::GridType::PointIndex + - Introduced new types nanovdb::GridType::Vec3u16 and nanovdb::GridType::Vec3u8 + used for compressed representations of point coordinates as blind data + - Added PrefixSum.h for concurrent computation of prefix sum on the host + - Primitives.h can now create grids on the CPU with SDF, FOG, and point or + torus + + Improvements: + - Transition from C++11 to C++17 in NanoVDB.h and its tools + - Improve NanoVDB Build Traits. + - CreateNanoGrid.h is replacing GridBuilder.h, IndexGridBuilder.h and + OpenToNanoVDB.h. + - Syncing PNanoVDB.h with NanoVDB.h. + + Build: + - Support for OpenEXR 2.X has been removed. + - Boost is no longer required if OPENVDB_USE_DELAYED_LOADING is OFF + - Better support for building with external package configurations + with CMAKE_FIND_PACKAGE_PREFER_CONFIG=ON. + + Python: + - Removed Python 2 support. + [Contributed by Matthew Cong] + - Removed explicit bindings for Math types. + [Contributed by Matthew Cong] + - Improved type casting for TypedMetadata. + [Contributed by Matthew Cong] + + Version 10.1.0 - October 11, 2023 Highlights: diff --git a/doc/changes.txt b/doc/changes.txt index 857f386906..a06d65ee17 100644 --- a/doc/changes.txt +++ b/doc/changes.txt @@ -2,6 +2,122 @@ @page changes Release Notes +@htmlonly @endhtmlonly +@par +Version 11.0.0 - October 31, 2023 + +@par +