Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3296fd06e2 | ||
|
|
9102630227 | ||
|
|
e72bf3e86f | ||
|
|
4a97f065af | ||
|
|
0bc279c387 | ||
|
|
6ce0707aae | ||
|
|
7c172d99a4 | ||
|
|
86e975ba58 | ||
|
|
1a3a7b7b10 | ||
|
|
4d923768fa | ||
|
|
7a1e66396d | ||
|
|
88a4f90569 | ||
|
|
03ed5c8f00 | ||
|
|
f22bf6a8f7 | ||
|
|
3c6f121493 | ||
|
|
68b26c4579 | ||
|
|
3183842375 | ||
|
|
2dcabf97dd | ||
|
|
efa513a569 | ||
|
|
dd67710848 | ||
|
|
2523040b08 |
@@ -120,6 +120,9 @@ jobs:
|
||||
- name: install
|
||||
shell: powershell
|
||||
run: .\ci\install_windows.ps1
|
||||
- name: install_numpy
|
||||
shell: powershell
|
||||
run: .\ci\install_windows_numpy.ps1
|
||||
- name: build
|
||||
# nvcc doesn't set _WIN32 when run in bash so we need to set it manually
|
||||
shell: bash
|
||||
|
||||
@@ -146,14 +146,13 @@ jobs:
|
||||
matrix:
|
||||
config:
|
||||
- { name: 'all', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=ON -DUSE_ZLIB=ON -DUSE_EXR=ON -DUSE_PNG=ON' }
|
||||
- { name: 'lite', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=OFF -DUSE_ZLIB=OFF -DUSE_EXR=OFF -DUSE_PNG=OFF' }
|
||||
- { name: 'lite', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=OFF -DUSE_ZLIB=OFF -DUSE_EXR=OFF -DUSE_PNG=OFF -DOPENVDB_USE_DELAYED_LOADING=OFF' }
|
||||
- { name: 'half', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=OFF -DUSE_IMATH_HALF=ON' }
|
||||
- { name: 'simd', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DOPENVDB_X86_INSTRSET=7 -DUSE_VCL=ON' } # 7=AVX
|
||||
- { name: 'pygrid', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DOPENVDB_PYTHON_WRAP_ALL_GRID_TYPES=ON' }
|
||||
- { name: 'asan', image: '2026', build: 'asan', components: 'core,test', cmake: '-DDISABLE_DEPENDENCY_VERSION_CHECKS=ON -DNANOVDB_USE_OPENVDB=ON -DOPENVDB_AX_STATIC=OFF -DOPENVDB_CORE_STATIC=OFF -DUSE_BLOSC=OFF' } # We never called blosc_destroy(), so disable blosc to silence these errors
|
||||
- { name: 'ubsan', image: '2026', build: 'ubsan', components: 'core,test', cmake: '-DDISABLE_DEPENDENCY_VERSION_CHECKS=ON -DCMAKE_CXX_FLAGS="-Wno-deprecated-declarations" ' }
|
||||
- { name: 'c++20', image: '2026', build: 'Release', components: 'core,test', cmake: '-DCMAKE_CXX_STANDARD=20' }
|
||||
- { name: 'abi14', image: '2026', build: 'Release', components: 'core,test', cmake: '-DOPENVDB_USE_FUTURE_ABI_14=ON -DOPENVDB_ENABLE_ASSERTS=ON' }
|
||||
- { name: 'conf', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DCMAKE_FIND_PACKAGE_PREFER_CONFIG=ON' }
|
||||
fail-fast: false
|
||||
steps:
|
||||
@@ -216,6 +215,7 @@ jobs:
|
||||
--components=\"core,axcore,python,bin,render,test,axbin\"
|
||||
--cargs=\'
|
||||
-DCMAKE_CXX_STANDARD=20
|
||||
-DOPENVDB_USE_DELAYED_LOADING=OFF
|
||||
-DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/install
|
||||
${{ matrix.config.cmake }}
|
||||
\'
|
||||
@@ -420,6 +420,7 @@ jobs:
|
||||
--components="${{ matrix.config.components }}"
|
||||
--cargs=\'
|
||||
-A x64 -G \"Visual Studio 17 2022\"
|
||||
-DOPENVDB_USE_DELAYED_LOADING=OFF
|
||||
-DUSE_BLOSC=OFF \
|
||||
-DUSE_ZLIB=OFF \
|
||||
-DVCPKG_TARGET_TRIPLET=${VCPKG_DEFAULT_TRIPLET}
|
||||
|
||||
@@ -84,6 +84,7 @@ option(OPENVDB_ENABLE_VEC_RELATIONAL_OPERATIONS [=[
|
||||
Enabled deprecated relational < and > operators on Vector types. This option
|
||||
will be switched to OFF and the feature will be subsequently removed in future
|
||||
releases.]=] ON)
|
||||
option(OPENVDB_USE_DELAYED_LOADING "Build the core OpenVDB library with delayed-loading." ON)
|
||||
option(OPENVDB_CXX_STRICT "Enable or disable pre-defined compiler warnings" OFF)
|
||||
cmake_dependent_option(OPENVDB_INSTALL_CMAKE_MODULES
|
||||
"Install the provided OpenVDB CMake modules when building the core library"
|
||||
@@ -263,6 +264,7 @@ mark_as_advanced(
|
||||
OPENVDB_BUILD_HOUDINI_ABITESTS
|
||||
OPENVDB_CXX_STRICT
|
||||
OPENVDB_ENABLE_RPATH
|
||||
OPENVDB_USE_DELAYED_LOADING
|
||||
OPENVDB_FUTURE_DEPRECATION
|
||||
OPENVDB_SIMD
|
||||
SYSTEM_LIBRARY_PATHS
|
||||
|
||||
@@ -64,6 +64,8 @@ This will define the following variables:
|
||||
True if the OpenVDB Library has been built with log4cplus support
|
||||
``OpenVDB_USES_IMATH_HALF``
|
||||
True if the OpenVDB Library has been built with Imath half support
|
||||
``OpenVDB_USES_DELAYED_LOADING``
|
||||
True if the OpenVDB Library has been built with delayed-loading
|
||||
``OpenVDB_ABI``
|
||||
Set if this module was able to determine the ABI number the located
|
||||
OpenVDB Library was built against. Unset otherwise.
|
||||
@@ -520,6 +522,7 @@ set(OpenVDB_USES_BLOSC ${USE_BLOSC})
|
||||
set(OpenVDB_USES_ZLIB ${USE_ZLIB})
|
||||
set(OpenVDB_USES_LOG4CPLUS ${USE_LOG4CPLUS})
|
||||
set(OpenVDB_USES_IMATH_HALF ${USE_IMATH_HALF})
|
||||
set(OpenVDB_USES_DELAYED_LOADING ${OPENVDB_USE_DELAYED_LOADING})
|
||||
set(OpenVDB_DEFINITIONS)
|
||||
|
||||
if(WIN32)
|
||||
@@ -552,6 +555,7 @@ if(_OPENVDB_HAS_NEW_VERSION_HEADER)
|
||||
OPENVDB_GET_VERSION_DEFINE(${_OPENVDB_VERSION_HEADER} "OPENVDB_USE_IMATH_HALF" OpenVDB_USES_IMATH_HALF)
|
||||
OPENVDB_GET_VERSION_DEFINE(${_OPENVDB_VERSION_HEADER} "OPENVDB_USE_BLOSC" OpenVDB_USES_BLOSC)
|
||||
OPENVDB_GET_VERSION_DEFINE(${_OPENVDB_VERSION_HEADER} "OPENVDB_USE_ZLIB" OpenVDB_USES_ZLIB)
|
||||
OPENVDB_GET_VERSION_DEFINE(${_OPENVDB_VERSION_HEADER} "OPENVDB_USE_DELAYED_LOADING" OpenVDB_USES_DELAYED_LOADING)
|
||||
elseif(NOT OPENVDB_USE_STATIC_LIBS)
|
||||
# Use GetPrerequisites to see which libraries this OpenVDB lib has linked to
|
||||
# which we can query for optional deps. This basically runs ldd/otoll/objdump
|
||||
@@ -598,6 +602,11 @@ elseif(NOT OPENVDB_USE_STATIC_LIBS)
|
||||
if(NOT ${_HAS_DEP} EQUAL -1)
|
||||
set(OpenVDB_USES_IMATH_HALF ON)
|
||||
endif()
|
||||
|
||||
string(FIND ${PREREQUISITE} "boost_iostreams" _HAS_DEP)
|
||||
if(NOT ${_HAS_DEP} EQUAL -1)
|
||||
set(OpenVDB_USES_DELAYED_LOADING ON)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
unset(_OPENVDB_PREREQUISITE_LIST)
|
||||
@@ -619,6 +628,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()
|
||||
@@ -630,6 +643,11 @@ endif()
|
||||
|
||||
set(_OPENVDB_VISIBLE_DEPENDENCIES "")
|
||||
|
||||
if(OpenVDB_USES_DELAYED_LOADING)
|
||||
list(APPEND _OPENVDB_VISIBLE_DEPENDENCIES Boost::iostreams)
|
||||
list(APPEND OpenVDB_DEFINITIONS OPENVDB_USE_DELAYED_LOADING)
|
||||
endif()
|
||||
|
||||
if(OpenVDB_USES_IMATH_HALF)
|
||||
list(APPEND _OPENVDB_VISIBLE_DEPENDENCIES Imath::Imath)
|
||||
endif()
|
||||
|
||||
@@ -7,6 +7,7 @@ A lightweight GPU friendly version of VDB initially targeting rendering applicat
|
||||
* [Frequently asked questions](../../doc/nanovdb/FAQ.md)
|
||||
* [Source tree](../../doc/nanovdb/SourceTree.md)
|
||||
* [Examples](../../doc/nanovdb/HelloWorld.md)
|
||||
* [Python examples](python/examples/)
|
||||
|
||||
### Copyright Contributors to the OpenVDB Project
|
||||
### SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
@@ -257,7 +257,7 @@ int main(int argc, char* argv[])
|
||||
}
|
||||
if (verbose) std::cout << "Opening OpenVDB file named \"" << inputFile << "\"" << std::endl;
|
||||
openvdb::io::File file(inputFile);
|
||||
file.open();
|
||||
file.open(false); //disable delayed loading
|
||||
if (gridName.empty()) {// convert all grid in the file
|
||||
auto grids = file.getGrids();
|
||||
std::vector<nanovdb::GridHandle<nanovdb::HostBuffer> > handles;
|
||||
|
||||
@@ -1,368 +0,0 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
/// @file nanovdb/cuda/Buffer.h
|
||||
///
|
||||
/// @brief Typed containers for CUDA memory: the owning, resource-aware,
|
||||
/// stream-ordered cuda::Buffer and the non-owning cuda::BufferView.
|
||||
|
||||
#ifndef NANOVDB_CUDA_BUFFER_H_HAS_BEEN_INCLUDED
|
||||
#define NANOVDB_CUDA_BUFFER_H_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <cuda_runtime_api.h>
|
||||
#include <nanovdb/cuda/DeviceResource.h>
|
||||
#include <nanovdb/util/cuda/Util.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace nanovdb {
|
||||
|
||||
namespace cuda {
|
||||
|
||||
/// @brief Tag type selecting the Buffer constructors that skip element
|
||||
/// initialization, leaving the contents indeterminate.
|
||||
struct NoInit {};
|
||||
inline constexpr NoInit noInit{};
|
||||
|
||||
namespace detail {
|
||||
|
||||
/// @brief Conditional stream storage for Buffer. The async specialization
|
||||
/// retains the stream of the most recent allocation; the synchronous
|
||||
/// specialization is an empty base, so a Buffer over a synchronous
|
||||
/// resource carries no stream state and exposes no stream API.
|
||||
template<bool IsAsync>
|
||||
struct StreamHolder {};
|
||||
|
||||
template<>
|
||||
struct StreamHolder<true> { cudaStream_t mStream = 0; };
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/// @brief Owning, typed container of @c T elements allocated from a memory
|
||||
/// resource @c R held by value.
|
||||
/// @tparam T element type; sizes are expressed in elements, not bytes
|
||||
/// @tparam R memory resource, either stream-ordered (AsyncResource concept,
|
||||
/// see is_async_resource) or synchronous (Resource concept, see
|
||||
/// is_resource). When @c R provides both interfaces the stream-ordered
|
||||
/// one is used.
|
||||
/// @details With a stream-ordered resource the Buffer retains the stream of
|
||||
/// the most recent allocation (or the one supplied via setStream)
|
||||
/// and orders its deallocation on that stream. Buffer is move-only.
|
||||
template<typename T, typename R = DeviceResource>
|
||||
class Buffer : private detail::StreamHolder<is_async_resource<R>::value>
|
||||
{
|
||||
static_assert(is_async_resource<R>::value || is_resource<R>::value,
|
||||
"Buffer requires R to model the AsyncResource or the Resource concept");
|
||||
static_assert(std::is_trivially_copyable<T>::value,
|
||||
"Buffer requires a trivially copyable T: elements are copied bytewise and never constructed or destroyed");
|
||||
|
||||
static constexpr bool IsAsync = is_async_resource<R>::value;
|
||||
|
||||
R mResource;
|
||||
T* mData = nullptr;
|
||||
size_t mSize = 0; // element count
|
||||
|
||||
public:
|
||||
/// @brief Default c-tor of an empty buffer; performs no allocation.
|
||||
Buffer() = default;
|
||||
|
||||
/// @brief C-tor allocating @c count uninitialized elements, stream-ordered
|
||||
/// on @c stream. Parameter order follows cuda::buffer:
|
||||
/// (stream, resource, count, no_init).
|
||||
/// @param stream cuda stream the allocation is ordered on
|
||||
/// @param resource resource instance the buffer takes ownership of
|
||||
/// @param count number of elements
|
||||
template<typename S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
|
||||
explicit Buffer(cudaStream_t stream, R resource, size_t count, NoInit)
|
||||
: detail::StreamHolder<true>{stream}
|
||||
, mResource(std::move(resource))
|
||||
{
|
||||
this->allocate(count, stream);
|
||||
}
|
||||
|
||||
/// @brief Convenience c-tor using a default-constructed resource.
|
||||
/// @param stream cuda stream the allocation is ordered on
|
||||
/// @param count number of elements
|
||||
/// @note There is deliberately no count c-tor without NoInit: implicit
|
||||
/// initialization of freshly allocated memory costs a hidden fill
|
||||
/// pass that the dominant allocate-then-overwrite pattern wastes,
|
||||
/// so initialization is always explicit (matching cuda::buffer,
|
||||
/// whose count c-tor likewise requires cuda::no_init).
|
||||
template<typename S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
|
||||
Buffer(cudaStream_t stream, size_t count, NoInit) : Buffer(stream, R(), count, noInit) {}
|
||||
|
||||
/// @brief C-tor allocating @c count uninitialized elements from a
|
||||
/// synchronous resource: the stream-less analog of
|
||||
/// (stream, resource, count, no_init).
|
||||
/// @param resource resource instance the buffer takes ownership of
|
||||
/// @param count number of elements
|
||||
template<typename S = R, std::enable_if_t<!is_async_resource<S>::value && is_resource<S>::value, int> = 0>
|
||||
explicit Buffer(R resource, size_t count, NoInit)
|
||||
: mResource(std::move(resource))
|
||||
{
|
||||
this->allocate(count, cudaStream_t{0});
|
||||
}
|
||||
|
||||
/// @brief Convenience c-tor using a default-constructed resource.
|
||||
/// @param count number of elements
|
||||
template<typename S = R, std::enable_if_t<!is_async_resource<S>::value && is_resource<S>::value, int> = 0>
|
||||
Buffer(size_t count, NoInit) : Buffer(R(), count, noInit) {}
|
||||
|
||||
/// @brief Explicitly disallow copy construction and assignment operation
|
||||
Buffer(const Buffer&) = delete;
|
||||
Buffer& operator=(const Buffer&) = delete;
|
||||
|
||||
/// @brief Move c-tor; steals the allocation (and retained stream, if any)
|
||||
/// and leaves @c other empty.
|
||||
Buffer(Buffer&& other) noexcept
|
||||
: detail::StreamHolder<IsAsync>(other)
|
||||
, mResource(std::move(other.mResource))
|
||||
, mData(other.mData)
|
||||
, mSize(other.mSize)
|
||||
{
|
||||
other.mData = nullptr;
|
||||
other.mSize = 0;
|
||||
}
|
||||
|
||||
/// @brief Move assignment; frees the current allocation first, then steals
|
||||
/// from @c other and leaves it empty. Self-move is a no-op.
|
||||
Buffer& operator=(Buffer&& other) noexcept
|
||||
{
|
||||
if (this != &other) {
|
||||
this->clear();
|
||||
static_cast<detail::StreamHolder<IsAsync>&>(*this) = other;
|
||||
mResource = std::move(other.mResource);
|
||||
mData = other.mData;
|
||||
mSize = other.mSize;
|
||||
other.mData = nullptr;
|
||||
other.mSize = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// @brief Returns a deep copy of this buffer, allocated from a copy of the
|
||||
/// resource; the allocation and element copy are ordered on @c stream,
|
||||
/// which becomes the copy's retained stream.
|
||||
/// @param stream cuda stream the allocation and element copy are ordered on
|
||||
template<typename S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
|
||||
Buffer copy(cudaStream_t stream) const
|
||||
{
|
||||
Buffer out(stream, mResource, mSize, noInit);
|
||||
if (mData) cudaCheck(cudaMemcpyAsync(out.mData, mData, this->size_bytes(), cudaMemcpyDefault, stream));
|
||||
return out;
|
||||
}
|
||||
|
||||
/// @brief Returns a deep copy of this buffer, allocated from a copy of the
|
||||
/// synchronous resource.
|
||||
template<typename S = R, std::enable_if_t<!is_async_resource<S>::value && is_resource<S>::value, int> = 0>
|
||||
Buffer copy() const
|
||||
{
|
||||
Buffer out(mResource, mSize, noInit);
|
||||
if (mData) cudaCheck(cudaMemcpy(out.mData, mData, this->size_bytes(), cudaMemcpyDefault));
|
||||
return out;
|
||||
}
|
||||
|
||||
/// @brief D-tor. A stream-ordered resource frees on the retained stream;
|
||||
/// a synchronous resource frees immediately.
|
||||
~Buffer() { this->clear(); }
|
||||
|
||||
/// @brief Returns the retained stream, i.e. the stream the buffer's memory
|
||||
/// will be freed on.
|
||||
template<typename S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
|
||||
cudaStream_t stream() const { return this->mStream; }
|
||||
|
||||
/// @brief Replaces the retained stream without synchronizing; subsequent
|
||||
/// deallocation (and destruction) is ordered on @c stream instead.
|
||||
/// @param stream cuda stream subsequent deallocation is ordered on
|
||||
/// @warning The caller is responsible for ordering @c stream after any
|
||||
/// in-flight work that uses the buffer's memory.
|
||||
template<typename S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
|
||||
void setStream(cudaStream_t stream) { this->mStream = stream; }
|
||||
|
||||
/// @brief Resizes the buffer to @c count elements, preserving the leading
|
||||
/// min(old, new) elements. Every operation — the new allocation, the
|
||||
/// prefix copy, and the free of the old block — is ordered on
|
||||
/// @c stream, which becomes the retained stream: the prefix copy is
|
||||
/// the old block's last use, so that is the stream its free must be
|
||||
/// ordered on.
|
||||
/// @param count number of elements
|
||||
/// @param stream cuda stream the reallocation is ordered on
|
||||
/// @warning The caller is responsible for ordering @c stream after any
|
||||
/// in-flight work on the previously retained stream that uses the
|
||||
/// buffer's memory.
|
||||
template<typename S = R, std::enable_if_t<is_async_resource<S>::value, int> = 0>
|
||||
void resize(size_t count, cudaStream_t stream)
|
||||
{
|
||||
if (count != mSize) {
|
||||
// No member is mutated until every throwing operation has
|
||||
// succeeded, so a failed resize leaves the buffer untouched --
|
||||
// including its retained stream.
|
||||
T* newData = count ? static_cast<T*>(mResource.allocate_async(checkedBytes(count), R::DEFAULT_ALIGNMENT, stream))
|
||||
: nullptr;
|
||||
if (newData && mData) {
|
||||
const size_t prefix = count < mSize ? count : mSize;
|
||||
try {
|
||||
cudaCheck(cudaMemcpyAsync(newData, mData, prefix * sizeof(T), cudaMemcpyDefault, stream));
|
||||
}
|
||||
catch (...) {
|
||||
mResource.deallocate_async(newData, checkedBytes(count), R::DEFAULT_ALIGNMENT, stream);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
this->mStream = stream;
|
||||
this->deallocate(mData, mSize); // ordered on stream: after the prefix copy
|
||||
mData = newData;
|
||||
mSize = count;
|
||||
}
|
||||
else {
|
||||
this->mStream = stream; // no reallocation: setStream semantics
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Resizes the buffer to @c count elements through the synchronous
|
||||
/// resource, preserving the leading min(old, new) elements.
|
||||
/// @param count number of elements
|
||||
template<typename S = R, std::enable_if_t<!is_async_resource<S>::value && is_resource<S>::value, int> = 0>
|
||||
void resize(size_t count)
|
||||
{
|
||||
if (count == mSize) return;
|
||||
// No member is mutated until every throwing operation has succeeded,
|
||||
// so a failed resize leaves the buffer untouched.
|
||||
T* newData = count ? static_cast<T*>(mResource.allocate(checkedBytes(count), R::DEFAULT_ALIGNMENT))
|
||||
: nullptr;
|
||||
if (newData && mData) {
|
||||
const size_t prefix = count < mSize ? count : mSize;
|
||||
try {
|
||||
cudaCheck(cudaMemcpy(newData, mData, prefix * sizeof(T), cudaMemcpyDefault));
|
||||
}
|
||||
catch (...) {
|
||||
mResource.deallocate(newData, checkedBytes(count), R::DEFAULT_ALIGNMENT);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
this->deallocate(mData, mSize);
|
||||
mData = newData;
|
||||
mSize = count;
|
||||
}
|
||||
|
||||
/// @brief Returns a pointer to the elements, or nullptr if empty.
|
||||
T* data() { return mData; }
|
||||
const T* data() const { return mData; }
|
||||
|
||||
/// @brief Returns the number of elements.
|
||||
size_t size() const { return mSize; }
|
||||
|
||||
/// @brief Returns the size of the buffer's allocation in bytes.
|
||||
size_t size_bytes() const { return mSize * sizeof(T); }
|
||||
|
||||
/// @brief Returns true if this buffer manages no memory.
|
||||
bool empty() const { return mSize == 0; }
|
||||
|
||||
/// @brief Frees the buffer memory (if any) and resets to the empty state.
|
||||
void clear()
|
||||
{
|
||||
this->deallocate(mData, mSize);
|
||||
mData = nullptr;
|
||||
mSize = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
/// @brief Returns @c count * sizeof(T), throwing std::runtime_error if the
|
||||
/// byte size would overflow size_t instead of silently wrapping into
|
||||
/// a tiny allocation.
|
||||
/// @param count number of elements
|
||||
static size_t checkedBytes(size_t count)
|
||||
{
|
||||
if (count > std::numeric_limits<size_t>::max() / sizeof(T))
|
||||
throw std::runtime_error("nanovdb::cuda::Buffer: element count overflows the byte size");
|
||||
return count * sizeof(T);
|
||||
}
|
||||
|
||||
/// @brief Allocates @c count elements through the resource and records the
|
||||
/// new extent. A zero count allocates nothing.
|
||||
/// @param count number of elements
|
||||
/// @param stream cuda stream the allocation is ordered on; used by the
|
||||
/// stream-ordered form and ignored by the synchronous one
|
||||
void allocate(size_t count, cudaStream_t stream)
|
||||
{
|
||||
if (count) {
|
||||
if constexpr (IsAsync)
|
||||
mData = static_cast<T*>(mResource.allocate_async(checkedBytes(count), R::DEFAULT_ALIGNMENT, stream));
|
||||
else
|
||||
mData = static_cast<T*>(mResource.allocate(checkedBytes(count), R::DEFAULT_ALIGNMENT));
|
||||
}
|
||||
mSize = count;
|
||||
}
|
||||
|
||||
/// @brief Frees @c count elements at @c p through the resource; the
|
||||
/// stream-ordered form frees on the retained stream. Null is a no-op.
|
||||
/// @param p pointer to the elements to free
|
||||
/// @param count number of elements
|
||||
void deallocate(T* p, size_t count)
|
||||
{
|
||||
if (!p) return;
|
||||
if constexpr (IsAsync)
|
||||
mResource.deallocate_async(p, count * sizeof(T), R::DEFAULT_ALIGNMENT, this->mStream);
|
||||
else
|
||||
mResource.deallocate(p, count * sizeof(T), R::DEFAULT_ALIGNMENT);
|
||||
}
|
||||
}; // Buffer<T, R> class
|
||||
|
||||
/// @brief Non-owning, trivially copyable view of a contiguous range of @c T
|
||||
/// elements, with span semantics.
|
||||
/// @tparam T element type; spell constness in the element type
|
||||
/// (e.g. BufferView<const std::byte> is the read-only form), since
|
||||
/// const on the view itself is shallow.
|
||||
template<typename T>
|
||||
class BufferView
|
||||
{
|
||||
T* mData = nullptr;
|
||||
size_t mSize = 0; // element count
|
||||
|
||||
public:
|
||||
/// @brief Default c-tor of an empty view.
|
||||
BufferView() = default;
|
||||
|
||||
/// @brief C-tor viewing a contiguous range; the caller guarantees the
|
||||
/// underlying storage outlives every use of the view.
|
||||
/// @param data pointer to the first element
|
||||
/// @param count number of elements
|
||||
/// @throw std::runtime_error if @c data is null while @c count is non-zero.
|
||||
BufferView(T* data, size_t count) : mData(data), mSize(count)
|
||||
{
|
||||
if (data == nullptr && count != 0)
|
||||
throw std::runtime_error("BufferView: null data with a non-zero element count");
|
||||
}
|
||||
|
||||
/// @brief Returns a pointer to the viewed elements, or nullptr if empty.
|
||||
T* data() const { return mData; }
|
||||
|
||||
/// @brief Returns the number of viewed elements.
|
||||
size_t size() const { return mSize; }
|
||||
|
||||
/// @brief Returns the size of the viewed range in bytes.
|
||||
size_t size_bytes() const { return mSize * sizeof(T); }
|
||||
|
||||
/// @brief Returns true if this view references no elements.
|
||||
bool empty() const { return mSize == 0; }
|
||||
|
||||
/// @brief Detaches the view (nulls the pointer and zeroes the size)
|
||||
/// without touching the underlying storage. This is the one
|
||||
/// deliberate deviation from std::span, required by the buffer
|
||||
/// static interface: GridHandle::reset() calls buffer.clear().
|
||||
void clear()
|
||||
{
|
||||
mData = nullptr;
|
||||
mSize = 0;
|
||||
}
|
||||
}; // BufferView<T> class
|
||||
|
||||
} // namespace cuda
|
||||
|
||||
} // namespace nanovdb
|
||||
|
||||
#endif // end of NANOVDB_CUDA_BUFFER_H_HAS_BEEN_INCLUDED
|
||||
@@ -18,61 +18,43 @@ namespace cuda {
|
||||
/// @brief Default stream-ordered device memory resource. Allocations are made
|
||||
/// with cudaMallocAsync and freed with cudaFreeAsync via the
|
||||
/// util::cuda wrappers.
|
||||
/// @note Models the AsyncResource concept, which refines the synchronous
|
||||
/// Resource concept (as in CCCL's cuda::mr): the instance methods
|
||||
/// provide both the stream-ordered allocate_async / deallocate_async
|
||||
/// and the synchronous allocate / deallocate. The type is stateless,
|
||||
/// so a default-constructed instance adds no overhead. The static
|
||||
/// allocateAsync / deallocateAsync methods are deprecated.
|
||||
/// @note Exposes both the legacy static methods (allocateAsync /
|
||||
/// deallocateAsync, retained for source compatibility) and the
|
||||
/// instance methods (allocate_async / deallocate_async) that model
|
||||
/// the AsyncResource concept. The instance methods forward to the
|
||||
/// static ones, so a default-constructed instance is stateless and
|
||||
/// adds no overhead.
|
||||
class DeviceResource
|
||||
{
|
||||
public:
|
||||
// cudaMalloc aligns memory to 256 bytes by default
|
||||
static constexpr size_t DEFAULT_ALIGNMENT = 256;
|
||||
|
||||
/// @brief Stream-ordered allocation.
|
||||
/// @param bytes number of bytes to allocate
|
||||
/// @param stream cuda stream the allocation is ordered on
|
||||
/// @note the alignment parameter is unnamed: cudaMallocAsync always
|
||||
/// 256B-aligns
|
||||
void* allocate_async(size_t bytes, size_t, cudaStream_t stream) {
|
||||
static void* allocateAsync(size_t bytes, size_t, cudaStream_t stream) {
|
||||
void* p = nullptr;
|
||||
cudaCheck(util::cuda::mallocAsync(&p, bytes, stream));
|
||||
return p;
|
||||
}
|
||||
|
||||
/// @brief Stream-ordered deallocation.
|
||||
/// @param p pointer previously returned by allocate_async
|
||||
/// @param stream cuda stream the deallocation is ordered on
|
||||
void deallocate_async(void* p, size_t, size_t, cudaStream_t stream) {
|
||||
static void deallocateAsync(void *p, size_t, size_t, cudaStream_t stream) {
|
||||
cudaCheck(util::cuda::freeAsync(p, stream));
|
||||
}
|
||||
|
||||
/// @brief Synchronous allocation; the returned memory is immediately
|
||||
/// valid on every stream.
|
||||
/// @brief Instance allocation entry point modelling the AsyncResource concept.
|
||||
/// @param bytes number of bytes to allocate
|
||||
/// @param alignment requested alignment
|
||||
void* allocate(size_t bytes, size_t alignment) {
|
||||
void* p = this->allocate_async(bytes, alignment, cudaStream_t(0));
|
||||
cudaCheck(cudaStreamSynchronize(cudaStream_t(0)));
|
||||
return p;
|
||||
/// @param alignment requested alignment (cudaMallocAsync always 256B-aligns)
|
||||
/// @param stream cuda stream the allocation is ordered on
|
||||
void* allocate_async(size_t bytes, size_t alignment, cudaStream_t stream) {
|
||||
return allocateAsync(bytes, alignment, stream);
|
||||
}
|
||||
|
||||
/// @brief Synchronous deallocation; the caller guarantees that device
|
||||
/// work touching the memory has completed.
|
||||
/// @param p pointer previously returned by allocate or allocate_async
|
||||
void deallocate(void* p, size_t bytes, size_t alignment) {
|
||||
this->deallocate_async(p, bytes, alignment, cudaStream_t(0));
|
||||
}
|
||||
|
||||
[[deprecated("use the instance method allocate_async")]]
|
||||
static void* allocateAsync(size_t bytes, size_t alignment, cudaStream_t stream) {
|
||||
return DeviceResource().allocate_async(bytes, alignment, stream);
|
||||
}
|
||||
|
||||
[[deprecated("use the instance method deallocate_async")]]
|
||||
static void deallocateAsync(void *p, size_t bytes, size_t alignment, cudaStream_t stream) {
|
||||
DeviceResource().deallocate_async(p, bytes, alignment, stream);
|
||||
/// @brief Instance deallocation entry point modelling the AsyncResource concept.
|
||||
/// @param p pointer previously returned by allocate_async
|
||||
/// @param bytes size passed to the matching allocate_async
|
||||
/// @param alignment alignment passed to the matching allocate_async
|
||||
/// @param stream cuda stream the deallocation is ordered on
|
||||
void deallocate_async(void* p, size_t bytes, size_t alignment, cudaStream_t stream) {
|
||||
deallocateAsync(p, bytes, alignment, stream);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -105,44 +87,13 @@ inline R& default_resource()
|
||||
/// return resource.allocate(bytes, alignment); // synchronous
|
||||
/// }
|
||||
/// @endcode
|
||||
/// @note AsyncResource refines the synchronous Resource concept, matching
|
||||
/// CCCL's cuda::mr: an async resource must also provide the
|
||||
/// synchronous allocate / deallocate, so is_async_resource<R> implies
|
||||
/// is_resource<R>. A synchronous-only resource (e.g. PinnedResource)
|
||||
/// models just is_resource. The synchronous methods of a stream-ordered
|
||||
/// resource are typically thin delegates (allocate_async on the null
|
||||
/// stream followed by a stream synchronize).
|
||||
template <class R, class = void>
|
||||
struct is_async_resource : std::false_type {};
|
||||
|
||||
template <class R>
|
||||
struct is_async_resource<R, std::void_t<
|
||||
decltype(std::declval<R&>().allocate_async(size_t{0}, size_t{0}, cudaStream_t{0})),
|
||||
decltype(std::declval<R&>().deallocate_async(std::declval<void*>(), size_t{0}, size_t{0}, cudaStream_t{0})),
|
||||
decltype(std::declval<R&>().allocate(size_t{0}, size_t{0})),
|
||||
decltype(std::declval<R&>().deallocate(std::declval<void*>(), size_t{0}, size_t{0}))>>
|
||||
: std::true_type {};
|
||||
|
||||
/// @brief Detection trait: @c is_resource<R>::value is true iff @c R models
|
||||
/// the synchronous Resource concept, i.e. exposes
|
||||
/// allocate(size_t, size_t) and deallocate(void*, size_t, size_t).
|
||||
/// @details Use it to constrain code paths that need a resource without a
|
||||
/// stream argument, e.g. host-side allocations:
|
||||
/// @code
|
||||
/// template<typename R>
|
||||
/// void* allocate(R& resource, size_t bytes, size_t alignment)
|
||||
/// {
|
||||
/// static_assert(nanovdb::cuda::is_resource<R>::value, "R must be a synchronous resource");
|
||||
/// return resource.allocate(bytes, alignment);
|
||||
/// }
|
||||
/// @endcode
|
||||
template <class R, class = void>
|
||||
struct is_resource : std::false_type {};
|
||||
|
||||
template <class R>
|
||||
struct is_resource<R, std::void_t<
|
||||
decltype(std::declval<R&>().allocate(size_t{0}, size_t{0})),
|
||||
decltype(std::declval<R&>().deallocate(std::declval<void*>(), size_t{0}, size_t{0}))>>
|
||||
decltype(std::declval<R&>().deallocate_async(std::declval<void*>(), size_t{0}, size_t{0}, cudaStream_t{0}))>>
|
||||
: std::true_type {};
|
||||
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ int main(int argc, char *argv[])
|
||||
cpuTimer.start("Read input VDB file");
|
||||
openvdb::initialize();
|
||||
openvdb::io::File inFile(argv[1]);
|
||||
inFile.open();
|
||||
inFile.open(false); // disable delayed loading
|
||||
auto baseGrids = inFile.getGrids();
|
||||
inFile.close();
|
||||
auto grid = openvdb::gridPtrCast<GridT>(baseGrids->at(0));
|
||||
|
||||
@@ -44,7 +44,7 @@ int main(int argc, char *argv[])
|
||||
cpuTimer.start("Read input VDB file");
|
||||
openvdb::initialize();
|
||||
openvdb::io::File inFile(argv[1]);
|
||||
inFile.open();
|
||||
inFile.open(false); // disable delayed loading
|
||||
auto baseGrids = inFile.getGrids();
|
||||
inFile.close();
|
||||
auto grid = openvdb::gridPtrCast<GridT>(baseGrids->at(0));
|
||||
|
||||
@@ -41,7 +41,7 @@ int main(int argc, char *argv[])
|
||||
cpuTimer.start("Read first input VDB file");
|
||||
openvdb::initialize();
|
||||
openvdb::io::File inFile1(argv[1]);
|
||||
inFile1.open();
|
||||
inFile1.open(false); // disable delayed loading
|
||||
auto baseGrids1 = inFile1.getGrids();
|
||||
inFile1.close();
|
||||
auto grid1 = openvdb::gridPtrCast<GridT>(baseGrids1->at(0));
|
||||
@@ -51,7 +51,7 @@ int main(int argc, char *argv[])
|
||||
cpuTimer.start("Read second input VDB file");
|
||||
openvdb::initialize();
|
||||
openvdb::io::File inFile2(argv[2]);
|
||||
inFile2.open();
|
||||
inFile2.open(false); // disable delayed loading
|
||||
auto baseGrids2 = inFile2.getGrids();
|
||||
inFile2.close();
|
||||
auto grid2 = openvdb::gridPtrCast<GridT>(baseGrids2->at(0));
|
||||
|
||||
@@ -37,7 +37,7 @@ void runOpenVDB(nanovdb::GridHandle<nanovdb::cuda::DeviceBuffer>& handle, int nu
|
||||
openvdb::initialize();
|
||||
std::string filename = "C:/Users/william/Downloads/dragon.vdb";
|
||||
openvdb::io::File file(filename);
|
||||
file.open();
|
||||
file.open(false); //disable delayed loading
|
||||
auto srcGrid = file.readGrid<BufferT>("ls_dragon");
|
||||
std::cout << "Loading OpenVDB grid[" << srcGrid->getName() << "]...\n";
|
||||
#endif
|
||||
|
||||
@@ -81,7 +81,7 @@ int main(int argc, char *argv[])
|
||||
cpuTimer.start("Read input VDB file");
|
||||
openvdb::initialize();
|
||||
openvdb::io::File inFile(argv[1]);
|
||||
inFile.open();
|
||||
inFile.open(false); // disable delayed loading
|
||||
auto baseGrids = inFile.getGrids();
|
||||
inFile.close();
|
||||
auto grid = openvdb::gridPtrCast<GridT>(baseGrids->at(0));
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// X-macro list of NanoVDB BuildT types currently exposed to Python.
|
||||
//
|
||||
// NanoVDB coding style says "avoid macro functions; use inline and templates
|
||||
// instead" (docs/codingstyle.txt). This file is a deliberate exception: it
|
||||
// must emit top-level template instantiations and binding registrations into
|
||||
// several translation units from one canonical list, which a C++ template
|
||||
// cannot do. The pattern is contained to this file plus the matching
|
||||
// per-consumer macro definitions; no runtime behavior is hidden behind a
|
||||
// macro function.
|
||||
//
|
||||
// Consumers define one or more of the kind-specific macros below before
|
||||
// including this file. Each macro is invoked once per matching type; any
|
||||
// macro the consumer does not define is treated as a no-op and reset after
|
||||
// the file is processed. This is the single point that must be edited when
|
||||
// adding (or removing) a Python-visible BuildT.
|
||||
//
|
||||
// Macros:
|
||||
// NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum)
|
||||
// Scalar value types with arithmetic semantics — exposed with full
|
||||
// NodeInfo + setVoxel accessors. Suffix forms Python class names
|
||||
// (e.g. "Float" -> "FloatGrid"). GridTypeEnum names the
|
||||
// nanovdb::GridType:: enumerator a grid of this BuildT carries
|
||||
// (used by the polymorphic handle.grid(n) dispatch).
|
||||
//
|
||||
// NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum)
|
||||
// Vector value types — exposed with a setVoxel accessor but no
|
||||
// NodeInfo. AccessorName is passed explicitly because the legacy
|
||||
// Python class names for the original two (Vec3f, RGBA8) are
|
||||
// inconsistent — kept as-is for backwards compatibility.
|
||||
//
|
||||
// NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum)
|
||||
// The nanovdb::Point build type — exposed with a bare accessor.
|
||||
// Point grids carry GridType::PointIndex (NOT a GridType::Point;
|
||||
// that enumerator doesn't exist), so GridTypeEnum is given
|
||||
// explicitly. Polymorphic dispatch routes PointIndex grids to
|
||||
// NanoGrid<Point>.
|
||||
//
|
||||
// NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum)
|
||||
// BuildTs whose nanovdb::SetVoxel<T> is unavailable (bool, the
|
||||
// quantized Fp4/Fp8/Fp16/FpN types, the index types ValueIndex /
|
||||
// ValueOnIndex, and ValueMask). nanovdb::BuildTraits<T>::is_special
|
||||
// is true for all of these. Exposed with a bare accessor only — no
|
||||
// setVoxel, no NodeInfo. The Python accessor's getValue() returns
|
||||
// the type given by nanovdb::BuildToValueMap<T>::Type — float for
|
||||
// Fp4 / Fp8 / Fp16 / FpN, uint64 for ValueIndex / ValueOnIndex,
|
||||
// bool for ValueMask and for the BuildT=bool (Boolean) grid.
|
||||
//
|
||||
// NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix)
|
||||
// Subset that has C++ sampler specializations (used by PyMath
|
||||
// samplers and PyCreateNanoGrid create*Grid factories).
|
||||
|
||||
#ifndef NANOVDB_PY_FOR_EACH_SCALAR_BUILDT
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum)
|
||||
#define NANOVDB_PY_LOCAL_DEFINED_SCALAR
|
||||
#endif
|
||||
#ifndef NANOVDB_PY_FOR_EACH_VECTOR_BUILDT
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum)
|
||||
#define NANOVDB_PY_LOCAL_DEFINED_VECTOR
|
||||
#endif
|
||||
#ifndef NANOVDB_PY_FOR_EACH_POINT_BUILDT
|
||||
#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum)
|
||||
#define NANOVDB_PY_LOCAL_DEFINED_POINT
|
||||
#endif
|
||||
#ifndef NANOVDB_PY_FOR_EACH_READONLY_BUILDT
|
||||
#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum)
|
||||
#define NANOVDB_PY_LOCAL_DEFINED_READONLY
|
||||
#endif
|
||||
#ifndef NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT
|
||||
#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix)
|
||||
#define NANOVDB_PY_LOCAL_DEFINED_SAMPLEABLE
|
||||
#endif
|
||||
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(float, Float, Float)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(double, Double, Double)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(int16_t, Int16, Int16)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(int32_t, Int32, Int32)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(int64_t, Int64, Int64)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(uint8_t, UInt8, UInt8)
|
||||
NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(uint32_t, UInt32, UInt32)
|
||||
// nanovdb::Half is intentionally not bound. The source declares it as
|
||||
// `class Half{};` (an empty placeholder for IEEE 754 half-precision, see
|
||||
// NanoVDB.h around line 180) and the C++ ProbeValue<Half> chain is
|
||||
// inconsistent — leaf storage carries Half but the ProbeValue specialization
|
||||
// expects float, causing a type mismatch during instantiation. When the
|
||||
// upstream Half implementation lands this can be added in a follow-up.
|
||||
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec3f, Vec3f,
|
||||
"Vec3fReadVectorAccessor", Vec3f)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec3d, Vec3d,
|
||||
"Vec3dReadAccessor", Vec3d)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec4f, Vec4f,
|
||||
"Vec4fReadAccessor", Vec4f)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec4d, Vec4d,
|
||||
"Vec4dReadAccessor", Vec4d)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec3u8, Vec3u8,
|
||||
"Vec3u8ReadAccessor", Vec3u8)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::Vec3u16, Vec3u16,
|
||||
"Vec3u16ReadAccessor", Vec3u16)
|
||||
NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(::nanovdb::math::Rgba8, RGBA8,
|
||||
"RGBA8ReadAccessor", RGBA8)
|
||||
|
||||
NANOVDB_PY_FOR_EACH_POINT_BUILDT(::nanovdb::Point, Point, PointIndex)
|
||||
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(bool, Boolean, Boolean)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::Fp4, Fp4, Fp4)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::Fp8, Fp8, Fp8)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::Fp16, Fp16, Fp16)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::FpN, FpN, FpN)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::ValueIndex, Index, Index)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::ValueOnIndex, OnIndex, OnIndex)
|
||||
NANOVDB_PY_FOR_EACH_READONLY_BUILDT(::nanovdb::ValueMask, Mask, Mask)
|
||||
|
||||
NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(float, Float)
|
||||
NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(double, Double)
|
||||
NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(int32_t, Int32)
|
||||
NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(::nanovdb::Vec3f, Vec3f)
|
||||
|
||||
#ifdef NANOVDB_PY_LOCAL_DEFINED_SCALAR
|
||||
#undef NANOVDB_PY_LOCAL_DEFINED_SCALAR
|
||||
#endif
|
||||
#ifdef NANOVDB_PY_LOCAL_DEFINED_VECTOR
|
||||
#undef NANOVDB_PY_LOCAL_DEFINED_VECTOR
|
||||
#endif
|
||||
#ifdef NANOVDB_PY_LOCAL_DEFINED_POINT
|
||||
#undef NANOVDB_PY_LOCAL_DEFINED_POINT
|
||||
#endif
|
||||
#ifdef NANOVDB_PY_LOCAL_DEFINED_READONLY
|
||||
#undef NANOVDB_PY_LOCAL_DEFINED_READONLY
|
||||
#endif
|
||||
#ifdef NANOVDB_PY_LOCAL_DEFINED_SAMPLEABLE
|
||||
#undef NANOVDB_PY_LOCAL_DEFINED_SAMPLEABLE
|
||||
#endif
|
||||
|
||||
#undef NANOVDB_PY_FOR_EACH_SCALAR_BUILDT
|
||||
#undef NANOVDB_PY_FOR_EACH_VECTOR_BUILDT
|
||||
#undef NANOVDB_PY_FOR_EACH_POINT_BUILDT
|
||||
#undef NANOVDB_PY_FOR_EACH_READONLY_BUILDT
|
||||
#undef NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT
|
||||
@@ -4,8 +4,19 @@ option(NANOVDB_BUILD_PYTHON_UNITTESTS [=[
|
||||
"Include the NanoVDB Python unit test. Requires a python interpreter]=]
|
||||
${NANOVDB_BUILD_UNITTESTS})
|
||||
|
||||
# Stubs are only consumed from an installed wheel (they ship next to the .so
|
||||
# in nanovdb/), so default ON only when SKBUILD is driving the build. In an
|
||||
# in-source dev / OpenVDB-CI build the .pyi is not used and stub generation
|
||||
# can be skipped — which also dodges the macOS CI's sanitizer-runtime
|
||||
# DYLD_INSERT_LIBRARIES wrapper, under which Python dlopen'ing the .so
|
||||
# triggers TSan "Interceptors are not working" and aborts stubgen.
|
||||
option(NANOVDB_BUILD_PYTHON_STUBS
|
||||
"Generate .pyi type stubs for the nanovdb Python module (requires nanobind 2.x or newer)."
|
||||
${SKBUILD})
|
||||
|
||||
nanobind_add_module(nanovdb_python NB_STATIC
|
||||
NanoVDBModule.cc
|
||||
PyBuildGrid.cc
|
||||
PyCreateNanoGrid.cc
|
||||
PyGridChecksum.cc
|
||||
PyGridHandle.cc
|
||||
@@ -18,6 +29,8 @@ nanobind_add_module(nanovdb_python NB_STATIC
|
||||
PyPrimitives.cc
|
||||
PySampleFromVoxels.cc
|
||||
PyTools.cc
|
||||
PyTree.cc
|
||||
PyVoxelBlockManager.cc
|
||||
cuda/PyDeviceBuffer.cc
|
||||
cuda/PyDeviceGridHandle.cu
|
||||
cuda/PyPointsToGrid.cu
|
||||
@@ -33,10 +46,47 @@ if(SKBUILD)
|
||||
set_target_properties(nanovdb_python PROPERTIES INSTALL_RPATH "$ORIGIN/../../openvdb/lib")
|
||||
install(TARGETS nanovdb_python DESTINATION ${NANOVDB_INSTALL_LIBDIR})
|
||||
install(FILES __init__.py DESTINATION nanovdb)
|
||||
# Ship the nanovdb C/C++ headers inside the Python wheel so downstream
|
||||
# extension authors can compile against the same NanoVDB the wheel was
|
||||
# built with. nanovdb.get_include() resolves to this directory at runtime.
|
||||
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../
|
||||
DESTINATION nanovdb/include/nanovdb
|
||||
FILES_MATCHING
|
||||
PATTERN "*.h"
|
||||
PATTERN "*.cuh"
|
||||
PATTERN "python" EXCLUDE
|
||||
PATTERN "examples" EXCLUDE
|
||||
PATTERN "unittest" EXCLUDE
|
||||
PATTERN "cmd" EXCLUDE
|
||||
PATTERN "docs" EXCLUDE)
|
||||
else()
|
||||
install(TARGETS nanovdb_python DESTINATION ${VDB_PYTHON_INSTALL_DIRECTORY})
|
||||
endif()
|
||||
|
||||
# .pyi type stubs for IDE / type-checker support. Driven by the CMake helper
|
||||
# that ships with nanobind 2.x; silently skipped on older nanobind where the
|
||||
# helper does not exist.
|
||||
if(NANOVDB_BUILD_PYTHON_STUBS AND COMMAND nanobind_add_stub)
|
||||
set(NANOVDB_STUB_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
nanobind_add_stub(nanovdb_python_stub
|
||||
MODULE nanovdb
|
||||
OUTPUT "${NANOVDB_STUB_OUTPUT_DIR}/nanovdb.pyi"
|
||||
PYTHON_PATH $<TARGET_FILE_DIR:nanovdb_python>
|
||||
DEPENDS nanovdb_python
|
||||
MARKER_FILE "${NANOVDB_STUB_OUTPUT_DIR}/py.typed"
|
||||
INCLUDE_PRIVATE)
|
||||
if(SKBUILD)
|
||||
install(FILES
|
||||
"${NANOVDB_STUB_OUTPUT_DIR}/nanovdb.pyi"
|
||||
"${NANOVDB_STUB_OUTPUT_DIR}/py.typed"
|
||||
DESTINATION nanovdb)
|
||||
endif()
|
||||
elseif(NANOVDB_BUILD_PYTHON_STUBS)
|
||||
message(STATUS
|
||||
"NANOVDB_BUILD_PYTHON_STUBS is ON but nanobind_add_stub is unavailable "
|
||||
"(nanobind >= 2.0 required). Skipping stub generation.")
|
||||
endif()
|
||||
|
||||
# pytest
|
||||
if(NANOVDB_BUILD_PYTHON_UNITTESTS)
|
||||
|
||||
@@ -49,12 +99,19 @@ if(NANOVDB_BUILD_PYTHON_UNITTESTS)
|
||||
COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/TestNanoVDB.py -v
|
||||
WORKING_DIRECTORY "${NANOVDB_PYTHON_WORKING_DIR}")
|
||||
|
||||
# Smoke-run every example script (they self-skip when optional
|
||||
# dependencies such as NumPy or OpenVDB are unavailable).
|
||||
add_test(NAME pytest_nanovdb_examples
|
||||
COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/TestExamples.py -v
|
||||
WORKING_DIRECTORY "${NANOVDB_PYTHON_WORKING_DIR}")
|
||||
set_tests_properties(pytest_nanovdb_examples PROPERTIES TIMEOUT 300)
|
||||
|
||||
if(WIN32)
|
||||
set(PYTHONPATH "$ENV{PYTHONPATH};${NANOVDB_PYTHON_WORKING_DIR}")
|
||||
string(REPLACE "\\;" ";" PYTHONPATH "${PYTHONPATH}")
|
||||
string(REPLACE ";" "\\;" PYTHONPATH "${PYTHONPATH}")
|
||||
set_tests_properties(pytest_nanovdb PROPERTIES ENVIRONMENT "PYTHONPATH=${PYTHONPATH}")
|
||||
set_tests_properties(pytest_nanovdb pytest_nanovdb_examples PROPERTIES ENVIRONMENT "PYTHONPATH=${PYTHONPATH}")
|
||||
else()
|
||||
set_tests_properties(pytest_nanovdb PROPERTIES ENVIRONMENT "PYTHONPATH=$ENV{PYTHONPATH}:${NANOVDB_PYTHON_WORKING_DIR}")
|
||||
set_tests_properties(pytest_nanovdb pytest_nanovdb_examples PROPERTIES ENVIRONMENT "PYTHONPATH=$ENV{PYTHONPATH}:${NANOVDB_PYTHON_WORKING_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
+1001
-194
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,239 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "PyBuildGrid.h"
|
||||
|
||||
#include <nanobind/stl/array.h>
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/HostBuffer.h>
|
||||
#include <nanovdb/tools/CreateNanoGrid.h>
|
||||
#include <nanovdb/tools/GridBuilder.h>
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
using namespace nanovdb;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// Bind nanovdb::tools::build::Grid<BuildT> together with its
|
||||
// ValueAccessor<BuildT> and Tree<BuildT>::WriteAccessor proxies, plus a
|
||||
// .to_nanovdb() shortcut that bakes the build grid into a host NanoGrid
|
||||
// via tools::createNanoGrid.
|
||||
//
|
||||
// One instantiation per writable BuildT in BuildTypes.def (scalars +
|
||||
// vectors). All three classes live under the nanovdb.tools.build submodule;
|
||||
// the Python class names mirror the existing typed-grid naming so
|
||||
// nanovdb.tools.build.FloatGrid is the mutable counterpart of the read-only
|
||||
// nanovdb.FloatGrid.
|
||||
template<typename BuildT>
|
||||
static void defineBuildGrid(nb::module_& m,
|
||||
const char* gridName,
|
||||
const char* valueAccName,
|
||||
const char* writeAccName)
|
||||
{
|
||||
using GridT = tools::build::Grid<BuildT>;
|
||||
using TreeT = tools::build::Tree<BuildT>;
|
||||
using AccT = tools::build::ValueAccessor<BuildT>;
|
||||
using WriteAccT = typename TreeT::WriteAccessor;
|
||||
using ValueT = typename GridT::ValueType;
|
||||
|
||||
// ----- build::Grid<BuildT> -----
|
||||
nb::class_<GridT>(m, gridName,
|
||||
"Mutable host-side grid builder for this BuildT. Write voxels via "
|
||||
"setValue / getAccessor / getWriteAccessor, then call to_nanovdb() "
|
||||
"to bake a frozen NanoGrid GridHandle.")
|
||||
.def(nb::init<const ValueT&, const std::string&, GridClass>(),
|
||||
"background"_a,
|
||||
"name"_a = std::string(""),
|
||||
"gridClass"_a = GridClass::Unknown,
|
||||
"Construct an empty mutable build grid. Voxels read as "
|
||||
"background until written.")
|
||||
.def("getValue",
|
||||
[](const GridT& self, const Coord& ijk) -> ValueT {
|
||||
return self.getValue(ijk);
|
||||
},
|
||||
"ijk"_a,
|
||||
"Return the value at ijk (background if no leaf covers it).")
|
||||
.def("setValue",
|
||||
[](GridT& self, const Coord& ijk, const ValueT& value) {
|
||||
self.setValue(ijk, value);
|
||||
},
|
||||
"ijk"_a, "value"_a,
|
||||
"Set the voxel value at ijk and mark the voxel active.")
|
||||
// build::Grid has no top-level isActive(ijk); the read path is via
|
||||
// ValueAccessor. Spin up a fresh accessor for the single query so
|
||||
// Python callers don't have to.
|
||||
.def("isActive",
|
||||
[](GridT& self, const Coord& ijk) {
|
||||
AccT acc = self.getAccessor();
|
||||
return acc.isActive(ijk);
|
||||
},
|
||||
"ijk"_a,
|
||||
"Return True iff ijk is in an active voxel. Equivalent to "
|
||||
"self.getAccessor().isActive(ijk), but allocates a fresh "
|
||||
"accessor for each call — for repeated queries use "
|
||||
"self.getAccessor() and reuse it.")
|
||||
.def("setValueOn",
|
||||
[](GridT& self, const Coord& ijk) {
|
||||
AccT acc = self.getAccessor();
|
||||
acc.setValueOn(ijk);
|
||||
},
|
||||
"ijk"_a,
|
||||
"Mark ijk active without changing the stored value. Equivalent "
|
||||
"to self.getAccessor().setValueOn(ijk).")
|
||||
.def("nodeCount",
|
||||
[](const GridT& self) -> std::array<size_t, 3> {
|
||||
return self.nodeCount();
|
||||
},
|
||||
"Return a 3-tuple (leaf_count, lower_count, upper_count).")
|
||||
.def("gridType", &GridT::gridType,
|
||||
"Return the GridType enumerator this BuildT carries.")
|
||||
.def("gridClass", &GridT::gridClass,
|
||||
"Return the GridClass assigned at construction time.")
|
||||
.def("getName", &GridT::getName,
|
||||
"Return the grid name (as passed at construction).")
|
||||
.def("setName", &GridT::setName, "name"_a,
|
||||
"Replace the grid name.")
|
||||
.def("setTransform", &GridT::setTransform,
|
||||
"scale"_a = 1.0,
|
||||
"translation"_a = Vec3d(0.0),
|
||||
"Set an affine index-to-world map from a uniform scale and "
|
||||
"translation. Replaces any prior transform.")
|
||||
.def_prop_ro("background",
|
||||
[](const GridT& self) -> ValueT {
|
||||
return self.mRoot.background();
|
||||
},
|
||||
"The background value supplied at construction.")
|
||||
.def("getAccessor", &GridT::getAccessor,
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a ValueAccessor wired to this grid's root. Thread-safe "
|
||||
"for reads, NOT thread-safe for writes. The accessor borrows "
|
||||
"from this grid — the grid must outlive it.")
|
||||
// WriteAccessor's defaulted move constructor would leave its
|
||||
// internal ValueAccessor's `mRoot&` reference dangling (it points
|
||||
// into the moved-from WriteAccessor's own mRoot field). Bypass
|
||||
// the move path entirely by heap-allocating and handing nanobind
|
||||
// ownership — the C++ object stays at a stable address for its
|
||||
// whole lifetime.
|
||||
.def("getWriteAccessor",
|
||||
[](GridT& self) -> WriteAccT* {
|
||||
return new WriteAccT(self.mRoot, self.mMutex);
|
||||
},
|
||||
nb::rv_policy::take_ownership,
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a WriteAccessor for thread-safe writes; the accessor "
|
||||
"buffers changes into a private root and merges them into the "
|
||||
"parent grid on destruction (or on an explicit merge() call). "
|
||||
"Held by nanobind on the heap so the accessor's internal "
|
||||
"references stay valid.")
|
||||
// Baking a large grid is the most expensive operation on this
|
||||
// class; release the GIL so other Python threads can run during
|
||||
// the conversion (the lambda only touches C++ state).
|
||||
.def("to_nanovdb",
|
||||
[](const GridT& self,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
int verbose) {
|
||||
return tools::createNanoGrid<GridT, BuildT, HostBuffer>(
|
||||
self, sMode, cMode, verbose);
|
||||
},
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"verbose"_a = 0,
|
||||
"Bake this mutable grid into a host NanoGrid<BuildT> and "
|
||||
"return its GridHandle. The build grid is unchanged. "
|
||||
"Releases the GIL during conversion.");
|
||||
|
||||
// ----- build::ValueAccessor<BuildT> -----
|
||||
//
|
||||
// Move-only (copy is deleted) — returned by getAccessor(). Caches the
|
||||
// last leaf / lower / upper node it touched, so repeated access to
|
||||
// neighboring coordinates is fast.
|
||||
nb::class_<AccT>(m, valueAccName,
|
||||
"Move-only read/write accessor returned by build.Grid.getAccessor(). "
|
||||
"Caches the most recently visited tree path for fast neighbor access.")
|
||||
.def("getValue",
|
||||
[](const AccT& self, const Coord& ijk) -> ValueT {
|
||||
return self.getValue(ijk);
|
||||
},
|
||||
"ijk"_a, "Return the value at ijk (uses the cache).")
|
||||
.def("setValue",
|
||||
[](AccT& self, const Coord& ijk, const ValueT& value) {
|
||||
self.setValue(ijk, value);
|
||||
},
|
||||
"ijk"_a, "value"_a,
|
||||
"Set the value at ijk and mark it active (uses the cache).")
|
||||
.def("setValueOn",
|
||||
[](AccT& self, const Coord& ijk) { self.setValueOn(ijk); },
|
||||
"ijk"_a,
|
||||
"Mark ijk active without changing the stored value.")
|
||||
.def("isActive",
|
||||
[](const AccT& self, const Coord& ijk) {
|
||||
return self.isActive(ijk);
|
||||
},
|
||||
"ijk"_a,
|
||||
"Return True iff ijk is in an active voxel.")
|
||||
.def("isValueOn",
|
||||
[](const AccT& self, const Coord& ijk) {
|
||||
return self.isValueOn(ijk);
|
||||
},
|
||||
"ijk"_a,
|
||||
"Alias for isActive(ijk).");
|
||||
|
||||
// ----- build::Tree<BuildT>::WriteAccessor -----
|
||||
//
|
||||
// Move-only. Holds its own root node + a reference to the parent root's
|
||||
// mutex; on destruction (or explicit merge()) it locks the mutex and
|
||||
// splices its buffered nodes into the parent. Designed for multi-thread
|
||||
// writes — one WriteAccessor per thread, no shared mutable state.
|
||||
nb::class_<WriteAccT>(m, writeAccName,
|
||||
"Thread-safe write accessor returned by build.Grid.getWriteAccessor(). "
|
||||
"Buffers writes into a private root and merges them into the parent "
|
||||
"grid on destruction or explicit merge().")
|
||||
.def("setValue",
|
||||
[](WriteAccT& self, const Coord& ijk, const ValueT& value) {
|
||||
self.setValue(ijk, value);
|
||||
},
|
||||
"ijk"_a, "value"_a,
|
||||
"Buffer a set into this accessor's private root.")
|
||||
.def("setValueOn",
|
||||
[](WriteAccT& self, const Coord& ijk) { self.setValueOn(ijk); },
|
||||
"ijk"_a,
|
||||
"Buffer an active-state set into this accessor's private root.")
|
||||
.def("merge", &WriteAccT::merge,
|
||||
"Lock the parent mutex and splice this accessor's buffered "
|
||||
"nodes into the parent grid. Called automatically when the "
|
||||
"accessor is destroyed; calling it explicitly is only "
|
||||
"necessary if you want the changes visible to the parent "
|
||||
"before the accessor goes out of scope.");
|
||||
}
|
||||
|
||||
void defineBuildGridModule(nb::module_& toolsModule)
|
||||
{
|
||||
nb::module_ buildModule = toolsModule.def_submodule("build");
|
||||
buildModule.doc() =
|
||||
"Mutable, voxel-by-voxel CPU grid builder mirroring "
|
||||
"nanovdb::tools::build::*. Construct a typed Grid (e.g. "
|
||||
"FloatGrid(0.0, 'mygrid')), populate it with setValue / "
|
||||
"ValueAccessor / WriteAccessor, then call .to_nanovdb() to bake "
|
||||
"a host NanoGrid handle.";
|
||||
|
||||
// X-macro instantiation over every writable BuildT: scalars (full
|
||||
// arithmetic) and vectors. Read-only special BuildTs (Boolean, Fp*,
|
||||
// Index, Mask) and Point are deliberately excluded — they have no
|
||||
// SetValue<T> specialization and can't be built voxel-by-voxel.
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
defineBuildGrid<T>(buildModule, #Suffix "Grid", \
|
||||
#Suffix "ValueAccessor", #Suffix "WriteAccessor");
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
defineBuildGrid<T>(buildModule, #Suffix "Grid", \
|
||||
#Suffix "ValueAccessor", #Suffix "WriteAccessor");
|
||||
#include "BuildTypes.def"
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#ifndef NANOVDB_PYBUILDGRID_HAS_BEEN_INCLUDED
|
||||
#define NANOVDB_PYBUILDGRID_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <nanobind/nanobind.h>
|
||||
|
||||
namespace nb = nanobind;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
/// @brief Register the nanovdb.tools.build submodule and its per-BuildT
|
||||
/// Grid / ValueAccessor / WriteAccessor classes (one set per writable
|
||||
/// scalar and vector BuildT in BuildTypes.def). Constructs the submodule
|
||||
/// as `toolsModule.def_submodule("build")`.
|
||||
void defineBuildGridModule(nb::module_& toolsModule);
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
@@ -11,6 +11,10 @@
|
||||
#include <nanovdb/tools/CreateNanoGrid.h>
|
||||
#include <nanovdb/tools/GridBuilder.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
using namespace nanovdb;
|
||||
@@ -35,20 +39,553 @@ GridHandle<HostBuffer> createNanoGridFromFunc(const BuildT&
|
||||
|
||||
template<typename BuildT> void defineCreateNanoGrid(nb::module_& m, const char* name)
|
||||
{
|
||||
m.def(name, &createNanoGridFromFunc<BuildT>, nb::call_guard<nb::gil_scoped_release>(), "background"_a, "name"_a, "gridClass"_a, "func"_a, "bbox"_a);
|
||||
m.def(name, &createNanoGridFromFunc<BuildT>, nb::call_guard<nb::gil_scoped_release>(), "background"_a, "name"_a, "gridClass"_a, "func"_a, "bbox"_a,
|
||||
"Construct a NanoGrid by evaluating func(Coord) over every voxel in bbox. "
|
||||
"Returns a GridHandle owning the freshly-built grid.");
|
||||
}
|
||||
|
||||
template<typename BufferT> void defineOpenToNanoVDB(nb::module_& m)
|
||||
{
|
||||
#ifdef NANOVDB_USE_OPENVDB
|
||||
m.def("openToNanoVDB", &tools::openToNanoVDB<BufferT>, "base"_a, "sMode"_a = tools::StatsMode::Default, "cMode"_a = CheckMode::Default, "verbose"_a = 0);
|
||||
m.def("openToNanoVDB", &tools::openToNanoVDB<BufferT>,
|
||||
"base"_a,
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"verbose"_a = 0,
|
||||
"Convert an OpenVDB base grid to a NanoVDB GridHandle.");
|
||||
#endif
|
||||
}
|
||||
|
||||
template void defineCreateNanoGrid<float>(nb::module_&, const char*);
|
||||
template void defineCreateNanoGrid<double>(nb::module_&, const char*);
|
||||
template void defineCreateNanoGrid<int32_t>(nb::module_&, const char*);
|
||||
template void defineCreateNanoGrid<Vec3f>(nb::module_&, const char*);
|
||||
// ============================================================================
|
||||
// Conversion bindings: AbsDiff/RelDiff oracle classes, and the polymorphic
|
||||
// createNanoGrid free functions for quantized + index destination BuildTs.
|
||||
// Each accepts source = NanoGrid<SrcBuildT> OR build::Grid<SrcBuildT>.
|
||||
// ============================================================================
|
||||
|
||||
namespace {
|
||||
|
||||
// ----- Quantized (Fp4/Fp8/Fp16) -----
|
||||
//
|
||||
// C++ signature: createNanoGrid<SrcGridT, DstBuildT, BufferT>(srcGrid,
|
||||
// sMode, cMode, ditherOn, verbose, buffer).
|
||||
//
|
||||
// Try SrcBuildT against both NanoGrid<SrcBuildT> and build::Grid<SrcBuildT>
|
||||
// and return an empty nb::object on no match so the caller can fall through
|
||||
// to the next SrcBuildT.
|
||||
//
|
||||
// GIL is held for the isinstance / cast dispatch (which touches the Python
|
||||
// object's type and reference graph) but released around the underlying
|
||||
// tools::createNanoGrid traversal — the source data lives in stable C++
|
||||
// storage whose lifetime is anchored by the Python wrapper passed in via
|
||||
// py_src, so it's safe to read without holding the GIL.
|
||||
template<typename SrcBuildT, typename DstBuildT>
|
||||
nb::object tryQuantizeFpX(nb::handle py_src,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
bool ditherOn,
|
||||
int verbose)
|
||||
{
|
||||
using NanoSrcT = NanoGrid<SrcBuildT>;
|
||||
using BuildSrcT = tools::build::Grid<SrcBuildT>;
|
||||
if (nb::isinstance<NanoSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const NanoSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<NanoSrcT, DstBuildT, HostBuffer>(
|
||||
src, sMode, cMode, ditherOn, verbose);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
if (nb::isinstance<BuildSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const BuildSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<BuildSrcT, DstBuildT, HostBuffer>(
|
||||
src, sMode, cMode, ditherOn, verbose);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
return nb::object();
|
||||
}
|
||||
|
||||
template<typename DstBuildT>
|
||||
nb::object createNanoGridFpX(nb::handle py_src,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
bool ditherOn,
|
||||
int verbose,
|
||||
const char* pyFnName)
|
||||
{
|
||||
// The C++ Fp{4,8,16,N} preProcess static_asserts SrcValueT == float;
|
||||
// double sources hit a compile-time error, so we accept float only.
|
||||
if (auto r = tryQuantizeFpX<float, DstBuildT>(
|
||||
py_src, sMode, cMode, ditherOn, verbose); r.is_valid()) return r;
|
||||
std::string msg(pyFnName);
|
||||
msg += ": source must be a FloatGrid or nanovdb.tools.build.FloatGrid "
|
||||
"(Fp4/Fp8/Fp16/FpN require a float source value type).";
|
||||
throw nb::type_error(msg.c_str());
|
||||
}
|
||||
|
||||
// ----- FpN (variable bit-width) -----
|
||||
//
|
||||
// C++ signature: createNanoGrid<SrcGridT, FpN, OracleT, BufferT>(srcGrid,
|
||||
// sMode, cMode, ditherOn, verbose, oracle, buffer). OracleT is AbsDiff or
|
||||
// RelDiff; the binding exposes both as separate Python overloads.
|
||||
template<typename SrcBuildT, typename OracleT>
|
||||
nb::object tryQuantizeFpN(nb::handle py_src,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
bool ditherOn,
|
||||
int verbose,
|
||||
const OracleT& oracle)
|
||||
{
|
||||
using NanoSrcT = NanoGrid<SrcBuildT>;
|
||||
using BuildSrcT = tools::build::Grid<SrcBuildT>;
|
||||
// Same GIL pattern as tryQuantizeFpX: hold the GIL through the
|
||||
// isinstance / cast dispatch, release it for the conversion.
|
||||
if (nb::isinstance<NanoSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const NanoSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<NanoSrcT, FpN, OracleT, HostBuffer>(
|
||||
src, sMode, cMode, ditherOn, verbose, oracle);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
if (nb::isinstance<BuildSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const BuildSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<BuildSrcT, FpN, OracleT, HostBuffer>(
|
||||
src, sMode, cMode, ditherOn, verbose, oracle);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
return nb::object();
|
||||
}
|
||||
|
||||
template<typename OracleT>
|
||||
nb::object createNanoGridFpNImpl(nb::handle py_src,
|
||||
const OracleT& oracle,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
bool ditherOn,
|
||||
int verbose)
|
||||
{
|
||||
if (auto r = tryQuantizeFpN<float, OracleT>(
|
||||
py_src, sMode, cMode, ditherOn, verbose, oracle); r.is_valid()) return r;
|
||||
throw nb::type_error(
|
||||
"createNanoGridFpN: source must be a FloatGrid or "
|
||||
"nanovdb.tools.build.FloatGrid (FpN requires a float source value type).");
|
||||
}
|
||||
|
||||
// ----- Index / OnIndex -----
|
||||
//
|
||||
// C++ signature: createNanoGrid<SrcGridT, DstBuildT, BufferT>(srcGrid,
|
||||
// channels, includeStats, includeTiles, verbose, buffer). DstBuildT is
|
||||
// ValueIndex or ValueOnIndex; the binding exposes both as separate
|
||||
// named functions. Source set is wider than the quantized variants —
|
||||
// any arithmetic or vector source can be re-cast as an index grid.
|
||||
template<typename SrcBuildT, typename DstBuildT>
|
||||
nb::object tryIndexify(nb::handle py_src,
|
||||
uint32_t channels,
|
||||
bool includeStats,
|
||||
bool includeTiles,
|
||||
int verbose)
|
||||
{
|
||||
using NanoSrcT = NanoGrid<SrcBuildT>;
|
||||
using BuildSrcT = tools::build::Grid<SrcBuildT>;
|
||||
// Same GIL pattern as tryQuantizeFpX.
|
||||
if (nb::isinstance<NanoSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const NanoSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<NanoSrcT, DstBuildT, HostBuffer>(
|
||||
src, channels, includeStats, includeTiles, verbose);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
if (nb::isinstance<BuildSrcT>(py_src)) {
|
||||
const auto& src = nb::cast<const BuildSrcT&>(py_src);
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
handle = tools::createNanoGrid<BuildSrcT, DstBuildT, HostBuffer>(
|
||||
src, channels, includeStats, includeTiles, verbose);
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
return nb::object();
|
||||
}
|
||||
|
||||
template<typename DstBuildT>
|
||||
nb::object createIndexImpl(nb::handle py_src,
|
||||
uint32_t channels,
|
||||
bool includeStats,
|
||||
bool includeTiles,
|
||||
int verbose,
|
||||
const char* pyFnName)
|
||||
{
|
||||
if (auto r = tryIndexify<float, DstBuildT>(py_src, channels, includeStats, includeTiles, verbose); r.is_valid()) return r;
|
||||
if (auto r = tryIndexify<double, DstBuildT>(py_src, channels, includeStats, includeTiles, verbose); r.is_valid()) return r;
|
||||
if (auto r = tryIndexify<int32_t, DstBuildT>(py_src, channels, includeStats, includeTiles, verbose); r.is_valid()) return r;
|
||||
if (auto r = tryIndexify<Vec3f, DstBuildT>(py_src, channels, includeStats, includeTiles, verbose); r.is_valid()) return r;
|
||||
std::string msg(pyFnName);
|
||||
msg += ": source must be a FloatGrid, DoubleGrid, Int32Grid, "
|
||||
"Vec3fGrid, or the matching nanovdb.tools.build.* mutable grid.";
|
||||
throw nb::type_error(msg.c_str());
|
||||
}
|
||||
|
||||
// ----- tools.CreateNanoGrid: converter class with blind-data authoring -----
|
||||
//
|
||||
// Mirrors nanovdb::tools::CreateNanoGrid<SrcGridT>. The C++ class is
|
||||
// templated on the source grid type, so this binding stores the source
|
||||
// Python object plus the recorded settings and addBlindData() calls, then
|
||||
// dispatches over the supported SrcBuildTs when getHandle() is called —
|
||||
// constructing the C++ converter, replaying the recorded state, and baking
|
||||
// the handle. The destination BuildT is the source BuildT (the C++
|
||||
// default); quantized and index destinations remain on the
|
||||
// createNanoGridFp* / createNanoGridIndex free functions above. Authored
|
||||
// channels come back zeroed — fill them through the writable NumPy view
|
||||
// returned by grid.getBlindData(n).
|
||||
|
||||
// Byte size of one element of the given blind-data GridType, or 0 when the
|
||||
// size cannot be derived (the caller must then pass it explicitly). Matches
|
||||
// the per-type size table enforced by GridBlindMetaData::isValid().
|
||||
uint32_t blindDataTypeSize(GridType dataType)
|
||||
{
|
||||
switch (dataType) {
|
||||
case GridType::Float: return 4u;
|
||||
case GridType::Double: return 8u;
|
||||
case GridType::Int16: return 2u;
|
||||
case GridType::Int32: return 4u;
|
||||
case GridType::Int64: return 8u;
|
||||
case GridType::UInt8: return 1u;
|
||||
case GridType::UInt32: return 4u;
|
||||
case GridType::Half: return 2u;
|
||||
case GridType::RGBA8: return 4u;
|
||||
case GridType::Fp8: return 1u;
|
||||
case GridType::Fp16: return 2u;
|
||||
case GridType::Vec3f: return 12u;
|
||||
case GridType::Vec3d: return 24u;
|
||||
case GridType::Vec4f: return 16u;
|
||||
case GridType::Vec4d: return 32u;
|
||||
case GridType::Vec3u8: return 3u;
|
||||
case GridType::Vec3u16: return 6u;
|
||||
default: return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
class PyCreateNanoGrid
|
||||
{
|
||||
struct BlindDataSpec
|
||||
{
|
||||
std::string name;
|
||||
GridBlindDataSemantic semantic;
|
||||
GridBlindDataClass dataClass;
|
||||
GridType dataType;
|
||||
uint64_t count;
|
||||
uint32_t size;
|
||||
};
|
||||
|
||||
public:
|
||||
explicit PyCreateNanoGrid(nb::object src)
|
||||
: mSrc(std::move(src))
|
||||
{
|
||||
if (!(matches<float>() || matches<double>() ||
|
||||
matches<int32_t>() || matches<Vec3f>())) {
|
||||
throw nb::type_error(
|
||||
"CreateNanoGrid: source must be a FloatGrid, DoubleGrid, "
|
||||
"Int32Grid, Vec3fGrid, or the matching "
|
||||
"nanovdb.tools.build.* mutable grid.");
|
||||
}
|
||||
}
|
||||
|
||||
// Validates eagerly (the C++ ctor only NANOVDB_ASSERTs, which release
|
||||
// builds skip) so mistakes surface here rather than as an invalid grid.
|
||||
uint64_t addBlindData(const std::string& name,
|
||||
uint64_t count,
|
||||
GridType dataType,
|
||||
GridBlindDataSemantic semantic,
|
||||
GridBlindDataClass dataClass,
|
||||
uint32_t size)
|
||||
{
|
||||
if (name.size() >= GridBlindMetaData::MaxNameSize) {
|
||||
throw nb::value_error(
|
||||
"addBlindData: name exceeds the 255 character limit.");
|
||||
}
|
||||
if (size == 0u) size = blindDataTypeSize(dataType);
|
||||
if (size == 0u) {
|
||||
throw nb::value_error(
|
||||
"addBlindData: the element size cannot be derived from this "
|
||||
"dataType — pass size explicitly.");
|
||||
}
|
||||
const GridBlindMetaData meta(0, count, size, semantic, dataClass, dataType);
|
||||
if (!meta.isValid()) {
|
||||
throw nb::value_error(
|
||||
"addBlindData: invalid combination of dataSemantic, "
|
||||
"dataClass, dataType, and size.");
|
||||
}
|
||||
mBlind.push_back(BlindDataSpec{name, semantic, dataClass, dataType, count, size});
|
||||
return static_cast<uint64_t>(mBlind.size() - 1);
|
||||
}
|
||||
|
||||
void setStats(tools::StatsMode mode) { mStats = mode; }
|
||||
void setChecksum(CheckMode mode) { mChecksum = mode; }
|
||||
void setVerbose(int mode) { mVerbose = mode; }
|
||||
void enableDithering(bool on) { mDither = on; }
|
||||
|
||||
nb::object getHandle() const
|
||||
{
|
||||
if (auto r = tryGetHandle<float>(); r.is_valid()) return r;
|
||||
if (auto r = tryGetHandle<double>(); r.is_valid()) return r;
|
||||
if (auto r = tryGetHandle<int32_t>(); r.is_valid()) return r;
|
||||
if (auto r = tryGetHandle<Vec3f>(); r.is_valid()) return r;
|
||||
throw nb::type_error("CreateNanoGrid: unsupported source grid type.");
|
||||
}
|
||||
|
||||
private:
|
||||
template<typename SrcBuildT> bool matches() const
|
||||
{
|
||||
return nb::isinstance<NanoGrid<SrcBuildT>>(mSrc) ||
|
||||
nb::isinstance<tools::build::Grid<SrcBuildT>>(mSrc);
|
||||
}
|
||||
|
||||
template<typename SrcBuildT> nb::object tryGetHandle() const
|
||||
{
|
||||
using NanoSrcT = NanoGrid<SrcBuildT>;
|
||||
using BuildSrcT = tools::build::Grid<SrcBuildT>;
|
||||
if (nb::isinstance<NanoSrcT>(mSrc)) return this->bake(nb::cast<const NanoSrcT&>(mSrc));
|
||||
if (nb::isinstance<BuildSrcT>(mSrc)) return this->bake(nb::cast<const BuildSrcT&>(mSrc));
|
||||
return nb::object();
|
||||
}
|
||||
|
||||
// Same GIL pattern as tryQuantizeFpX: the dispatch above runs with the
|
||||
// GIL held, the traversal runs without it (the source's lifetime is
|
||||
// anchored by mSrc).
|
||||
template<typename SrcGridT> nb::object bake(const SrcGridT& src) const
|
||||
{
|
||||
GridHandle<HostBuffer> handle;
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
tools::CreateNanoGrid<SrcGridT> converter(src);
|
||||
converter.setStats(mStats);
|
||||
converter.setChecksum(mChecksum);
|
||||
converter.setVerbose(mVerbose);
|
||||
converter.enableDithering(mDither);
|
||||
for (const auto& b : mBlind) {
|
||||
converter.addBlindData(b.name, b.semantic, b.dataClass, b.dataType,
|
||||
static_cast<size_t>(b.count),
|
||||
static_cast<size_t>(b.size));
|
||||
}
|
||||
handle = converter.getHandle();
|
||||
// The C++ converter allocates authored channels without clearing
|
||||
// them (C++ callers memcpy their payload in). Zero-fill here so
|
||||
// the NumPy view starts deterministic. The authored channels are
|
||||
// the first mBlind.size() blind-data entries — any converter-
|
||||
// added channel (e.g. a long grid name) is appended after them.
|
||||
if (!mBlind.empty()) {
|
||||
if (auto* dst = const_cast<GridData*>(handle.gridData())) {
|
||||
for (size_t i = 0; i < mBlind.size(); ++i) {
|
||||
const GridBlindMetaData* meta = dst->blindMetaData(uint32_t(i));
|
||||
std::memset(const_cast<void*>(meta->blindData()), 0,
|
||||
meta->blindDataSize());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nb::cast(std::move(handle));
|
||||
}
|
||||
|
||||
nb::object mSrc;
|
||||
std::vector<BlindDataSpec> mBlind;
|
||||
tools::StatsMode mStats = tools::StatsMode::Default;
|
||||
CheckMode mChecksum = CheckMode::Default;
|
||||
int mVerbose = 0;
|
||||
bool mDither = false;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void defineCreateNanoGridConversions(nb::module_& toolsModule)
|
||||
{
|
||||
// ------ Oracle classes ------
|
||||
nb::class_<tools::AbsDiff>(toolsModule, "AbsDiff",
|
||||
"Compression oracle for FpN: accept the approximation when "
|
||||
"|exact - approx| <= tolerance. A tolerance of -1.0 (the "
|
||||
"default) means uninitialized; any non-negative value (including "
|
||||
"0.0) is treated as initialized by the operator bool() check, "
|
||||
"or the C++ create function can fill it in via init().")
|
||||
.def(nb::init<float>(), "tolerance"_a = -1.0f,
|
||||
"Construct an AbsDiff oracle with the given absolute tolerance.")
|
||||
.def("getTolerance", &tools::AbsDiff::getTolerance,
|
||||
"Return the current absolute tolerance.")
|
||||
.def("setTolerance", &tools::AbsDiff::setTolerance, "tolerance"_a,
|
||||
"Replace the current absolute tolerance.")
|
||||
.def("__bool__",
|
||||
[](const tools::AbsDiff& self) { return bool(self); },
|
||||
"True iff the tolerance has been initialized (>= 0).");
|
||||
|
||||
nb::class_<tools::RelDiff>(toolsModule, "RelDiff",
|
||||
"Compression oracle for FpN: accept the approximation when "
|
||||
"|exact - approx| / max(|exact|, |approx|) <= tolerance.")
|
||||
.def(nb::init<float>(), "tolerance"_a = -1.0f,
|
||||
"Construct a RelDiff oracle with the given relative tolerance.")
|
||||
.def("getTolerance", &tools::RelDiff::getTolerance,
|
||||
"Return the current relative tolerance.")
|
||||
.def("setTolerance", &tools::RelDiff::setTolerance, "tolerance"_a,
|
||||
"Replace the current relative tolerance.")
|
||||
.def("__bool__",
|
||||
[](const tools::RelDiff& self) { return bool(self); },
|
||||
"True iff the tolerance has been initialized (>= 0).");
|
||||
|
||||
// ------ Quantized fixed-width: Fp4 / Fp8 / Fp16 ------
|
||||
toolsModule.def("createNanoGridFp4",
|
||||
[](nb::handle src, tools::StatsMode sMode, CheckMode cMode,
|
||||
bool ditherOn, int verbose) {
|
||||
return createNanoGridFpX<Fp4>(src, sMode, cMode, ditherOn, verbose,
|
||||
"createNanoGridFp4");
|
||||
},
|
||||
"src"_a, "sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0,
|
||||
"Quantize a NanoGrid<float> or nanovdb.tools.build.FloatGrid into a "
|
||||
"NanoGrid<Fp4> (4 bits per voxel). ditherOn adds sub-quantum "
|
||||
"noise to break up banding.");
|
||||
|
||||
toolsModule.def("createNanoGridFp8",
|
||||
[](nb::handle src, tools::StatsMode sMode, CheckMode cMode,
|
||||
bool ditherOn, int verbose) {
|
||||
return createNanoGridFpX<Fp8>(src, sMode, cMode, ditherOn, verbose,
|
||||
"createNanoGridFp8");
|
||||
},
|
||||
"src"_a, "sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0,
|
||||
"Quantize a NanoGrid<float> or nanovdb.tools.build.FloatGrid into a NanoGrid<Fp8> (8 bits "
|
||||
"per voxel). ditherOn adds sub-quantum noise.");
|
||||
|
||||
toolsModule.def("createNanoGridFp16",
|
||||
[](nb::handle src, tools::StatsMode sMode, CheckMode cMode,
|
||||
bool ditherOn, int verbose) {
|
||||
return createNanoGridFpX<Fp16>(src, sMode, cMode, ditherOn, verbose,
|
||||
"createNanoGridFp16");
|
||||
},
|
||||
"src"_a, "sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0,
|
||||
"Quantize a NanoGrid<float> or nanovdb.tools.build.FloatGrid into a NanoGrid<Fp16> (16 bits "
|
||||
"per voxel). ditherOn adds sub-quantum noise.");
|
||||
|
||||
// ------ Variable bit-width: FpN with AbsDiff or RelDiff oracle ------
|
||||
//
|
||||
// Two overloads — one per oracle type. Python dispatch picks the
|
||||
// right one from the oracle argument's type. The createNanoGrid C++
|
||||
// template's parameter order is (src, sMode, cMode, ditherOn, verbose,
|
||||
// oracle, buffer); we reorder for Python so oracle comes second
|
||||
// (most callers want to specify it explicitly), then mode/dither
|
||||
// parameters as kwargs with defaults.
|
||||
toolsModule.def("createNanoGridFpN",
|
||||
[](nb::handle src, const tools::AbsDiff& oracle,
|
||||
tools::StatsMode sMode, CheckMode cMode, bool ditherOn, int verbose) {
|
||||
return createNanoGridFpNImpl(src, oracle, sMode, cMode, ditherOn, verbose);
|
||||
},
|
||||
"src"_a, "oracle"_a = tools::AbsDiff(),
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0,
|
||||
"Quantize a NanoGrid<float> or nanovdb.tools.build.FloatGrid into a NanoGrid<FpN> (variable "
|
||||
"bits per voxel; each leaf picks the smallest N that satisfies "
|
||||
"the oracle's tolerance). Pass an AbsDiff oracle for absolute "
|
||||
"error bound, or use the RelDiff overload for relative error.");
|
||||
|
||||
toolsModule.def("createNanoGridFpN",
|
||||
[](nb::handle src, const tools::RelDiff& oracle,
|
||||
tools::StatsMode sMode, CheckMode cMode, bool ditherOn, int verbose) {
|
||||
return createNanoGridFpNImpl(src, oracle, sMode, cMode, ditherOn, verbose);
|
||||
},
|
||||
"src"_a, "oracle"_a,
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default, "ditherOn"_a = false, "verbose"_a = 0,
|
||||
"FpN overload accepting a RelDiff oracle for relative error.");
|
||||
|
||||
// ------ Index / OnIndex ------
|
||||
//
|
||||
// createNanoGridIndex / createNanoGridOnIndex are the canonical names
|
||||
// for the broad-source-coverage index conversion bindings. A narrower
|
||||
// createOnIndexGrid factory still lives in PyVoxelBlockManager.cc as
|
||||
// the test scaffolding entry point used by the VBM unit tests.
|
||||
toolsModule.def("createNanoGridIndex",
|
||||
[](nb::handle src, uint32_t channels, bool includeStats,
|
||||
bool includeTiles, int verbose) {
|
||||
return createIndexImpl<ValueIndex>(
|
||||
src, channels, includeStats, includeTiles, verbose,
|
||||
"createNanoGridIndex");
|
||||
},
|
||||
"src"_a, "channels"_a = 0u, "includeStats"_a = true,
|
||||
"includeTiles"_a = true, "verbose"_a = 0,
|
||||
"Convert a source grid into a NanoGrid<ValueIndex>. Every voxel "
|
||||
"(active or inactive) gets a unique uint64 sequential index, with "
|
||||
"the original values stored as blind data when channels > 0.");
|
||||
|
||||
toolsModule.def("createNanoGridOnIndex",
|
||||
[](nb::handle src, uint32_t channels, bool includeStats,
|
||||
bool includeTiles, int verbose) {
|
||||
return createIndexImpl<ValueOnIndex>(
|
||||
src, channels, includeStats, includeTiles, verbose,
|
||||
"createNanoGridOnIndex");
|
||||
},
|
||||
"src"_a, "channels"_a = 0u, "includeStats"_a = true,
|
||||
"includeTiles"_a = true, "verbose"_a = 0,
|
||||
"Convert a source grid into a NanoGrid<ValueOnIndex>. Only the "
|
||||
"active voxels get a sequential index — the canonical input to "
|
||||
"buildVoxelBlockManager.");
|
||||
|
||||
// ------ CreateNanoGrid converter class (blind-data authoring) ------
|
||||
nb::class_<PyCreateNanoGrid>(toolsModule, "CreateNanoGrid",
|
||||
"Reusable converter mirroring nanovdb::tools::CreateNanoGrid. "
|
||||
"Construct from a NanoGrid or nanovdb.tools.build.* grid (float, "
|
||||
"double, int32, or Vec3f), optionally declare blind-data channels "
|
||||
"with addBlindData(), then bake a fresh NanoGrid of the same BuildT "
|
||||
"with getHandle(). Authored channels come back zero-filled — write "
|
||||
"their contents through the writable NumPy view returned by "
|
||||
"grid.getBlindData(n). For quantized (Fp*) or index destinations "
|
||||
"use the createNanoGridFp* / createNanoGridIndex functions instead.")
|
||||
.def(nb::init<nb::object>(), "srcGrid"_a,
|
||||
"Construct a converter reading from srcGrid (a NanoGrid or "
|
||||
"nanovdb.tools.build.* grid of BuildT float, double, int32, or "
|
||||
"Vec3f). The converter keeps srcGrid alive.")
|
||||
.def("addBlindData", &PyCreateNanoGrid::addBlindData,
|
||||
"name"_a, "count"_a, "dataType"_a = GridType::Float,
|
||||
"dataSemantic"_a = GridBlindDataSemantic::Unknown,
|
||||
"dataClass"_a = GridBlindDataClass::AttributeArray,
|
||||
"size"_a = 0u,
|
||||
"Declare a blind-data channel of count elements of dataType to "
|
||||
"be allocated in the destination grid, and return its channel "
|
||||
"index. size (bytes per element) is derived from dataType when "
|
||||
"omitted; pass it explicitly for dataTypes without a fixed "
|
||||
"element size. The C++ signature orders the parameters (name, "
|
||||
"dataSemantic, dataClass, dataType, count, size) — reordered "
|
||||
"here so the common case reads addBlindData(name, count).")
|
||||
.def("setStats", &PyCreateNanoGrid::setStats, "mode"_a,
|
||||
"Set the StatsMode used when baking the destination grid.")
|
||||
.def("setChecksum", &PyCreateNanoGrid::setChecksum, "mode"_a,
|
||||
"Set the CheckMode used when baking the destination grid.")
|
||||
.def("setVerbose", &PyCreateNanoGrid::setVerbose, "mode"_a = 1,
|
||||
"Set the verbosity level used when baking the destination grid.")
|
||||
.def("enableDithering", &PyCreateNanoGrid::enableDithering, "on"_a = true,
|
||||
"Toggle dithering of the destination grid (only meaningful for "
|
||||
"quantized BuildTs; kept for parity with the C++ class).")
|
||||
.def("getHandle", &PyCreateNanoGrid::getHandle,
|
||||
"Bake and return a GridHandle owning a NanoGrid of the source's "
|
||||
"BuildT, including any channels declared via addBlindData(). "
|
||||
"Each call bakes a fresh grid.");
|
||||
}
|
||||
|
||||
#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) \
|
||||
template void defineCreateNanoGrid<T>(nb::module_&, const char*);
|
||||
#include "BuildTypes.def"
|
||||
|
||||
template void defineOpenToNanoVDB<HostBuffer>(nb::module_&);
|
||||
|
||||
|
||||
@@ -15,6 +15,21 @@ template<typename BuildT> void defineCreateNanoGrid(nb::module_& m, const char*
|
||||
template<typename BufferT> void defineOpenToNanoVDB(nb::module_& m);
|
||||
#endif
|
||||
|
||||
/// @brief Bind the AbsDiff / RelDiff quantization oracle classes and the
|
||||
/// polymorphic createNanoGridFp4 / Fp8 / Fp16 / FpN / Index / OnIndex
|
||||
/// free functions on the nanovdb.tools submodule. Sources accepted
|
||||
/// include both NanoGrid<SrcBuildT> and tools::build::Grid<SrcBuildT>:
|
||||
/// the quantized createNanoGridFp* / FpN paths accept float only
|
||||
/// (C++ Fp{4,8,16,N}::preProcess static-asserts SrcValueT == float);
|
||||
/// the createNanoGridIndex / OnIndex paths accept float, double,
|
||||
/// int32_t, and Vec3f sources. Additional source BuildTs can be
|
||||
/// added by extending the explicit try-each-SrcBuildT chains in
|
||||
/// createNanoGridFpX / FpNImpl / createIndexImpl. Also binds the
|
||||
/// tools.CreateNanoGrid converter class, which mirrors the C++
|
||||
/// tools::CreateNanoGrid and adds blind-data authoring via
|
||||
/// addBlindData() (same source BuildT set as the index paths).
|
||||
void defineCreateNanoGridConversions(nb::module_& toolsModule);
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
|
||||
@@ -14,7 +14,10 @@ namespace pynanovdb {
|
||||
|
||||
void defineCheckMode(nb::module_& m)
|
||||
{
|
||||
nb::enum_<CheckMode>(m, "CheckMode")
|
||||
nb::enum_<CheckMode>(m, "CheckMode",
|
||||
"Selector controlling how aggressively a grid checksum is computed: "
|
||||
"Disable skips checksumming, Partial covers only the header, Full "
|
||||
"covers the whole grid, and Default picks the recommended mode.")
|
||||
.value("Disable", CheckMode::Disable)
|
||||
.value("Partial", CheckMode::Partial)
|
||||
.value("Full", CheckMode::Full)
|
||||
@@ -24,13 +27,65 @@ void defineCheckMode(nb::module_& m)
|
||||
|
||||
void defineChecksum(nb::module_& m)
|
||||
{
|
||||
nb::class_<Checksum>(m, "Checksum").def(nb::self == nb::self, "rhs"_a).def(nb::self != nb::self, "rhs"_a);
|
||||
nb::class_<Checksum>(m, "Checksum",
|
||||
"64-bit checksum value stored in a grid header. Produced by "
|
||||
"tools.evalChecksum and compared against the stored one by "
|
||||
"tools.validateChecksum.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Equality of two Checksum values.")
|
||||
.def(nb::self != nb::self, "rhs"_a,
|
||||
"Inequality of two Checksum values.")
|
||||
.def("isEmpty", &Checksum::isEmpty,
|
||||
"True iff no checksum is stored (checksumming was disabled).")
|
||||
.def("isHalf", &Checksum::isHalf,
|
||||
"True iff only the header portion (grid + tree + root) is checksummed.")
|
||||
.def("isFull", &Checksum::isFull,
|
||||
"True iff both the header portion and all nodes are checksummed.")
|
||||
.def("mode", &Checksum::mode,
|
||||
"CheckMode this checksum was computed with (Disable, Partial, or Full).");
|
||||
}
|
||||
|
||||
void defineUpdateChecksum(nb::module_& m)
|
||||
{
|
||||
m.def(
|
||||
"updateChecksum", [](GridData* gridData, CheckMode mode) { tools::updateChecksum(gridData, mode); }, "gridData"_a, "mode"_a);
|
||||
"updateChecksum", [](GridData* gridData, CheckMode mode) { tools::updateChecksum(gridData, mode); }, "gridData"_a, "mode"_a,
|
||||
"Recompute and store the checksum of gridData using the given CheckMode.");
|
||||
}
|
||||
|
||||
void defineEvalChecksumModule(nb::module_& toolsModule)
|
||||
{
|
||||
// tools.evalChecksum(grid, mode) — compute a fresh checksum for the
|
||||
// given grid without writing it back. Mirrors the GridData* overload
|
||||
// in tools/GridChecksum.h; the polymorphism over BuildT is implicit
|
||||
// because every NanoGrid<T> is-a GridData in the C++ hierarchy (and
|
||||
// the Python class binding declares NanoGrid<T> as derived from
|
||||
// GridData).
|
||||
toolsModule.def("evalChecksum",
|
||||
[](const GridData* gridData, CheckMode mode) -> Checksum {
|
||||
if (gridData == nullptr) {
|
||||
throw nb::value_error("evalChecksum: grid is None.");
|
||||
}
|
||||
return tools::evalChecksum(gridData, mode);
|
||||
},
|
||||
"grid"_a, "mode"_a = CheckMode::Default,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Compute and return the Checksum for the given grid using the "
|
||||
"specified CheckMode. Does not modify the grid.");
|
||||
|
||||
// tools.validateChecksum(grid, mode) — compare the stored checksum
|
||||
// against a freshly computed one and return a bool.
|
||||
toolsModule.def("validateChecksum",
|
||||
[](const GridData* gridData, CheckMode mode) -> bool {
|
||||
if (gridData == nullptr) {
|
||||
throw nb::value_error("validateChecksum: grid is None.");
|
||||
}
|
||||
return tools::validateChecksum(gridData, mode);
|
||||
},
|
||||
"grid"_a, "mode"_a = CheckMode::Default,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Return True iff the grid's stored checksum matches a freshly "
|
||||
"computed one for the given CheckMode. A grid with no stored "
|
||||
"checksum (Checksum.isEmpty()) is considered valid.");
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -13,6 +13,11 @@ void defineCheckMode(nb::module_& m);
|
||||
void defineChecksum(nb::module_& m);
|
||||
void defineUpdateChecksum(nb::module_& m);
|
||||
|
||||
/// @brief Bind tools.evalChecksum and tools.validateChecksum on the
|
||||
/// nanovdb.tools submodule. Both accept any bound NanoGrid via
|
||||
/// GridData* upcast (handled by nanobind's class hierarchy).
|
||||
void defineEvalChecksumModule(nb::module_& toolsModule);
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
|
||||
@@ -22,6 +22,7 @@ void defineHostGridHandle(nb::module_& m)
|
||||
new (&handle) GridHandle<BufferT>(std::move(buffer));
|
||||
},
|
||||
"t"_a.noconvert());
|
||||
defineGridHandleUtilities<BufferT>(m);
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -7,62 +7,226 @@
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
#include <nanovdb/GridHandle.h>
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/tools/GridChecksum.h> // for tools::updateGridCount
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace nb = nanobind;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// Polymorphic host-side `handle.grid(n)`: dispatch on gridType(n) to the
|
||||
// matching NanoGrid<BuildT> subclass currently bound in Python. Returns
|
||||
// None when the underlying BuildT is not bound in this build — see the
|
||||
// list of bound types in BuildTypes.def. The returned object is parented
|
||||
// to the handle so the handle is kept alive at least as long as the grid.
|
||||
template<typename BufferT>
|
||||
inline nb::object pyHostGrid(nb::handle py_handle, uint32_t n)
|
||||
{
|
||||
auto& handle = nb::cast<nanovdb::GridHandle<BufferT>&>(py_handle);
|
||||
if (n >= handle.gridCount()) return nb::none();
|
||||
switch (handle.gridType(n)) {
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template grid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template grid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template grid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template grid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#include "BuildTypes.def"
|
||||
default:
|
||||
return nb::none();
|
||||
}
|
||||
}
|
||||
|
||||
// Free functions splitGrids / mergeGrids exposed at module scope. Templated
|
||||
// on BufferT so the same definitions work for both the host GridHandle and
|
||||
// the device GridHandle bindings.
|
||||
template<typename BufferT> void defineGridHandleUtilities(nb::module_& m)
|
||||
{
|
||||
using HandleT = nanovdb::GridHandle<BufferT>;
|
||||
m.def("splitGrids", [](const HandleT& handle) {
|
||||
auto handles = nanovdb::splitGrids(handle);
|
||||
nb::list out;
|
||||
for (auto& h : handles) {
|
||||
out.append(std::move(h));
|
||||
}
|
||||
return out;
|
||||
}, nb::arg("handle"),
|
||||
"Split a multi-grid handle into a list of single-grid handles, "
|
||||
"each owning a freshly-allocated buffer.");
|
||||
// mergeGrids: walk the Python sequence by CONST ref to each handle and
|
||||
// concatenate buffer bytes into a freshly-allocated output. The original
|
||||
// nanovdb::mergeGrids takes a `const std::vector<GridHandle>&`, but
|
||||
// building such a vector from a Python list requires moving from each
|
||||
// wrapper (GridHandle is move-only) — which would silently empty the
|
||||
// caller's `h1`/`h2` Python objects. Instead we inline the merge logic
|
||||
// here so we never need to move from the inputs.
|
||||
m.def("mergeGrids", [](nb::sequence handles) {
|
||||
// Collect const refs so we touch each Python wrapper exactly once.
|
||||
std::vector<const HandleT*> sources;
|
||||
sources.reserve(nb::len(handles));
|
||||
for (nb::handle item : handles) {
|
||||
sources.push_back(&nb::cast<const HandleT&>(item));
|
||||
}
|
||||
|
||||
uint64_t totalSize = 0;
|
||||
uint32_t totalGrids = 0;
|
||||
for (const HandleT* h : sources) {
|
||||
totalGrids += h->gridCount();
|
||||
for (uint32_t n = 0; n < h->gridCount(); ++n) {
|
||||
totalSize += h->gridSize(n);
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing to merge (empty sequence, or only empty handles): return an
|
||||
// empty handle. BufferT::create(0) is ill-defined — for HostBuffer it
|
||||
// yields a non-null data() over a zero-byte region, and the
|
||||
// GridHandle(buffer) ctor would then read a full GridData header out of
|
||||
// it (heap overflow / "invalid host buffer" throw).
|
||||
if (totalGrids == 0) return HandleT();
|
||||
|
||||
auto buffer = BufferT::create(totalSize);
|
||||
uint8_t* dst = static_cast<uint8_t*>(buffer.data());
|
||||
uint32_t writeIndex = 0;
|
||||
for (const HandleT* h : sources) {
|
||||
for (uint32_t n = 0; n < h->gridCount(); ++n) {
|
||||
// gridData(n) is the authoritative per-grid start pointer (it
|
||||
// applies mMetaData[n].offset), so we don't assume the source
|
||||
// grids are laid out contiguously in the buffer.
|
||||
const uint64_t gs = h->gridSize(n);
|
||||
std::memcpy(dst, h->gridData(n), gs);
|
||||
auto* gd = reinterpret_cast<nanovdb::GridData*>(dst);
|
||||
nanovdb::tools::updateGridCount(gd, writeIndex++, totalGrids);
|
||||
dst += gs;
|
||||
}
|
||||
}
|
||||
return HandleT(std::move(buffer));
|
||||
}, nb::arg("handles"),
|
||||
"Combine a list of GridHandles into a single multi-grid GridHandle. "
|
||||
"Input handles are read by const reference; the new handle owns a "
|
||||
"freshly-allocated buffer and the inputs are left untouched.");
|
||||
}
|
||||
|
||||
template<typename BufferT> nb::class_<nanovdb::GridHandle<BufferT>> defineGridHandle(nb::module_& m, const char* name)
|
||||
{
|
||||
return nb::class_<nanovdb::GridHandle<BufferT>>(m, name)
|
||||
.def(nb::init<>())
|
||||
.def("reset", &nanovdb::GridHandle<BufferT>::reset)
|
||||
.def("size", &nanovdb::GridHandle<BufferT>::bufferSize)
|
||||
.def("isEmpty", &nanovdb::GridHandle<BufferT>::isEmpty)
|
||||
.def("empty", &nanovdb::GridHandle<BufferT>::empty)
|
||||
return nb::class_<nanovdb::GridHandle<BufferT>>(m, name,
|
||||
"Owns a buffer holding one or more serialized NanoVDB grids. "
|
||||
"Construct via nanovdb.tools.create* factories or nanovdb.io.readGrid(s); "
|
||||
"access individual grids via handle.grid(n).")
|
||||
.def(nb::init<>(),
|
||||
"Construct an empty handle. Use the nanovdb.tools.create* "
|
||||
"factories or nanovdb.io.readGrid(s) instead in normal use.")
|
||||
.def("reset", &nanovdb::GridHandle<BufferT>::reset,
|
||||
"Drop the underlying buffer; the handle becomes empty.")
|
||||
.def("size", &nanovdb::GridHandle<BufferT>::bufferSize,
|
||||
"Total byte size of the buffer backing this handle (sum of "
|
||||
"every grid plus any internal padding).")
|
||||
.def("isEmpty", &nanovdb::GridHandle<BufferT>::isEmpty,
|
||||
"True iff the handle owns no buffer.")
|
||||
.def("empty", &nanovdb::GridHandle<BufferT>::empty,
|
||||
"Same as isEmpty(). Retained for parity with the C++ "
|
||||
"GridHandle::empty() member.")
|
||||
.def(
|
||||
"__bool__", [](const nanovdb::GridHandle<BufferT>& handle) { handle.empty(); }, nb::is_operator())
|
||||
.def("floatGrid", nb::overload_cast<uint32_t>(&nanovdb::GridHandle<BufferT>::template grid<float>), nb::arg("n") = 0, nb::rv_policy::reference_internal)
|
||||
.def("doubleGrid",
|
||||
nb::overload_cast<uint32_t>(&nanovdb::GridHandle<BufferT>::template grid<double>),
|
||||
nb::arg("n") = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
.def("int32Grid",
|
||||
nb::overload_cast<uint32_t>(&nanovdb::GridHandle<BufferT>::template grid<int32_t>),
|
||||
nb::arg("n") = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
.def("vec3fGrid",
|
||||
nb::overload_cast<uint32_t>(&nanovdb::GridHandle<BufferT>::template grid<nanovdb::Vec3f>),
|
||||
nb::arg("n") = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
.def("rgba8Grid",
|
||||
nb::overload_cast<uint32_t>(&nanovdb::GridHandle<BufferT>::template grid<nanovdb::math::Rgba8>),
|
||||
nb::arg("n") = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
.def("isPadded", &nanovdb::GridHandle<BufferT>::isPadded)
|
||||
.def("gridCount", &nanovdb::GridHandle<BufferT>::gridCount)
|
||||
.def("gridSize", &nanovdb::GridHandle<BufferT>::gridSize, nb::arg("n") = 0)
|
||||
.def("gridType", &nanovdb::GridHandle<BufferT>::gridType, nb::arg("n") = 0)
|
||||
"__bool__",
|
||||
[](const nanovdb::GridHandle<BufferT>& handle) { return !handle.empty(); },
|
||||
nb::is_operator(),
|
||||
"True iff the handle owns a non-empty buffer (`not isEmpty()`).")
|
||||
.def("copy",
|
||||
[](const nanovdb::GridHandle<BufferT>& handle) {
|
||||
return handle.template copy<BufferT>();
|
||||
},
|
||||
"Return a deep copy of this GridHandle backed by a freshly-allocated buffer.")
|
||||
.def("grid", &pyHostGrid<BufferT>, nb::arg("n") = 0,
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return the n-th grid as a typed Grid subclass selected by "
|
||||
"gridType(n), or None if the BuildT is not bound in Python. "
|
||||
"The returned grid keeps this handle alive.")
|
||||
.def("isPadded", &nanovdb::GridHandle<BufferT>::isPadded,
|
||||
"True iff this handle's buffer is aligned past the natural "
|
||||
"GridData alignment (used by the I/O code path).")
|
||||
.def("gridCount", &nanovdb::GridHandle<BufferT>::gridCount,
|
||||
"Number of grids stored in this handle.")
|
||||
.def(
|
||||
"gridSize",
|
||||
[](const nanovdb::GridHandle<BufferT>& handle, uint32_t n) {
|
||||
// GridHandle::gridSize(n) indexes mMetaData[n] unchecked, so an
|
||||
// out-of-range n (including any n on an empty handle) is UB.
|
||||
if (n >= handle.gridCount())
|
||||
throw nb::index_error("gridSize: grid index out of range [0, gridCount()).");
|
||||
return handle.gridSize(n);
|
||||
},
|
||||
nb::arg("n") = 0,
|
||||
"Byte size of the n-th grid (without padding).")
|
||||
.def(
|
||||
"gridType",
|
||||
[](const nanovdb::GridHandle<BufferT>& handle, uint32_t n) {
|
||||
// GridHandle::gridType(n) indexes mMetaData[n] unchecked, so an
|
||||
// out-of-range n (including any n on an empty handle) is UB.
|
||||
if (n >= handle.gridCount())
|
||||
throw nb::index_error("gridType: grid index out of range [0, gridCount()).");
|
||||
return handle.gridType(n);
|
||||
},
|
||||
nb::arg("n") = 0,
|
||||
"GridType enumerator of the n-th grid (e.g. GridType.Float). "
|
||||
"Cheap to query — does not require materializing the grid.")
|
||||
.def(
|
||||
"gridData",
|
||||
[](nanovdb::GridHandle<BufferT>& handle, uint32_t n) { return nb::bytes(handle.gridData(n), handle.gridSize(n)); },
|
||||
[](nanovdb::GridHandle<BufferT>& handle, uint32_t n) {
|
||||
// gridData(n) returns nullptr for out-of-range n, but gridSize(n)
|
||||
// below indexes mMetaData[n] unchecked — guard both here.
|
||||
if (n >= handle.gridCount())
|
||||
throw nb::index_error("gridData: grid index out of range [0, gridCount()).");
|
||||
return nb::bytes(handle.gridData(n), handle.gridSize(n));
|
||||
},
|
||||
nb::arg("n") = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
.def("write", nb::overload_cast<const std::string&>(&nanovdb::GridHandle<BufferT>::write, nb::const_), nb::arg("fileName"))
|
||||
.def("write", nb::overload_cast<const std::string&, uint32_t>(&nanovdb::GridHandle<BufferT>::write, nb::const_), nb::arg("fileName"), nb::arg("n"))
|
||||
nb::rv_policy::reference_internal,
|
||||
"Raw byte contents of the n-th grid as a Python bytes object. "
|
||||
"Useful for hashing or for handing off to non-NanoVDB tooling.")
|
||||
.def("write", nb::overload_cast<const std::string&>(&nanovdb::GridHandle<BufferT>::write, nb::const_),
|
||||
nb::arg("fileName"),
|
||||
"Write every grid in this handle to the given .nvdb file.")
|
||||
.def("write", nb::overload_cast<const std::string&, uint32_t>(&nanovdb::GridHandle<BufferT>::write, nb::const_),
|
||||
nb::arg("fileName"), nb::arg("n"),
|
||||
"Write just the n-th grid in this handle to the given .nvdb file.")
|
||||
.def(
|
||||
"read", [](nanovdb::GridHandle<BufferT>& handle, const std::string& fileName) { handle.read(fileName); }, nb::arg("fileName"))
|
||||
"read", [](nanovdb::GridHandle<BufferT>& handle, const std::string& fileName) { handle.read(fileName); },
|
||||
nb::arg("fileName"),
|
||||
"Replace this handle's contents with every grid read from the "
|
||||
"given .nvdb file.")
|
||||
.def(
|
||||
"read",
|
||||
[](nanovdb::GridHandle<BufferT>& handle, const std::string& fileName, uint32_t n) { handle.read(fileName, n); },
|
||||
nb::arg("fileName"),
|
||||
nb::arg("n"))
|
||||
nb::arg("n"),
|
||||
"Replace this handle's contents with the n-th grid read from "
|
||||
"the given .nvdb file.")
|
||||
.def(
|
||||
"read",
|
||||
[](nanovdb::GridHandle<BufferT>& handle, const std::string& fileName, const std::string& gridName) { handle.read(fileName, gridName); },
|
||||
nb::arg("fileName"),
|
||||
nb::arg("gridName"));
|
||||
|
||||
nb::arg("gridName"),
|
||||
"Replace this handle's contents with the grid of the given "
|
||||
"name read from the .nvdb file.");
|
||||
}
|
||||
|
||||
void defineHostGridHandle(nb::module_& m);
|
||||
|
||||
@@ -2,16 +2,26 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "PyGridStats.h"
|
||||
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/tools/GridStats.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
using namespace nanovdb;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
void defineStatsMode(nb::module_& m)
|
||||
{
|
||||
nb::enum_<tools::StatsMode>(m, "StatsMode")
|
||||
nb::enum_<tools::StatsMode>(m, "StatsMode",
|
||||
"Selector controlling which per-node statistics are computed by "
|
||||
"tools.updateGridStats: Disable skips stats, BBox refreshes only "
|
||||
"bounding boxes, MinMax adds min/max, and All adds average and "
|
||||
"standard deviation as well.")
|
||||
.value("Disable", tools::StatsMode::Disable)
|
||||
.value("BBox", tools::StatsMode::BBox)
|
||||
.value("MinMax", tools::StatsMode::MinMax)
|
||||
@@ -20,4 +30,201 @@ void defineStatsMode(nb::module_& m)
|
||||
.value("End", tools::StatsMode::End);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// ----- Extrema<ValueT> binding (rank 0 and rank 1 share the same surface) -----
|
||||
template<typename BuildT>
|
||||
static void defineExtrema(nb::module_& m, const char* name)
|
||||
{
|
||||
using ValueT = typename NanoGrid<BuildT>::ValueType;
|
||||
using ExtremaT = tools::Extrema<ValueT>;
|
||||
|
||||
nb::class_<ExtremaT>(m, name,
|
||||
"Running minimum / maximum accumulator over a stream of values. "
|
||||
"Build via repeated add(v) calls or via tools.getExtrema(grid, bbox).")
|
||||
.def(nb::init<>(),
|
||||
"Default-construct an Extrema with min = numeric_limits::max and "
|
||||
"max = numeric_limits::lowest, so any subsequent .add(v) gives "
|
||||
"exact min/max.")
|
||||
.def("min",
|
||||
[](const ExtremaT& self) -> ValueT { return self.min(); },
|
||||
"Return the minimum value observed so far.")
|
||||
.def("max",
|
||||
[](const ExtremaT& self) -> ValueT { return self.max(); },
|
||||
"Return the maximum value observed so far.")
|
||||
.def("add",
|
||||
[](ExtremaT& self, const ValueT& v) { self.add(v); },
|
||||
"value"_a,
|
||||
"Update min/max with a single sample.")
|
||||
.def("__bool__",
|
||||
[](const ExtremaT& self) { return bool(self); },
|
||||
"True iff the Extrema has accumulated at least one sample "
|
||||
"(i.e. min <= max).")
|
||||
.def_static("hasMinMax", &ExtremaT::hasMinMax,
|
||||
"True for value types where min/max is meaningful "
|
||||
"(everything except bool).")
|
||||
.def_static("hasAverage", &ExtremaT::hasAverage,
|
||||
"Always False — Extrema does not compute averages; "
|
||||
"use Stats for that.")
|
||||
.def_static("hasStdDeviation", &ExtremaT::hasStdDeviation,
|
||||
"Always False — Extrema does not compute standard "
|
||||
"deviation; use Stats for that.")
|
||||
.def_static("hasStats", &ExtremaT::hasStats,
|
||||
"True iff the value type supports the min/max bookkeeping "
|
||||
"(everything except bool).");
|
||||
}
|
||||
|
||||
// ----- Stats<ValueT> binding (inherits Extrema) -----
|
||||
template<typename BuildT>
|
||||
static void defineStats(nb::module_& m, const char* name)
|
||||
{
|
||||
using ValueT = typename NanoGrid<BuildT>::ValueType;
|
||||
using BaseT = tools::Extrema<ValueT>;
|
||||
using StatsT = tools::Stats<ValueT>;
|
||||
|
||||
nb::class_<StatsT, BaseT>(m, name,
|
||||
"Running min/max/mean/variance/std accumulator over a stream of "
|
||||
"values. Extends Extrema with sample-count-weighted moments.")
|
||||
.def(nb::init<>(),
|
||||
"Default-construct a Stats accumulator with zero samples.")
|
||||
.def("add",
|
||||
[](StatsT& self, const ValueT& v) { self.add(v); },
|
||||
"value"_a,
|
||||
"Add a single sample.")
|
||||
.def("size",
|
||||
[](const StatsT& self) -> size_t { return self.size(); },
|
||||
"Number of samples accumulated so far.")
|
||||
.def("avg",
|
||||
[](const StatsT& self) -> double { return self.avg(); },
|
||||
"Arithmetic mean of all samples.")
|
||||
.def("mean",
|
||||
[](const StatsT& self) -> double { return self.mean(); },
|
||||
"Alias for avg().")
|
||||
.def("var",
|
||||
[](const StatsT& self) -> double { return self.var(); },
|
||||
"Population variance (Sum(x-mean)^2 / N). Returns 0 if "
|
||||
"fewer than two samples have been added.")
|
||||
.def("variance",
|
||||
[](const StatsT& self) -> double { return self.variance(); },
|
||||
"Alias for var().")
|
||||
.def("std",
|
||||
[](const StatsT& self) -> double { return self.std(); },
|
||||
"Standard deviation = sqrt(var()).")
|
||||
.def("stdDev",
|
||||
[](const StatsT& self) -> double { return self.stdDev(); },
|
||||
"Alias for std().")
|
||||
.def_static("hasMinMax", &StatsT::hasMinMax,
|
||||
"True for value types where min/max is meaningful.")
|
||||
.def_static("hasAverage", &StatsT::hasAverage,
|
||||
"True for value types that support mean/variance.")
|
||||
.def_static("hasStdDeviation", &StatsT::hasStdDeviation,
|
||||
"True for value types that support standard deviation.")
|
||||
.def_static("hasStats", &StatsT::hasStats,
|
||||
"True for value types where full statistics is meaningful.");
|
||||
}
|
||||
|
||||
// ----- updateGridStats polymorphic dispatch ----------------------------------
|
||||
//
|
||||
// Match the IsNanoGridValid/callNanoGrid pattern: an Op struct with `known<T>`
|
||||
// (called for every BuildT that's in scope) and `unknown` (fallback). Only
|
||||
// scalar + vector BuildTs have a meaningful Stats specialization, so the
|
||||
// other arms raise.
|
||||
struct UpdateGridStatsOp
|
||||
{
|
||||
template<typename BuildT>
|
||||
static void known(GridData* gridData, tools::StatsMode mode)
|
||||
{
|
||||
using GridT = NanoGrid<BuildT>;
|
||||
using ValueT = typename GridT::ValueType;
|
||||
if constexpr (BuildTraits<BuildT>::is_special &&
|
||||
!util::is_same<bool, BuildT>::value) {
|
||||
// Special / quantized / index / mask BuildTs don't have an
|
||||
// arithmetic ValueT, so tools::updateGridStats's MinMax / All
|
||||
// branches would instantiate Stats<ValueT> / Extrema<ValueT>
|
||||
// with no meaningful semantics (and may not even compile).
|
||||
// The Disable and BBox branches use NoopStats, which works
|
||||
// for any ValueT — drive that directly here so the BBox path
|
||||
// remains available on special grids (it just recomputes
|
||||
// node bounding boxes without touching min/max/avg).
|
||||
if (mode == tools::StatsMode::Disable) {
|
||||
return;
|
||||
} else if (mode == tools::StatsMode::BBox) {
|
||||
tools::GridStats<GridT, tools::NoopStats<ValueT>> stats;
|
||||
stats.update(*static_cast<GridT*>(gridData));
|
||||
} else {
|
||||
throw nb::value_error(
|
||||
"updateGridStats: this grid's BuildT (special / "
|
||||
"quantized / index / mask) has no arithmetic value "
|
||||
"type — only StatsMode.Disable and StatsMode.BBox "
|
||||
"are supported.");
|
||||
}
|
||||
} else {
|
||||
tools::updateGridStats(static_cast<GridT*>(gridData), mode);
|
||||
}
|
||||
}
|
||||
static void unknown(GridData*, tools::StatsMode) {
|
||||
throw nb::value_error(
|
||||
"updateGridStats: unsupported GridType / BuildT combination.");
|
||||
}
|
||||
};
|
||||
|
||||
// ----- getExtrema (per-BuildT factory) ---------------------------------------
|
||||
template<typename BuildT>
|
||||
static tools::Extrema<typename NanoGrid<BuildT>::ValueType>
|
||||
pyGetExtrema(const NanoGrid<BuildT>& grid, const CoordBBox& bbox)
|
||||
{
|
||||
return tools::getExtrema<BuildT>(grid, bbox);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void defineGridStatsModule(nb::module_& toolsModule)
|
||||
{
|
||||
// Per-BuildT Extrema + Stats. One pair per scalar/vector BuildT — the
|
||||
// value types are all distinct so we get N pairs of new Python classes.
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
defineExtrema<T>(toolsModule, #Suffix "Extrema"); \
|
||||
defineStats<T>(toolsModule, #Suffix "Stats");
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
defineExtrema<T>(toolsModule, #Suffix "Extrema"); \
|
||||
defineStats<T>(toolsModule, #Suffix "Stats");
|
||||
#include "BuildTypes.def"
|
||||
|
||||
// Polymorphic updateGridStats. Accepts any bound NanoGrid<BuildT> (via
|
||||
// upcast to GridData*) and dispatches on its mGridType.
|
||||
toolsModule.def("updateGridStats",
|
||||
[](GridData* gridData, tools::StatsMode mode) {
|
||||
if (gridData == nullptr) {
|
||||
throw nb::value_error("updateGridStats: grid is None.");
|
||||
}
|
||||
callNanoGrid<UpdateGridStatsOp>(gridData, mode);
|
||||
},
|
||||
"grid"_a, "mode"_a = tools::StatsMode::Default,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Recompute and write per-node statistics into the given grid in "
|
||||
"place. Polymorphic over BuildT. Scalar, vector, and Boolean "
|
||||
"grids accept every StatsMode (Disable / BBox / MinMax / All); "
|
||||
"Boolean grids use the C++ NoopStats path internally regardless "
|
||||
"of mode because there's no arithmetic min/max/avg/dev on bool. "
|
||||
"Other special (quantized / index / mask) grids accept Disable "
|
||||
"and BBox (the latter recomputes node bounding boxes only); "
|
||||
"MinMax and All raise ValueError because their value type has "
|
||||
"no arithmetic semantics.");
|
||||
|
||||
// Per-BuildT getExtrema. We expose one overload per scalar/vector
|
||||
// BuildT — they each return a Python-side Extrema<ValueT> of the
|
||||
// matching name.
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
toolsModule.def("getExtrema", &pyGetExtrema<T>, \
|
||||
"grid"_a, "bbox"_a, nb::call_guard<nb::gil_scoped_release>(), \
|
||||
"Return the Extrema of all values in the grid that intersect " \
|
||||
"the given bbox.");
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
toolsModule.def("getExtrema", &pyGetExtrema<T>, \
|
||||
"grid"_a, "bbox"_a, nb::call_guard<nb::gil_scoped_release>(), \
|
||||
"Return the Extrema of all values in the grid that intersect " \
|
||||
"the given bbox.");
|
||||
#include "BuildTypes.def"
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -11,6 +11,12 @@ namespace pynanovdb {
|
||||
|
||||
void defineStatsMode(nb::module_& m);
|
||||
|
||||
/// @brief Register per-BuildT Extrema and Stats classes (one set per
|
||||
/// scalar / vector BuildT in BuildTypes.def) and the polymorphic
|
||||
/// tools.updateGridStats / tools.getExtrema helpers under the
|
||||
/// nanovdb.tools submodule.
|
||||
void defineGridStatsModule(nb::module_& toolsModule);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2,13 +2,21 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "PyGridValidator.h"
|
||||
|
||||
#include <nanobind/operators.h>
|
||||
#include <nanobind/stl/pair.h>
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
#include <nanovdb/GridHandle.h>
|
||||
#include <nanovdb/HostBuffer.h>
|
||||
#include <nanovdb/tools/GridValidator.h>
|
||||
#ifdef NANOVDB_USE_CUDA
|
||||
#include <nanovdb/cuda/DeviceBuffer.h>
|
||||
#endif
|
||||
|
||||
#include <nanobind/operators.h>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
@@ -18,7 +26,8 @@ namespace pynanovdb {
|
||||
|
||||
template<typename BufferT> void defineValidateGrids(nb::module_& m)
|
||||
{
|
||||
m.def("validateGrids", &tools::validateGrids<GridHandle<BufferT>>, "handle"_a, "mode"_a, "verbose"_a);
|
||||
m.def("validateGrids", &tools::validateGrids<GridHandle<BufferT>>,
|
||||
"handle"_a, "mode"_a, "verbose"_a);
|
||||
}
|
||||
|
||||
template void defineValidateGrids<HostBuffer>(nb::module_&);
|
||||
@@ -26,4 +35,126 @@ template void defineValidateGrids<HostBuffer>(nb::module_&);
|
||||
template void defineValidateGrids<cuda::DeviceBuffer>(nb::module_&);
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
// callNanoGrid op for checkGrid: writes into a fixed-size buffer and returns
|
||||
// it as a std::pair<bool, std::string> for nb::make_tuple consumption.
|
||||
//
|
||||
// tools::checkGrid writes error messages with util::sprint / util::strcpy,
|
||||
// neither of which is bounded — they trust the caller's buffer is big
|
||||
// enough. Size the buffer well above what any current error message
|
||||
// produces (the longest formatted message in GridValidator.h is on the
|
||||
// order of ~80 characters once both GridType and GridClass enumerator
|
||||
// names are stringified) and leave generous headroom for future
|
||||
// additions.
|
||||
struct CheckGridOp
|
||||
{
|
||||
static constexpr size_t kErrorBufSize = 4096;
|
||||
|
||||
template<typename BuildT>
|
||||
static std::pair<bool, std::string> known(const GridData* gridData,
|
||||
CheckMode mode)
|
||||
{
|
||||
char buf[kErrorBufSize];
|
||||
buf[0] = '\0';
|
||||
tools::checkGrid<BuildT>(
|
||||
static_cast<const NanoGrid<BuildT>*>(gridData), buf, mode);
|
||||
const bool ok = (buf[0] == '\0');
|
||||
return {ok, std::string(buf)};
|
||||
}
|
||||
static std::pair<bool, std::string> unknown(const GridData* gridData,
|
||||
CheckMode /*mode*/)
|
||||
{
|
||||
(void)gridData;
|
||||
return {false, "Unsupported GridType for checkGrid"};
|
||||
}
|
||||
};
|
||||
|
||||
// callNanoGrid op for isValid — wraps tools::isValid<BuildT> for every
|
||||
// switched-over BuildT.
|
||||
struct IsValidOp
|
||||
{
|
||||
template<typename BuildT>
|
||||
static bool known(const GridData* gridData, CheckMode mode, bool verbose)
|
||||
{
|
||||
return tools::isValid<BuildT>(
|
||||
static_cast<const NanoGrid<BuildT>*>(gridData), mode, verbose);
|
||||
}
|
||||
static bool unknown(const GridData* gridData, CheckMode /*mode*/, bool verbose)
|
||||
{
|
||||
if (verbose && gridData != nullptr) {
|
||||
char str[16];
|
||||
std::cerr << "Validation failed: Unsupported GridType: \""
|
||||
<< toStr(str, gridData->mGridType) << "\""
|
||||
<< std::endl;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void defineGridValidatorModule(nb::module_& toolsModule)
|
||||
{
|
||||
// Single-grid validate. Takes a handle, a grid index, and the
|
||||
// usual mode + verbose flags. Returns true iff the grid passes all
|
||||
// tests for the given mode. Bound for both host and device handles
|
||||
// (when CUDA is enabled), matching validateGrids' coverage —
|
||||
// tools::validateGrid does host-side dispatch via callNanoGrid on
|
||||
// the host-resident gridData() pointer that DeviceGridHandle also
|
||||
// exposes, so the same overload pair is appropriate.
|
||||
toolsModule.def("validateGrid",
|
||||
&tools::validateGrid<GridHandle<HostBuffer>>,
|
||||
"handle"_a, "gridID"_a,
|
||||
"mode"_a = CheckMode::Default, "verbose"_a = false,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Validate the gridID'th grid in the handle against the given "
|
||||
"CheckMode. Returns False (without raising) if gridID is out "
|
||||
"of range or the grid fails any check. CheckMode.Disable is a "
|
||||
"short-circuit that always returns True without inspecting "
|
||||
"the grid (even when gridID is out of range), matching the "
|
||||
"C++ behavior. Complements validateGrids() which checks the "
|
||||
"whole handle.");
|
||||
#ifdef NANOVDB_USE_CUDA
|
||||
toolsModule.def("validateGrid",
|
||||
&tools::validateGrid<GridHandle<cuda::DeviceBuffer>>,
|
||||
"handle"_a, "gridID"_a,
|
||||
"mode"_a = CheckMode::Default, "verbose"_a = false,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Validate the gridID'th grid in the device handle (uses the "
|
||||
"host-resident copy of the grid metadata for the actual "
|
||||
"checks). Same semantics as the host-handle overload.");
|
||||
#endif
|
||||
|
||||
// Polymorphic checkGrid — returns (ok, error_message). Mirrors the C++
|
||||
// char-buffer-out signature, but the buffer is hidden inside the
|
||||
// binding so Python callers get a Python str.
|
||||
toolsModule.def("checkGrid",
|
||||
[](const GridData* gridData, CheckMode mode)
|
||||
-> std::pair<bool, std::string> {
|
||||
if (gridData == nullptr) {
|
||||
return {false, "Grid is None"};
|
||||
}
|
||||
return callNanoGrid<CheckGridOp>(gridData, mode);
|
||||
},
|
||||
"grid"_a, "mode"_a = CheckMode::Full,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Run structural validation checks on the grid for the given "
|
||||
"CheckMode. Returns a (ok, error_message) tuple — error_message "
|
||||
"is empty when ok is True.");
|
||||
|
||||
// Polymorphic isValid — convenience wrapper. Same as checkGrid + a
|
||||
// checksum check, returning just the bool.
|
||||
toolsModule.def("isValid",
|
||||
[](const GridData* gridData, CheckMode mode, bool verbose) {
|
||||
if (gridData == nullptr) return false;
|
||||
return callNanoGrid<IsValidOp>(gridData, mode, verbose);
|
||||
},
|
||||
"grid"_a, "mode"_a = CheckMode::Default, "verbose"_a = false,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Return True iff the grid passes structural validation AND its "
|
||||
"stored checksum matches a freshly computed one for the given "
|
||||
"CheckMode.");
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -11,6 +11,12 @@ namespace pynanovdb {
|
||||
|
||||
template<typename BufferT> void defineValidateGrids(nb::module_& m);
|
||||
|
||||
/// @brief Register tools.validateGrid (single grid in a handle),
|
||||
/// tools.checkGrid (polymorphic, returns (bool, error_str)) and
|
||||
/// tools.isValid (polymorphic shortcut) under the nanovdb.tools
|
||||
/// submodule. Bound once; polymorphic dispatch uses callNanoGrid.
|
||||
void defineGridValidatorModule(nb::module_& toolsModule);
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,7 +11,10 @@ namespace pynanovdb {
|
||||
|
||||
void defineHostBuffer(nb::module_& m)
|
||||
{
|
||||
nb::class_<HostBuffer>(m, "HostBuffer");
|
||||
nb::class_<HostBuffer>(m, "HostBuffer",
|
||||
"Default host-side buffer used to back a GridHandle. Memory is "
|
||||
"owned by this buffer and freed when the handle (and therefore "
|
||||
"the buffer) is destroyed.");
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -21,40 +21,67 @@ namespace {
|
||||
|
||||
void defineFileGridMetaData(nb::module_& m)
|
||||
{
|
||||
nb::class_<io::FileMetaData>(m, "FileMetaData")
|
||||
.def_ro("gridSize", &io::FileMetaData::gridSize)
|
||||
.def_ro("fileSize", &io::FileMetaData::fileSize)
|
||||
.def_ro("nameKey", &io::FileMetaData::nameKey)
|
||||
.def_ro("voxelCount", &io::FileMetaData::voxelCount)
|
||||
.def_ro("gridType", &io::FileMetaData::gridType)
|
||||
.def_ro("gridClass", &io::FileMetaData::gridClass)
|
||||
.def_ro("indexBBox", &io::FileMetaData::indexBBox)
|
||||
.def_ro("worldBBox", &io::FileMetaData::worldBBox)
|
||||
.def_ro("voxelSize", &io::FileMetaData::voxelSize)
|
||||
.def_ro("nameSize", &io::FileMetaData::nameSize)
|
||||
nb::class_<io::FileMetaData>(m, "FileMetaData",
|
||||
"Per-grid header read from the .nvdb file index. Mirrors the C++ "
|
||||
"io::FileMetaData layout; subclassed by FileGridMetaData which adds "
|
||||
"the grid name string.")
|
||||
.def_ro("gridSize", &io::FileMetaData::gridSize,
|
||||
"Uncompressed grid size in bytes.")
|
||||
.def_ro("fileSize", &io::FileMetaData::fileSize,
|
||||
"On-disk byte size of this grid (post-codec).")
|
||||
.def_ro("nameKey", &io::FileMetaData::nameKey,
|
||||
"Hash of the grid name used as a fast lookup key.")
|
||||
.def_ro("voxelCount", &io::FileMetaData::voxelCount,
|
||||
"Number of active voxels in this grid.")
|
||||
.def_ro("gridType", &io::FileMetaData::gridType,
|
||||
"GridType enumerator naming the BuildT of this grid.")
|
||||
.def_ro("gridClass", &io::FileMetaData::gridClass,
|
||||
"GridClass enumerator (LevelSet, FogVolume, ...).")
|
||||
.def_ro("indexBBox", &io::FileMetaData::indexBBox,
|
||||
"Axis-aligned bounding box of active voxels in index space.")
|
||||
.def_ro("worldBBox", &io::FileMetaData::worldBBox,
|
||||
"Axis-aligned bounding box of active voxels in world space.")
|
||||
.def_ro("voxelSize", &io::FileMetaData::voxelSize,
|
||||
"World-space size of a single voxel.")
|
||||
.def_ro("nameSize", &io::FileMetaData::nameSize,
|
||||
"Length of the grid name string including the null terminator.")
|
||||
.def_prop_ro("nodeCount",
|
||||
[](io::FileMetaData& metaData) {
|
||||
return std::make_tuple(metaData.nodeCount[0], metaData.nodeCount[1], metaData.nodeCount[2], metaData.nodeCount[3]);
|
||||
})
|
||||
},
|
||||
"Tuple (leaf, lower, upper, root) of node counts in this grid.")
|
||||
.def_prop_ro("tileCount",
|
||||
[](io::FileMetaData& metaData) { return std::make_tuple(metaData.tileCount[0], metaData.tileCount[1], metaData.tileCount[2]); })
|
||||
.def_ro("codec", &io::FileMetaData::codec)
|
||||
.def_ro("blindDataCount", &io::FileMetaData::blindDataCount)
|
||||
.def_ro("version", &io::FileMetaData::version);
|
||||
[](io::FileMetaData& metaData) { return std::make_tuple(metaData.tileCount[0], metaData.tileCount[1], metaData.tileCount[2]); },
|
||||
"Tuple (lower-tile, upper-tile, root-tile) of active-tile counts.")
|
||||
.def_ro("codec", &io::FileMetaData::codec,
|
||||
"Codec used to compress this grid on disk.")
|
||||
.def_ro("blindDataCount", &io::FileMetaData::blindDataCount,
|
||||
"Number of blind-data channels attached to this grid.")
|
||||
.def_ro("version", &io::FileMetaData::version,
|
||||
"NanoVDB version stored in the file when this grid was written.");
|
||||
|
||||
nb::bind_vector<std::vector<io::FileMetaData>>(m, "FileMetaDataVector");
|
||||
nb::bind_vector<std::vector<io::FileMetaData>>(m, "FileMetaDataVector",
|
||||
"List of FileMetaData entries, one per grid in a .nvdb file.");
|
||||
|
||||
nb::class_<io::FileGridMetaData, io::FileMetaData>(m, "FileGridMetaData")
|
||||
.def_ro("gridName", &io::FileGridMetaData::gridName)
|
||||
.def("memUsage", &io::FileGridMetaData::memUsage);
|
||||
nb::class_<io::FileGridMetaData, io::FileMetaData>(m, "FileGridMetaData",
|
||||
"FileMetaData extended with the grid name. Returned by "
|
||||
"readGridMetaData() so callers can identify grids by name without "
|
||||
"materializing them.")
|
||||
.def_ro("gridName", &io::FileGridMetaData::gridName,
|
||||
"Grid name as a Python string.")
|
||||
.def("memUsage", &io::FileGridMetaData::memUsage,
|
||||
"Byte size of this metadata record in memory.");
|
||||
|
||||
nb::bind_vector<std::vector<io::FileGridMetaData>>(m, "FileGridMetaDataVector");
|
||||
nb::bind_vector<std::vector<io::FileGridMetaData>>(m, "FileGridMetaDataVector",
|
||||
"List of FileGridMetaData entries, one per grid in a .nvdb file.");
|
||||
}
|
||||
|
||||
template<typename BufferT> void defineReadWriteGrid(nb::module_& m)
|
||||
{
|
||||
m.def("hasGrid", nb::overload_cast<const std::string&, const std::string&>(&io::hasGrid), "fileName"_a, "gridName"_a);
|
||||
m.def("readGridMetaData", nb::overload_cast<const std::string&>(&io::readGridMetaData), "fileName"_a);
|
||||
m.def("hasGrid", nb::overload_cast<const std::string&, const std::string&>(&io::hasGrid), "fileName"_a, "gridName"_a,
|
||||
"Return True iff the .nvdb file at fileName contains a grid named gridName.");
|
||||
m.def("readGridMetaData", nb::overload_cast<const std::string&>(&io::readGridMetaData), "fileName"_a,
|
||||
"Return a FileGridMetaDataVector describing every grid stored in the .nvdb file.");
|
||||
}
|
||||
|
||||
template<typename BufferT> nb::list readGrids(const std::string& fileName, int verbose, const BufferT& buffer)
|
||||
@@ -91,21 +118,26 @@ void defineHostReadWriteGrid(nb::module_& m)
|
||||
"fileName"_a,
|
||||
"handle"_a,
|
||||
"codec"_a = io::Codec::NONE,
|
||||
"verbose"_a = 0);
|
||||
m.def("writeGrids", &writeGrids<BufferT>, "fileName"_a, "handles"_a, "codec"_a = io::Codec::NONE, "verbose"_a = 0);
|
||||
"verbose"_a = 0,
|
||||
"Write the grids in handle to the .nvdb file at fileName using the given codec.");
|
||||
m.def("writeGrids", &writeGrids<BufferT>, "fileName"_a, "handles"_a, "codec"_a = io::Codec::NONE, "verbose"_a = 0,
|
||||
"Write every GridHandle in the handles list to the .nvdb file at fileName.");
|
||||
m.def("readGrid",
|
||||
nb::overload_cast<const std::string&, int, int, const BufferT&>(&io::template readGrid<BufferT>),
|
||||
"fileName"_a,
|
||||
"n"_a = 0,
|
||||
"verbose"_a = 0,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Read the n-th grid from the .nvdb file at fileName into a fresh GridHandle.");
|
||||
m.def("readGrid",
|
||||
nb::overload_cast<const std::string&, const std::string&, int, const BufferT&>(&io::template readGrid<BufferT>),
|
||||
"fileName"_a,
|
||||
"gridName"_a,
|
||||
"verbose"_a = 0,
|
||||
"buffer"_a = BufferT());
|
||||
m.def("readGrids", &readGrids<BufferT>, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Read the grid named gridName from the .nvdb file at fileName into a fresh GridHandle.");
|
||||
m.def("readGrids", &readGrids<BufferT>, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT(),
|
||||
"Read every grid from the .nvdb file at fileName, returning a list of GridHandles.");
|
||||
}
|
||||
|
||||
#ifdef NANOVDB_USE_CUDA
|
||||
@@ -119,21 +151,26 @@ void defineDeviceReadWriteGrid(nb::module_& m)
|
||||
"fileName"_a,
|
||||
"handle"_a,
|
||||
"codec"_a = io::Codec::NONE,
|
||||
"verbose"_a = 0);
|
||||
m.def("deviceWriteGrids", &writeGrids<BufferT>, "fileName"_a, "handles"_a, "codec"_a = io::Codec::NONE, "verbose"_a = 0);
|
||||
"verbose"_a = 0,
|
||||
"Write the grids in a device-backed handle to the .nvdb file at fileName.");
|
||||
m.def("deviceWriteGrids", &writeGrids<BufferT>, "fileName"_a, "handles"_a, "codec"_a = io::Codec::NONE, "verbose"_a = 0,
|
||||
"Write every device-backed GridHandle in handles to the .nvdb file at fileName.");
|
||||
m.def("deviceReadGrid",
|
||||
nb::overload_cast<const std::string&, int, int, const BufferT&>(&io::template readGrid<BufferT>),
|
||||
"fileName"_a,
|
||||
"n"_a = 0,
|
||||
"verbose"_a = 0,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Read the n-th grid from the .nvdb file at fileName into a fresh DeviceGridHandle.");
|
||||
m.def("deviceReadGrid",
|
||||
nb::overload_cast<const std::string&, const std::string&, int, const BufferT&>(&io::template readGrid<BufferT>),
|
||||
"fileName"_a,
|
||||
"gridName"_a,
|
||||
"verbose"_a = 0,
|
||||
"buffer"_a = BufferT());
|
||||
m.def("deviceReadGrids", &readGrids<BufferT>, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Read the grid named gridName from the .nvdb file at fileName into a fresh DeviceGridHandle.");
|
||||
m.def("deviceReadGrids", &readGrids<BufferT>, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT(),
|
||||
"Read every grid from the .nvdb file at fileName into device-backed handles.");
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -141,16 +178,19 @@ void defineDeviceReadWriteGrid(nb::module_& m)
|
||||
|
||||
void defineIOModule(nb::module_& m)
|
||||
{
|
||||
nb::enum_<io::Codec>(m, "Codec")
|
||||
nb::enum_<io::Codec>(m, "Codec",
|
||||
"Compression codec selector used when writing a .nvdb file. NONE "
|
||||
"writes raw bytes; ZIP uses zlib; BLOSC uses the blosc codec when "
|
||||
"compiled in.")
|
||||
.value("NONE", io::Codec::NONE)
|
||||
.value("ZIP", io::Codec::ZIP)
|
||||
.value("BLOSC", io::Codec::BLOSC)
|
||||
.export_values();
|
||||
// .def("__repr__", [](const io::Codec& codec) {
|
||||
// char str[strlen<io::Codec>()];
|
||||
// toStr(str, codec);
|
||||
// return std::string(str);
|
||||
// });
|
||||
.export_values()
|
||||
.def("__repr__", [](const io::Codec& codec) {
|
||||
char str[strlen<io::Codec>()];
|
||||
toStr(str, codec);
|
||||
return std::string(str);
|
||||
});
|
||||
|
||||
defineFileGridMetaData(m);
|
||||
defineHostReadWriteGrid(m);
|
||||
|
||||
+325
-187
@@ -11,7 +11,6 @@
|
||||
#include <nanobind/stl/bind_vector.h>
|
||||
|
||||
#include "PySampleFromVoxels.h"
|
||||
#include "cuda/PySampleFromVoxels.h"
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
@@ -26,18 +25,27 @@ void defineCoord(nb::module_& m)
|
||||
using ValueType = math::Coord::ValueType;
|
||||
|
||||
nb::class_<math::Coord>(m, "Coord", "Signed (i, j, k) 32-bit integer coordinate class, similar to openvdb::math::Coord")
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<ValueType>(), "n"_a)
|
||||
.def(nb::init<ValueType, ValueType, ValueType>(), "i"_a, "j"_a, "k"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct (0, 0, 0).")
|
||||
.def(nb::init<ValueType>(), "n"_a,
|
||||
"Construct (n, n, n).")
|
||||
.def(nb::init<ValueType, ValueType, ValueType>(), "i"_a, "j"_a, "k"_a,
|
||||
"Construct (i, j, k).")
|
||||
.def_prop_rw(
|
||||
"x", [](const math::Coord& ijk) { return ijk.x(); }, [](math::Coord& ijk, int32_t i) { ijk.x() = i; })
|
||||
"x", [](const math::Coord& ijk) { return ijk.x(); }, [](math::Coord& ijk, int32_t i) { ijk.x() = i; },
|
||||
"First component of the (i, j, k) triple.")
|
||||
.def_prop_rw(
|
||||
"y", [](const math::Coord& ijk) { return ijk.y(); }, [](math::Coord& ijk, int32_t j) { ijk.y() = j; })
|
||||
"y", [](const math::Coord& ijk) { return ijk.y(); }, [](math::Coord& ijk, int32_t j) { ijk.y() = j; },
|
||||
"Second component of the (i, j, k) triple.")
|
||||
.def_prop_rw(
|
||||
"z", [](const math::Coord& ijk) { return ijk.z(); }, [](math::Coord& ijk, int32_t k) { ijk.z() = k; })
|
||||
.def_static("max", &math::Coord::max)
|
||||
.def_static("min", &math::Coord::min)
|
||||
.def_static("memUsage", &math::Coord::memUsage)
|
||||
"z", [](const math::Coord& ijk) { return ijk.z(); }, [](math::Coord& ijk, int32_t k) { ijk.z() = k; },
|
||||
"Third component of the (i, j, k) triple.")
|
||||
.def_static("max", &math::Coord::max,
|
||||
"Largest representable Coord (INT32_MAX in every component).")
|
||||
.def_static("min", &math::Coord::min,
|
||||
"Smallest representable Coord (INT32_MIN in every component).")
|
||||
.def_static("memUsage", &math::Coord::memUsage,
|
||||
"Byte size of a Coord instance.")
|
||||
.def(
|
||||
"__getitem__",
|
||||
[](const math::Coord& ijk, size_t i) {
|
||||
@@ -46,7 +54,8 @@ void defineCoord(nb::module_& m)
|
||||
}
|
||||
return ijk[static_cast<math::Coord::IndexType>(i)];
|
||||
},
|
||||
"i"_a)
|
||||
"i"_a,
|
||||
"Read the i-th component (0=x, 1=y, 2=z).")
|
||||
.def(
|
||||
"__setitem__",
|
||||
[](math::Coord& ijk, size_t i, ValueType value) {
|
||||
@@ -56,41 +65,69 @@ void defineCoord(nb::module_& m)
|
||||
ijk[static_cast<math::Coord::IndexType>(i)] = value;
|
||||
},
|
||||
"i"_a,
|
||||
"value"_a)
|
||||
"value"_a,
|
||||
"Write the i-th component (0=x, 1=y, 2=z).")
|
||||
.def(
|
||||
"__and__", [](const math::Coord& a, math::Coord::IndexType b) { return a & b; }, nb::is_operator(), "n"_a)
|
||||
"__and__", [](const math::Coord& a, math::Coord::IndexType b) { return a & b; }, nb::is_operator(), "n"_a,
|
||||
"Component-wise bitwise AND with the scalar n.")
|
||||
.def(
|
||||
"__lshift__", [](const math::Coord& a, math::Coord::IndexType b) { return a << b; }, nb::is_operator(), "n"_a)
|
||||
"__lshift__", [](const math::Coord& a, math::Coord::IndexType b) { return a << b; }, nb::is_operator(), "n"_a,
|
||||
"Component-wise left shift by n bits.")
|
||||
.def(
|
||||
"__rshift__", [](const math::Coord& a, math::Coord::IndexType b) { return a >> b; }, nb::is_operator(), "n"_a)
|
||||
.def(nb::self < nb::self, "rhs"_a)
|
||||
.def(nb::self == nb::self, "rhs"_a)
|
||||
.def(nb::self != nb::self, "rhs"_a)
|
||||
"__rshift__", [](const math::Coord& a, math::Coord::IndexType b) { return a >> b; }, nb::is_operator(), "n"_a,
|
||||
"Component-wise right shift by n bits.")
|
||||
.def(nb::self < nb::self, "rhs"_a,
|
||||
"Lexicographic less-than comparison.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Equality of all three components.")
|
||||
.def(nb::self != nb::self, "rhs"_a,
|
||||
"Inequality of any one component.")
|
||||
.def(
|
||||
"__iand__", [](math::Coord& a, int b) { return a &= b; }, nb::is_operator(), "n"_a)
|
||||
"__iand__", [](math::Coord& a, int b) { return a &= b; }, nb::is_operator(), "n"_a,
|
||||
"In-place component-wise bitwise AND with the scalar n.")
|
||||
.def(
|
||||
"__ilshift__", [](math::Coord& a, uint32_t b) { return a <<= b; }, nb::is_operator(), "n"_a)
|
||||
"__ilshift__", [](math::Coord& a, uint32_t b) { return a <<= b; }, nb::is_operator(), "n"_a,
|
||||
"In-place component-wise left shift by n bits.")
|
||||
.def(
|
||||
"__irshift__", [](math::Coord& a, uint32_t b) { return a >>= b; }, nb::is_operator(), "n"_a)
|
||||
"__irshift__", [](math::Coord& a, uint32_t b) { return a >>= b; }, nb::is_operator(), "n"_a,
|
||||
"In-place component-wise right shift by n bits.")
|
||||
.def(
|
||||
"__iadd__", [](math::Coord& a, int b) { return a += b; }, nb::is_operator(), "n"_a)
|
||||
.def(nb::self + nb::self, "rhs"_a)
|
||||
.def(nb::self - nb::self, "rhs"_a)
|
||||
.def(-nb::self)
|
||||
.def(nb::self += nb::self, "rhs"_a)
|
||||
.def(nb::self -= nb::self, "rhs"_a)
|
||||
.def("minComponent", &math::Coord::minComponent, "other"_a)
|
||||
.def("maxComponent", &math::Coord::maxComponent, "other"_a)
|
||||
.def("offsetBy", nb::overload_cast<ValueType, ValueType, ValueType>(&math::Coord::offsetBy, nb::const_), "dx"_a, "dy"_a, "dz"_a)
|
||||
.def("offsetBy", nb::overload_cast<ValueType>(&math::Coord::offsetBy, nb::const_), "n"_a)
|
||||
.def_static("lessThan", &math::Coord::lessThan, "a"_a, "b"_a)
|
||||
.def_static("Floor", &math::Coord::template Floor<math::Vec3<float>>, "xyz"_a)
|
||||
.def_static("Floor", &math::Coord::template Floor<math::Vec3<double>>, "xyz"_a)
|
||||
.def("hash", &math::Coord::template hash<12>)
|
||||
.def("octant", &math::Coord::octant)
|
||||
.def("asVec3s", &math::Coord::asVec3s)
|
||||
.def("asVec3d", &math::Coord::asVec3d)
|
||||
.def("round", &math::Coord::round)
|
||||
"__iadd__", [](math::Coord& a, int b) { return a += b; }, nb::is_operator(), "n"_a,
|
||||
"In-place add the scalar n to every component.")
|
||||
.def(nb::self + nb::self, "rhs"_a,
|
||||
"Component-wise addition.")
|
||||
.def(nb::self - nb::self, "rhs"_a,
|
||||
"Component-wise subtraction.")
|
||||
.def(-nb::self,
|
||||
"Negate every component.")
|
||||
.def(nb::self += nb::self, "rhs"_a,
|
||||
"In-place component-wise addition.")
|
||||
.def(nb::self -= nb::self, "rhs"_a,
|
||||
"In-place component-wise subtraction.")
|
||||
.def("minComponent", &math::Coord::minComponent, "other"_a,
|
||||
"Component-wise minimum with other. See nanovdb::math::Coord::minComponent in NanoVDB.h.")
|
||||
.def("maxComponent", &math::Coord::maxComponent, "other"_a,
|
||||
"Component-wise maximum with other. See nanovdb::math::Coord::maxComponent in NanoVDB.h.")
|
||||
.def("offsetBy", nb::overload_cast<ValueType, ValueType, ValueType>(&math::Coord::offsetBy, nb::const_), "dx"_a, "dy"_a, "dz"_a,
|
||||
"Return a new Coord offset by (dx, dy, dz). See nanovdb::math::Coord::offsetBy in NanoVDB.h.")
|
||||
.def("offsetBy", nb::overload_cast<ValueType>(&math::Coord::offsetBy, nb::const_), "n"_a,
|
||||
"Return a new Coord offset by n in every component.")
|
||||
.def_static("lessThan", &math::Coord::lessThan, "a"_a, "b"_a,
|
||||
"Component-wise a < b returning a Coord of 0 / 1 flags.")
|
||||
.def_static("Floor", &math::Coord::template Floor<math::Vec3<float>>, "xyz"_a,
|
||||
"Floor each component of a Vec3f to produce an integer Coord.")
|
||||
.def_static("Floor", &math::Coord::template Floor<math::Vec3<double>>, "xyz"_a,
|
||||
"Floor each component of a Vec3d to produce an integer Coord.")
|
||||
.def("hash", &math::Coord::template hash<12>,
|
||||
"Spatial hash of this coordinate suited for hashed root-table lookups.")
|
||||
.def("octant", &math::Coord::octant,
|
||||
"Return the 0..7 octant index of this coordinate's sign bits.")
|
||||
.def("asVec3s", &math::Coord::asVec3s,
|
||||
"Convert to a Vec3f (float) with no scaling.")
|
||||
.def("asVec3d", &math::Coord::asVec3d,
|
||||
"Convert to a Vec3d (double) with no scaling.")
|
||||
.def("round", &math::Coord::round,
|
||||
"Component-wise round; for an integer Coord this is the identity.")
|
||||
.def("__repr__", [](const math::Coord& ijk) {
|
||||
std::stringstream ostr;
|
||||
ostr << ijk;
|
||||
@@ -101,13 +138,20 @@ void defineCoord(nb::module_& m)
|
||||
template<typename T> void defineVec3(nb::module_& m, const char* name, const char* doc)
|
||||
{
|
||||
nb::class_<math::Vec3<T>>(m, name, doc)
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<T>(), "x"_a)
|
||||
.def(nb::init<T, T, T>(), "x"_a, "y"_a, "z"_a)
|
||||
.def(nb::init<math::Vec3<T>>(), "v"_a)
|
||||
.def(nb::init<math::Coord>(), "ijk"_a)
|
||||
.def(nb::self == nb::self, "rhs"_a)
|
||||
.def(nb::self != nb::self, "rhs"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct a zero-initialized vector.")
|
||||
.def(nb::init<T>(), "x"_a,
|
||||
"Construct (x, x, x).")
|
||||
.def(nb::init<T, T, T>(), "x"_a, "y"_a, "z"_a,
|
||||
"Construct (x, y, z).")
|
||||
.def(nb::init<math::Vec3<T>>(), "v"_a,
|
||||
"Copy-construct from another Vec3.")
|
||||
.def(nb::init<math::Coord>(), "ijk"_a,
|
||||
"Construct from an integer Coord, casting each component.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Component-wise equality.")
|
||||
.def(nb::self != nb::self, "rhs"_a,
|
||||
"Component-wise inequality.")
|
||||
.def(
|
||||
"__getitem__",
|
||||
[](const math::Vec3<T>& v, size_t i) {
|
||||
@@ -116,7 +160,8 @@ template<typename T> void defineVec3(nb::module_& m, const char* name, const cha
|
||||
}
|
||||
return v[static_cast<int>(i)];
|
||||
},
|
||||
"i"_a)
|
||||
"i"_a,
|
||||
"Read the i-th component (0=x, 1=y, 2=z).")
|
||||
.def(
|
||||
"__setitem__",
|
||||
[](math::Vec3<T>& v, size_t i, T value) {
|
||||
@@ -126,38 +171,68 @@ template<typename T> void defineVec3(nb::module_& m, const char* name, const cha
|
||||
v[static_cast<int>(i)] = value;
|
||||
},
|
||||
"i"_a,
|
||||
"value"_a)
|
||||
.def("dot", &math::Vec3<T>::template dot<math::Vec3<T>>, "v"_a)
|
||||
.def("cross", &math::Vec3<T>::template cross<math::Vec3<T>>, "v"_a)
|
||||
.def("lengthSqr", &math::Vec3<T>::lengthSqr)
|
||||
.def("length", &math::Vec3<T>::length)
|
||||
.def(-nb::self)
|
||||
.def(nb::self * nb::self, "v"_a)
|
||||
.def(nb::self / nb::self, "v"_a)
|
||||
.def(nb::self + nb::self, "v"_a)
|
||||
.def(nb::self - nb::self, "v"_a)
|
||||
.def(nb::self + math::Coord(), "ijk"_a)
|
||||
.def(nb::self - math::Coord(), "ijk"_a)
|
||||
.def(nb::self * T(), "s"_a)
|
||||
.def(nb::self / T(), "s"_a)
|
||||
.def(nb::self += nb::self, "v"_a)
|
||||
.def(nb::self += math::Coord(), "ijk"_a)
|
||||
.def(nb::self -= nb::self, "v"_a)
|
||||
.def(nb::self -= math::Coord(), "ijk"_a)
|
||||
.def(nb::self *= T(), "s"_a)
|
||||
.def(nb::self /= T(), "s"_a)
|
||||
.def("normalize", &math::Vec3<T>::normalize)
|
||||
.def("minComponent", &math::Vec3<T>::minComponent, "other"_a)
|
||||
.def("maxComponent", &math::Vec3<T>::maxComponent, "other"_a)
|
||||
.def("min", &math::Vec3<T>::min)
|
||||
.def("max", &math::Vec3<T>::max)
|
||||
.def("floor", &math::Vec3<T>::floor)
|
||||
.def("ceil", &math::Vec3<T>::ceil)
|
||||
.def("round", &math::Vec3<T>::round)
|
||||
"value"_a,
|
||||
"Write the i-th component (0=x, 1=y, 2=z).")
|
||||
.def("dot", &math::Vec3<T>::template dot<math::Vec3<T>>, "v"_a,
|
||||
"Dot product with another vector.")
|
||||
.def("cross", &math::Vec3<T>::template cross<math::Vec3<T>>, "v"_a,
|
||||
"Cross product with another vector.")
|
||||
.def("lengthSqr", &math::Vec3<T>::lengthSqr,
|
||||
"Squared Euclidean length (cheaper than length()).")
|
||||
.def("length", &math::Vec3<T>::length,
|
||||
"Euclidean length of this vector.")
|
||||
.def(-nb::self,
|
||||
"Negate every component.")
|
||||
.def(nb::self * nb::self, "v"_a,
|
||||
"Component-wise multiplication.")
|
||||
.def(nb::self / nb::self, "v"_a,
|
||||
"Component-wise division.")
|
||||
.def(nb::self + nb::self, "v"_a,
|
||||
"Component-wise addition.")
|
||||
.def(nb::self - nb::self, "v"_a,
|
||||
"Component-wise subtraction.")
|
||||
.def(nb::self + math::Coord(), "ijk"_a,
|
||||
"Add an integer Coord component-wise.")
|
||||
.def(nb::self - math::Coord(), "ijk"_a,
|
||||
"Subtract an integer Coord component-wise.")
|
||||
.def(nb::self * T(), "s"_a,
|
||||
"Multiply every component by the scalar s.")
|
||||
.def(nb::self / T(), "s"_a,
|
||||
"Divide every component by the scalar s.")
|
||||
.def(nb::self += nb::self, "v"_a,
|
||||
"In-place component-wise addition.")
|
||||
.def(nb::self += math::Coord(), "ijk"_a,
|
||||
"In-place add an integer Coord.")
|
||||
.def(nb::self -= nb::self, "v"_a,
|
||||
"In-place component-wise subtraction.")
|
||||
.def(nb::self -= math::Coord(), "ijk"_a,
|
||||
"In-place subtract an integer Coord.")
|
||||
.def(nb::self *= T(), "s"_a,
|
||||
"In-place scalar multiply.")
|
||||
.def(nb::self /= T(), "s"_a,
|
||||
"In-place scalar divide.")
|
||||
.def("normalize", &math::Vec3<T>::normalize,
|
||||
"Scale this vector to unit length in place.")
|
||||
.def("minComponent", &math::Vec3<T>::minComponent, "other"_a,
|
||||
"Component-wise minimum with other.")
|
||||
.def("maxComponent", &math::Vec3<T>::maxComponent, "other"_a,
|
||||
"Component-wise maximum with other.")
|
||||
.def("min", &math::Vec3<T>::min,
|
||||
"Smallest single component of this vector.")
|
||||
.def("max", &math::Vec3<T>::max,
|
||||
"Largest single component of this vector.")
|
||||
.def("floor", &math::Vec3<T>::floor,
|
||||
"Component-wise floor.")
|
||||
.def("ceil", &math::Vec3<T>::ceil,
|
||||
"Component-wise ceiling.")
|
||||
.def("round", &math::Vec3<T>::round,
|
||||
"Component-wise round.")
|
||||
.def(
|
||||
"__mul__", [](const T& a, math::Vec3<T> b) { return a * b; }, nb::is_operator(), "b"_a)
|
||||
"__mul__", [](const T& a, math::Vec3<T> b) { return a * b; }, nb::is_operator(), "b"_a,
|
||||
"Right-multiply: scalar * Vec3.")
|
||||
.def(
|
||||
"__truediv__", [](const T& a, math::Vec3<T> b) { return a / b; }, nb::is_operator(), "b"_a)
|
||||
"__truediv__", [](const T& a, math::Vec3<T> b) { return a / b; }, nb::is_operator(), "b"_a,
|
||||
"Right-divide: scalar / Vec3, component-wise.")
|
||||
.def("__repr__", [](const math::Vec3<T>& v) {
|
||||
std::stringstream ostr;
|
||||
ostr << v;
|
||||
@@ -168,12 +243,18 @@ template<typename T> void defineVec3(nb::module_& m, const char* name, const cha
|
||||
template<typename T> void defineVec4(nb::module_& m, const char* name, const char* doc)
|
||||
{
|
||||
nb::class_<math::Vec4<T>>(m, name, doc)
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<T>(), "x"_a)
|
||||
.def(nb::init<T, T, T, T>(), "x"_a, "y"_a, "z"_a, "w"_a)
|
||||
.def(nb::init<math::Vec4<T>>(), "v"_a)
|
||||
.def(nb::self == nb::self, "rhs"_a)
|
||||
.def(nb::self != nb::self, "rhs"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct a zero-initialized vector.")
|
||||
.def(nb::init<T>(), "x"_a,
|
||||
"Construct (x, x, x, x).")
|
||||
.def(nb::init<T, T, T, T>(), "x"_a, "y"_a, "z"_a, "w"_a,
|
||||
"Construct (x, y, z, w).")
|
||||
.def(nb::init<math::Vec4<T>>(), "v"_a,
|
||||
"Copy-construct from another Vec4.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Component-wise equality.")
|
||||
.def(nb::self != nb::self, "rhs"_a,
|
||||
"Component-wise inequality.")
|
||||
.def(
|
||||
"__getitem__",
|
||||
[](const math::Vec4<T>& v, size_t i) {
|
||||
@@ -182,7 +263,8 @@ template<typename T> void defineVec4(nb::module_& m, const char* name, const cha
|
||||
}
|
||||
return v[static_cast<int>(i)];
|
||||
},
|
||||
"i"_a)
|
||||
"i"_a,
|
||||
"Read the i-th component (0=x, 1=y, 2=z, 3=w).")
|
||||
.def(
|
||||
"__setitem__",
|
||||
[](math::Vec4<T>& v, size_t i, T value) {
|
||||
@@ -192,28 +274,48 @@ template<typename T> void defineVec4(nb::module_& m, const char* name, const cha
|
||||
v[static_cast<int>(i)] = value;
|
||||
},
|
||||
"i"_a,
|
||||
"value"_a)
|
||||
.def("dot", &math::Vec4<T>::template dot<math::Vec4<T>>, "v"_a)
|
||||
.def("lengthSqr", &math::Vec4<T>::lengthSqr)
|
||||
.def("length", &math::Vec4<T>::length)
|
||||
.def(-nb::self)
|
||||
.def(nb::self * nb::self, "v"_a)
|
||||
.def(nb::self / nb::self, "v"_a)
|
||||
.def(nb::self + nb::self, "v"_a)
|
||||
.def(nb::self - nb::self, "v"_a)
|
||||
.def(nb::self * T(), "s"_a)
|
||||
.def(nb::self / T(), "s"_a)
|
||||
.def(nb::self += nb::self, "v"_a)
|
||||
.def(nb::self -= nb::self, "v"_a)
|
||||
.def(nb::self *= T(), "s"_a)
|
||||
.def(nb::self /= T(), "s"_a)
|
||||
.def("normalize", &math::Vec4<T>::normalize)
|
||||
.def("minComponent", &math::Vec4<T>::minComponent, "other"_a)
|
||||
.def("maxComponent", &math::Vec4<T>::maxComponent, "other"_a)
|
||||
"value"_a,
|
||||
"Write the i-th component (0=x, 1=y, 2=z, 3=w).")
|
||||
.def("dot", &math::Vec4<T>::template dot<math::Vec4<T>>, "v"_a,
|
||||
"Dot product with another vector.")
|
||||
.def("lengthSqr", &math::Vec4<T>::lengthSqr,
|
||||
"Squared Euclidean length (cheaper than length()).")
|
||||
.def("length", &math::Vec4<T>::length,
|
||||
"Euclidean length of this vector.")
|
||||
.def(-nb::self,
|
||||
"Negate every component.")
|
||||
.def(nb::self * nb::self, "v"_a,
|
||||
"Component-wise multiplication.")
|
||||
.def(nb::self / nb::self, "v"_a,
|
||||
"Component-wise division.")
|
||||
.def(nb::self + nb::self, "v"_a,
|
||||
"Component-wise addition.")
|
||||
.def(nb::self - nb::self, "v"_a,
|
||||
"Component-wise subtraction.")
|
||||
.def(nb::self * T(), "s"_a,
|
||||
"Multiply every component by the scalar s.")
|
||||
.def(nb::self / T(), "s"_a,
|
||||
"Divide every component by the scalar s.")
|
||||
.def(nb::self += nb::self, "v"_a,
|
||||
"In-place component-wise addition.")
|
||||
.def(nb::self -= nb::self, "v"_a,
|
||||
"In-place component-wise subtraction.")
|
||||
.def(nb::self *= T(), "s"_a,
|
||||
"In-place scalar multiply.")
|
||||
.def(nb::self /= T(), "s"_a,
|
||||
"In-place scalar divide.")
|
||||
.def("normalize", &math::Vec4<T>::normalize,
|
||||
"Scale this vector to unit length in place.")
|
||||
.def("minComponent", &math::Vec4<T>::minComponent, "other"_a,
|
||||
"Component-wise minimum with other.")
|
||||
.def("maxComponent", &math::Vec4<T>::maxComponent, "other"_a,
|
||||
"Component-wise maximum with other.")
|
||||
.def(
|
||||
"__mul__", [](const T& a, math::Vec4<T> b) { return a * b; }, nb::is_operator(), "b"_a)
|
||||
"__mul__", [](const T& a, math::Vec4<T> b) { return a * b; }, nb::is_operator(), "b"_a,
|
||||
"Right-multiply: scalar * Vec4.")
|
||||
.def(
|
||||
"__truediv__", [](const T& a, math::Vec4<T> b) { return a / b; }, nb::is_operator(), "b"_a)
|
||||
"__truediv__", [](const T& a, math::Vec4<T> b) { return a / b; }, nb::is_operator(), "b"_a,
|
||||
"Right-divide: scalar / Vec4, component-wise.")
|
||||
.def("__repr__", [](const math::Vec4<T>& v) {
|
||||
std::stringstream ostr;
|
||||
ostr << v;
|
||||
@@ -226,18 +328,30 @@ void defineRgba8(nb::module_& m)
|
||||
using ValueType = math::Rgba8::ValueType;
|
||||
|
||||
nb::class_<math::Rgba8>(m, "Rgba8", "8-bit red, green, blue, alpha packed into 32 bit unsigned int")
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<const math::Rgba8&>(), "other"_a)
|
||||
.def(nb::init<uint8_t, uint8_t, uint8_t, uint8_t>(), "r"_a, "g"_a, "b"_a, "a"_a = 255)
|
||||
.def(nb::init<uint8_t>(), "v"_a)
|
||||
.def(nb::init<float, float, float, float>(), "r"_a, "g"_a, "b"_a, "a"_a = 1.0)
|
||||
.def(nb::init<Vec3f>(), "rgb"_a)
|
||||
.def(nb::init<Vec4f>(), "rgba"_a)
|
||||
.def(nb::self < nb::self, "rhs"_a)
|
||||
.def(nb::self == nb::self, "rhs"_a)
|
||||
.def("lengthSqr", &math::Rgba8::lengthSqr)
|
||||
.def("length", &math::Rgba8::length)
|
||||
.def("asFloat", &math::Rgba8::asFloat, "n"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct a fully transparent black Rgba8 (all components 0).")
|
||||
.def(nb::init<const math::Rgba8&>(), "other"_a,
|
||||
"Copy-construct from another Rgba8.")
|
||||
.def(nb::init<uint8_t, uint8_t, uint8_t, uint8_t>(), "r"_a, "g"_a, "b"_a, "a"_a = 255,
|
||||
"Construct from four 0..255 uint8 channels; a defaults to fully opaque.")
|
||||
.def(nb::init<uint8_t>(), "v"_a,
|
||||
"Construct a gray Rgba8 with every channel set to v.")
|
||||
.def(nb::init<float, float, float, float>(), "r"_a, "g"_a, "b"_a, "a"_a = 1.0,
|
||||
"Construct from four 0..1 floats, clamped and quantized to uint8.")
|
||||
.def(nb::init<Vec3f>(), "rgb"_a,
|
||||
"Construct from an RGB float triple; alpha defaults to opaque.")
|
||||
.def(nb::init<Vec4f>(), "rgba"_a,
|
||||
"Construct from an RGBA float quadruple.")
|
||||
.def(nb::self < nb::self, "rhs"_a,
|
||||
"Less-than comparison on the packed uint32 representation.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Equality on the packed uint32 representation.")
|
||||
.def("lengthSqr", &math::Rgba8::lengthSqr,
|
||||
"Squared length over (r, g, b, a) as integers.")
|
||||
.def("length", &math::Rgba8::length,
|
||||
"Euclidean length over (r, g, b, a) as floats.")
|
||||
.def("asFloat", &math::Rgba8::asFloat, "n"_a,
|
||||
"Return the n-th channel as a 0..1 float.")
|
||||
.def(
|
||||
"__getitem__",
|
||||
[](const math::Rgba8& rgba, size_t i) {
|
||||
@@ -246,7 +360,8 @@ void defineRgba8(nb::module_& m)
|
||||
}
|
||||
return rgba[static_cast<int>(i)];
|
||||
},
|
||||
"i"_a)
|
||||
"i"_a,
|
||||
"Read the i-th channel as a uint8 (0=r, 1=g, 2=b, 3=a).")
|
||||
.def(
|
||||
"__setitem__",
|
||||
[](math::Rgba8& rgba, size_t i, ValueType value) {
|
||||
@@ -256,26 +371,38 @@ void defineRgba8(nb::module_& m)
|
||||
rgba[static_cast<int>(i)] = value;
|
||||
},
|
||||
"i"_a,
|
||||
"value"_a)
|
||||
"value"_a,
|
||||
"Write the i-th channel as a uint8 (0=r, 1=g, 2=b, 3=a).")
|
||||
.def_prop_rw(
|
||||
"packed", [](const math::Rgba8& rgba) { return rgba.packed(); }, [](math::Rgba8& rgba, uint32_t packed) { rgba.packed() = packed; })
|
||||
"packed", [](const math::Rgba8& rgba) { return rgba.packed(); }, [](math::Rgba8& rgba, uint32_t packed) { rgba.packed() = packed; },
|
||||
"The raw 32-bit packed RGBA representation.")
|
||||
.def_prop_rw(
|
||||
"r", [](const math::Rgba8& rgba) { return rgba.r(); }, [](math::Rgba8& rgba, uint8_t r) { rgba.r() = r; })
|
||||
"r", [](const math::Rgba8& rgba) { return rgba.r(); }, [](math::Rgba8& rgba, uint8_t r) { rgba.r() = r; },
|
||||
"Red channel as a uint8 0..255.")
|
||||
.def_prop_rw(
|
||||
"g", [](const math::Rgba8& rgba) { return rgba.g(); }, [](math::Rgba8& rgba, uint8_t g) { rgba.g() = g; })
|
||||
"g", [](const math::Rgba8& rgba) { return rgba.g(); }, [](math::Rgba8& rgba, uint8_t g) { rgba.g() = g; },
|
||||
"Green channel as a uint8 0..255.")
|
||||
.def_prop_rw(
|
||||
"b", [](const math::Rgba8& rgba) { return rgba.b(); }, [](math::Rgba8& rgba, uint8_t b) { rgba.b() = b; })
|
||||
"b", [](const math::Rgba8& rgba) { return rgba.b(); }, [](math::Rgba8& rgba, uint8_t b) { rgba.b() = b; },
|
||||
"Blue channel as a uint8 0..255.")
|
||||
.def_prop_rw(
|
||||
"a", [](const math::Rgba8& rgba) { return rgba.a(); }, [](math::Rgba8& rgba, uint8_t a) { rgba.a() = a; })
|
||||
.def("asVec3f", [](const math::Rgba8& rgba) { return Vec3f(rgba); })
|
||||
.def("asVec4f", [](const math::Rgba8& rgba) { return Vec4f(rgba); });
|
||||
"a", [](const math::Rgba8& rgba) { return rgba.a(); }, [](math::Rgba8& rgba, uint8_t a) { rgba.a() = a; },
|
||||
"Alpha channel as a uint8 0..255.")
|
||||
.def("asVec3f", [](const math::Rgba8& rgba) { return Vec3f(rgba); },
|
||||
"Convert RGB channels to a Vec3f of 0..1 floats (alpha dropped).")
|
||||
.def("asVec4f", [](const math::Rgba8& rgba) { return Vec4f(rgba); },
|
||||
"Convert RGBA channels to a Vec4f of 0..1 floats.");
|
||||
}
|
||||
|
||||
template<typename Vec3T> void defineBaseBBox(nb::module_& m, const char* name)
|
||||
{
|
||||
nb::class_<math::BaseBBox<Vec3T>>(m, name)
|
||||
.def(nb::self == nb::self, "rhs"_a)
|
||||
.def(nb::self != nb::self, "rhs"_a)
|
||||
nb::class_<math::BaseBBox<Vec3T>>(m, name,
|
||||
"Axis-aligned bounding-box base class. Stores a min / max corner; "
|
||||
"concrete subclasses add the open-interval vs closed-interval semantics.")
|
||||
.def(nb::self == nb::self, "rhs"_a,
|
||||
"Equality of both min and max corners.")
|
||||
.def(nb::self != nb::self, "rhs"_a,
|
||||
"Inequality of either min or max corner.")
|
||||
.def(
|
||||
"__getitem__",
|
||||
[](const math::BaseBBox<Vec3T>& bbox, size_t i) {
|
||||
@@ -284,7 +411,8 @@ template<typename Vec3T> void defineBaseBBox(nb::module_& m, const char* name)
|
||||
}
|
||||
return bbox[static_cast<int>(i)];
|
||||
},
|
||||
"i"_a)
|
||||
"i"_a,
|
||||
"Read corner 0 (min) or corner 1 (max).")
|
||||
.def(
|
||||
"__setitem__",
|
||||
[](math::BaseBBox<Vec3T>& bbox, size_t i, const Vec3T& value) {
|
||||
@@ -294,29 +422,45 @@ template<typename Vec3T> void defineBaseBBox(nb::module_& m, const char* name)
|
||||
bbox[static_cast<int>(i)] = value;
|
||||
},
|
||||
"i"_a,
|
||||
"value"_a)
|
||||
"value"_a,
|
||||
"Write corner 0 (min) or corner 1 (max).")
|
||||
.def_prop_rw(
|
||||
"min", [](const math::BaseBBox<Vec3T>& bbox) { return bbox.min(); }, [](math::BaseBBox<Vec3T>& bbox, const Vec3T& min) { bbox.min() = min; })
|
||||
"min", [](const math::BaseBBox<Vec3T>& bbox) { return bbox.min(); }, [](math::BaseBBox<Vec3T>& bbox, const Vec3T& min) { bbox.min() = min; },
|
||||
"Minimum corner of the bounding box.")
|
||||
.def_prop_rw(
|
||||
"max", [](const math::BaseBBox<Vec3T>& bbox) { return bbox.max(); }, [](math::BaseBBox<Vec3T>& bbox, const Vec3T& max) { bbox.max() = max; })
|
||||
.def("translate", &math::BaseBBox<Vec3T>::translate, "xyz"_a)
|
||||
.def("expand", nb::overload_cast<const Vec3T&>(&math::BaseBBox<Vec3T>::expand), "xyz"_a)
|
||||
.def("expand", nb::overload_cast<const math::BaseBBox<Vec3T>&>(&math::BaseBBox<Vec3T>::expand), "bbox"_a)
|
||||
.def("intersect", &math::BaseBBox<Vec3T>::intersect, "bbox"_a)
|
||||
.def("isInside", &math::BaseBBox<Vec3T>::isInside, "xyz"_a);
|
||||
"max", [](const math::BaseBBox<Vec3T>& bbox) { return bbox.max(); }, [](math::BaseBBox<Vec3T>& bbox, const Vec3T& max) { bbox.max() = max; },
|
||||
"Maximum corner of the bounding box.")
|
||||
.def("translate", &math::BaseBBox<Vec3T>::translate, "xyz"_a,
|
||||
"Translate this bounding box by xyz in place.")
|
||||
.def("expand", nb::overload_cast<const Vec3T&>(&math::BaseBBox<Vec3T>::expand), "xyz"_a,
|
||||
"Grow this bounding box to include the point xyz.")
|
||||
.def("expand", nb::overload_cast<const math::BaseBBox<Vec3T>&>(&math::BaseBBox<Vec3T>::expand), "bbox"_a,
|
||||
"Grow this bounding box to include another bbox in its entirety.")
|
||||
.def("intersect", &math::BaseBBox<Vec3T>::intersect, "bbox"_a,
|
||||
"Shrink this bounding box to the intersection with bbox.")
|
||||
.def("isInside", &math::BaseBBox<Vec3T>::isInside, "xyz"_a,
|
||||
"True iff xyz lies inside this bounding box.");
|
||||
}
|
||||
|
||||
template<typename Vec3T> void defineBBoxFloatingPoint(nb::module_& m, const char* name, const char* doc)
|
||||
{
|
||||
nb::class_<math::BBox<Vec3T, true>, math::BaseBBox<Vec3T>>(m, name, doc)
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<const Vec3T&, const Vec3T&>(), "min"_a, "max"_a)
|
||||
.def(nb::init<const math::Coord&, const math::Coord&>(), "min"_a, "max"_a)
|
||||
.def_static("createCube", &math::BBox<Vec3T>::createCube, "min"_a, "dim"_a)
|
||||
.def(nb::init<const math::BaseBBox<math::Coord>&>(), "bbox"_a)
|
||||
.def("empty", &math::BBox<Vec3T>::empty)
|
||||
.def("dim", &math::BBox<Vec3T>::dim)
|
||||
.def("isInside", &math::BBox<Vec3T>::isInside, "p"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct an empty bounding box (min > max sentinel).")
|
||||
.def(nb::init<const Vec3T&, const Vec3T&>(), "min"_a, "max"_a,
|
||||
"Construct from explicit min and max corners.")
|
||||
.def(nb::init<const math::Coord&, const math::Coord&>(), "min"_a, "max"_a,
|
||||
"Construct from integer Coord corners, cast to floating-point.")
|
||||
.def_static("createCube", &math::BBox<Vec3T>::createCube, "min"_a, "dim"_a,
|
||||
"Construct an axis-aligned cube of side dim anchored at min.")
|
||||
.def(nb::init<const math::BaseBBox<math::Coord>&>(), "bbox"_a,
|
||||
"Construct from an integer CoordBBox, cast to floating-point.")
|
||||
.def("empty", &math::BBox<Vec3T>::empty,
|
||||
"True iff this bbox is empty (any min component > the matching max).")
|
||||
.def("dim", &math::BBox<Vec3T>::dim,
|
||||
"Return max - min as a Vec3 of side lengths.")
|
||||
.def("isInside", &math::BBox<Vec3T>::isInside, "p"_a,
|
||||
"True iff p lies inside this bounding box (half-open interval).")
|
||||
.def("__repr__", [](const math::BBox<Vec3T>& b) {
|
||||
std::stringstream ostr;
|
||||
ostr << b;
|
||||
@@ -329,24 +473,39 @@ template<typename CoordT> void defineBBoxInteger(nb::module_& m, const char* nam
|
||||
using ValueType = typename CoordT::ValueType;
|
||||
|
||||
nb::class_<math::BBox<CoordT, false>, math::BaseBBox<CoordT>>(m, name, doc)
|
||||
.def(nb::init<>())
|
||||
.def(nb::init<const CoordT&, const CoordT&>(), "min"_a, "max"_a)
|
||||
.def(nb::init<>(),
|
||||
"Construct an empty CoordBBox (min > max sentinel).")
|
||||
.def(nb::init<const CoordT&, const CoordT&>(), "min"_a, "max"_a,
|
||||
"Construct from explicit min and max Coord corners (inclusive).")
|
||||
.def(
|
||||
"__iter__",
|
||||
[](const math::BBox<CoordT>& b) { return nb::make_iterator(nb::type<math::BBox<CoordT>>(), "CoordBBoxIterator", b.begin(), b.end()); },
|
||||
nb::keep_alive<0, 1>())
|
||||
.def_static("createCube", nb::overload_cast<const CoordT&, ValueType>(&math::BBox<CoordT>::createCube), "min"_a, "dim"_a)
|
||||
.def_static("createCube", nb::overload_cast<ValueType, ValueType>(&math::BBox<CoordT>::createCube), "min"_a, "max"_a)
|
||||
.def("is_divisible", &math::BBox<CoordT>::is_divisible)
|
||||
.def("empty", &math::BBox<CoordT>::empty)
|
||||
.def("dim", &math::BBox<CoordT>::dim)
|
||||
.def("volume", &math::BBox<CoordT>::volume)
|
||||
.def("isInside", nb::overload_cast<const CoordT&>(&math::BBox<CoordT>::isInside, nb::const_), "p"_a)
|
||||
.def("isInside", nb::overload_cast<const math::BBox<CoordT>&>(&math::BBox<CoordT>::isInside, nb::const_), "b"_a)
|
||||
.def("asFloat", &math::BBox<CoordT>::template asReal<float>)
|
||||
.def("asDouble", &math::BBox<CoordT>::template asReal<double>)
|
||||
.def("hasOverlap", &math::BBox<CoordT>::hasOverlap, "b"_a)
|
||||
.def("expandBy", &math::BBox<CoordT>::expandBy, "padding"_a)
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Iterate over every Coord in this CoordBBox in row-major order.")
|
||||
.def_static("createCube", nb::overload_cast<const CoordT&, ValueType>(&math::BBox<CoordT>::createCube), "min"_a, "dim"_a,
|
||||
"Construct a cube of side dim voxels anchored at the min Coord.")
|
||||
.def_static("createCube", nb::overload_cast<ValueType, ValueType>(&math::BBox<CoordT>::createCube), "min"_a, "max"_a,
|
||||
"Construct a cube spanning [min, max] in every axis.")
|
||||
.def("is_divisible", &math::BBox<CoordT>::is_divisible,
|
||||
"True iff this CoordBBox has more than one voxel in every axis.")
|
||||
.def("empty", &math::BBox<CoordT>::empty,
|
||||
"True iff this CoordBBox is empty (any min component > the matching max).")
|
||||
.def("dim", &math::BBox<CoordT>::dim,
|
||||
"Return max - min + 1 as a Coord of side lengths (inclusive).")
|
||||
.def("volume", &math::BBox<CoordT>::volume,
|
||||
"Total number of voxels enclosed by this CoordBBox.")
|
||||
.def("isInside", nb::overload_cast<const CoordT&>(&math::BBox<CoordT>::isInside, nb::const_), "p"_a,
|
||||
"True iff the integer point p lies inside this CoordBBox.")
|
||||
.def("isInside", nb::overload_cast<const math::BBox<CoordT>&>(&math::BBox<CoordT>::isInside, nb::const_), "b"_a,
|
||||
"True iff b lies entirely inside this CoordBBox.")
|
||||
.def("asFloat", &math::BBox<CoordT>::template asReal<float>,
|
||||
"Convert this CoordBBox to a floating-point BBox of Vec3f.")
|
||||
.def("asDouble", &math::BBox<CoordT>::template asReal<double>,
|
||||
"Convert this CoordBBox to a floating-point BBox of Vec3d.")
|
||||
.def("hasOverlap", &math::BBox<CoordT>::hasOverlap, "b"_a,
|
||||
"True iff this CoordBBox shares any voxel with b.")
|
||||
.def("expandBy", &math::BBox<CoordT>::expandBy, "padding"_a,
|
||||
"Grow this CoordBBox by the given padding in every direction.")
|
||||
.def("__repr__", [](const CoordBBox& b) {
|
||||
std::stringstream ostr;
|
||||
ostr << b;
|
||||
@@ -377,33 +536,12 @@ void defineMathModule(nb::module_& m)
|
||||
defineBaseBBox<math::Coord>(m, "CoordBaseBBox");
|
||||
defineBBoxInteger<math::Coord>(m, "CoordBBox", "Bounding box for Coord minimum and maximum");
|
||||
|
||||
defineNearestNeighborSampler<float>(m, "FloatNearestNeighborSampler");
|
||||
defineTrilinearSampler<float>(m, "FloatTrilinearSampler");
|
||||
defineTriquadraticSampler<float>(m, "FloatTriquadraticSampler");
|
||||
defineTricubicSampler<float>(m, "FloatTricubicSampler");
|
||||
|
||||
defineNearestNeighborSampler<double>(m, "DoubleNearestNeighborSampler");
|
||||
defineTrilinearSampler<double>(m, "DoubleTrilinearSampler");
|
||||
defineTriquadraticSampler<double>(m, "DoubleTriquadraticSampler");
|
||||
defineTricubicSampler<double>(m, "DoubleTricubicSampler");
|
||||
|
||||
defineNearestNeighborSampler<int32_t>(m, "Int32NearestNeighborSampler");
|
||||
defineTrilinearSampler<int32_t>(m, "Int32TrilinearSampler");
|
||||
defineTriquadraticSampler<int32_t>(m, "Int32TriquadraticSampler");
|
||||
defineTricubicSampler<int32_t>(m, "Int32TricubicSampler");
|
||||
|
||||
defineNearestNeighborSampler<Vec3f>(m, "Vec3fNearestNeighborSampler");
|
||||
defineTrilinearSampler<Vec3f>(m, "Vec3fTrilinearSampler");
|
||||
defineTriquadraticSampler<Vec3f>(m, "Vec3fTriquadraticSampler");
|
||||
defineTricubicSampler<Vec3f>(m, "Vec3fTricubicSampler");
|
||||
|
||||
#ifdef NANOVDB_USE_CUDA
|
||||
nb::module_ cudaModule = m.def_submodule("cuda");
|
||||
cudaModule.doc() = "A submodule that implements CUDA-accelerated math functions";
|
||||
|
||||
defineSampleFromVoxels<float>(cudaModule, "sampleFromVoxels");
|
||||
defineSampleFromVoxels<double>(cudaModule, "sampleFromVoxels");
|
||||
#endif
|
||||
#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) \
|
||||
defineNearestNeighborSampler<T>(m, #Suffix "NearestNeighborSampler"); \
|
||||
defineTrilinearSampler<T>(m, #Suffix "TrilinearSampler"); \
|
||||
defineTriquadraticSampler<T>(m, #Suffix "TriquadraticSampler"); \
|
||||
defineTricubicSampler<T>(m, #Suffix "TricubicSampler");
|
||||
#include "BuildTypes.def"
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -32,11 +32,9 @@ GridHandle<BufferT> createLevelSetSphere(GridType gridType,
|
||||
switch (gridType) {
|
||||
case GridType::Float: return createLevelSetSphere<float, BufferT>(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double: return createLevelSetSphere<double, BufferT>(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default: {
|
||||
std::stringstream ss;
|
||||
// ss << "Cannot createLevelSetSphere for grid of type \"" << toStr(gridType);
|
||||
throw std::runtime_error(ss.str() + "\"");
|
||||
}
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createLevelSetSphere: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,11 +56,9 @@ GridHandle<BufferT> createLevelSetTorus(GridType gridType,
|
||||
return createLevelSetTorus<float, BufferT>(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return createLevelSetTorus<double, BufferT>(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default: {
|
||||
std::stringstream ss;
|
||||
// ss << "Cannot createLevelSetTorus for grid of type \"" << toStr(gridType);
|
||||
throw std::runtime_error(ss.str() + "\"");
|
||||
}
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createLevelSetTorus: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,11 +77,9 @@ GridHandle<BufferT> createFogVolumeSphere(GridType gridType,
|
||||
switch (gridType) {
|
||||
case GridType::Float: return createFogVolumeSphere<float, BufferT>(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double: return createFogVolumeSphere<double, BufferT>(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default: {
|
||||
std::stringstream ss;
|
||||
// ss << "Cannot createFogVolumeSphere for grid of type \"" << toStr(gridType);
|
||||
throw std::runtime_error(ss.str() + "\"");
|
||||
}
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createFogVolumeSphere: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,14 +101,218 @@ GridHandle<BufferT> createFogVolumeTorus(GridType gridType,
|
||||
return createFogVolumeTorus<float, BufferT>(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return createFogVolumeTorus<double, BufferT>(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default: {
|
||||
std::stringstream ss;
|
||||
// ss << "Cannot createFogVolumeTorus for grid of type \"" << toStr(gridType);
|
||||
throw std::runtime_error(ss.str() + "\"");
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createFogVolumeTorus: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- New primitives ----------
|
||||
// Same float/double switch pattern as the four existing primitives above.
|
||||
// The C++ templates also accept Fp4/Fp8/Fp16/FpN; those flavors live on
|
||||
// the createNanoGrid path with explicit oracle and dither parameters
|
||||
// rather than being expressed as primitive overloads here.
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createLevelSetBox(GridType gridType,
|
||||
double width,
|
||||
double height,
|
||||
double depth,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
double halfWidth,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
switch (gridType) {
|
||||
case GridType::Float:
|
||||
return tools::createLevelSetBox<float, BufferT>(
|
||||
width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return tools::createLevelSetBox<double, BufferT>(
|
||||
width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createLevelSetBox: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createLevelSetBBox(GridType gridType,
|
||||
double width,
|
||||
double height,
|
||||
double depth,
|
||||
double thickness,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
double halfWidth,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
switch (gridType) {
|
||||
case GridType::Float:
|
||||
return tools::createLevelSetBBox<float, BufferT>(
|
||||
width, height, depth, thickness, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return tools::createLevelSetBBox<double, BufferT>(
|
||||
width, height, depth, thickness, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createLevelSetBBox: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createLevelSetOctahedron(GridType gridType,
|
||||
double scale,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
double halfWidth,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
switch (gridType) {
|
||||
case GridType::Float:
|
||||
return tools::createLevelSetOctahedron<float, BufferT>(
|
||||
scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return tools::createLevelSetOctahedron<double, BufferT>(
|
||||
scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createLevelSetOctahedron: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createFogVolumeBox(GridType gridType,
|
||||
double width,
|
||||
double height,
|
||||
double depth,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
double halfWidth,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
switch (gridType) {
|
||||
case GridType::Float:
|
||||
return tools::createFogVolumeBox<float, BufferT>(
|
||||
width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return tools::createFogVolumeBox<double, BufferT>(
|
||||
width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createFogVolumeBox: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createFogVolumeOctahedron(GridType gridType,
|
||||
double scale,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
double halfWidth,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
tools::StatsMode sMode,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
switch (gridType) {
|
||||
case GridType::Float:
|
||||
return tools::createFogVolumeOctahedron<float, BufferT>(
|
||||
scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
case GridType::Double:
|
||||
return tools::createFogVolumeOctahedron<double, BufferT>(
|
||||
scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer);
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"createFogVolumeOctahedron: only float and double grid types are supported");
|
||||
}
|
||||
}
|
||||
|
||||
// Point primitives. The result is always a PointDataGrid (uint32 storage),
|
||||
// so unlike the level-set / fog-volume primitives there's no value-type
|
||||
// dispatch worth exposing — the intermediate level-set's precision is
|
||||
// not user-controllable in this binding. The C++ template also accepts
|
||||
// BuildT=double, but that path segfaults during scatter at least with the
|
||||
// current C++ implementation, so the binding stays on the float-only
|
||||
// instantiation that's exercised by the C++ unit tests.
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createPointSphere(int pointsPerVoxel,
|
||||
double radius,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
CheckMode mode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
return tools::createPointSphere<float, BufferT>(
|
||||
pointsPerVoxel, radius, center, voxelSize, origin, name, mode, buffer);
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createPointTorus(int pointsPerVoxel,
|
||||
double majorRadius,
|
||||
double minorRadius,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
CheckMode cMode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
return tools::createPointTorus<float, BufferT>(
|
||||
pointsPerVoxel, majorRadius, minorRadius, center, voxelSize, origin, name, cMode, buffer);
|
||||
}
|
||||
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createPointBox(int pointsPerVoxel,
|
||||
double width,
|
||||
double height,
|
||||
double depth,
|
||||
const Vec3d& center,
|
||||
double voxelSize,
|
||||
const Vec3d& origin,
|
||||
const std::string& name,
|
||||
CheckMode mode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
return tools::createPointBox<float, BufferT>(
|
||||
pointsPerVoxel, width, height, depth, center, voxelSize, origin, name, mode, buffer);
|
||||
}
|
||||
|
||||
// createPointScatter takes an existing level set as its source. We bind
|
||||
// the float source variant — the C++ template also accepts double, but
|
||||
// the existing primitives and tests use float, and the source grid is
|
||||
// the runtime-typed nanovdb::NanoGrid<float>, so a single overload keeps
|
||||
// the Python surface simple.
|
||||
template<typename BufferT>
|
||||
GridHandle<BufferT> createPointScatter(const NanoGrid<float>& srcGrid,
|
||||
int pointsPerVoxel,
|
||||
const std::string& name,
|
||||
CheckMode mode,
|
||||
const BufferT& buffer)
|
||||
{
|
||||
return tools::createPointScatter<float, BufferT>(
|
||||
srcGrid, pointsPerVoxel, name, mode, buffer);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
template<typename BufferT> void definePrimitives(nb::module_& m)
|
||||
@@ -131,7 +329,8 @@ template<typename BufferT> void definePrimitives(nb::module_& m)
|
||||
"name"_a = "sphere_ls",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Narrow-band level set of a sphere of the given radius and center.");
|
||||
|
||||
m.def("createLevelSetTorus",
|
||||
nb::overload_cast<GridType,
|
||||
@@ -155,7 +354,8 @@ template<typename BufferT> void definePrimitives(nb::module_& m)
|
||||
"name"_a = "torus_ls",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Narrow-band level set of a torus with the given major and minor radii.");
|
||||
|
||||
m.def("createFogVolumeSphere",
|
||||
nb::overload_cast<GridType, double, const Vec3d&, double, double, const Vec3d&, const std::string&, tools::StatsMode, CheckMode, const BufferT&>(
|
||||
@@ -169,7 +369,8 @@ template<typename BufferT> void definePrimitives(nb::module_& m)
|
||||
"name"_a = "sphere_fog",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Sparse fog volume of a sphere of the given radius and center.");
|
||||
|
||||
m.def("createFogVolumeTorus",
|
||||
nb::overload_cast<GridType,
|
||||
@@ -193,7 +394,139 @@ template<typename BufferT> void definePrimitives(nb::module_& m)
|
||||
"name"_a = "torus_fog",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT());
|
||||
"buffer"_a = BufferT(),
|
||||
"Sparse fog volume of a torus with the given major and minor radii.");
|
||||
|
||||
// ---------- Level-set / fog-volume box / bbox / octahedron primitives ----
|
||||
m.def("createLevelSetBox", &createLevelSetBox<BufferT>,
|
||||
"gridType"_a = GridType::Float,
|
||||
"width"_a = 40.0,
|
||||
"height"_a = 60.0,
|
||||
"depth"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"halfWidth"_a = 3.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "box_ls",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Narrow-band level set of an axis-aligned box.");
|
||||
|
||||
m.def("createLevelSetBBox", &createLevelSetBBox<BufferT>,
|
||||
"gridType"_a = GridType::Float,
|
||||
"width"_a = 40.0,
|
||||
"height"_a = 60.0,
|
||||
"depth"_a = 100.0,
|
||||
"thickness"_a = 10.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"halfWidth"_a = 3.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "bbox_ls",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Narrow-band level set of a hollow box wireframe (BBox = bounding "
|
||||
"box edges with the given thickness).");
|
||||
|
||||
m.def("createLevelSetOctahedron", &createLevelSetOctahedron<BufferT>,
|
||||
"gridType"_a = GridType::Float,
|
||||
"scale"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"halfWidth"_a = 3.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
// Default name spells the shape correctly even though the
|
||||
// upstream C++ default still carries the historical
|
||||
// "octadedron_ls" typo. Callers can override either way.
|
||||
"name"_a = "octahedron_ls",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Narrow-band level set of an octahedron.");
|
||||
|
||||
m.def("createFogVolumeBox", &createFogVolumeBox<BufferT>,
|
||||
"gridType"_a = GridType::Float,
|
||||
"width"_a = 40.0,
|
||||
"height"_a = 60.0,
|
||||
"depth"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"halfWidth"_a = 3.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "box_fog",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Sparse fog volume of a box (exterior 0/inactive, interior active "
|
||||
"with values smoothly varying from 0 at the surface to 1 inside).");
|
||||
|
||||
m.def("createFogVolumeOctahedron", &createFogVolumeOctahedron<BufferT>,
|
||||
"gridType"_a = GridType::Float,
|
||||
"scale"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"halfWidth"_a = 3.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "octahedron_fog",
|
||||
"sMode"_a = tools::StatsMode::Default,
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Sparse fog volume of an octahedron.");
|
||||
|
||||
// ---------- Point primitives ----------
|
||||
m.def("createPointSphere", &createPointSphere<BufferT>,
|
||||
"pointsPerVoxel"_a = 1,
|
||||
"radius"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "sphere_points",
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"PointDataGrid of points scattered on the surface of a sphere. "
|
||||
"The output grid is always a UInt32 PointDataGrid; the "
|
||||
"intermediate level-set's value type is hard-coded to float.");
|
||||
|
||||
m.def("createPointTorus", &createPointTorus<BufferT>,
|
||||
"pointsPerVoxel"_a = 1,
|
||||
"majorRadius"_a = 100.0,
|
||||
"minorRadius"_a = 50.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "torus_points",
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"PointDataGrid of points scattered on the surface of a torus. "
|
||||
"Always returns a UInt32 PointDataGrid.");
|
||||
|
||||
m.def("createPointBox", &createPointBox<BufferT>,
|
||||
"pointsPerVoxel"_a = 1,
|
||||
"width"_a = 40.0,
|
||||
"height"_a = 60.0,
|
||||
"depth"_a = 100.0,
|
||||
"center"_a = Vec3d(0.0),
|
||||
"voxelSize"_a = 1.0,
|
||||
"origin"_a = Vec3d(0.0),
|
||||
"name"_a = "box_points",
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"PointDataGrid of points scattered on the surface of a box. "
|
||||
"Always returns a UInt32 PointDataGrid.");
|
||||
|
||||
m.def("createPointScatter", &createPointScatter<BufferT>,
|
||||
"srcGrid"_a,
|
||||
"pointsPerVoxel"_a = 1,
|
||||
"name"_a = "point_scatter",
|
||||
"cMode"_a = CheckMode::Default,
|
||||
"buffer"_a = BufferT(),
|
||||
"Scatter a PointDataGrid into the active voxels of a "
|
||||
"NanoGrid<float> level set. The source grid must satisfy "
|
||||
"srcGrid.isLevelSet() and have an active bounding box; "
|
||||
"non-level-set sources (e.g. fog volumes) raise RuntimeError. "
|
||||
"Point coordinates are stored as blind data in world space.");
|
||||
}
|
||||
|
||||
template void definePrimitives<HostBuffer>(nb::module_&);
|
||||
|
||||
@@ -15,20 +15,53 @@ namespace {
|
||||
|
||||
template<typename TreeT, int Order> void defineSampleFromVoxels(nb::module_& m, const char* name)
|
||||
{
|
||||
using CoordT = typename TreeT::CoordType;
|
||||
nb::class_<math::SampleFromVoxels<TreeT, Order, false>>(m, name)
|
||||
using CoordT = typename TreeT::CoordType;
|
||||
using ValueT = typename TreeT::ValueType;
|
||||
using SamplerT = math::SampleFromVoxels<TreeT, Order, false>;
|
||||
auto cls = nb::class_<SamplerT>(m, name,
|
||||
"Callable sampler that reconstructs a grid value at an arbitrary "
|
||||
"index-space position. Build via the matching create*Sampler() factory.")
|
||||
.def(
|
||||
"__call__", [](const math::SampleFromVoxels<TreeT, Order, false>& sampler, const CoordT& ijk) { return sampler(ijk); }, nb::is_operator(), "ijk"_a)
|
||||
"__call__", [](const SamplerT& sampler, const CoordT& ijk) { return sampler(ijk); }, nb::is_operator(), "ijk"_a,
|
||||
"Sample the grid at integer voxel coordinate ijk.")
|
||||
.def(
|
||||
"__call__", [](const math::SampleFromVoxels<TreeT, Order, false>& sampler, const Vec3f& xyz) { return sampler(xyz); }, nb::is_operator(), "xyz"_a)
|
||||
"__call__", [](const SamplerT& sampler, const Vec3f& xyz) { return sampler(xyz); }, nb::is_operator(), "xyz"_a,
|
||||
"Sample the grid at fractional index-space position xyz.")
|
||||
.def(
|
||||
"__call__", [](const math::SampleFromVoxels<TreeT, Order, false>& sampler, const Vec3d& xyz) { return sampler(xyz); }, nb::is_operator(), "xyz"_a);
|
||||
"__call__", [](const SamplerT& sampler, const Vec3d& xyz) { return sampler(xyz); }, nb::is_operator(), "xyz"_a,
|
||||
"Sample the grid at fractional index-space position xyz (double).");
|
||||
// gradient() exists on the trilinear sampler only, zeroCrossing() on the
|
||||
// trilinear and triquadratic samplers, and both static_assert a
|
||||
// floating-point ValueT in C++ — mirror that gating here.
|
||||
if constexpr (Order == 1 && util::is_floating_point<ValueT>::value) {
|
||||
cls.def(
|
||||
"gradient", [](const SamplerT& sampler, const Vec3f& xyz) { return sampler.gradient(xyz); }, "xyz"_a,
|
||||
"Return the index-space gradient of the trilinear reconstruction "
|
||||
"at fractional index-space position xyz. Use "
|
||||
"grid.indexToWorldGrad() to move it to world space.")
|
||||
.def(
|
||||
"gradient", [](const SamplerT& sampler, const Vec3d& xyz) { return sampler.gradient(xyz); }, "xyz"_a,
|
||||
"Return the index-space gradient at fractional index-space "
|
||||
"position xyz (double).");
|
||||
}
|
||||
if constexpr ((Order == 1 || Order == 2) && util::is_floating_point<ValueT>::value) {
|
||||
cls.def(
|
||||
"zeroCrossing", [](const SamplerT& sampler, const Vec3f& xyz) { return sampler.zeroCrossing(xyz); }, "xyz"_a,
|
||||
"True iff the reconstruction stencil at fractional index-space "
|
||||
"position xyz straddles the zero iso-surface.")
|
||||
.def(
|
||||
"zeroCrossing", [](const SamplerT& sampler, const Vec3d& xyz) { return sampler.zeroCrossing(xyz); }, "xyz"_a,
|
||||
"True iff the reconstruction stencil at fractional index-space "
|
||||
"position xyz (double) straddles the zero iso-surface.");
|
||||
}
|
||||
}
|
||||
|
||||
template<typename TreeT, int Order> void defineCreateSampler(nb::module_& m, const char* name)
|
||||
{
|
||||
m.def(
|
||||
name, [](const Grid<TreeT>& grid) { return math::createSampler<Order, TreeT, false>(grid.tree()); }, "grid"_a);
|
||||
name, [](const Grid<TreeT>& grid) { return math::createSampler<Order, TreeT, false>(grid.tree()); }, "grid"_a,
|
||||
"Build a sampler of the matching order that reads values from the "
|
||||
"given grid's tree.");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -65,24 +98,11 @@ template<typename BuildT> void defineTricubicSampler(nb::module_& m, const char*
|
||||
defineCreateSampler<TreeT, 3>(m, "createTricubicSampler");
|
||||
}
|
||||
|
||||
template void defineNearestNeighborSampler<float>(nb::module_&, const char*);
|
||||
template void defineTrilinearSampler<float>(nb::module_&, const char*);
|
||||
template void defineTriquadraticSampler<float>(nb::module_&, const char*);
|
||||
template void defineTricubicSampler<float>(nb::module_&, const char*);
|
||||
|
||||
template void defineNearestNeighborSampler<double>(nb::module_&, const char*);
|
||||
template void defineTrilinearSampler<double>(nb::module_&, const char*);
|
||||
template void defineTriquadraticSampler<double>(nb::module_&, const char*);
|
||||
template void defineTricubicSampler<double>(nb::module_&, const char*);
|
||||
|
||||
template void defineNearestNeighborSampler<int32_t>(nb::module_&, const char*);
|
||||
template void defineTrilinearSampler<int32_t>(nb::module_&, const char*);
|
||||
template void defineTriquadraticSampler<int32_t>(nb::module_&, const char*);
|
||||
template void defineTricubicSampler<int32_t>(nb::module_&, const char*);
|
||||
|
||||
template void defineNearestNeighborSampler<Vec3f>(nb::module_&, const char*);
|
||||
template void defineTrilinearSampler<Vec3f>(nb::module_&, const char*);
|
||||
template void defineTriquadraticSampler<Vec3f>(nb::module_&, const char*);
|
||||
template void defineTricubicSampler<Vec3f>(nb::module_&, const char*);
|
||||
#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) \
|
||||
template void defineNearestNeighborSampler<T>(nb::module_&, const char*); \
|
||||
template void defineTrilinearSampler<T>(nb::module_&, const char*); \
|
||||
template void defineTriquadraticSampler<T>(nb::module_&, const char*); \
|
||||
template void defineTricubicSampler<T>(nb::module_&, const char*);
|
||||
#include "BuildTypes.def"
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "PyNanoToOpenVDB.h"
|
||||
#ifdef NANOVDB_USE_CUDA
|
||||
#include "cuda/PyPointsToGrid.h"
|
||||
#include "cuda/PySampleFromVoxels.h"
|
||||
#include "cuda/PySignedFloodFill.h"
|
||||
#endif
|
||||
|
||||
@@ -31,12 +32,17 @@ void defineToolsModule(nb::module_& m)
|
||||
|
||||
defineStatsMode(m);
|
||||
|
||||
defineGridStatsModule(m);
|
||||
defineGridValidatorModule(m);
|
||||
defineEvalChecksumModule(m);
|
||||
|
||||
defineCreateNanoGridConversions(m);
|
||||
|
||||
definePrimitives<HostBuffer>(m);
|
||||
|
||||
defineCreateNanoGrid<float>(m, "createFloatGrid");
|
||||
defineCreateNanoGrid<double>(m, "createDoubleGrid");
|
||||
defineCreateNanoGrid<int32_t>(m, "createInt32Grid");
|
||||
defineCreateNanoGrid<Vec3f>(m, "createVec3fGrid");
|
||||
#define NANOVDB_PY_FOR_EACH_SAMPLEABLE_BUILDT(T, Suffix) \
|
||||
defineCreateNanoGrid<T>(m, "create" #Suffix "Grid");
|
||||
#include "BuildTypes.def"
|
||||
|
||||
#ifdef NANOVDB_USE_OPENVDB
|
||||
defineOpenToNanoVDB<HostBuffer>(m);
|
||||
@@ -55,6 +61,9 @@ void defineToolsModule(nb::module_& m)
|
||||
defineSignedFloodFill<double>(cudaModule, "signedFloodFill");
|
||||
|
||||
definePointsToGrid<math::Rgba8>(cudaModule, "pointsToRGBA8Grid");
|
||||
|
||||
defineSampleFromVoxels<float>(cudaModule, "sampleFromVoxels");
|
||||
defineSampleFromVoxels<double>(cudaModule, "sampleFromVoxels");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "PyTree.h"
|
||||
|
||||
#include <nanobind/stl/string.h>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
using namespace nanovdb;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// Polymorphic mgr() that returns the right typed NodeManager based on the
|
||||
// handle's stored gridType. Dispatch follows the same X-macro pattern as
|
||||
// pyHostGrid / pyDeviceGrid; unbound BuildTs return None rather than the
|
||||
// generic getMgr<BuildT>() ptr that would be reinterpreted.
|
||||
template<typename BufferT>
|
||||
static nb::object pyNodeMgr(nb::handle py_self)
|
||||
{
|
||||
using HandleT = NodeManagerHandle<BufferT>;
|
||||
auto& handle = nb::cast<HandleT&>(py_self);
|
||||
if (!handle.data()) return nb::none();
|
||||
// We need to read the stored gridType, but it's private. The public
|
||||
// mgr<BuildT>() returns NULL for type mismatch, so iterate by BuildT.
|
||||
// The X-macro produces one case per bound BuildT; first non-null wins.
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto* m = handle.template mgr<T>()) { \
|
||||
return nb::cast(m, nb::rv_policy::reference, py_self); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
if (auto* m = handle.template mgr<T>()) { \
|
||||
return nb::cast(m, nb::rv_policy::reference, py_self); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto* m = handle.template mgr<T>()) { \
|
||||
return nb::cast(m, nb::rv_policy::reference, py_self); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto* m = handle.template mgr<T>()) { \
|
||||
return nb::cast(m, nb::rv_policy::reference, py_self); \
|
||||
}
|
||||
#include "BuildTypes.def"
|
||||
return nb::none();
|
||||
}
|
||||
|
||||
void defineNodeManagerHandle(nb::module_& m)
|
||||
{
|
||||
using HandleT = NodeManagerHandle<HostBuffer>;
|
||||
nb::class_<HandleT>(m, "NodeManagerHandle",
|
||||
"Owns the memory backing a NodeManager. Move-only. "
|
||||
"Obtain via nanovdb.createNodeManager(grid).")
|
||||
.def("size",
|
||||
[](const HandleT& h) { return h.size(); },
|
||||
"Byte size of the buffer backing this NodeManagerHandle.")
|
||||
.def(
|
||||
"__bool__",
|
||||
[](const HandleT& h) { return h.data() != nullptr; },
|
||||
nb::is_operator(),
|
||||
"True iff this handle owns a non-empty buffer.")
|
||||
.def("mgr", &pyNodeMgr<HostBuffer>,
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return the typed NodeManager for the grid this handle was "
|
||||
"built from, or None if the BuildT is not Python-visible. The "
|
||||
"returned NodeManager keeps this handle alive.");
|
||||
}
|
||||
|
||||
// createNodeManager has one template instantiation per BuildT. We expose a
|
||||
// single polymorphic `createNodeManager(grid)` that picks the right one
|
||||
// based on the runtime type of `grid` (any nb::class_-bound NanoGrid<T>).
|
||||
// nb::isinstance is a fast type check that avoids the exception-on-mismatch
|
||||
// overhead that would come from trying nb::cast and catching cast_error for
|
||||
// every non-matching BuildT.
|
||||
template<typename BuildT>
|
||||
static nb::object tryCreateNodeManager(nb::handle py_grid)
|
||||
{
|
||||
using GridT = NanoGrid<BuildT>;
|
||||
if (!nb::isinstance<GridT>(py_grid)) {
|
||||
return nb::object(); // sentinel: "not this BuildT, try next"
|
||||
}
|
||||
auto& grid = nb::cast<GridT&>(py_grid);
|
||||
return nb::cast(createNodeManager<BuildT, HostBuffer>(grid));
|
||||
}
|
||||
|
||||
void defineCreateNodeManager(nb::module_& m)
|
||||
{
|
||||
m.def("createNodeManager",
|
||||
[](nb::handle py_grid) -> nb::object {
|
||||
// Try every bound BuildT; first successful cast wins.
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto obj = tryCreateNodeManager<T>(py_grid); obj.is_valid()) { \
|
||||
return obj; \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
if (auto obj = tryCreateNodeManager<T>(py_grid); obj.is_valid()) { \
|
||||
return obj; \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto obj = tryCreateNodeManager<T>(py_grid); obj.is_valid()) { \
|
||||
return obj; \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
if (auto obj = tryCreateNodeManager<T>(py_grid); obj.is_valid()) { \
|
||||
return obj; \
|
||||
}
|
||||
#include "BuildTypes.def"
|
||||
throw nb::type_error(
|
||||
"createNodeManager: argument is not a NanoVDB grid of any "
|
||||
"bound BuildT");
|
||||
},
|
||||
"grid"_a,
|
||||
// The constructed NodeManager stores a raw pointer back to the
|
||||
// grid; the handle must therefore keep the grid (and transitively
|
||||
// the GridHandle that owns the grid's buffer) alive.
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Build a NodeManager for the given grid, returning a "
|
||||
"NodeManagerHandle that owns the underlying buffer. The handle's "
|
||||
"mgr() method returns the typed NodeManager. The handle keeps the "
|
||||
"source grid alive for as long as it lives.");
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
@@ -0,0 +1,486 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#ifndef NANOVDB_PYTREE_HAS_BEEN_INCLUDED
|
||||
#define NANOVDB_PYTREE_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <nanobind/nanobind.h>
|
||||
#include <nanobind/ndarray.h>
|
||||
#include <nanobind/stl/tuple.h>
|
||||
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/NodeManager.h>
|
||||
#include <nanovdb/HostBuffer.h>
|
||||
|
||||
#include <type_traits> // std::is_arithmetic_v, std::enable_if
|
||||
|
||||
namespace nb = nanobind;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// -------------------- NanoLeaf<BuildT> --------------------
|
||||
//
|
||||
// Binds the 8^3 leaf node. Methods that don't depend on whether the leaf
|
||||
// stores a contiguous T[512] array are bound unconditionally; the
|
||||
// zero-copy 512-element NumPy values() view is only bound when the leaf
|
||||
// layout actually carries T mValues[512] (the BuildTraits<T>::is_special
|
||||
// types use packed / index / mask layouts where a 512-element T view is
|
||||
// either impossible or misleading).
|
||||
template<typename BuildT> void defineNanoLeaf(nb::module_& m, const char* name)
|
||||
{
|
||||
using LeafT = nanovdb::NanoLeaf<BuildT>;
|
||||
using ValueT = typename LeafT::ValueType;
|
||||
using CoordT = typename LeafT::CoordType;
|
||||
|
||||
auto cls = nb::class_<LeafT>(m, name,
|
||||
"Leaf node — 8x8x8 voxels. Inherits stats and bbox from the same "
|
||||
"leaf-data block bound across BuildTs.");
|
||||
|
||||
cls.def("origin", &LeafT::origin,
|
||||
"Index-space origin (minimum corner) of this leaf node.")
|
||||
.def("bbox", &LeafT::bbox,
|
||||
"Index-space bounding box of this leaf's active voxels.")
|
||||
.def("hasBBox", &LeafT::hasBBox,
|
||||
"True iff this leaf carries a cached active bounding box.")
|
||||
.def_static("dim", &LeafT::dim,
|
||||
"Side length of a leaf node in voxels (always 8).")
|
||||
.def_static("voxelCount", &LeafT::voxelCount,
|
||||
"Total voxel count in a leaf (always 512).")
|
||||
.def("memUsage", &LeafT::memUsage,
|
||||
"Byte size of this leaf node.")
|
||||
.def("isActive",
|
||||
nb::overload_cast<const CoordT&>(&LeafT::isActive, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"True iff the voxel at index-space ijk is active.")
|
||||
.def("isActive",
|
||||
[](const LeafT& leaf, uint32_t n) {
|
||||
// Underlying mValueMask.isOn(n) is unchecked; release builds
|
||||
// skip the C++ NANOVDB_ASSERT and would silently read OOB.
|
||||
if (n >= LeafT::voxelCount()) {
|
||||
throw nb::index_error(
|
||||
"Leaf.isActive(n): n out of range [0, voxelCount)");
|
||||
}
|
||||
return leaf.isActive(n);
|
||||
},
|
||||
nb::arg("n"),
|
||||
"True iff the n-th voxel (linear index into the 512-element leaf) is active.")
|
||||
.def("getValue",
|
||||
[](const LeafT& leaf, uint32_t offset) {
|
||||
// mValues[offset] is unchecked in C++; guard the Python side.
|
||||
if (offset >= LeafT::voxelCount()) {
|
||||
throw nb::index_error(
|
||||
"Leaf.getValue(offset): offset out of range [0, voxelCount)");
|
||||
}
|
||||
return leaf.getValue(offset);
|
||||
},
|
||||
nb::arg("offset"),
|
||||
"Return the value at linear offset (0..511) within this leaf.")
|
||||
.def("getValue",
|
||||
nb::overload_cast<const CoordT&>(&LeafT::getValue, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"Return the value at index-space ijk; ijk must lie inside this leaf.")
|
||||
.def("getFirstValue", &LeafT::getFirstValue,
|
||||
"Value at the leaf's first voxel (linear offset 0).")
|
||||
.def("getLastValue", &LeafT::getLastValue,
|
||||
"Value at the leaf's last voxel (linear offset 511).")
|
||||
.def("minimum", &LeafT::minimum,
|
||||
"Minimum active value within this leaf.")
|
||||
.def("maximum", &LeafT::maximum,
|
||||
"Maximum active value within this leaf.")
|
||||
.def("average", &LeafT::average,
|
||||
"Average of active values within this leaf.")
|
||||
.def("stdDeviation", &LeafT::stdDeviation,
|
||||
"Standard deviation of active values within this leaf.")
|
||||
// NOTE: variance() omitted — NanoVDB.h line 4388 uses unqualified
|
||||
// Pow2() which fails ADL for non-float ValueTs (ValueIndex /
|
||||
// ValueMask / etc.). Users can compute it as stdDeviation() ** 2.
|
||||
.def("flags", &LeafT::flags,
|
||||
"Raw leaf flag bits.")
|
||||
.def("valueMask", &LeafT::valueMask, nb::rv_policy::reference_internal,
|
||||
"Reference to the leaf's 512-bit active-value mask.")
|
||||
.def("probeValue",
|
||||
[](const LeafT& leaf, const CoordT& ijk) {
|
||||
ValueT v;
|
||||
bool on = leaf.probeValue(ijk, v);
|
||||
return std::make_tuple(v, on);
|
||||
},
|
||||
nb::arg("ijk"),
|
||||
"Return (value, isActive) for the voxel at index-space ijk.");
|
||||
|
||||
// Zero-copy 512-element NumPy view of mValues. Only enabled for
|
||||
// BuildTs whose ValueType is a primitive arithmetic type (float,
|
||||
// double, int*). For Fp* / Index / Mask / bool / Point the leaf uses
|
||||
// packed / mask / void layouts. For Vec3f / Vec3d / Vec4f / Vec4d /
|
||||
// Vec3u8 / Vec3u16 / Rgba8 the leaf carries a struct array that
|
||||
// nanobind's ndarray can't represent directly — those would need a
|
||||
// flattened (count, dim) component-typed view which a follow-up can
|
||||
// add. Users can still walk every leaf via the bound getValue().
|
||||
if constexpr (std::is_arithmetic_v<ValueT>
|
||||
&& !nanovdb::BuildTraits<BuildT>::is_special) {
|
||||
cls.def("values",
|
||||
[](nb::handle py_self) {
|
||||
auto& leaf = nb::cast<LeafT&>(py_self);
|
||||
size_t shape[1] = {LeafT::voxelCount()};
|
||||
return nb::cast(
|
||||
nb::ndarray<nb::numpy, ValueT, nb::ndim<1>, nb::c_contig, nb::device::cpu>(
|
||||
static_cast<void*>(leaf.data()->mValues),
|
||||
size_t(1), shape, py_self),
|
||||
nb::rv_policy::reference);
|
||||
},
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a zero-copy NumPy view of the 512 leaf values. The view "
|
||||
"keeps the leaf (and transitively the GridHandle that owns the "
|
||||
"underlying buffer) alive.");
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- NanoUpper / NanoLower<BuildT> --------------------
|
||||
//
|
||||
// Both internal node levels share the same C++ API surface (just different
|
||||
// LOG2DIM). One helper templated on the concrete InternalNode type.
|
||||
template<typename InternalT>
|
||||
void defineInternalNodeBase(nb::class_<InternalT>& cls)
|
||||
{
|
||||
using ValueT = typename InternalT::ValueType;
|
||||
using CoordT = typename InternalT::CoordType;
|
||||
cls.def("origin", &InternalT::origin,
|
||||
"Index-space origin (minimum corner) of this internal node.")
|
||||
.def("bbox", &InternalT::bbox,
|
||||
"Index-space bounding box of this internal node's active voxels.")
|
||||
.def_static("dim", &InternalT::dim,
|
||||
"Side length in voxels covered by this internal node.")
|
||||
.def_static("memUsage", []() { return InternalT::memUsage(); },
|
||||
"Byte size of an internal node.")
|
||||
.def("minimum", &InternalT::minimum,
|
||||
"Minimum active value within this node's subtree.")
|
||||
.def("maximum", &InternalT::maximum,
|
||||
"Maximum active value within this node's subtree.")
|
||||
.def("average", &InternalT::average,
|
||||
"Average of active values within this node's subtree.")
|
||||
.def("stdDeviation", &InternalT::stdDeviation,
|
||||
"Standard deviation of active values within this node's subtree.")
|
||||
// variance() omitted for parity with the leaf binding; compute as
|
||||
// stdDeviation() ** 2 in Python.
|
||||
.def("valueMask", &InternalT::valueMask, nb::rv_policy::reference_internal,
|
||||
"Reference to the node's active-tile mask.")
|
||||
.def("childMask", &InternalT::childMask, nb::rv_policy::reference_internal,
|
||||
"Reference to the node's child-pointer mask (1 where a child node exists).")
|
||||
.def("getValue",
|
||||
nb::overload_cast<const CoordT&>(&InternalT::getValue, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"Return the value at index-space ijk by descending into the subtree.")
|
||||
.def("getFirstValue", &InternalT::getFirstValue,
|
||||
"Value at the first (lowest-indexed) tile in this node.")
|
||||
.def("getLastValue", &InternalT::getLastValue,
|
||||
"Value at the last (highest-indexed) tile in this node.")
|
||||
.def("isActive",
|
||||
nb::overload_cast<const CoordT&>(&InternalT::isActive, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"True iff the voxel at index-space ijk is active.")
|
||||
.def("probeValue",
|
||||
[](const InternalT& node, const CoordT& ijk) {
|
||||
ValueT v;
|
||||
bool on = node.probeValue(ijk, v);
|
||||
return std::make_tuple(v, on);
|
||||
},
|
||||
nb::arg("ijk"),
|
||||
"Return (value, isActive) for the voxel at index-space ijk.");
|
||||
}
|
||||
|
||||
template<typename BuildT> void defineNanoUpper(nb::module_& m, const char* name)
|
||||
{
|
||||
using UpperT = nanovdb::NanoUpper<BuildT>;
|
||||
nb::class_<UpperT> cls(m, name,
|
||||
"Upper internal node — 32x32x32 (covers a 4096^3 region in index space).");
|
||||
defineInternalNodeBase(cls);
|
||||
}
|
||||
|
||||
template<typename BuildT> void defineNanoLower(nb::module_& m, const char* name)
|
||||
{
|
||||
using LowerT = nanovdb::NanoLower<BuildT>;
|
||||
nb::class_<LowerT> cls(m, name,
|
||||
"Lower internal node — 16x16x16 (covers a 128^3 region in index space).");
|
||||
defineInternalNodeBase(cls);
|
||||
}
|
||||
|
||||
// -------------------- NanoRoot<BuildT> --------------------
|
||||
template<typename BuildT> void defineNanoRoot(nb::module_& m, const char* name)
|
||||
{
|
||||
using RootT = nanovdb::NanoRoot<BuildT>;
|
||||
using ValueT = typename RootT::ValueType;
|
||||
using CoordT = typename RootT::CoordType;
|
||||
|
||||
nb::class_<RootT>(m, name, "Root node — top of the tree, holds the tile table.")
|
||||
.def("background", &RootT::background, nb::rv_policy::reference_internal,
|
||||
"Background value returned for inactive voxels.")
|
||||
.def("tileCount", &RootT::tileCount,
|
||||
"Number of active tiles in the root tile table.")
|
||||
.def("getTableSize", &RootT::getTableSize,
|
||||
"Total number of entries in the root tile table.")
|
||||
.def("isEmpty", &RootT::isEmpty,
|
||||
"True iff the root has no active tiles.")
|
||||
.def("bbox", &RootT::bbox, nb::rv_policy::reference_internal,
|
||||
"Index-space bounding box of every active value in this tree.")
|
||||
.def("minimum", &RootT::minimum, nb::rv_policy::reference_internal,
|
||||
"Minimum active value in the tree.")
|
||||
.def("maximum", &RootT::maximum, nb::rv_policy::reference_internal,
|
||||
"Maximum active value in the tree.")
|
||||
.def("average", &RootT::average, nb::rv_policy::reference_internal,
|
||||
"Average of active values in the tree.")
|
||||
.def("stdDeviation", &RootT::stdDeviation, nb::rv_policy::reference_internal,
|
||||
"Standard deviation of active values in the tree.")
|
||||
.def("memUsage",
|
||||
nb::overload_cast<>(&RootT::memUsage, nb::const_),
|
||||
"Byte size of the root node and its tile table.")
|
||||
.def("getValue",
|
||||
nb::overload_cast<const CoordT&>(&RootT::getValue, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"Return the value at index-space ijk by walking from the root.")
|
||||
.def("isActive",
|
||||
nb::overload_cast<const CoordT&>(&RootT::isActive, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"True iff the voxel at index-space ijk is active.")
|
||||
.def("probeValue",
|
||||
[](const RootT& root, const CoordT& ijk) {
|
||||
ValueT v;
|
||||
bool on = root.probeValue(ijk, v);
|
||||
return std::make_tuple(v, on);
|
||||
},
|
||||
nb::arg("ijk"),
|
||||
"Return (value, isActive) for the voxel at index-space ijk.");
|
||||
}
|
||||
|
||||
// -------------------- NanoTree<BuildT> --------------------
|
||||
template<typename BuildT> void defineNanoTree(nb::module_& m, const char* name)
|
||||
{
|
||||
using TreeT = nanovdb::NanoTree<BuildT>;
|
||||
using ValueT = typename TreeT::ValueType;
|
||||
using CoordT = typename TreeT::CoordType;
|
||||
using RootT = typename TreeT::RootType;
|
||||
using UpperT = typename TreeT::UpperNodeType;
|
||||
using LowerT = typename TreeT::LowerNodeType;
|
||||
using LeafT = typename TreeT::LeafNodeType;
|
||||
|
||||
nb::class_<TreeT>(m, name,
|
||||
"Tree — owns the root and provides bulk metadata queries "
|
||||
"(node counts, active voxel count, extrema).")
|
||||
.def("root",
|
||||
nb::overload_cast<>(&TreeT::root, nb::const_),
|
||||
nb::rv_policy::reference_internal,
|
||||
"Root node of this tree. Lifetime is anchored to the tree.")
|
||||
.def("background", &TreeT::background, nb::rv_policy::reference_internal,
|
||||
"Background value returned for inactive voxels.")
|
||||
.def("activeVoxelCount", &TreeT::activeVoxelCount,
|
||||
"Total number of active voxels in this tree.")
|
||||
// activeTileCount(level): valid range is 1..3 (lower / upper / root
|
||||
// tile counts). C++ uses NANOVDB_ASSERT(level > 0 && level <= 3)
|
||||
// which is a no-op in release builds — so guard explicitly.
|
||||
.def("activeTileCount",
|
||||
[](const TreeT& tree, uint32_t level) -> uint32_t {
|
||||
if (level < 1 || level > 3) {
|
||||
throw nb::value_error(
|
||||
"Tree.activeTileCount(level): level must be 1, 2, or 3");
|
||||
}
|
||||
return tree.activeTileCount(level);
|
||||
},
|
||||
nb::arg("level"),
|
||||
"Number of active tiles at the given tree level (1=lower, 2=upper, 3=root).")
|
||||
// nodeCount(level): valid range is 0..2 (leaf / lower / upper).
|
||||
// C++ uses NANOVDB_ASSERT(level < 3), again no-op in release.
|
||||
// The lambda's `int level` argument disambiguates the call against
|
||||
// Tree's templated nodeCount<NodeT>() overload at the C++ level,
|
||||
// so we don't need an overload_cast / static_cast wrapper here.
|
||||
.def("nodeCount",
|
||||
[](const TreeT& tree, int level) -> uint32_t {
|
||||
if (level < 0 || level >= 3) {
|
||||
throw nb::value_error(
|
||||
"Tree.nodeCount(level): level must be 0, 1, or 2");
|
||||
}
|
||||
return tree.nodeCount(level);
|
||||
},
|
||||
nb::arg("level"),
|
||||
"Number of nodes at the given tree level (0=leaf, 1=lower, 2=upper).")
|
||||
.def("totalNodeCount", &TreeT::totalNodeCount,
|
||||
"Sum of node counts across every tree level.")
|
||||
.def_static("memUsage", &TreeT::memUsage,
|
||||
"Byte size of a single tree structure (header only).")
|
||||
.def("getValue",
|
||||
nb::overload_cast<const CoordT&>(&TreeT::getValue, nb::const_),
|
||||
nb::arg("ijk"),
|
||||
"Return the value at index-space ijk by descending from the root.")
|
||||
.def("isActive", &TreeT::isActive, nb::arg("ijk"),
|
||||
"True iff the voxel at index-space ijk is active.")
|
||||
.def("probeValue",
|
||||
[](const TreeT& tree, const CoordT& ijk) {
|
||||
ValueT v;
|
||||
bool on = tree.probeValue(ijk, v);
|
||||
return std::make_tuple(v, on);
|
||||
},
|
||||
nb::arg("ijk"),
|
||||
"Return (value, isActive) for the voxel at index-space ijk.")
|
||||
.def("extrema",
|
||||
[](const TreeT& tree) {
|
||||
ValueT mn, mx;
|
||||
tree.extrema(mn, mx);
|
||||
return std::make_tuple(mn, mx);
|
||||
},
|
||||
"Return (min, max) of the active values over the whole tree.")
|
||||
.def("getFirstLeaf",
|
||||
nb::overload_cast<>(&TreeT::getFirstLeaf, nb::const_),
|
||||
nb::rv_policy::reference_internal,
|
||||
"First leaf node in breadth-first order, or None if the tree is empty.")
|
||||
.def("getFirstLower",
|
||||
nb::overload_cast<>(&TreeT::getFirstLower, nb::const_),
|
||||
nb::rv_policy::reference_internal,
|
||||
"First lower internal node in breadth-first order, or None if none exists.")
|
||||
.def("getFirstUpper",
|
||||
nb::overload_cast<>(&TreeT::getFirstUpper, nb::const_),
|
||||
nb::rv_policy::reference_internal,
|
||||
"First upper internal node in breadth-first order, or None if none exists.");
|
||||
}
|
||||
|
||||
// -------------------- NodeManager<BuildT> --------------------
|
||||
//
|
||||
// NodeManager is heap-managed by a NodeManagerHandle (move-only, owns the
|
||||
// underlying memory). We bind one NodeManager class per BuildT and one
|
||||
// host-side NodeManagerHandle class. Users get a handle from
|
||||
// nanovdb.createNodeManager(grid); they then call handle.mgr() to obtain a
|
||||
// borrowed pointer to the typed NodeManager — its lifetime is anchored to
|
||||
// the handle via reference_internal.
|
||||
template<typename BuildT> void defineNodeManager(nb::module_& m, const char* name)
|
||||
{
|
||||
using NMT = nanovdb::NodeManager<BuildT>;
|
||||
nb::class_<NMT>(m, name,
|
||||
"Sequential breadth-first accessor for the leaf / lower / upper "
|
||||
"internal nodes of a NanoGrid. Construct via "
|
||||
"nanovdb.createNodeManager(grid).")
|
||||
.def("isLinear",
|
||||
nb::overload_cast<>(&NMT::isLinear, nb::const_),
|
||||
"True iff this NodeManager is laid out as a linear offset table over a breadth-first grid.")
|
||||
.def("memUsage",
|
||||
nb::overload_cast<>(&NMT::memUsage, nb::const_),
|
||||
"Byte size of this NodeManager.")
|
||||
.def("nodeCount",
|
||||
[](const NMT& nm, int level) -> uint64_t {
|
||||
// Mirror Tree.nodeCount bounds (NodeManager forwards to Tree).
|
||||
if (level < 0 || level >= 3) {
|
||||
throw nb::value_error(
|
||||
"NodeManager.nodeCount(level): level must be 0, 1, or 2");
|
||||
}
|
||||
return nm.nodeCount(level);
|
||||
},
|
||||
nb::arg("level"),
|
||||
"Number of nodes at the given tree level (0=leaf, 1=lower, 2=upper).")
|
||||
.def("leafCount", &NMT::leafCount,
|
||||
"Number of leaf nodes managed by this NodeManager.")
|
||||
.def("lowerCount", &NMT::lowerCount,
|
||||
"Number of lower internal nodes managed by this NodeManager.")
|
||||
.def("upperCount", &NMT::upperCount,
|
||||
"Number of upper internal nodes managed by this NodeManager.")
|
||||
// leaf / lower / upper: NANOVDB_ASSERT(i < nodeCount(LEVEL)) in C++ is
|
||||
// no-op in release, so guard explicitly to convert OOB access into a
|
||||
// Python IndexError instead of memory corruption.
|
||||
.def("leaf",
|
||||
[](const NMT& nm, uint32_t i) -> const nanovdb::NanoLeaf<BuildT>& {
|
||||
if (i >= nm.leafCount()) {
|
||||
throw nb::index_error(
|
||||
"NodeManager.leaf(i): i out of range [0, leafCount)");
|
||||
}
|
||||
return nm.leaf(i);
|
||||
},
|
||||
nb::rv_policy::reference_internal, nb::arg("i"),
|
||||
"Return the i-th leaf node in breadth-first order.")
|
||||
.def("lower",
|
||||
[](const NMT& nm, uint32_t i) -> const nanovdb::NanoLower<BuildT>& {
|
||||
if (i >= nm.lowerCount()) {
|
||||
throw nb::index_error(
|
||||
"NodeManager.lower(i): i out of range [0, lowerCount)");
|
||||
}
|
||||
return nm.lower(i);
|
||||
},
|
||||
nb::rv_policy::reference_internal, nb::arg("i"),
|
||||
"Return the i-th lower internal node in breadth-first order.")
|
||||
.def("upper",
|
||||
[](const NMT& nm, uint32_t i) -> const nanovdb::NanoUpper<BuildT>& {
|
||||
if (i >= nm.upperCount()) {
|
||||
throw nb::index_error(
|
||||
"NodeManager.upper(i): i out of range [0, upperCount)");
|
||||
}
|
||||
return nm.upper(i);
|
||||
},
|
||||
nb::rv_policy::reference_internal, nb::arg("i"),
|
||||
"Return the i-th upper internal node in breadth-first order.");
|
||||
}
|
||||
|
||||
void defineNodeManagerHandle(nb::module_& m);
|
||||
void defineCreateNodeManager(nb::module_& m);
|
||||
|
||||
// -------------------- grid.leaf_values() bulk extractor --------------------
|
||||
//
|
||||
// For non-special BuildTs with breadth-first, fixed-size leaves, the leaf
|
||||
// values can be reached as a contiguous (N_leaves, 512) array — every leaf
|
||||
// occupies sizeof(NanoLeaf<T>) bytes and mValues starts at a known offset
|
||||
// inside each leaf. We bind this on NanoGrid<T> as leaf_values() for
|
||||
// efficient bulk analytics from Python.
|
||||
template<typename BuildT, typename = void>
|
||||
struct PyLeafValuesBinder
|
||||
{
|
||||
template<typename ClsT> static void apply(ClsT&) {}
|
||||
};
|
||||
|
||||
template<typename BuildT>
|
||||
struct PyLeafValuesBinder<BuildT,
|
||||
typename std::enable_if<
|
||||
std::is_arithmetic_v<typename nanovdb::NanoLeaf<BuildT>::ValueType>
|
||||
&& !nanovdb::BuildTraits<BuildT>::is_special>::type>
|
||||
{
|
||||
template<typename ClsT>
|
||||
static void apply(ClsT& cls)
|
||||
{
|
||||
using GridT = nanovdb::NanoGrid<BuildT>;
|
||||
using LeafT = nanovdb::NanoLeaf<BuildT>;
|
||||
using ValueT = typename LeafT::ValueType;
|
||||
cls.def("leaf_values",
|
||||
[](nb::handle py_self) -> nb::object {
|
||||
auto& grid = nb::cast<GridT&>(py_self);
|
||||
const auto& tree = grid.tree();
|
||||
const uint32_t nLeaves = tree.template nodeCount<LeafT>();
|
||||
if (!grid.isBreadthFirst()) {
|
||||
throw nb::value_error(
|
||||
"leaf_values() requires a breadth-first grid "
|
||||
"layout; rebuild via "
|
||||
"nanovdb.tools.createNanoGrid(...).");
|
||||
}
|
||||
// For an empty grid (no leaves) we still return an ndarray
|
||||
// — shape (0, 512) — so callers can iterate / np.asarray()
|
||||
// / shape-test without branching on a None sentinel.
|
||||
LeafT* first = const_cast<LeafT*>(tree.getFirstLeaf());
|
||||
size_t shape[2] = {nLeaves, LeafT::voxelCount()};
|
||||
int64_t strides[2] = {
|
||||
static_cast<int64_t>(sizeof(LeafT) / sizeof(ValueT)),
|
||||
1
|
||||
};
|
||||
// first is non-null whenever nLeaves > 0; when nLeaves == 0
|
||||
// we pass a dummy non-null aligned pointer (the grid itself)
|
||||
// so nanobind has something to base the empty array on.
|
||||
// Nothing will be read since the leading shape is 0.
|
||||
void* data = (first != nullptr)
|
||||
? static_cast<void*>(first->data()->mValues)
|
||||
: static_cast<void*>(&grid);
|
||||
return nb::cast(
|
||||
nb::ndarray<nb::numpy, ValueT, nb::ndim<2>, nb::device::cpu>(
|
||||
data, size_t(2), shape, py_self, strides),
|
||||
nb::rv_policy::reference);
|
||||
},
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a zero-copy (N_leaves, 512) NumPy view of every leaf's "
|
||||
"values, in breadth-first leaf order. Available only for "
|
||||
"BuildTs whose leaf layout carries T mValues[512] (i.e. not "
|
||||
"Fp*, Index, Mask, bool, or Point) and only on breadth-first "
|
||||
"grids. Returns an empty (0, 512) array for grids with no "
|
||||
"leaves. The view keeps the grid alive.");
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,547 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "PyVoxelBlockManager.h"
|
||||
|
||||
#include <nanobind/ndarray.h>
|
||||
#include <nanobind/stl/tuple.h>
|
||||
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
#include <nanovdb/HostBuffer.h>
|
||||
#include <nanovdb/tools/CreateNanoGrid.h>
|
||||
#include <nanovdb/tools/VoxelBlockManager.h>
|
||||
#include <nanovdb/util/Util.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
using namespace nanovdb;
|
||||
using nanovdb::tools::VoxelBlockManager;
|
||||
using nanovdb::tools::VoxelBlockManagerBase;
|
||||
using nanovdb::tools::VoxelBlockManagerHandle;
|
||||
using nanovdb::tools::buildVoxelBlockManager;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// ----------------------- Log2BlockWidth dispatch --------------------------
|
||||
//
|
||||
// Log2BlockWidth is a compile-time template parameter on every VBM helper.
|
||||
// We expose it to Python as a runtime int and dispatch via a switch that
|
||||
// instantiates the four useful widths (BlockWidth = 64, 128, 256, 512).
|
||||
// Larger widths are not bound by default; callers who need them can add a
|
||||
// new case below.
|
||||
|
||||
template<typename F>
|
||||
static auto dispatchLog2BlockWidth(int log2BlockWidth, F&& fn)
|
||||
{
|
||||
switch (log2BlockWidth) {
|
||||
case 6: return fn(std::integral_constant<int, 6>{});
|
||||
case 7: return fn(std::integral_constant<int, 7>{});
|
||||
case 8: return fn(std::integral_constant<int, 8>{});
|
||||
case 9: return fn(std::integral_constant<int, 9>{});
|
||||
default:
|
||||
throw nb::value_error(
|
||||
"VoxelBlockManager: log2_block_width must be 6, 7, 8, or 9 "
|
||||
"(BlockWidth = 64, 128, 256, or 512). Larger widths are not "
|
||||
"bound in Python by default.");
|
||||
}
|
||||
}
|
||||
|
||||
// PyVBMHandle wraps the C++ VoxelBlockManagerHandle and carries the
|
||||
// log2_block_width the handle was built with. The C++ handle does NOT store
|
||||
// log2_block_width itself, so without this wrapper the Python binding would
|
||||
// have to ask the caller every time — which the user can lie about and
|
||||
// trigger out-of-bounds reads of the metadata buffers. Storing it once at
|
||||
// build time and consulting it in every accessor closes that hole.
|
||||
struct PyVBMHandle
|
||||
{
|
||||
VoxelBlockManagerHandle<HostBuffer> handle;
|
||||
int log2BlockWidth = 6;
|
||||
|
||||
PyVBMHandle() = default;
|
||||
PyVBMHandle(VoxelBlockManagerHandle<HostBuffer>&& h, int lbw) noexcept
|
||||
: handle(std::move(h)), log2BlockWidth(lbw) {}
|
||||
|
||||
PyVBMHandle(const PyVBMHandle&) = delete;
|
||||
PyVBMHandle& operator=(const PyVBMHandle&) = delete;
|
||||
PyVBMHandle(PyVBMHandle&&) = default;
|
||||
PyVBMHandle& operator=(PyVBMHandle&&) = default;
|
||||
|
||||
uint64_t blockCount() const { return handle.blockCount(); }
|
||||
uint64_t firstOffset() const { return handle.firstOffset(); }
|
||||
uint64_t lastOffset() const { return handle.lastOffset(); }
|
||||
void reset() { handle.reset(); }
|
||||
int blockWidth() const { return 1 << log2BlockWidth; }
|
||||
int jumpMapLength() const { return 1 << (log2BlockWidth - 6); }
|
||||
};
|
||||
|
||||
// ----------------------- decodeInverseMaps helper -------------------------
|
||||
//
|
||||
// Common implementation used by both the free function and the
|
||||
// handle.decodeBlock(i) method. Allocates fresh leafIndex (uint32) and
|
||||
// voxelOffset (uint16) NumPy arrays of length BlockWidth and fills them.
|
||||
template<int Log2BlockWidth>
|
||||
static nb::object pyDecodeInverseMapsImpl(const NanoGrid<ValueOnIndex>& grid,
|
||||
uint32_t firstLeafID,
|
||||
const uint64_t* jumpMap,
|
||||
uint64_t blockFirstOffset)
|
||||
{
|
||||
constexpr int BlockWidth = 1 << Log2BlockWidth;
|
||||
constexpr int JumpMapLength =
|
||||
VoxelBlockManagerBase<Log2BlockWidth>::JumpMapLength;
|
||||
|
||||
// The C++ decodeInverseMaps iterates leafID = firstLeafID ..
|
||||
// firstLeafID + nExtraLeaves, where nExtraLeaves is the popcount of
|
||||
// this block's jumpMap (each set bit marks an additional leaf
|
||||
// boundary crossed within the block). If the jumpMap is corrupt or
|
||||
// was built against a different grid, the loop could read past
|
||||
// tree.getFirstNode<0>(). Pre-compute the upper bound and validate
|
||||
// it against grid.tree().nodeCount(0) before any allocation.
|
||||
uint32_t nExtraLeaves = 0;
|
||||
for (int i = 0; i < JumpMapLength; ++i)
|
||||
nExtraLeaves += util::countOn(jumpMap[i]);
|
||||
const uint32_t nLeaves = grid.tree().nodeCount(0);
|
||||
if (uint64_t(firstLeafID) + uint64_t(nExtraLeaves) >= nLeaves) {
|
||||
throw nb::value_error(
|
||||
"decodeInverseMaps: firstLeafID + popcount(jumpMap) would "
|
||||
"index past grid.tree().nodeCount(0) — the jumpMap is "
|
||||
"either corrupt or was paired with a different grid.");
|
||||
}
|
||||
|
||||
// Each call allocates fresh BlockWidth-sized output arrays for the
|
||||
// leaf-index and voxel-offset results. We use plain new[] (rather than
|
||||
// a numpy-allocated buffer) because the produced ndarrays are returned
|
||||
// by reference and Python owns them via the capsule deleters below —
|
||||
// when the ndarray is destroyed, the capsule's deleter runs delete[].
|
||||
//
|
||||
// The raw pointers live in std::unique_ptr until the matching capsule
|
||||
// has been constructed; that way if the second allocation, the
|
||||
// decodeInverseMaps call, or either capsule construction throws, the
|
||||
// unique_ptr unwinds the half-built state cleanly. After a capsule
|
||||
// takes ownership we release() so the unique_ptr no longer double-frees.
|
||||
std::unique_ptr<uint32_t[]> leafIndex(new uint32_t[BlockWidth]);
|
||||
std::unique_ptr<uint16_t[]> voxelOffset(new uint16_t[BlockWidth]);
|
||||
|
||||
using VBM = VoxelBlockManager<Log2BlockWidth>;
|
||||
{
|
||||
// Release the GIL around the pure-C++ decode kernel — the heavy part,
|
||||
// and the only part of this helper that touches no Python objects. The
|
||||
// GIL is re-acquired on scope exit (including during exception unwind)
|
||||
// before the capsules / ndarrays below are constructed.
|
||||
nb::gil_scoped_release release;
|
||||
VBM::template decodeInverseMaps<ValueOnIndex>(
|
||||
&grid, firstLeafID, jumpMap, blockFirstOffset,
|
||||
leafIndex.get(), voxelOffset.get());
|
||||
}
|
||||
|
||||
// nb::capsule wraps the raw pointer + matching delete[] so it can serve
|
||||
// as the ndarray's owner — the capsule lives as long as the ndarray and
|
||||
// its destruction runs the deleter.
|
||||
nb::capsule leafOwner(leafIndex.get(),
|
||||
[](void* p) noexcept { delete[] static_cast<uint32_t*>(p); });
|
||||
auto* leafRaw = leafIndex.release();
|
||||
nb::capsule offsetOwner(voxelOffset.get(),
|
||||
[](void* p) noexcept { delete[] static_cast<uint16_t*>(p); });
|
||||
auto* offsetRaw = voxelOffset.release();
|
||||
|
||||
size_t shape[1] = {static_cast<size_t>(BlockWidth)};
|
||||
nb::ndarray<nb::numpy, uint32_t, nb::ndim<1>, nb::c_contig, nb::device::cpu>
|
||||
leafArr(leafRaw, size_t(1), shape, leafOwner);
|
||||
nb::ndarray<nb::numpy, uint16_t, nb::ndim<1>, nb::c_contig, nb::device::cpu>
|
||||
offsetArr(offsetRaw, size_t(1), shape, offsetOwner);
|
||||
return nb::make_tuple(
|
||||
nb::cast(leafArr, nb::rv_policy::reference),
|
||||
nb::cast(offsetArr, nb::rv_policy::reference));
|
||||
}
|
||||
|
||||
// ------------------- VoxelBlockManagerHandle binding ----------------------
|
||||
|
||||
static const NanoGrid<ValueOnIndex>* castOnIndexGrid(nb::handle py_grid,
|
||||
const char* fn_name)
|
||||
{
|
||||
if (!nb::isinstance<NanoGrid<ValueOnIndex>>(py_grid)) {
|
||||
std::string msg(fn_name);
|
||||
msg += ": grid must be a NanoVDB grid of build type ValueOnIndex (OnIndexGrid)";
|
||||
throw nb::type_error(msg.c_str());
|
||||
}
|
||||
return &nb::cast<const NanoGrid<ValueOnIndex>&>(py_grid);
|
||||
}
|
||||
|
||||
static void defineHandle(nb::module_& toolsModule)
|
||||
{
|
||||
nb::class_<PyVBMHandle>(toolsModule, "VoxelBlockManagerHandle",
|
||||
"Owns the firstLeafID / jumpMap metadata buffers backing a "
|
||||
"VoxelBlockManager. Constructed by nanovdb.tools.buildVoxelBlockManager.")
|
||||
.def(nb::init<>(),
|
||||
"Construct an empty VoxelBlockManagerHandle with no buffers.")
|
||||
.def("blockCount", &PyVBMHandle::blockCount,
|
||||
"Number of voxel blocks managed by this handle.")
|
||||
.def("firstOffset", &PyVBMHandle::firstOffset,
|
||||
"Sequential voxel index of the first active voxel covered "
|
||||
"by this handle (1 by default when the handle covers the "
|
||||
"full grid).")
|
||||
.def("lastOffset", &PyVBMHandle::lastOffset,
|
||||
"Sequential voxel index of the last active voxel covered "
|
||||
"by this handle.")
|
||||
.def("reset", &PyVBMHandle::reset,
|
||||
"Release this handle's buffers and reset it to the empty state.")
|
||||
.def_prop_ro("log2_block_width", [](const PyVBMHandle& h) { return h.log2BlockWidth; },
|
||||
"The log2_block_width this handle was built with. The jumpMap "
|
||||
"and decodeBlock outputs derive their shapes from this value.")
|
||||
.def_prop_ro("block_width", &PyVBMHandle::blockWidth,
|
||||
"BlockWidth = 1 << log2_block_width (64, 128, 256, or 512).")
|
||||
.def_prop_ro("jump_map_length", &PyVBMHandle::jumpMapLength,
|
||||
"JumpMapLength = BlockWidth / 64 (1, 2, 4, or 8).")
|
||||
.def(
|
||||
"__bool__",
|
||||
[](const PyVBMHandle& h) { return h.blockCount() > 0; },
|
||||
nb::is_operator())
|
||||
// Zero-copy view of the (blockCount,) firstLeafID array.
|
||||
.def("firstLeafID",
|
||||
[](nb::handle py_self) -> nb::object {
|
||||
auto& h = nb::cast<PyVBMHandle&>(py_self);
|
||||
size_t shape[1] = {static_cast<size_t>(h.blockCount())};
|
||||
// A default-constructed or reset() handle has a null
|
||||
// hostFirstLeafID(); we still return an empty (0,) ndarray
|
||||
// so callers don't have to branch on a None sentinel. The
|
||||
// dummy non-null pointer (the handle itself) keeps nanobind
|
||||
// happy; nothing is read since the leading shape is 0.
|
||||
uint32_t* raw = h.handle.hostFirstLeafID();
|
||||
void* data = (raw != nullptr) ? static_cast<void*>(raw)
|
||||
: static_cast<void*>(&h);
|
||||
return nb::cast(
|
||||
nb::ndarray<nb::numpy, uint32_t, nb::ndim<1>,
|
||||
nb::c_contig, nb::device::cpu>(
|
||||
data, size_t(1), shape, py_self),
|
||||
nb::rv_policy::reference);
|
||||
},
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a zero-copy (blockCount,) uint32 NumPy view of the "
|
||||
"firstLeafID array. Returns an empty (0,) array on a "
|
||||
"default-constructed or reset() handle. The view keeps this "
|
||||
"handle alive.")
|
||||
// jumpMap is uint64_t[blockCount * JumpMapLength]. JumpMapLength is
|
||||
// determined by the log2_block_width recorded on the handle, not by
|
||||
// the caller — that way the returned view always covers exactly the
|
||||
// allocated buffer, with no risk of OOB reads.
|
||||
.def("jumpMap",
|
||||
[](nb::handle py_self) -> nb::object {
|
||||
auto& h = nb::cast<PyVBMHandle&>(py_self);
|
||||
size_t shape[2] = {static_cast<size_t>(h.blockCount()),
|
||||
static_cast<size_t>(h.jumpMapLength())};
|
||||
// Same null-buffer guard as firstLeafID(): a
|
||||
// default-constructed / reset() handle has a null
|
||||
// hostJumpMap(); return an empty (0, jump_map_length)
|
||||
// ndarray rather than passing nullptr to nanobind.
|
||||
uint64_t* raw = h.handle.hostJumpMap();
|
||||
void* data = (raw != nullptr) ? static_cast<void*>(raw)
|
||||
: static_cast<void*>(&h);
|
||||
return nb::cast(
|
||||
nb::ndarray<nb::numpy, uint64_t, nb::ndim<2>,
|
||||
nb::c_contig, nb::device::cpu>(
|
||||
data, size_t(2), shape, py_self),
|
||||
nb::rv_policy::reference);
|
||||
},
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return a zero-copy (blockCount, jump_map_length) uint64 NumPy "
|
||||
"view of the jumpMap. The shape is determined by the "
|
||||
"log2_block_width the handle was built with. Returns an empty "
|
||||
"(0, jump_map_length) array on a default-constructed or reset() "
|
||||
"handle. The view keeps this handle alive.")
|
||||
// Decode the inverse maps for a single block of this VBM. The
|
||||
// log2_block_width is taken from the handle, so the caller cannot
|
||||
// request a width that doesn't match what was built.
|
||||
.def("decodeBlock",
|
||||
[](PyVBMHandle& self,
|
||||
nb::handle py_grid,
|
||||
uint64_t block_index) -> nb::object {
|
||||
const auto* grid = castOnIndexGrid(py_grid,
|
||||
"VoxelBlockManagerHandle.decodeBlock");
|
||||
if (block_index >= self.blockCount()) {
|
||||
throw nb::index_error(
|
||||
"VoxelBlockManagerHandle.decodeBlock(block_index): "
|
||||
"block_index out of range [0, blockCount).");
|
||||
}
|
||||
// Defensive: NanoVDB's buildVoxelBlockManager doesn't always
|
||||
// initialize firstLeafID for blocks where no leaf starts at
|
||||
// a block boundary AND no leaf's iteration sweep reaches
|
||||
// them (e.g. when the source grid is tile-compressed, so
|
||||
// some sequential offsets correspond to tile values rather
|
||||
// than leaf voxels). The slot is then uninitialized memory;
|
||||
// passing it into decodeInverseMaps would lead to an OOB
|
||||
// read of tree.getFirstNode<0>()[garbage]. Catch the case
|
||||
// and raise rather than segfault.
|
||||
const uint32_t firstLeafID =
|
||||
self.handle.hostFirstLeafID()[block_index];
|
||||
const uint32_t nLeaves = grid->tree().nodeCount(0);
|
||||
if (firstLeafID >= nLeaves) {
|
||||
throw nb::value_error(
|
||||
"VoxelBlockManagerHandle.decodeBlock: the VBM's "
|
||||
"firstLeafID for this block was not initialized by "
|
||||
"buildVoxelBlockManager (the underlying algorithm "
|
||||
"doesn't cover blocks that no leaf reaches via its "
|
||||
"iteration). This typically happens on OnIndex "
|
||||
"grids built from tile-compressed source grids; "
|
||||
"until the issue is fixed upstream the workaround "
|
||||
"is to build the source grid voxel-by-voxel with "
|
||||
"build::Grid so it stays uncompressed.");
|
||||
}
|
||||
return dispatchLog2BlockWidth(self.log2BlockWidth, [&](auto W) {
|
||||
constexpr int LBW = decltype(W)::value;
|
||||
constexpr int BlockWidth = 1 << LBW;
|
||||
constexpr int JumpMapLength =
|
||||
VoxelBlockManagerBase<LBW>::JumpMapLength;
|
||||
const uint64_t blockFirstOffset =
|
||||
self.firstOffset() + block_index * BlockWidth;
|
||||
return pyDecodeInverseMapsImpl<LBW>(
|
||||
*grid,
|
||||
firstLeafID,
|
||||
self.handle.hostJumpMap() + block_index * JumpMapLength,
|
||||
blockFirstOffset);
|
||||
});
|
||||
},
|
||||
"grid"_a, "block_index"_a,
|
||||
"Decode the inverse maps for the block_index-th block of this "
|
||||
"VBM. Returns (leaf_index, voxel_offset) uint32 / uint16 NumPy "
|
||||
"arrays of length BlockWidth = 1<<log2_block_width, using the "
|
||||
"log2_block_width the handle was built with.");
|
||||
}
|
||||
|
||||
// ------------------- buildVoxelBlockManager binding -----------------------
|
||||
|
||||
static void defineBuild(nb::module_& toolsModule)
|
||||
{
|
||||
toolsModule.def("buildVoxelBlockManager",
|
||||
[](nb::handle py_grid,
|
||||
int log2_block_width,
|
||||
uint64_t first_offset,
|
||||
uint64_t last_offset,
|
||||
uint64_t n_blocks) -> PyVBMHandle {
|
||||
const auto* grid = castOnIndexGrid(py_grid, "buildVoxelBlockManager");
|
||||
// The C++ implementation only NANOVDB_ASSERTs these preconditions,
|
||||
// which makes them no-ops in release builds. Validate them here
|
||||
// so Python callers get a clear error instead of UB / abort.
|
||||
if (!grid->isSequential()) {
|
||||
throw nb::value_error(
|
||||
"buildVoxelBlockManager: grid must satisfy "
|
||||
"grid.isSequential() (fixed-size, breadth-first node "
|
||||
"layout). NanoVDB grids constructed via "
|
||||
"tools.createOnIndexGrid satisfy this by default.");
|
||||
}
|
||||
return dispatchLog2BlockWidth(log2_block_width, [&](auto W) {
|
||||
constexpr int LBW = decltype(W)::value;
|
||||
using Base = VoxelBlockManagerBase<LBW>;
|
||||
constexpr uint64_t BlockWidth = Base::BlockWidth;
|
||||
constexpr uint64_t JumpMapLength = Base::JumpMapLength;
|
||||
// first_offset must be 1 (mod BlockWidth). The single-arg
|
||||
// C++ helper would normalize a zero input to 1; we do the
|
||||
// same here so the in-place builder below sees a valid
|
||||
// value. Validate the nonzero case ourselves.
|
||||
if (first_offset != 0 &&
|
||||
((first_offset - 1) & (BlockWidth - 1)) != 0) {
|
||||
throw nb::value_error(
|
||||
"buildVoxelBlockManager: first_offset must satisfy "
|
||||
"first_offset == 1 (mod BlockWidth). Pass 0 (the "
|
||||
"default) to let the implementation use 1.");
|
||||
}
|
||||
if (first_offset == 0) first_offset = 1;
|
||||
if (last_offset == 0) last_offset = grid->activeVoxelCount();
|
||||
if (last_offset < first_offset) return PyVBMHandle();
|
||||
// Capacity must hold at least ceil((last - first + 1) /
|
||||
// BlockWidth) blocks; otherwise the handle's lastOffset
|
||||
// would advertise more coverage than blockCount allows
|
||||
// and decodeBlock would silently truncate. The formula
|
||||
// below equals the ceil() above when BlockWidth is a
|
||||
// power of two.
|
||||
const uint64_t minBlocks =
|
||||
(last_offset - first_offset + BlockWidth) >> LBW;
|
||||
if (n_blocks != 0 && n_blocks < minBlocks) {
|
||||
std::string msg(
|
||||
"buildVoxelBlockManager: n_blocks must be at "
|
||||
"least ceil((last_offset - first_offset + 1) / "
|
||||
"BlockWidth) = ");
|
||||
msg += std::to_string(minBlocks);
|
||||
msg += ". Pass 0 (the default) to use the minimum "
|
||||
"required capacity.";
|
||||
throw nb::value_error(msg.c_str());
|
||||
}
|
||||
if (n_blocks == 0) n_blocks = minBlocks;
|
||||
// Allocate the metadata buffers ourselves so we can
|
||||
// pre-initialize firstLeafID with a sentinel value before
|
||||
// the in-place builder runs. The C++ allocating overload
|
||||
// calls HostBuffer::create() which returns uninitialized
|
||||
// memory; blocks that the algorithm doesn't touch would
|
||||
// then retain arbitrary values, and our decodeBlock guard
|
||||
// (firstLeafID >= nLeaves) might miss any garbage value
|
||||
// that happens to be < nLeaves. By prefilling with nLeaves
|
||||
// up front, every untouched slot deterministically trips
|
||||
// the guard.
|
||||
auto firstLeafIDBuf = HostBuffer::create(
|
||||
n_blocks * sizeof(uint32_t));
|
||||
auto jumpMapBuf = HostBuffer::create(
|
||||
n_blocks * JumpMapLength * sizeof(uint64_t));
|
||||
const uint32_t nLeaves = grid->tree().nodeCount(0);
|
||||
{
|
||||
uint32_t* slots = static_cast<uint32_t*>(
|
||||
firstLeafIDBuf.data());
|
||||
for (uint64_t i = 0; i < n_blocks; ++i) {
|
||||
slots[i] = nLeaves;
|
||||
}
|
||||
}
|
||||
VoxelBlockManagerHandle<HostBuffer> handle(
|
||||
std::move(firstLeafIDBuf), std::move(jumpMapBuf),
|
||||
n_blocks, first_offset, last_offset);
|
||||
// In-place builder zeros the jumpMap itself and only
|
||||
// touches firstLeafID slots it actually visits. Release the
|
||||
// GIL around it — it's pure C++ (touches no Python objects)
|
||||
// and may parallelize internally via util::forEach.
|
||||
{
|
||||
nb::gil_scoped_release release;
|
||||
buildVoxelBlockManager<LBW, HostBuffer>(grid, handle);
|
||||
}
|
||||
return PyVBMHandle(std::move(handle), LBW);
|
||||
});
|
||||
},
|
||||
"grid"_a,
|
||||
"log2_block_width"_a = 6,
|
||||
"first_offset"_a = 0,
|
||||
"last_offset"_a = 0,
|
||||
"n_blocks"_a = 0,
|
||||
"Build a host-side VoxelBlockManager from an OnIndexGrid. "
|
||||
"log2_block_width selects the per-block active-voxel count "
|
||||
"(6=64, 7=128, 8=256, 9=512). Pass 0 for first_offset / "
|
||||
"last_offset / n_blocks to use the full grid (first active "
|
||||
"voxel through grid.activeVoxelCount(), minimum block count). "
|
||||
"first_offset, if nonzero, must satisfy first_offset == 1 "
|
||||
"(mod BlockWidth).");
|
||||
}
|
||||
|
||||
// --------------------- decodeInverseMaps binding --------------------------
|
||||
|
||||
static void defineDecode(nb::module_& toolsModule)
|
||||
{
|
||||
toolsModule.def("decodeInverseMaps",
|
||||
[](nb::handle py_grid,
|
||||
uint32_t first_leaf_id,
|
||||
nb::ndarray<const uint64_t, nb::ndim<1>,
|
||||
nb::c_contig, nb::device::cpu> jump_map,
|
||||
uint64_t block_first_offset,
|
||||
int log2_block_width) -> nb::object {
|
||||
const auto* grid = castOnIndexGrid(py_grid, "decodeInverseMaps");
|
||||
// The C++ helper indexes tree.getFirstNode<0>()[first_leaf_id]
|
||||
// without a bounds check, so a stray first_leaf_id leads to an
|
||||
// OOB read. Validate up front. (We also require isSequential();
|
||||
// getFirstNode only makes sense on a sequential tree.)
|
||||
if (!grid->isSequential()) {
|
||||
throw nb::value_error(
|
||||
"decodeInverseMaps: grid must satisfy "
|
||||
"grid.isSequential().");
|
||||
}
|
||||
const uint32_t nLeaves = grid->tree().nodeCount(0);
|
||||
if (first_leaf_id >= nLeaves) {
|
||||
throw nb::index_error(
|
||||
"decodeInverseMaps: first_leaf_id out of range "
|
||||
"[0, grid.tree().nodeCount(0)).");
|
||||
}
|
||||
return dispatchLog2BlockWidth(log2_block_width, [&](auto W) {
|
||||
constexpr int LBW = decltype(W)::value;
|
||||
constexpr int JumpMapLength =
|
||||
VoxelBlockManagerBase<LBW>::JumpMapLength;
|
||||
if (jump_map.shape(0) != JumpMapLength) {
|
||||
std::string msg("decodeInverseMaps: jump_map must have "
|
||||
"length JumpMapLength = ");
|
||||
msg += std::to_string(JumpMapLength);
|
||||
msg += " for log2_block_width=";
|
||||
msg += std::to_string(LBW);
|
||||
throw nb::value_error(msg.c_str());
|
||||
}
|
||||
return pyDecodeInverseMapsImpl<LBW>(
|
||||
*grid, first_leaf_id, jump_map.data(),
|
||||
block_first_offset);
|
||||
});
|
||||
},
|
||||
"grid"_a,
|
||||
"first_leaf_id"_a,
|
||||
"jump_map"_a,
|
||||
"block_first_offset"_a,
|
||||
"log2_block_width"_a = 6,
|
||||
"Decode the inverse maps for a single voxel block of an OnIndexGrid. "
|
||||
"Returns a (leaf_index, voxel_offset) tuple of fresh NumPy arrays of "
|
||||
"length BlockWidth = 1<<log2_block_width. jump_map must have length "
|
||||
"BlockWidth/64. first_leaf_id must be in [0, grid.tree().nodeCount(0)).");
|
||||
}
|
||||
|
||||
// ----- createOnIndexGrid test-scaffold factory ----------------------------
|
||||
//
|
||||
// Narrow source-coverage factory used by the VoxelBlockManager unit tests.
|
||||
// New code should prefer tools.createNanoGridOnIndex (in PyCreateNanoGrid.cc)
|
||||
// which accepts a wider source set.
|
||||
|
||||
template<typename SrcBuildT>
|
||||
static nb::object tryCreateOnIndexGrid(nb::handle py_grid,
|
||||
uint32_t channels,
|
||||
bool include_stats,
|
||||
bool include_tiles,
|
||||
int verbose)
|
||||
{
|
||||
using SrcGridT = NanoGrid<SrcBuildT>;
|
||||
if (!nb::isinstance<SrcGridT>(py_grid)) {
|
||||
return nb::object();
|
||||
}
|
||||
const SrcGridT& src = nb::cast<const SrcGridT&>(py_grid);
|
||||
return nb::cast(
|
||||
tools::createNanoGrid<SrcGridT, ValueOnIndex, HostBuffer>(
|
||||
src, channels, include_stats, include_tiles, verbose));
|
||||
}
|
||||
|
||||
static void defineCreateOnIndexGrid(nb::module_& toolsModule)
|
||||
{
|
||||
toolsModule.def("createOnIndexGrid",
|
||||
[](nb::handle py_grid,
|
||||
uint32_t channels,
|
||||
bool include_stats,
|
||||
bool include_tiles,
|
||||
int verbose) -> nb::object {
|
||||
// Try every source BuildT we accept.
|
||||
if (auto r = tryCreateOnIndexGrid<float>(
|
||||
py_grid, channels, include_stats, include_tiles, verbose);
|
||||
r.is_valid()) return r;
|
||||
if (auto r = tryCreateOnIndexGrid<double>(
|
||||
py_grid, channels, include_stats, include_tiles, verbose);
|
||||
r.is_valid()) return r;
|
||||
if (auto r = tryCreateOnIndexGrid<int32_t>(
|
||||
py_grid, channels, include_stats, include_tiles, verbose);
|
||||
r.is_valid()) return r;
|
||||
if (auto r = tryCreateOnIndexGrid<Vec3f>(
|
||||
py_grid, channels, include_stats, include_tiles, verbose);
|
||||
r.is_valid()) return r;
|
||||
throw nb::type_error(
|
||||
"createOnIndexGrid: source grid must be a FloatGrid, "
|
||||
"DoubleGrid, Int32Grid, or Vec3fGrid (other source BuildTs "
|
||||
"are not yet bound).");
|
||||
},
|
||||
"src_grid"_a,
|
||||
"channels"_a = 0u,
|
||||
"include_stats"_a = true,
|
||||
"include_tiles"_a = true,
|
||||
"verbose"_a = 0,
|
||||
"Convert a source grid into a NanoGrid<ValueOnIndex> "
|
||||
"(OnIndexGrid). Accepts FloatGrid / DoubleGrid / Int32Grid / "
|
||||
"Vec3fGrid. This is a narrow helper kept alongside "
|
||||
"buildVoxelBlockManager; for general index conversion (broader "
|
||||
"source coverage, blind-data channels) prefer "
|
||||
"nanovdb.tools.createNanoGridOnIndex.");
|
||||
}
|
||||
|
||||
void defineVoxelBlockManagerModule(nb::module_& toolsModule)
|
||||
{
|
||||
defineHandle(toolsModule);
|
||||
defineBuild(toolsModule);
|
||||
defineDecode(toolsModule);
|
||||
defineCreateOnIndexGrid(toolsModule);
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#ifndef NANOVDB_PYVOXELBLOCKMANAGER_HAS_BEEN_INCLUDED
|
||||
#define NANOVDB_PYVOXELBLOCKMANAGER_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <nanobind/nanobind.h>
|
||||
|
||||
namespace nb = nanobind;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
/// @brief Bind VoxelBlockManagerHandle<HostBuffer>,
|
||||
/// tools.buildVoxelBlockManager, tools.decodeInverseMaps, and the
|
||||
/// test-scaffold tools.createOnIndexGrid factory under the given
|
||||
/// Python submodule (expected to be the existing nanovdb.tools).
|
||||
void defineVoxelBlockManagerModule(nb::module_& toolsModule);
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
#endif
|
||||
@@ -1,9 +1,26 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import os
|
||||
import sys
|
||||
if sys.platform == "win32":
|
||||
import os
|
||||
openvdb_dll_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, 'openvdb', 'lib')
|
||||
os.add_dll_directory(directory)
|
||||
|
||||
from .lib.nanovdb import *
|
||||
if sys.platform == "win32":
|
||||
_openvdb_dll_directory = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), os.pardir, "openvdb", "lib"
|
||||
)
|
||||
if os.path.isdir(_openvdb_dll_directory):
|
||||
os.add_dll_directory(_openvdb_dll_directory)
|
||||
|
||||
|
||||
def get_include():
|
||||
"""Return the absolute path to the bundled NanoVDB C/C++ include directory.
|
||||
|
||||
Use this from a downstream Python extension's build system so the extension
|
||||
compiles against the same NanoVDB headers the installed wheel was built with::
|
||||
|
||||
import nanovdb
|
||||
ext_kwargs = dict(include_dirs=[nanovdb.get_include()])
|
||||
"""
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "include")
|
||||
|
||||
|
||||
from .lib.nanovdb import * # noqa: E402,F401,F403
|
||||
|
||||
@@ -13,7 +13,10 @@ namespace pynanovdb {
|
||||
|
||||
void defineDeviceBuffer(nb::module_& m)
|
||||
{
|
||||
nb::class_<cuda::DeviceBuffer>(m, "DeviceBuffer");
|
||||
nb::class_<cuda::DeviceBuffer>(m, "DeviceBuffer",
|
||||
"CUDA device-side buffer used to back a DeviceGridHandle. Holds a "
|
||||
"host mirror and a device pointer; deviceUpload / deviceDownload on "
|
||||
"the handle move bytes between the two.");
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <nanobind/ndarray.h>
|
||||
|
||||
#include <nanovdb/cuda/DeviceBuffer.h>
|
||||
#include <nanovdb/NanoVDB.h>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace nb::literals;
|
||||
@@ -13,6 +14,48 @@ using namespace nanovdb;
|
||||
|
||||
namespace pynanovdb {
|
||||
|
||||
// Device-side polymorphic deviceGrid(n) — same dispatch shape as
|
||||
// pyHostGrid<BufferT> in PyGridHandle.h, but returns the device pointer.
|
||||
// gridType(n) is read from the host-side GridData header (the handle keeps
|
||||
// a host mirror), so this works whether or not the grid has been uploaded.
|
||||
// Returns None if the device-side grid is null (i.e. no deviceUpload yet)
|
||||
// or the BuildT is not Python-visible.
|
||||
static nb::object pyDeviceGrid(nb::handle py_handle, uint32_t n)
|
||||
{
|
||||
using BufferT = nanovdb::cuda::DeviceBuffer;
|
||||
auto& handle = nb::cast<GridHandle<BufferT>&>(py_handle);
|
||||
if (n >= handle.gridCount()) return nb::none();
|
||||
switch (handle.gridType(n)) {
|
||||
#define NANOVDB_PY_FOR_EACH_SCALAR_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template deviceGrid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_VECTOR_BUILDT(T, Suffix, AccessorName, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template deviceGrid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_POINT_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template deviceGrid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#define NANOVDB_PY_FOR_EACH_READONLY_BUILDT(T, Suffix, GridTypeEnum) \
|
||||
case nanovdb::GridType::GridTypeEnum: { \
|
||||
auto* grid = handle.template deviceGrid<T>(n); \
|
||||
return grid ? nb::cast(grid, nb::rv_policy::reference, py_handle) \
|
||||
: nb::none(); \
|
||||
}
|
||||
#include "../BuildTypes.def"
|
||||
default:
|
||||
return nb::none();
|
||||
}
|
||||
}
|
||||
|
||||
void defineDeviceGridHandle(nb::module_& m)
|
||||
{
|
||||
using BufferT = nanovdb::cuda::DeviceBuffer;
|
||||
@@ -27,19 +70,31 @@ void defineDeviceGridHandle(nb::module_& m)
|
||||
new (&handle) GridHandle<BufferT>(std::move(buffer));
|
||||
},
|
||||
"cpu_t"_a.noconvert(),
|
||||
"cuda_t"_a.noconvert())
|
||||
.def("deviceFloatGrid", nb::overload_cast<uint32_t>(&GridHandle<BufferT>::template deviceGrid<float>), "n"_a = 0, nb::rv_policy::reference_internal)
|
||||
.def("deviceDoubleGrid", nb::overload_cast<uint32_t>(&GridHandle<BufferT>::template deviceGrid<double>), "n"_a = 0, nb::rv_policy::reference_internal)
|
||||
.def("deviceInt32Grid", nb::overload_cast<uint32_t>(&GridHandle<BufferT>::template deviceGrid<int32_t>), "n"_a = 0, nb::rv_policy::reference_internal)
|
||||
.def("deviceVec3fGrid", nb::overload_cast<uint32_t>(&GridHandle<BufferT>::template deviceGrid<Vec3f>), "n"_a = 0, nb::rv_policy::reference_internal)
|
||||
.def("deviceRGBA8Grid",
|
||||
nb::overload_cast<uint32_t>(&GridHandle<BufferT>::template deviceGrid<math::Rgba8>),
|
||||
"n"_a = 0,
|
||||
nb::rv_policy::reference_internal)
|
||||
"cuda_t"_a.noconvert(),
|
||||
"Construct a DeviceGridHandle that wraps an existing pair of "
|
||||
"host and device uint32 arrays of equal length.")
|
||||
.def("deviceGrid", &pyDeviceGrid, "n"_a = 0,
|
||||
nb::keep_alive<0, 1>(),
|
||||
"Return the n-th device-resident grid as a typed Grid subclass "
|
||||
"selected by gridType(n), or None if the BuildT is not bound in "
|
||||
"Python or the device copy has not been uploaded yet. The "
|
||||
"returned grid keeps this handle alive.")
|
||||
.def(
|
||||
"deviceUpload", [](GridHandle<BufferT>& handle, bool sync) { handle.deviceUpload(nullptr, sync); }, "sync"_a = true)
|
||||
"deviceUpload", [](GridHandle<BufferT>& handle, bool sync) { handle.deviceUpload(nullptr, sync); }, "sync"_a = true,
|
||||
"Copy the host-side buffer to the device. If sync is True the "
|
||||
"call blocks until the transfer completes.")
|
||||
.def(
|
||||
"deviceDownload", [](GridHandle<BufferT>& handle, bool sync) { handle.deviceDownload(nullptr, sync); }, "sync"_a = true);
|
||||
"deviceDownload", [](GridHandle<BufferT>& handle, bool sync) { handle.deviceDownload(nullptr, sync); }, "sync"_a = true,
|
||||
"Copy the device-side buffer back to the host. If sync is True "
|
||||
"the call blocks until the transfer completes.");
|
||||
// NOTE: defineGridHandleUtilities<BufferT> intentionally NOT called for
|
||||
// DeviceBuffer. Registering nanovdb.splitGrids / nanovdb.mergeGrids as a
|
||||
// second overload taking a DeviceGridHandle list conflicts with the host
|
||||
// overload because both signatures take nb::list, and nanobind's
|
||||
// overload resolution can't disambiguate by element type — it picks one
|
||||
// and the inner cast fails with std::bad_cast. A properly typed device
|
||||
// variant (with its own name, or strongly-typed std::vector<HandleT>
|
||||
// args via nanobind/stl/vector.h) can land later if it's needed.
|
||||
}
|
||||
|
||||
} // namespace pynanovdb
|
||||
|
||||
@@ -50,7 +50,8 @@ template<typename BuildT> void definePointsToGrid(nb::module_& m, const char* na
|
||||
auto handle = converter.getHandle(points, tensor.shape(0));
|
||||
return handle;
|
||||
},
|
||||
"tensor"_a);
|
||||
"tensor"_a,
|
||||
"Rasterize the given (N, 3) int32 device tensor of points into a fresh GridHandle.");
|
||||
}
|
||||
|
||||
template void definePointsToGrid<nanovdb::math::Rgba8>(nb::module_&, const char*);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# NanoVDB Python Examples
|
||||
|
||||
Runnable `.py` scripts demonstrating the NanoVDB Python bindings.
|
||||
Each script is self-contained, builds its own input data, and prints
|
||||
a small summary to stdout.
|
||||
|
||||
Run any example with:
|
||||
|
||||
```bash
|
||||
python load_inspect.py
|
||||
```
|
||||
|
||||
When working from the source tree, the `nanovdb` module needs to be
|
||||
on `PYTHONPATH` (e.g. via the build output directory):
|
||||
|
||||
```bash
|
||||
cd <build>/nanovdb/nanovdb/python
|
||||
PYTHONPATH=. python /path/to/<example>.py
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
| Script | What it shows |
|
||||
| --- | --- |
|
||||
| [`load_inspect.py`](load_inspect.py) | Polymorphic `handle.grid(n)` access, `GridMetaData` type-erased introspection, mixed-type handles via `mergeGrids`. |
|
||||
| [`build_grid.py`](build_grid.py) | Voxel-by-voxel construction with `nanovdb.tools.build.FloatGrid`, the cached `ValueAccessor`, the thread-safe `WriteAccessor`, and `.to_nanovdb()` to bake into a host `GridHandle`. |
|
||||
| [`bulk_leaf_numpy.py`](bulk_leaf_numpy.py) | Zero-copy `(N_leaves, 512)` NumPy view of every leaf's mValues via `grid.leaf_values()`. Includes a global-stats reduction and an in-place mutation that propagates back into the grid. Requires NumPy. |
|
||||
| [`quantize.py`](quantize.py) | Quantize a `NanoGrid<float>` through `nanovdb.tools.createNanoGridFp{4,8,16,N}`. Shows fixed-width quantization with dithering and variable-width `FpN` with both `AbsDiff` and `RelDiff` oracles. |
|
||||
| [`validate.py`](validate.py) | `nanovdb.tools.validateGrid` / `validateGrids`, `checkGrid`, `isValid`, and the `evalChecksum` / `validateChecksum` / `updateChecksum` round-trip. |
|
||||
| [`io_roundtrip.py`](io_roundtrip.py) | `nanovdb.io` write/read round-trip over five primitives: `writeGrids` with codec fallback, `readGridMetaData`, `hasGrid`, `readGrid` by name, `splitGrids`, and zero-copy point positions via `getBlindData`. Port of `ex_write_nanovdb_grids` + `ex_read_nanovdb_sphere_accessor`. |
|
||||
| [`make_funny_nanovdb.py`](make_funny_nanovdb.py) | Functor-based construction: `tools.createFloatGrid(background, name, gridClass, func, bbox)` evaluates a Python callback at every voxel. Port of `ex_make_funny_nanovdb` on a reduced domain. |
|
||||
| [`make_typed_grids.py`](make_typed_grids.py) | One solid sphere per value type via `tools.build.{Float,Double,Int16,Int32,Int64,UInt32,Vec3f}Grid`, written to a single file and re-read with polymorphic `handle.grid()` dispatch. Port of `ex_make_typed_grids`. |
|
||||
| [`raytrace_level_set.py`](raytrace_level_set.py) | CPU sphere-traced level-set render to PGM using `worldToIndex`, the trilinear sampler, `zeroCrossing()`, and normalized `gradient()` shading. Port of `ex_raytrace_level_set` (host path). |
|
||||
| [`raytrace_fog_volume.py`](raytrace_fog_volume.py) | CPU transmittance ray-march of a fog volume to PGM using a `ReadAccessor` and `Coord.Floor` — the accessor-based sampling idiom. Port of `ex_raytrace_fog_volume` (host path). |
|
||||
| [`collide_level_set.py`](collide_level_set.py) | Particles colliding with a level set: `worldToIndexF`, `tree.isActive` narrow-band test, accessor distance reads, and `sampler.gradient()` collision normals. Port of `ex_collide_level_set` (host path). |
|
||||
| [`index_grid_channels.py`](index_grid_channels.py) | `tools.createNanoGridOnIndex(src, channels=1)`, `grid.valueCount()`, coordinate reads through `createChannelAccessor`, and blind-data authoring with `tools.CreateNanoGrid.addBlindData` + the writable `getBlindData` view. Extends the host half of `ex_index_grid_cuda`. Requires NumPy for the authoring section. |
|
||||
| [`node_manager.py`](node_manager.py) | Linearized node iteration with `createNodeManager`: per-level counts, `leaf(i)` / `lower(i)` access, node origins, masks, and stats. Port of the host half of `ex_nodemanager_cuda`. |
|
||||
| [`openvdb_interop.py`](openvdb_interop.py) | `tools.openToNanoVDB` / `nanoToOpenVDB` round-trip with accessor comparison on both sides. Self-skips unless built with `NANOVDB_USE_OPENVDB` and `openvdb` is importable. Port of `ex_openvdb_to_nanovdb_accessor`. |
|
||||
|
||||
Scripts that produce files write them to a fresh temporary directory
|
||||
and print its path, so the source tree stays clean. The whole set is
|
||||
smoke-tested by [`../test/TestExamples.py`](../test/TestExamples.py)
|
||||
(ctest name `pytest_nanovdb_examples`) when the module is configured
|
||||
with `NANOVDB_BUILD_PYTHON_UNITTESTS=ON`.
|
||||
|
||||
For full API signatures and per-argument docstrings, use Python's
|
||||
`help()` on any symbol — e.g. `help(nanovdb.tools.createNanoGridFpN)`.
|
||||
The bindings ship `.pyi` type stubs for IDE / type-checker support.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Construct a NanoVDB grid voxel-by-voxel in pure Python.
|
||||
|
||||
nanovdb.tools.build.<Suffix>Grid is the mutable CPU grid builder
|
||||
that mirrors nanovdb::tools::build::Grid<T>. This example shows the
|
||||
three construction loops you'll typically reach for (setValue
|
||||
directly, the cached ValueAccessor, and the thread-safe
|
||||
WriteAccessor), then bakes each build grid into a host NanoGrid via
|
||||
.to_nanovdb() and reads it back through the regular polymorphic
|
||||
handle.grid() API.
|
||||
|
||||
Run with: python build_grid.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def fill_with_setValue():
|
||||
"""Simplest path: setValue() directly on the build grid."""
|
||||
g = nanovdb.tools.build.FloatGrid(
|
||||
background=0.0, name="setValue_demo",
|
||||
gridClass=nanovdb.GridClass.FogVolume)
|
||||
# Plant five active voxels along the x axis.
|
||||
for i in range(5):
|
||||
g.setValue(nanovdb.math.Coord(i, 0, 0), float(i + 1))
|
||||
return g
|
||||
|
||||
|
||||
def fill_with_accessor():
|
||||
"""Cached path: getAccessor() avoids re-walking the tree for
|
||||
each write when consecutive coordinates share a leaf."""
|
||||
g = nanovdb.tools.build.FloatGrid(background=0.0, name="accessor_demo")
|
||||
acc = g.getAccessor()
|
||||
# The accessor caches the last leaf / lower / upper node, so a
|
||||
# burst of writes in a 16^3 neighborhood only walks the tree once.
|
||||
for x in range(8):
|
||||
for y in range(8):
|
||||
for z in range(8):
|
||||
if x + y + z == 7:
|
||||
acc.setValue(nanovdb.math.Coord(x, y, z),
|
||||
float(x * 64 + y * 8 + z))
|
||||
return g
|
||||
|
||||
|
||||
def fill_with_write_accessor():
|
||||
"""Thread-safe path: getWriteAccessor() buffers writes into a
|
||||
private root and merges them into the parent on destruction or
|
||||
on an explicit .merge() call. Useful when fanning out to multiple
|
||||
threads — one WriteAccessor per thread, no shared mutable state."""
|
||||
g = nanovdb.tools.build.FloatGrid(background=0.0, name="write_accessor_demo")
|
||||
wa = g.getWriteAccessor()
|
||||
wa.setValue(nanovdb.math.Coord(50, 50, 50), 9.0)
|
||||
# Before merge, the parent doesn't see the change yet.
|
||||
assert g.getValue(nanovdb.math.Coord(50, 50, 50)) == 0.0
|
||||
wa.merge()
|
||||
assert g.getValue(nanovdb.math.Coord(50, 50, 50)) == 9.0
|
||||
return g
|
||||
|
||||
|
||||
def main():
|
||||
for builder in (fill_with_setValue, fill_with_accessor,
|
||||
fill_with_write_accessor):
|
||||
g = builder()
|
||||
print(f"=== {g.getName()} ===")
|
||||
print(f" nodeCount (leaf, lower, upper) = {g.nodeCount()}")
|
||||
|
||||
# Bake into a host NanoGrid<float>.
|
||||
handle = g.to_nanovdb(sMode=nanovdb.tools.StatsMode.All)
|
||||
ng = handle.grid()
|
||||
print(f" baked NanoGrid: type={ng.gridType()}, "
|
||||
f"active={ng.activeVoxelCount()}, "
|
||||
f"worldBBox={ng.worldBBox()}")
|
||||
|
||||
# The build grid is left untouched — we can bake again.
|
||||
handle2 = g.to_nanovdb()
|
||||
assert handle2.grid().activeVoxelCount() == ng.activeVoxelCount()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,63 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Bulk per-leaf value access as a zero-copy NumPy array.
|
||||
|
||||
grid.leaf_values() is the highest-bandwidth path from NanoVDB into
|
||||
NumPy. It returns an (N_leaves, 512) view of every leaf's mValues
|
||||
without copying — modify it, slice it, feed it into a PyTorch tensor,
|
||||
hash it for cache lookup, whatever you need.
|
||||
|
||||
Run with: python bulk_leaf_numpy.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
print("This example requires numpy. Install it with: pip install numpy")
|
||||
return
|
||||
|
||||
# Build a fog volume sphere with stats so the leaves have meaningful
|
||||
# min/max attached (just for the printing below — not required by
|
||||
# leaf_values itself).
|
||||
handle = nanovdb.tools.createFogVolumeSphere(
|
||||
radius=20.0, name="bulk_demo")
|
||||
grid = handle.grid()
|
||||
print(f"Grid: {grid.gridType()}, active voxels = {grid.activeVoxelCount()}, "
|
||||
f"leaves = {grid.tree().nodeCount(0)}")
|
||||
|
||||
# leaf_values() is the zero-copy view. Modifying it modifies the grid.
|
||||
bulk = grid.leaf_values()
|
||||
# np.asarray adds a NumPy wrapper but doesn't copy.
|
||||
arr = np.asarray(bulk)
|
||||
print(f"leaf_values: shape={arr.shape}, dtype={arr.dtype}, "
|
||||
f"backed by grid memory (no copy).")
|
||||
|
||||
# Global statistics across every voxel in every leaf, computed in C.
|
||||
# 0.0 voxels (background) are excluded by using a mask.
|
||||
nonzero = arr[arr != 0.0]
|
||||
print(f" non-background voxels = {nonzero.size}")
|
||||
print(f" min = {nonzero.min()}, max = {nonzero.max()}, "
|
||||
f"mean = {nonzero.mean()}")
|
||||
|
||||
# Per-leaf reductions: each row of `arr` is one leaf's 512 voxels.
|
||||
per_leaf_max = arr.max(axis=1)
|
||||
print(f" per-leaf max (first 5): {per_leaf_max[:5]}")
|
||||
|
||||
# Zero-copy means writes propagate. Zero out the first leaf's values
|
||||
# and read one back through the regular accessor to confirm the grid
|
||||
# actually changed. (The active *mask* is unchanged — we wrote into
|
||||
# mValues only — so activeVoxelCount() stays the same.)
|
||||
arr[0] = 0.0
|
||||
leaf = grid.tree().getFirstLeaf()
|
||||
if leaf is not None:
|
||||
first_value_after = leaf.getFirstValue()
|
||||
print(f" zeroed first leaf's values in place: "
|
||||
f"leaf.getFirstValue() = {first_value_after}, "
|
||||
f"activeVoxelCount unchanged: {grid.activeVoxelCount()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,85 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Collide falling particles against a level-set surface on the CPU.
|
||||
|
||||
Python port of the host path of ex_collide_level_set: particles fall
|
||||
under gravity; each step transforms the candidate position to index
|
||||
space with grid.worldToIndexF, tests the narrow band with
|
||||
tree.isActive, reads the signed distance through a ReadAccessor, and
|
||||
on penetration reflects the velocity about the surface normal. The C++
|
||||
original builds the normal from 6-tap finite differences of the SDF;
|
||||
here the bound sampler.gradient() provides the same quantity in one
|
||||
call (index-space units — normalized before use).
|
||||
|
||||
Run with: python collide_level_set.py
|
||||
"""
|
||||
import random
|
||||
|
||||
import nanovdb
|
||||
|
||||
NUM_PARTICLES = 500
|
||||
NUM_STEPS = 40
|
||||
DT = 0.1
|
||||
GRAVITY = -9.8
|
||||
|
||||
|
||||
def main():
|
||||
handle = nanovdb.tools.createLevelSetSphere(radius=100.0, name="sphere")
|
||||
grid = handle.grid()
|
||||
tree = grid.tree()
|
||||
acc = grid.getAccessor()
|
||||
sampler = nanovdb.math.createTrilinearSampler(grid)
|
||||
|
||||
# Seed particles above the north pole of the sphere, falling down.
|
||||
rng = random.Random(42)
|
||||
particles = []
|
||||
for _ in range(NUM_PARTICLES):
|
||||
p = [rng.uniform(-30.0, 30.0), rng.uniform(115.0, 140.0),
|
||||
rng.uniform(-30.0, 30.0)]
|
||||
particles.append((p, [0.0, -20.0, 0.0]))
|
||||
|
||||
total_collisions = 0
|
||||
for step in range(NUM_STEPS):
|
||||
collisions = 0
|
||||
for p, v in particles:
|
||||
v[1] += GRAVITY * DT
|
||||
next_p = [p[i] + v[i] * DT for i in range(3)]
|
||||
|
||||
ijk = nanovdb.math.Coord.Floor(grid.worldToIndexF(
|
||||
nanovdb.math.Vec3f(next_p[0], next_p[1], next_p[2])))
|
||||
if tree.isActive(ijk): # inside the narrow band?
|
||||
d = acc.getValue(ijk)
|
||||
if d <= 0.0: # inside the level set?
|
||||
ipos = grid.worldToIndexF(
|
||||
nanovdb.math.Vec3f(next_p[0], next_p[1], next_p[2]))
|
||||
n = sampler.gradient(ipos)
|
||||
n.normalize()
|
||||
# Project the position back to the surface and
|
||||
# reflect the velocity, as in the C++ example.
|
||||
for i in range(3):
|
||||
next_p[i] -= d * n[i]
|
||||
v_dot_n = sum(v[i] * n[i] for i in range(3))
|
||||
for i in range(3):
|
||||
v[i] -= 2.0 * v_dot_n * n[i]
|
||||
collisions += 1
|
||||
p[:] = next_p
|
||||
total_collisions += collisions
|
||||
if collisions:
|
||||
print(f"step {step:2d}: {collisions} collisions")
|
||||
|
||||
print(f"total collisions over {NUM_STEPS} steps: {total_collisions}")
|
||||
assert total_collisions > 0
|
||||
|
||||
# No particle should end up inside the surface.
|
||||
worst = 0.0
|
||||
for p, _ in particles:
|
||||
ijk = nanovdb.math.Coord.Floor(grid.worldToIndexF(
|
||||
nanovdb.math.Vec3f(p[0], p[1], p[2])))
|
||||
if tree.isActive(ijk):
|
||||
worst = min(worst, acc.getValue(ijk))
|
||||
print(f"deepest final penetration: {worst:.3f} world units")
|
||||
assert worst > -2.0 * grid.voxelSize()[0]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,86 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Index grids, value channels, and blind-data authoring.
|
||||
|
||||
Part A ports the host half of ex_index_grid_cuda: a float level set is
|
||||
re-encoded as a NanoGrid<ValueOnIndex> whose voxels store sequential
|
||||
uint64 indices, with the original float values copied into a blind-data
|
||||
channel — then read back by coordinate through a ChannelAccessor,
|
||||
which the C++ example only does in a CUDA kernel. Part B authors a
|
||||
brand-new blind-data channel on a grid with tools.CreateNanoGrid and
|
||||
fills it in place through the writable getBlindData() NumPy view.
|
||||
|
||||
Run with: python index_grid_channels.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def index_grid_with_channel():
|
||||
src = nanovdb.tools.createLevelSetSphere(radius=50.0, name="sphere")
|
||||
src_grid = src.grid()
|
||||
|
||||
# channels=1 copies the source values into blind-data channel 0,
|
||||
# indexed by the per-voxel uint64 indices the OnIndex grid stores.
|
||||
handle = nanovdb.tools.createNanoGridOnIndex(src_grid, channels=1)
|
||||
grid = handle.grid()
|
||||
print(f"OnIndex grid: valueCount={grid.valueCount()}, "
|
||||
f"source activeVoxelCount={src_grid.activeVoxelCount()}")
|
||||
assert grid.valueCount() >= src_grid.activeVoxelCount()
|
||||
|
||||
# createChannelAccessor inspects the channel's recorded dataType and
|
||||
# returns the matching typed accessor (here: OnIndexFloat...).
|
||||
channel = nanovdb.createChannelAccessor(grid, 0)
|
||||
print(f"channel accessor: {type(channel).__name__}, "
|
||||
f"valueCount={channel.valueCount()}")
|
||||
|
||||
src_acc = src_grid.getAccessor()
|
||||
for ijk in (nanovdb.math.Coord(48, 0, 0), nanovdb.math.Coord(0, 50, 0),
|
||||
nanovdb.math.Coord(0, 0, 52)):
|
||||
via_channel = channel.getValue(ijk)
|
||||
via_source = src_acc.getValue(ijk)
|
||||
print(f" {ijk}: channel={via_channel:.3f} source={via_source:.3f} "
|
||||
f"(linear offset {channel.getIndex(ijk)})")
|
||||
assert via_channel == via_source
|
||||
|
||||
|
||||
def author_blind_data():
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
print("NumPy not found. Skipping the blind-data authoring section.")
|
||||
return
|
||||
src = nanovdb.tools.build.FloatGrid(0.0, "authored",
|
||||
nanovdb.GridClass.Unknown)
|
||||
for i in range(8):
|
||||
src.setValue(nanovdb.math.Coord(i, 0, 0), float(i))
|
||||
|
||||
# Declare a channel at conversion time; it is allocated zero-filled
|
||||
# in the baked grid and filled afterwards through the writable view.
|
||||
conv = nanovdb.tools.CreateNanoGrid(src)
|
||||
ch = conv.addBlindData("temperature", count=64,
|
||||
dataType=nanovdb.GridType.Float)
|
||||
handle = conv.getHandle()
|
||||
grid = handle.grid()
|
||||
print(f"authored grid has {grid.blindDataCount()} blind-data channel(s)")
|
||||
|
||||
view = grid.getBlindData(ch)
|
||||
view[:] = np.linspace(273.0, 373.0, num=64, dtype=np.float32)
|
||||
|
||||
# Re-resolve the channel by name and confirm the writes persisted.
|
||||
n = grid.findBlindData("temperature")
|
||||
meta = grid.blindMetaData(n)
|
||||
stored = grid.getBlindData(n)
|
||||
print(f" {meta.name()!r}: {meta.valueCount} x {meta.dataType}, "
|
||||
f"range [{stored.min():.1f}, {stored.max():.1f}]")
|
||||
assert n == ch
|
||||
assert stored[0] == np.float32(273.0)
|
||||
|
||||
|
||||
def main():
|
||||
index_grid_with_channel()
|
||||
print()
|
||||
author_blind_data()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,103 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Write a bundle of primitive grids to a .nvdb file and read it back.
|
||||
|
||||
Python port of ex_write_nanovdb_grids, ex_read_nanovdb_sphere, and
|
||||
ex_read_nanovdb_sphere_accessor, consolidated into one self-contained
|
||||
round trip: bake five primitives, write them to a single file (with
|
||||
codec fallback), inspect the file metadata without loading the grids,
|
||||
re-read one grid by name and all grids at once, and split a merged
|
||||
handle back apart. The point-sphere section reads the world-space
|
||||
point positions through the zero-copy getBlindData() NumPy view.
|
||||
|
||||
Run with: python io_roundtrip.py
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import nanovdb
|
||||
|
||||
|
||||
def write_primitives(path):
|
||||
handles = [
|
||||
nanovdb.tools.createLevelSetSphere(radius=50.0, name="sphere_ls"),
|
||||
nanovdb.tools.createLevelSetTorus(majorRadius=50.0, minorRadius=20.0,
|
||||
name="torus_ls"),
|
||||
nanovdb.tools.createLevelSetBox(width=40.0, height=60.0, depth=80.0,
|
||||
name="box_ls"),
|
||||
nanovdb.tools.createLevelSetBBox(width=40.0, height=60.0, depth=80.0,
|
||||
thickness=10.0, name="bbox_ls"),
|
||||
nanovdb.tools.createPointSphere(pointsPerVoxel=2, radius=50.0,
|
||||
name="sphere_points"),
|
||||
]
|
||||
# BLOSC gives the best compression but is a build-time option; fall
|
||||
# back to an uncompressed file when this module was built without it.
|
||||
try:
|
||||
nanovdb.io.writeGrids(path, handles, codec=nanovdb.io.Codec.BLOSC)
|
||||
codec = "BLOSC"
|
||||
except RuntimeError:
|
||||
nanovdb.io.writeGrids(path, handles, codec=nanovdb.io.Codec.NONE)
|
||||
codec = "NONE"
|
||||
print(f"Wrote {len(handles)} grids to {path} (codec={codec})")
|
||||
|
||||
|
||||
def inspect_file(path):
|
||||
# readGridMetaData parses the per-grid file headers without loading
|
||||
# any voxel data — the cheap way to answer "what's in this file?".
|
||||
for meta in nanovdb.io.readGridMetaData(path):
|
||||
print(f" {meta.gridName!r}: type={meta.gridType}, "
|
||||
f"class={meta.gridClass}, voxels={meta.voxelCount}")
|
||||
assert nanovdb.io.hasGrid(path, "sphere_ls")
|
||||
assert not nanovdb.io.hasGrid(path, "no_such_grid")
|
||||
|
||||
|
||||
def read_back(path):
|
||||
# Read a single grid by name, then print the recognizable
|
||||
# ex_read_nanovdb_sphere_accessor cross-section along the x-axis.
|
||||
handle = nanovdb.io.readGrid(path, "sphere_ls")
|
||||
acc = handle.grid().getAccessor()
|
||||
for i in range(47, 54):
|
||||
ijk = nanovdb.math.Coord(i, 0, 0)
|
||||
print(f" sphere_ls({i},0,0) = {acc.getValue(ijk):.2f}")
|
||||
|
||||
# Read every grid, merge them into one multi-grid handle, and split
|
||||
# that handle back into one handle per grid.
|
||||
handles = nanovdb.io.readGrids(path)
|
||||
merged = nanovdb.mergeGrids(handles)
|
||||
print(f" merged handle holds {merged.gridCount()} grids")
|
||||
parts = nanovdb.splitGrids(merged)
|
||||
assert len(parts) == len(handles)
|
||||
print(f" splitGrids -> {len(parts)} single-grid handles")
|
||||
return handles
|
||||
|
||||
|
||||
def point_positions(handles):
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
print("NumPy not found. Skipping the point-positions section.")
|
||||
return
|
||||
# The point sphere stores its world-space positions as a blind-data
|
||||
# channel; getBlindData() exposes it as a zero-copy (N, 3) view.
|
||||
points = next(h.grid() for h in handles
|
||||
if h.grid().gridName() == "sphere_points")
|
||||
positions = points.getBlindData(0)
|
||||
radii = np.linalg.norm(positions, axis=1)
|
||||
print(f" {positions.shape[0]} points, "
|
||||
f"|p| in [{radii.min():.2f}, {radii.max():.2f}]")
|
||||
# Points are jittered within their voxel, so allow ~1.5 voxels.
|
||||
assert np.all(np.abs(radii - 50.0) < 1.5)
|
||||
|
||||
|
||||
def main():
|
||||
out_dir = tempfile.mkdtemp(prefix="nanovdb_")
|
||||
path = os.path.join(out_dir, "primitives.nvdb")
|
||||
write_primitives(path)
|
||||
inspect_file(path)
|
||||
handles = read_back(path)
|
||||
point_positions(handles)
|
||||
print(f"Output left in {out_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Load a NanoVDB grid and inspect it polymorphically.
|
||||
|
||||
handle.grid(i) returns the correct typed Python subclass for whatever
|
||||
GridType the grid carries, so a single call site can handle a mixed
|
||||
bundle of grid types. This example builds two grids of different
|
||||
value types into one handle, then walks the handle inspecting each
|
||||
grid via the polymorphic accessor plus GridMetaData (the type-erased
|
||||
introspector that answers "what's in this buffer?" without knowing
|
||||
BuildT at compile time).
|
||||
|
||||
Run with: python load_inspect.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def describe_handle(handle):
|
||||
print(f"Handle contains {handle.gridCount()} grid(s).")
|
||||
for i in range(handle.gridCount()):
|
||||
# GridType / gridSize are cheap to query on the handle itself.
|
||||
gtype = handle.gridType(i)
|
||||
gsize = handle.gridSize(i)
|
||||
print(f" [{i}] type={gtype}, size={gsize} bytes")
|
||||
|
||||
# handle.grid(i) returns the matching <Suffix>Grid subclass
|
||||
# at runtime — no isinstance dispatch needed at the call site.
|
||||
# The grid name lives on the grid itself, not the handle.
|
||||
grid = handle.grid(i)
|
||||
print(f" name={grid.gridName()!r}, "
|
||||
f"gridClass={grid.gridClass()}")
|
||||
print(f" activeVoxelCount={grid.activeVoxelCount()}")
|
||||
print(f" worldBBox={grid.worldBBox()}")
|
||||
|
||||
# GridMetaData is the type-erased introspector — answers
|
||||
# "what's in this buffer?" without knowing BuildT.
|
||||
meta = nanovdb.GridMetaData(grid)
|
||||
print(f" gridSize={meta.gridSize()}, "
|
||||
f"isLevelSet={meta.isLevelSet()}, "
|
||||
f"isFogVolume={meta.isFogVolume()}")
|
||||
|
||||
|
||||
def main():
|
||||
# Build two grids of different types into one handle so we can
|
||||
# exercise the polymorphic accessor.
|
||||
h_float = nanovdb.tools.createLevelSetSphere(
|
||||
radius=10.0, name="sphere_float")
|
||||
h_double = nanovdb.tools.createLevelSetSphere(
|
||||
gridType=nanovdb.GridType.Double, radius=10.0, name="sphere_double")
|
||||
handle = nanovdb.mergeGrids([h_float, h_double])
|
||||
|
||||
describe_handle(handle)
|
||||
|
||||
print()
|
||||
print("Polymorphic dispatch from a runtime GridType:")
|
||||
for i in range(handle.gridCount()):
|
||||
grid = handle.grid(i)
|
||||
# Each typed grid carries a getAccessor() that returns the
|
||||
# appropriate <Suffix>ReadAccessor — float for FloatGrid,
|
||||
# double for DoubleGrid, etc.
|
||||
acc = grid.getAccessor()
|
||||
v = acc.getValue(nanovdb.math.Coord(0, 0, 0))
|
||||
print(f" grid[{i}] accessor.getValue(0,0,0) = {v}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Build a level set from a Python function evaluated at every voxel.
|
||||
|
||||
Python port of ex_make_funny_nanovdb: a trigonometric interference
|
||||
pattern is CSG-intersected with a sphere and clamped to a narrow band,
|
||||
using the functor-based nanovdb.tools.createFloatGrid factory. The
|
||||
callback is one Python call per voxel in the bbox, so this port uses a
|
||||
65^3 domain (~275k calls, a few seconds) where the C++ original fills
|
||||
[-500,500]^3 — scale `SIZE` up only if you are willing to wait.
|
||||
|
||||
Run with: python make_funny_nanovdb.py
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import nanovdb
|
||||
|
||||
SIZE = 32 # half-width of the cubic domain, in voxels
|
||||
BACKGROUND = 5.0 # narrow-band half-width, in world units
|
||||
FREQ = 0.8 # rescaled from the C++ 0.1 to fit the smaller domain
|
||||
|
||||
|
||||
def funny(ijk):
|
||||
v = 4.0 + 5.0 * (math.cos(ijk.x * FREQ) * math.sin(ijk.y * FREQ) +
|
||||
math.cos(ijk.y * FREQ) * math.sin(ijk.z * FREQ) +
|
||||
math.cos(ijk.z * FREQ) * math.sin(ijk.x * FREQ))
|
||||
# CSG intersection with a sphere of radius SIZE.
|
||||
r = math.sqrt(ijk.x ** 2 + ijk.y ** 2 + ijk.z ** 2)
|
||||
v = max(v, r - SIZE)
|
||||
# Clamp to the narrow band.
|
||||
return max(-BACKGROUND, min(BACKGROUND, v))
|
||||
|
||||
|
||||
def main():
|
||||
bbox = nanovdb.math.CoordBBox(nanovdb.math.Coord(-SIZE),
|
||||
nanovdb.math.Coord(SIZE))
|
||||
print(f"Evaluating funny() over {bbox} ...")
|
||||
handle = nanovdb.tools.createFloatGrid(
|
||||
BACKGROUND, "funny", nanovdb.GridClass.LevelSet, funny, bbox)
|
||||
|
||||
grid = handle.grid()
|
||||
print(f"activeVoxelCount = {grid.activeVoxelCount()}")
|
||||
acc = grid.getAccessor()
|
||||
probe = nanovdb.math.Coord(0, 0, 0)
|
||||
print(f"value at {probe} = {acc.getValue(probe):.3f}")
|
||||
assert grid.isLevelSet()
|
||||
assert grid.activeVoxelCount() > 0
|
||||
|
||||
out_dir = tempfile.mkdtemp(prefix="nanovdb_")
|
||||
path = os.path.join(out_dir, "funny.nvdb")
|
||||
try:
|
||||
nanovdb.io.writeGrid(path, handle, codec=nanovdb.io.Codec.BLOSC)
|
||||
except RuntimeError:
|
||||
nanovdb.io.writeGrid(path, handle, codec=nanovdb.io.Codec.NONE)
|
||||
print(f"Wrote {path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,72 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Build grids of many value types and read them back polymorphically.
|
||||
|
||||
Python port of ex_make_typed_grids: a small solid sphere is baked once
|
||||
per value type through the matching nanovdb.tools.build.<Suffix>Grid
|
||||
mutable builder, all handles are written to a single .nvdb file, and
|
||||
the file is re-read with handle.grid() returning the correct typed
|
||||
subclass for each grid at runtime.
|
||||
|
||||
Run with: python make_typed_grids.py
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import nanovdb
|
||||
|
||||
RADIUS = 8 # voxels; ~2.1k active voxels per grid keeps this quick
|
||||
|
||||
|
||||
def solid_sphere_coords():
|
||||
r2 = RADIUS * RADIUS
|
||||
for i in range(-RADIUS, RADIUS + 1):
|
||||
for j in range(-RADIUS, RADIUS + 1):
|
||||
for k in range(-RADIUS, RADIUS + 1):
|
||||
if i * i + j * j + k * k < r2:
|
||||
yield nanovdb.math.Coord(i, j, k)
|
||||
|
||||
|
||||
def build_typed_grids():
|
||||
# (builder class, grid name, background, voxel value)
|
||||
specs = [
|
||||
(nanovdb.tools.build.FloatGrid, "float_grid", 0.0, 1.0),
|
||||
(nanovdb.tools.build.DoubleGrid, "double_grid", 0.0, 1.0),
|
||||
(nanovdb.tools.build.Int16Grid, "int16_grid", 0, 1),
|
||||
(nanovdb.tools.build.Int32Grid, "int32_grid", 0, 1),
|
||||
(nanovdb.tools.build.Int64Grid, "int64_grid", 0, 1),
|
||||
(nanovdb.tools.build.UInt32Grid, "uint32_grid", 0, 1),
|
||||
(nanovdb.tools.build.Vec3fGrid, "vec3f_grid",
|
||||
nanovdb.math.Vec3f(0.0), nanovdb.math.Vec3f(1.0, 0.0, 0.0)),
|
||||
]
|
||||
handles = []
|
||||
for cls, name, background, value in specs:
|
||||
grid = cls(background, name, nanovdb.GridClass.Unknown)
|
||||
for ijk in solid_sphere_coords():
|
||||
grid.setValue(ijk, value)
|
||||
handles.append(grid.to_nanovdb())
|
||||
print(f"built {name} ({cls.__name__})")
|
||||
return handles
|
||||
|
||||
|
||||
def main():
|
||||
handles = build_typed_grids()
|
||||
|
||||
out_dir = tempfile.mkdtemp(prefix="nanovdb_")
|
||||
path = os.path.join(out_dir, "custom_types.nvdb")
|
||||
nanovdb.io.writeGrids(path, handles)
|
||||
print(f"Wrote {len(handles)} grids to {path}")
|
||||
|
||||
# Re-read and dispatch: handle.grid() returns FloatGrid, Int16Grid,
|
||||
# Vec3fGrid, ... according to the GridType each grid carries.
|
||||
probe = nanovdb.math.Coord(0, 0, 0)
|
||||
for handle in nanovdb.io.readGrids(path):
|
||||
grid = handle.grid()
|
||||
value = grid.getAccessor().getValue(probe)
|
||||
print(f" {grid.gridName():<12} -> {type(grid).__name__:<12} "
|
||||
f"active={grid.activeVoxelCount()} value(0,0,0)={value}")
|
||||
assert grid.activeVoxelCount() > 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,50 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Iterate a grid's nodes linearly through a NodeManager.
|
||||
|
||||
Python port of the host half of ex_nodemanager_cuda: createNodeManager
|
||||
builds linearized arrays of the tree's leaf, lower, and upper nodes so
|
||||
they can be visited by index instead of by tree traversal. Each node
|
||||
exposes its origin, per-node stats, and (on leaves) the raw 512-value
|
||||
buffer. For bulk NumPy analytics over every leaf at once, see
|
||||
bulk_leaf_numpy.py's grid.leaf_values() instead.
|
||||
|
||||
Run with: python node_manager.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def main():
|
||||
handle = nanovdb.tools.createLevelSetSphere(radius=50.0, name="sphere")
|
||||
grid = handle.grid()
|
||||
tree = grid.tree()
|
||||
|
||||
nmh = nanovdb.createNodeManager(grid)
|
||||
nm = nmh.mgr()
|
||||
print(f"NodeManager over {grid.gridName()!r} (linear={nm.isLinear()}):")
|
||||
print(f" leaves={nm.leafCount()}, lower={nm.lowerCount()}, "
|
||||
f"upper={nm.upperCount()}")
|
||||
|
||||
# The counts mirror the tree's per-level node counts.
|
||||
assert nm.leafCount() == tree.nodeCount(0)
|
||||
assert nm.lowerCount() == tree.nodeCount(1)
|
||||
assert nm.upperCount() == tree.nodeCount(2)
|
||||
|
||||
# Linear access agrees with tree traversal.
|
||||
assert nm.leaf(0).origin() == tree.getFirstLeaf().origin()
|
||||
|
||||
# Visit a few leaves by index: origin, activity, and value range.
|
||||
for i in range(min(5, nm.leafCount())):
|
||||
leaf = nm.leaf(i)
|
||||
print(f" leaf[{i}] origin={leaf.origin()} "
|
||||
f"on={leaf.valueMask().countOn()} "
|
||||
f"min={leaf.minimum():.3f} max={leaf.maximum():.3f}")
|
||||
|
||||
# Internal nodes are reachable the same way.
|
||||
lower = nm.lower(0)
|
||||
print(f" lower[0] origin={lower.origin()} "
|
||||
f"children={lower.childMask().countOn()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,53 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Round-trip a grid between OpenVDB and NanoVDB.
|
||||
|
||||
Python port of ex_openvdb_to_nanovdb and ex_openvdb_to_nanovdb_accessor:
|
||||
an OpenVDB level-set sphere is converted to NanoVDB with openToNanoVDB,
|
||||
values are compared through both libraries' accessors, and the NanoVDB
|
||||
grid is converted back with nanoToOpenVDB. Requires a build with
|
||||
NANOVDB_USE_OPENVDB and an importable `openvdb` module; the script
|
||||
skips (exit 0) when either is missing.
|
||||
|
||||
Run with: python openvdb_interop.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def main():
|
||||
if not hasattr(nanovdb.tools, "openToNanoVDB"):
|
||||
print("nanovdb was built without NANOVDB_USE_OPENVDB. Skipping...")
|
||||
return
|
||||
try:
|
||||
import openvdb
|
||||
except ImportError:
|
||||
print("openvdb not found. Skipping...")
|
||||
return
|
||||
|
||||
sphere = openvdb.createLevelSetSphere(100.0)
|
||||
sphere.name = "sphere"
|
||||
handle = nanovdb.tools.openToNanoVDB(sphere)
|
||||
grid = handle.grid()
|
||||
print(f"openToNanoVDB: {grid.gridName()!r}, class={grid.gridClass()}, "
|
||||
f"active={grid.activeVoxelCount()}")
|
||||
assert grid.gridClass() == nanovdb.GridClass.LevelSet
|
||||
|
||||
# Compare a cross-section through both accessors, as the C++
|
||||
# accessor example does.
|
||||
open_acc = sphere.getAccessor()
|
||||
nano_acc = grid.getAccessor()
|
||||
for i in range(97, 104):
|
||||
open_v = open_acc.getValue((i, 0, 0))
|
||||
nano_v = nano_acc.getValue(nanovdb.math.Coord(i, 0, 0))
|
||||
print(f" ({i},0,0): openvdb={open_v:.3f} nanovdb={nano_v:.3f}")
|
||||
assert abs(open_v - nano_v) < 1e-5
|
||||
|
||||
# And back: NanoVDB -> OpenVDB.
|
||||
back = nanovdb.tools.nanoToOpenVDB(handle)
|
||||
print(f"nanoToOpenVDB: {back.name!r}, empty={back.empty()}")
|
||||
assert back.name == "sphere"
|
||||
assert not back.empty()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,59 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Quantize a NanoGrid<float> down to a quantized BuildT.
|
||||
|
||||
nanovdb.tools.createNanoGridFp4 / Fp8 / Fp16 / FpN are the quantized
|
||||
counterparts of the regular grid types. Fp4 / Fp8 / Fp16 use a fixed
|
||||
bit-width per voxel. FpN picks the bit-width per leaf so each leaf
|
||||
hits a user-supplied tolerance (the "oracle"). Smaller bit widths
|
||||
give smaller files at the cost of approximation error.
|
||||
|
||||
Run with: python quantize.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def gridSize_in_kb(handle):
|
||||
return handle.gridSize(0) / 1024
|
||||
|
||||
|
||||
def main():
|
||||
# Source: a 50-radius sphere fog volume at full float precision.
|
||||
src_handle = nanovdb.tools.createFogVolumeSphere(radius=50.0)
|
||||
src_grid = src_handle.grid()
|
||||
print(f"Source FloatGrid: {gridSize_in_kb(src_handle):.1f} KB, "
|
||||
f"active voxels = {src_grid.activeVoxelCount()}")
|
||||
|
||||
# Fixed-width quantization. Each subsequent format roughly halves
|
||||
# the per-voxel storage cost; dithering optional.
|
||||
for fn_name, label in [
|
||||
("createNanoGridFp16", "Fp16 (16-bit fixed)"),
|
||||
("createNanoGridFp8", "Fp8 (8-bit fixed)"),
|
||||
("createNanoGridFp4", "Fp4 (4-bit fixed)"),
|
||||
]:
|
||||
h = getattr(nanovdb.tools, fn_name)(src_grid, ditherOn=True)
|
||||
print(f" {label}: {gridSize_in_kb(h):.1f} KB, "
|
||||
f"active voxels = {h.grid().activeVoxelCount()}")
|
||||
|
||||
# Variable-width FpN. The oracle picks the per-leaf bit-width to
|
||||
# meet a tolerance — AbsDiff for absolute error, RelDiff for
|
||||
# relative error. -1 tolerance means "uninitialized" so we pass
|
||||
# an explicit value.
|
||||
abs_oracle = nanovdb.tools.AbsDiff(0.05) # ±0.05 per voxel
|
||||
h_fpn_abs = nanovdb.tools.createNanoGridFpN(src_grid, abs_oracle)
|
||||
print(f" FpN (AbsDiff 0.05): {gridSize_in_kb(h_fpn_abs):.1f} KB")
|
||||
|
||||
rel_oracle = nanovdb.tools.RelDiff(0.1) # 10% relative error
|
||||
h_fpn_rel = nanovdb.tools.createNanoGridFpN(src_grid, rel_oracle)
|
||||
print(f" FpN (RelDiff 0.10): {gridSize_in_kb(h_fpn_rel):.1f} KB")
|
||||
|
||||
# The output is a regular NanoGrid<FpN> — read-only, but exposes
|
||||
# the standard surface (gridType, activeVoxelCount, accessor.getValue
|
||||
# returning a decoded float, etc).
|
||||
fpn = h_fpn_abs.grid()
|
||||
print(f"\nFpN grid type: {fpn.gridType()}, "
|
||||
f"voxel at (0,0,0) = {fpn.getAccessor().getValue(nanovdb.math.Coord(0, 0, 0))}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,95 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Ray-march a fog volume on the CPU and write a PGM image.
|
||||
|
||||
Python port of the host path of ex_raytrace_fog_volume: each ray is
|
||||
clipped to the grid's index bounding box, then integrated with fixed
|
||||
steps, accumulating transmittance from the density sampled through a
|
||||
ReadAccessor at Coord.Floor of the march position — the accessor-based
|
||||
idiom, in contrast to the sampler-based raytrace_level_set.py. Every
|
||||
step is one Python-to-C++ call, so the default RES is modest; raise it
|
||||
for a nicer image.
|
||||
|
||||
Run with: python raytrace_fog_volume.py
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import nanovdb
|
||||
|
||||
RES = 64 # image is RES x RES pixels
|
||||
FOV_DEG = 45.0
|
||||
DT = 1.0 # march step, in voxels
|
||||
SIGMA = 0.2 # extinction scale applied to the sampled density
|
||||
|
||||
|
||||
def clip_to_bbox(eye, direction, bbox):
|
||||
"""Slab-clip a ray against a CoordBBox; returns (t0, t1) or None."""
|
||||
t0, t1 = 0.0, math.inf
|
||||
for axis in range(3):
|
||||
lo = bbox.min[axis] - eye[axis]
|
||||
hi = bbox.max[axis] + 1.0 - eye[axis]
|
||||
d = direction[axis]
|
||||
if abs(d) < 1e-12:
|
||||
if lo > 0.0 or hi < 0.0:
|
||||
return None
|
||||
continue
|
||||
near, far = lo / d, hi / d
|
||||
if near > far:
|
||||
near, far = far, near
|
||||
t0, t1 = max(t0, near), min(t1, far)
|
||||
if t0 > t1:
|
||||
return None
|
||||
return t0, t1
|
||||
|
||||
|
||||
def main():
|
||||
handle = nanovdb.tools.createFogVolumeSphere(radius=50.0, name="fog")
|
||||
grid = handle.grid()
|
||||
bbox = grid.indexBBox()
|
||||
acc = grid.getAccessor()
|
||||
|
||||
# Perspective camera looking down -z, as in the C++ RayGenOp,
|
||||
# working directly in index space (the grid transform is uniform).
|
||||
dim = [bbox.max[i] + 1 - bbox.min[i] for i in range(3)]
|
||||
center = [bbox.min[i] + 0.5 * dim[i] for i in range(3)]
|
||||
eye = (center[0], center[1], center[2] + 2.0 * dim[2])
|
||||
tan_fov = math.tan(math.radians(FOV_DEG) * 0.5)
|
||||
|
||||
pixels = bytearray(RES * RES)
|
||||
for y in range(RES):
|
||||
for x in range(RES):
|
||||
px = (2.0 * (x + 0.5) / RES - 1.0) * tan_fov
|
||||
py = (2.0 * (y + 0.5) / RES - 1.0) * tan_fov
|
||||
norm = math.sqrt(px * px + py * py + 1.0)
|
||||
direction = (px / norm, py / norm, -1.0 / norm)
|
||||
|
||||
span = clip_to_bbox(eye, direction, bbox)
|
||||
if span is None:
|
||||
continue
|
||||
transmittance = 1.0
|
||||
t = span[0]
|
||||
while t < span[1]:
|
||||
pos = nanovdb.math.Vec3d(eye[0] + t * direction[0],
|
||||
eye[1] + t * direction[1],
|
||||
eye[2] + t * direction[2])
|
||||
density = acc.getValue(nanovdb.math.Coord.Floor(pos))
|
||||
transmittance *= 1.0 - density * SIGMA * DT
|
||||
if transmittance < 0.005:
|
||||
break
|
||||
t += DT
|
||||
pixels[y * RES + x] = int(255 * (1.0 - transmittance))
|
||||
|
||||
out_dir = tempfile.mkdtemp(prefix="nanovdb_")
|
||||
path = os.path.join(out_dir, "raytrace_fog_volume.pgm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(f"P5\n{RES} {RES}\n255\n".encode("ascii"))
|
||||
f.write(bytes(pixels))
|
||||
lit = sum(1 for p in pixels if p > 0)
|
||||
print(f"Rendered {RES}x{RES} image, {lit} foggy pixels -> {path}")
|
||||
assert lit > 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,120 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Ray-trace a narrow-band level set on the CPU and write a PGM image.
|
||||
|
||||
Python port of the host path of ex_raytrace_level_set. The C++ example
|
||||
finds surface hits with math::Ray + HDDA ZeroCrossing, which are not
|
||||
bound in Python; this port re-expresses the search as sphere tracing —
|
||||
the clamped SDF value itself bounds the safe step size — using the
|
||||
trilinear sampler, with sampler.zeroCrossing() confirming the interval
|
||||
and sampler.gradient() shading the hit. Every sample is one
|
||||
Python-to-C++ call, so the default RES is modest; raise it for a nicer
|
||||
image.
|
||||
|
||||
Run with: python raytrace_level_set.py
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import nanovdb
|
||||
|
||||
RES = 64 # image is RES x RES pixels
|
||||
FOV_DEG = 45.0
|
||||
LIGHT = (0.577, 0.577, 0.577) # unit vector toward the light
|
||||
|
||||
|
||||
def clip_to_bbox(eye, direction, bbox):
|
||||
"""Slab-clip a ray against a CoordBBox; returns (t0, t1) or None."""
|
||||
t0, t1 = 0.0, math.inf
|
||||
for axis in range(3):
|
||||
lo = bbox.min[axis] - eye[axis]
|
||||
hi = bbox.max[axis] + 1.0 - eye[axis]
|
||||
d = direction[axis]
|
||||
if abs(d) < 1e-12:
|
||||
if lo > 0.0 or hi < 0.0:
|
||||
return None
|
||||
continue
|
||||
near, far = lo / d, hi / d
|
||||
if near > far:
|
||||
near, far = far, near
|
||||
t0, t1 = max(t0, near), min(t1, far)
|
||||
if t0 > t1:
|
||||
return None
|
||||
return t0, t1
|
||||
|
||||
|
||||
def trace(sampler, eye, direction, t0, t1, voxel_size):
|
||||
"""Sphere-trace from t0 to t1; returns (hit t, crossing seen) or None."""
|
||||
t = t0
|
||||
while t < t1:
|
||||
pos = nanovdb.math.Vec3d(eye[0] + t * direction[0],
|
||||
eye[1] + t * direction[1],
|
||||
eye[2] + t * direction[2])
|
||||
d = sampler(pos) # world-unit SDF, clamped to the narrow band
|
||||
if d <= 0.0:
|
||||
# zeroCrossing() reports whether the reconstruction stencil
|
||||
# here straddles the iso-surface — the sampler-level analog
|
||||
# of the HDDA ZeroCrossing test in the C++ example.
|
||||
return t, sampler.zeroCrossing(pos)
|
||||
# The clamped SDF bounds the distance to the surface, so it is
|
||||
# a safe (index-space) step size; never step below half a voxel.
|
||||
t += max(d / voxel_size, 0.5)
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
handle = nanovdb.tools.createLevelSetSphere(radius=100.0, name="sphere")
|
||||
grid = handle.grid()
|
||||
bbox = grid.indexBBox()
|
||||
voxel_size = grid.voxelSize()[0]
|
||||
sampler = nanovdb.math.createTrilinearSampler(grid)
|
||||
|
||||
# Perspective camera looking down -z, as in the C++ RayGenOp,
|
||||
# working directly in index space (the grid transform is uniform).
|
||||
dim = [bbox.max[i] + 1 - bbox.min[i] for i in range(3)]
|
||||
center = [bbox.min[i] + 0.5 * dim[i] for i in range(3)]
|
||||
eye = nanovdb.math.Vec3d(center[0], center[1], center[2] + 2.0 * dim[2])
|
||||
tan_fov = math.tan(math.radians(FOV_DEG) * 0.5)
|
||||
|
||||
pixels = bytearray(RES * RES)
|
||||
crossings = 0
|
||||
for y in range(RES):
|
||||
for x in range(RES):
|
||||
px = (2.0 * (x + 0.5) / RES - 1.0) * tan_fov
|
||||
py = (2.0 * (y + 0.5) / RES - 1.0) * tan_fov
|
||||
norm = math.sqrt(px * px + py * py + 1.0)
|
||||
direction = (px / norm, py / norm, -1.0 / norm)
|
||||
|
||||
span = clip_to_bbox(eye, direction, bbox)
|
||||
if span is None:
|
||||
continue
|
||||
hit = trace(sampler, eye, direction, span[0], span[1],
|
||||
voxel_size)
|
||||
if hit is None:
|
||||
continue
|
||||
t_hit, crossed = hit
|
||||
crossings += crossed
|
||||
pos = nanovdb.math.Vec3d(eye[0] + t_hit * direction[0],
|
||||
eye[1] + t_hit * direction[1],
|
||||
eye[2] + t_hit * direction[2])
|
||||
# gradient() is in index-space units — normalize before use.
|
||||
n = sampler.gradient(pos)
|
||||
n.normalize()
|
||||
shade = max(0.0, n[0] * LIGHT[0] + n[1] * LIGHT[1]
|
||||
+ n[2] * LIGHT[2])
|
||||
pixels[y * RES + x] = int(255 * shade)
|
||||
|
||||
out_dir = tempfile.mkdtemp(prefix="nanovdb_")
|
||||
path = os.path.join(out_dir, "raytrace_level_set.pgm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(f"P5\n{RES} {RES}\n255\n".encode("ascii"))
|
||||
f.write(bytes(pixels))
|
||||
lit = sum(1 for p in pixels if p > 0)
|
||||
print(f"Rendered {RES}x{RES} image, {lit} lit pixels "
|
||||
f"({crossings} confirmed zero-crossings) -> {path}")
|
||||
assert lit > 0 and crossings > 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Validate a NanoVDB grid and round-trip its checksum.
|
||||
|
||||
nanovdb.tools exposes the grid-quality surface: validateGrid (single
|
||||
grid), checkGrid -> (ok, error), isValid, plus the checksum helpers
|
||||
evalChecksum / validateChecksum / updateChecksum. This example walks
|
||||
through a typical "load and verify" workflow.
|
||||
|
||||
Run with: python validate.py
|
||||
"""
|
||||
import nanovdb
|
||||
|
||||
|
||||
def main():
|
||||
# A well-formed grid: createLevelSetSphere returns a NanoGrid<float>
|
||||
# with stats and checksum populated to the defaults.
|
||||
handle = nanovdb.tools.createLevelSetSphere(radius=10.0)
|
||||
grid = handle.grid()
|
||||
|
||||
# Whole-handle validation. validateGrids returns a single bool.
|
||||
# The verbose=True flag prints failure details to std::cerr
|
||||
# (visible from Python via std::cerr -> sys.stderr on most stdlibs).
|
||||
all_ok = nanovdb.tools.validateGrids(
|
||||
handle, nanovdb.CheckMode.Default, verbose=False)
|
||||
print(f"validateGrids(handle, Default) = {all_ok}")
|
||||
|
||||
# Per-grid validation, with a helpful message on failure.
|
||||
one_ok = nanovdb.tools.validateGrid(handle, 0, nanovdb.CheckMode.Full)
|
||||
print(f"validateGrid(handle, 0, Full) = {one_ok}")
|
||||
|
||||
# validateGrid returns False (no raise) on out-of-range gridID;
|
||||
# CheckMode.Disable short-circuits and always returns True.
|
||||
print(f"validateGrid(handle, 99) = "
|
||||
f"{nanovdb.tools.validateGrid(handle, 99)} (out of range)")
|
||||
print(f"validateGrid(handle, 99, Disable) = "
|
||||
f"{nanovdb.tools.validateGrid(handle, 99, nanovdb.CheckMode.Disable)}"
|
||||
f" (Disable short-circuit)")
|
||||
|
||||
# tools.checkGrid returns the structural check result plus a
|
||||
# human-readable error message (empty on success).
|
||||
ok, msg = nanovdb.tools.checkGrid(grid, nanovdb.CheckMode.Full)
|
||||
print(f"checkGrid(grid, Full) = ok={ok}, msg={msg!r}")
|
||||
|
||||
# tools.isValid is checkGrid + checksum verification, returning
|
||||
# one bool.
|
||||
print(f"isValid(grid, Default) = "
|
||||
f"{nanovdb.tools.isValid(grid, nanovdb.CheckMode.Default)}")
|
||||
|
||||
# Checksum round-trip. evalChecksum is non-mutating; updateChecksum
|
||||
# writes back into the grid header.
|
||||
cs1 = nanovdb.tools.evalChecksum(grid, nanovdb.CheckMode.Full)
|
||||
nanovdb.tools.updateChecksum(grid, nanovdb.CheckMode.Full)
|
||||
cs2 = nanovdb.tools.evalChecksum(grid, nanovdb.CheckMode.Full)
|
||||
print(f"evalChecksum equal after no-op update: {cs1 == cs2}")
|
||||
print(f"validateChecksum(grid, Full) = "
|
||||
f"{nanovdb.tools.validateChecksum(grid, nanovdb.CheckMode.Full)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Smoke test: run every example script and require a clean exit.
|
||||
|
||||
The examples are self-contained and degrade gracefully (exit 0 with a
|
||||
skip message) when an optional dependency such as NumPy or OpenVDB is
|
||||
missing, so a non-zero exit or an uncaught exception always indicates
|
||||
a real breakage — typically the bindings drifting under the examples.
|
||||
|
||||
The script list is deliberately explicit rather than a glob so that a
|
||||
missing listed example fails loudly and unrelated files placed under
|
||||
examples/ are not executed.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
EXAMPLES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
os.pardir, "examples")
|
||||
|
||||
EXAMPLE_SCRIPTS = [
|
||||
"build_grid.py",
|
||||
"bulk_leaf_numpy.py",
|
||||
"collide_level_set.py",
|
||||
"index_grid_channels.py",
|
||||
"io_roundtrip.py",
|
||||
"load_inspect.py",
|
||||
"make_funny_nanovdb.py",
|
||||
"make_typed_grids.py",
|
||||
"node_manager.py",
|
||||
"openvdb_interop.py",
|
||||
"quantize.py",
|
||||
"raytrace_fog_volume.py",
|
||||
"raytrace_level_set.py",
|
||||
"validate.py",
|
||||
]
|
||||
|
||||
|
||||
class TestExamples(unittest.TestCase):
|
||||
pass
|
||||
|
||||
|
||||
def _make_test(script_name):
|
||||
def test(self):
|
||||
script = os.path.join(EXAMPLES_DIR, script_name)
|
||||
self.assertTrue(os.path.isfile(script),
|
||||
f"example script is missing: {script}")
|
||||
command = [sys.executable, script]
|
||||
env = os.environ.copy()
|
||||
# Preserve output up to a native crash and ask Python to report the
|
||||
# active frame for fatal signals and Windows exceptions.
|
||||
env["PYTHONFAULTHANDLER"] = "1"
|
||||
env["PYTHONUNBUFFERED"] = "1"
|
||||
if hasattr(os, "add_dll_directory"):
|
||||
# Match TestNanoVDB.py's in-tree OpenVDB DLL lookup. The child
|
||||
# starts in a temporary directory, so resolve this while the
|
||||
# parent is still in the CMake configuration directory.
|
||||
config = os.path.basename(os.getcwd())
|
||||
openvdb_dll_directory = os.path.abspath(os.path.join(
|
||||
os.getcwd(), os.pardir, os.pardir, os.pardir, os.pardir,
|
||||
"openvdb", "openvdb", config))
|
||||
dll_directories = [openvdb_dll_directory]
|
||||
cuda_root = env.get("CUDA_PATH")
|
||||
if cuda_root:
|
||||
dll_directories.append(os.path.join(cuda_root, "bin"))
|
||||
env["NANOVDB_TEST_DLL_DIRECTORIES"] = os.pathsep.join(
|
||||
dll_directories)
|
||||
# Python imports sitecustomize during startup. Add this test
|
||||
# directory to the child's module path so sitecustomize.py can
|
||||
# register only the required OpenVDB and CUDA DLL directories
|
||||
# before the example imports nanovdb, while still running the
|
||||
# example as a normal script.
|
||||
test_directory = os.path.dirname(os.path.abspath(__file__))
|
||||
env["PYTHONPATH"] = os.pathsep.join(
|
||||
(test_directory, env.get("PYTHONPATH", "")))
|
||||
# A fresh cwd per run keeps any output files out of the source
|
||||
# tree (the examples themselves also write to tempdirs).
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=tempfile.mkdtemp(prefix="nanovdb_example_"),
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
self.assertEqual(
|
||||
result.returncode, 0,
|
||||
f"{script_name} exited with {result.returncode}\n"
|
||||
f"--- stdout ---\n{result.stdout}\n"
|
||||
f"--- stderr ---\n{result.stderr}")
|
||||
|
||||
return test
|
||||
|
||||
|
||||
for _name in EXAMPLE_SCRIPTS:
|
||||
_test_name = "test_" + _name.replace(".py", "")
|
||||
setattr(TestExamples, _test_name, _make_test(_name))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright Contributors to the OpenVDB Project
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Configure dependent-DLL lookup for in-tree Windows Python tests."""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
# Keep these handles alive for the lifetime of the child Python process.
|
||||
_dll_directory_handles = []
|
||||
|
||||
if hasattr(os, "add_dll_directory"):
|
||||
for _path in os.environ.get(
|
||||
"NANOVDB_TEST_DLL_DIRECTORIES", "").split(os.pathsep):
|
||||
if os.path.isdir(_path):
|
||||
try:
|
||||
_dll_directory_handles.append(os.add_dll_directory(_path))
|
||||
except OSError:
|
||||
pass
|
||||
@@ -373,13 +373,6 @@ public:
|
||||
template<typename PtrT>
|
||||
void processPoints(const PtrT points, size_t pointCount);
|
||||
|
||||
// Only instantiated when BuildT == Point, from the corresponding branch of
|
||||
// processPoints. Holds the encode kernels because nvcc rejects an extended
|
||||
// lambda defined directly inside the block of an if constexpr statement on
|
||||
// some host compilers.
|
||||
template<typename PtrT>
|
||||
void encodePoints(const PtrT points, size_t pointCount);
|
||||
|
||||
void processBBox();
|
||||
|
||||
// the following methods are only defined when BuildT == Point
|
||||
@@ -1171,24 +1164,23 @@ inline void PointsToGrid<BuildT, ResourceT>::processLeafNodes(size_t pointCount)
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// The point-encode kernels operate on device data and are independent of the
|
||||
// resource type; only the trailing d_indx deallocation routes through ResourceT.
|
||||
// Dispatching the encode with if constexpr keeps it reachable for any ResourceT.
|
||||
template<typename BuildT, typename ResourceT>
|
||||
template<typename PtrT>
|
||||
inline void PointsToGrid<BuildT, ResourceT>::processPoints(const PtrT points, size_t pointCount)
|
||||
inline void PointsToGrid<BuildT, ResourceT>::processPoints(const PtrT, size_t pointCount)
|
||||
{
|
||||
if constexpr(util::is_same<BuildT, Point>::value) this->encodePoints(points, pointCount);
|
||||
mResource->deallocate_async(mData.d_indx, pointCount*sizeof(uint32_t), ResourceT::DEFAULT_ALIGNMENT, mStream);
|
||||
}// PointsToGrid<BuildT, ResourceT>::processPoints
|
||||
}
|
||||
|
||||
template<typename BuildT, typename ResourceT>
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Template specialization with BuildT = Point
|
||||
template<>
|
||||
template<typename PtrT>
|
||||
inline void PointsToGrid<BuildT, ResourceT>::encodePoints(const PtrT points, size_t pointCount)
|
||||
inline void PointsToGrid<Point>::processPoints(const PtrT points, size_t pointCount)
|
||||
{
|
||||
switch (mPointType){
|
||||
case PointType::Disable:
|
||||
throw std::runtime_error("PointsToGrid<Point, ResourceT>::encodePoints: mPointType == PointType::Disable\n");
|
||||
throw std::runtime_error("PointsToGrid<Point>::processPoints: mPointType == PointType::Disable\n");
|
||||
case PointType::PointID:
|
||||
util::cuda::lambdaKernel<<<numBlocks(pointCount), mNumThreads, 0, mStream>>>(pointCount, [=] __device__(size_t tid, PointsToGridData<Point> *d_data) {
|
||||
d_data->template getPoint<uint32_t>(tid) = d_data->d_indx[tid];
|
||||
@@ -1235,9 +1227,12 @@ inline void PointsToGrid<BuildT, ResourceT>::encodePoints(const PtrT points, siz
|
||||
}, mDeviceData); cudaCheckError();
|
||||
break;
|
||||
default:
|
||||
printf("Internal error in PointsToGrid<Point, ResourceT>::encodePoints\n");
|
||||
printf("Internal error in PointsToGrid<Point>::processPoints\n");
|
||||
}
|
||||
}// PointsToGrid<BuildT, ResourceT>::encodePoints
|
||||
// This is the PointsToGrid<Point> (== <Point, DeviceResource>) member specialization,
|
||||
// so ResourceT is not a name here; mResource is a DeviceResource* and the alignment is concrete.
|
||||
mResource->deallocate_async(mData.d_indx, pointCount*sizeof(uint32_t), nanovdb::cuda::DeviceResource::DEFAULT_ALIGNMENT, mStream);
|
||||
}// PointsToGrid<Point>::processPoints
|
||||
|
||||
//-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -62,23 +62,6 @@ if(NANOVDB_USE_CUDA)
|
||||
target_link_libraries(nanovdb_test_cuda_memory_resource PRIVATE nanovdb GTest::GTest GTest::Main)
|
||||
set_target_properties(nanovdb_test_cuda_memory_resource PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
add_test(nanovdb_cuda_memory_resource_unit_test nanovdb_test_cuda_memory_resource)
|
||||
|
||||
add_executable(nanovdb_test_cuda_util "TestUtilCuda.cu")
|
||||
target_link_libraries(nanovdb_test_cuda_util PRIVATE nanovdb GTest::GTest GTest::Main)
|
||||
set_target_properties(nanovdb_test_cuda_util PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
add_test(nanovdb_cuda_util_unit_test nanovdb_test_cuda_util)
|
||||
|
||||
# Same tests with every allocation forced through cudaMalloc/cudaFree --
|
||||
# the configuration a pool-less vGPU relies on.
|
||||
add_executable(nanovdb_test_cuda_util_sync "TestUtilCuda.cu")
|
||||
target_compile_definitions(nanovdb_test_cuda_util_sync PRIVATE NANOVDB_USE_SYNC_CUDA_MALLOC)
|
||||
target_link_libraries(nanovdb_test_cuda_util_sync PRIVATE nanovdb GTest::GTest GTest::Main)
|
||||
set_target_properties(nanovdb_test_cuda_util_sync PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
add_test(nanovdb_cuda_util_sync_unit_test nanovdb_test_cuda_util_sync)
|
||||
add_executable(nanovdb_test_cuda_buffer "TestBuffer.cu")
|
||||
target_link_libraries(nanovdb_test_cuda_buffer PRIVATE nanovdb GTest::GTest GTest::Main)
|
||||
set_target_properties(nanovdb_test_cuda_buffer PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
|
||||
add_test(nanovdb_cuda_buffer_unit_test nanovdb_test_cuda_buffer)
|
||||
endif()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -1,631 +0,0 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// @file TestBuffer.cu
|
||||
///
|
||||
/// @brief Unit tests for the typed, resource-aware CUDA containers
|
||||
/// (cuda::Buffer, cuda::BufferView) and the synchronous Resource
|
||||
/// trait (cuda::is_resource).
|
||||
|
||||
#include <nanovdb/GridHandle.h>
|
||||
#include <nanovdb/cuda/Buffer.h>
|
||||
#include <nanovdb/cuda/DeviceResource.h>
|
||||
#include <nanovdb/cuda/PinnedResource.h>
|
||||
#include <nanovdb/tools/CreatePrimitives.h>
|
||||
|
||||
#include <cuda_runtime_api.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
//======================================================================
|
||||
// Shared test doubles
|
||||
//======================================================================
|
||||
|
||||
/// @brief Bookkeeping shared by all copies of a CountingResource, so counts
|
||||
/// survive the resource being held by value inside a Buffer.
|
||||
struct Counters
|
||||
{
|
||||
int allocs = 0;
|
||||
int deallocs = 0;
|
||||
size_t allocBytes = 0; // bytes of the most recent allocation
|
||||
size_t deallocBytes = 0; // bytes of the most recent deallocation
|
||||
};
|
||||
|
||||
/// @brief Resource that counts (non-null) allocations and deallocations so
|
||||
/// leaks can be asserted. Delegates the actual work to DeviceResource.
|
||||
struct CountingResource
|
||||
{
|
||||
static constexpr size_t DEFAULT_ALIGNMENT = nanovdb::cuda::DeviceResource::DEFAULT_ALIGNMENT;
|
||||
Counters* counters = nullptr;
|
||||
void* allocate_async(size_t bytes, size_t alignment, cudaStream_t stream) {
|
||||
void* p = nanovdb::cuda::DeviceResource{}.allocate_async(bytes, alignment, stream);
|
||||
if (p) { ++counters->allocs; counters->allocBytes = bytes; }
|
||||
return p;
|
||||
}
|
||||
void deallocate_async(void* p, size_t bytes, size_t alignment, cudaStream_t stream) {
|
||||
if (p) { ++counters->deallocs; counters->deallocBytes = bytes; }
|
||||
nanovdb::cuda::DeviceResource{}.deallocate_async(p, bytes, alignment, stream);
|
||||
}
|
||||
void* allocate(size_t bytes, size_t alignment) {
|
||||
void* p = nanovdb::cuda::DeviceResource{}.allocate(bytes, alignment);
|
||||
if (p) { ++counters->allocs; counters->allocBytes = bytes; }
|
||||
return p;
|
||||
}
|
||||
void deallocate(void* p, size_t bytes, size_t alignment) {
|
||||
if (p) { ++counters->deallocs; counters->deallocBytes = bytes; }
|
||||
nanovdb::cuda::DeviceResource{}.deallocate(p, bytes, alignment);
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief Streams recorded by all copies of a StreamRecordingResource.
|
||||
struct StreamLog
|
||||
{
|
||||
std::vector<cudaStream_t> allocStreams;
|
||||
std::vector<cudaStream_t> deallocStreams;
|
||||
};
|
||||
|
||||
/// @brief Resource that records the stream of every allocation/deallocation,
|
||||
/// to verify stream-ordered teardown. Delegates work to DeviceResource.
|
||||
struct StreamRecordingResource
|
||||
{
|
||||
static constexpr size_t DEFAULT_ALIGNMENT = nanovdb::cuda::DeviceResource::DEFAULT_ALIGNMENT;
|
||||
StreamLog* log = nullptr;
|
||||
void* allocate_async(size_t bytes, size_t alignment, cudaStream_t stream) {
|
||||
void* p = nanovdb::cuda::DeviceResource{}.allocate_async(bytes, alignment, stream);
|
||||
if (p) log->allocStreams.push_back(stream);
|
||||
return p;
|
||||
}
|
||||
void deallocate_async(void* p, size_t bytes, size_t alignment, cudaStream_t stream) {
|
||||
if (p) log->deallocStreams.push_back(stream);
|
||||
nanovdb::cuda::DeviceResource{}.deallocate_async(p, bytes, alignment, stream);
|
||||
}
|
||||
void* allocate(size_t bytes, size_t alignment) {
|
||||
void* p = nanovdb::cuda::DeviceResource{}.allocate(bytes, alignment);
|
||||
if (p) log->allocStreams.push_back(cudaStream_t(0));
|
||||
return p;
|
||||
}
|
||||
void deallocate(void* p, size_t bytes, size_t alignment) {
|
||||
if (p) log->deallocStreams.push_back(cudaStream_t(0));
|
||||
nanovdb::cuda::DeviceResource{}.deallocate(p, bytes, alignment);
|
||||
}
|
||||
};
|
||||
|
||||
//======================================================================
|
||||
// Resource concept traits: is_resource (synchronous) / is_async_resource
|
||||
//======================================================================
|
||||
|
||||
/// @brief Local double that provides both the synchronous and the
|
||||
/// stream-ordered allocation interface, like a CCCL-style resource.
|
||||
struct DualResource
|
||||
{
|
||||
static constexpr size_t DEFAULT_ALIGNMENT = 256;
|
||||
void* allocate(size_t, size_t) { return nullptr; }
|
||||
void deallocate(void*, size_t, size_t) {}
|
||||
void* allocate_async(size_t, size_t, cudaStream_t) { return nullptr; }
|
||||
void deallocate_async(void*, size_t, size_t, cudaStream_t) {}
|
||||
};
|
||||
|
||||
// PinnedResource is synchronous-only: allocate/deallocate without a stream.
|
||||
static_assert(nanovdb::cuda::is_resource<nanovdb::cuda::PinnedResource>::value,
|
||||
"PinnedResource must satisfy the synchronous Resource concept");
|
||||
static_assert(!nanovdb::cuda::is_async_resource<nanovdb::cuda::PinnedResource>::value,
|
||||
"PinnedResource must not satisfy the AsyncResource concept");
|
||||
|
||||
// DeviceResource models the AsyncResource refinement: both the
|
||||
// stream-ordered and the synchronous interface (as in CCCL's cuda::mr).
|
||||
static_assert(nanovdb::cuda::is_async_resource<nanovdb::cuda::DeviceResource>::value,
|
||||
"DeviceResource must satisfy the AsyncResource concept");
|
||||
static_assert(nanovdb::cuda::is_resource<nanovdb::cuda::DeviceResource>::value,
|
||||
"DeviceResource must also satisfy the synchronous Resource concept");
|
||||
|
||||
/// @brief Local double with only the stream-ordered interface; under the
|
||||
/// refinement this is NOT an async resource (the sync pair is missing).
|
||||
struct AsyncOnlyResource
|
||||
{
|
||||
static constexpr size_t DEFAULT_ALIGNMENT = 256;
|
||||
void* allocate_async(size_t, size_t, cudaStream_t) { return nullptr; }
|
||||
void deallocate_async(void*, size_t, size_t, cudaStream_t) {}
|
||||
};
|
||||
static_assert(!nanovdb::cuda::is_async_resource<AsyncOnlyResource>::value,
|
||||
"the async pair alone must not satisfy the AsyncResource refinement");
|
||||
static_assert(!nanovdb::cuda::is_resource<AsyncOnlyResource>::value,
|
||||
"the async pair alone must not satisfy the Resource concept");
|
||||
|
||||
// An unrelated type satisfies neither concept.
|
||||
static_assert(!nanovdb::cuda::is_resource<int>::value,
|
||||
"int must not satisfy the Resource concept");
|
||||
static_assert(!nanovdb::cuda::is_async_resource<int>::value,
|
||||
"int must not satisfy the AsyncResource concept");
|
||||
|
||||
// A resource with both interfaces satisfies both concepts.
|
||||
static_assert(nanovdb::cuda::is_resource<DualResource>::value,
|
||||
"DualResource must satisfy the synchronous Resource concept");
|
||||
static_assert(nanovdb::cuda::is_async_resource<DualResource>::value,
|
||||
"DualResource must satisfy the AsyncResource concept");
|
||||
|
||||
TEST(TestBuffer, ResourceTraits)
|
||||
{
|
||||
// The static_asserts above are the real test; this anchors them in a
|
||||
// runnable suite entry.
|
||||
EXPECT_TRUE(nanovdb::cuda::is_resource<nanovdb::cuda::PinnedResource>::value);
|
||||
EXPECT_TRUE(nanovdb::cuda::is_async_resource<nanovdb::cuda::DeviceResource>::value);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// Buffer<T, R> with a stream-ordered (async) resource: construction,
|
||||
// destruction, element counting, value-initialization
|
||||
//======================================================================
|
||||
|
||||
TEST(TestBuffer, AsyncCountingAllocFree)
|
||||
{
|
||||
cudaStream_t s = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&s), cudaSuccess);
|
||||
Counters c;
|
||||
const size_t n = 100;
|
||||
{
|
||||
nanovdb::cuda::Buffer<float, CountingResource> buf(s, CountingResource{&c}, n, nanovdb::cuda::noInit);
|
||||
EXPECT_NE(buf.data(), nullptr);
|
||||
EXPECT_EQ(buf.size(), n); // elements, not bytes
|
||||
EXPECT_EQ(buf.size_bytes(), n * sizeof(float));
|
||||
EXPECT_FALSE(buf.empty());
|
||||
EXPECT_EQ(c.allocs, 1); // exactly one allocation
|
||||
EXPECT_EQ(c.allocBytes, n * sizeof(float));
|
||||
EXPECT_EQ(c.deallocs, 0);
|
||||
}
|
||||
EXPECT_EQ(c.deallocs, 1); // exactly one deallocation
|
||||
EXPECT_EQ(c.deallocBytes, n * sizeof(float)); // with identical size
|
||||
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(s), cudaSuccess);
|
||||
}
|
||||
|
||||
TEST(TestBuffer, EmptyBuffersAllocateNothing)
|
||||
{
|
||||
Counters c;
|
||||
{
|
||||
nanovdb::cuda::Buffer<float, CountingResource> def; // default ctor: empty
|
||||
EXPECT_EQ(def.data(), nullptr);
|
||||
EXPECT_EQ(def.size(), 0u);
|
||||
EXPECT_EQ(def.size_bytes(), 0u);
|
||||
EXPECT_TRUE(def.empty());
|
||||
|
||||
nanovdb::cuda::Buffer<float, CountingResource> zero(0, CountingResource{&c}, 0, nanovdb::cuda::noInit);
|
||||
EXPECT_EQ(zero.data(), nullptr);
|
||||
EXPECT_TRUE(zero.empty());
|
||||
}
|
||||
EXPECT_EQ(c.allocs, 0);
|
||||
EXPECT_EQ(c.deallocs, 0);
|
||||
}
|
||||
|
||||
TEST(TestBuffer, CountConstructionRequiresNoInit)
|
||||
{
|
||||
// A count without NoInit must not construct: implicit initialization of
|
||||
// freshly allocated memory is a hidden fill pass the dominant
|
||||
// allocate-then-overwrite pattern wastes (the cuda::buffer rule).
|
||||
using DevBuf = nanovdb::cuda::Buffer<int>;
|
||||
using PinBuf = nanovdb::cuda::Buffer<int, nanovdb::cuda::PinnedResource>;
|
||||
static_assert(!std::is_constructible<DevBuf, cudaStream_t, size_t>::value,
|
||||
"async count c-tor without NoInit must not exist");
|
||||
static_assert(!std::is_constructible<DevBuf, cudaStream_t, nanovdb::cuda::DeviceResource, size_t>::value,
|
||||
"async count+resource c-tor without NoInit must not exist");
|
||||
static_assert(!std::is_constructible<PinBuf, size_t>::value,
|
||||
"sync count c-tor without NoInit must not exist");
|
||||
static_assert(!std::is_constructible<PinBuf, nanovdb::cuda::PinnedResource, size_t>::value,
|
||||
"sync count+resource c-tor without NoInit must not exist");
|
||||
static_assert(std::is_constructible<DevBuf, cudaStream_t, size_t, nanovdb::cuda::NoInit>::value,
|
||||
"async count c-tor with NoInit exists");
|
||||
static_assert(std::is_constructible<PinBuf, size_t, nanovdb::cuda::NoInit>::value,
|
||||
"sync count c-tor with NoInit exists");
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
TEST(TestBuffer, OversizedCountThrows)
|
||||
{
|
||||
// An element count whose byte size overflows size_t must be rejected up
|
||||
// front rather than silently wrapping into a tiny allocation.
|
||||
const size_t oversized = std::numeric_limits<size_t>::max() / sizeof(int) + 1;
|
||||
EXPECT_THROW((nanovdb::cuda::Buffer<int>(0, oversized, nanovdb::cuda::noInit)), std::runtime_error);
|
||||
EXPECT_THROW((nanovdb::cuda::Buffer<int, nanovdb::cuda::PinnedResource>(oversized, nanovdb::cuda::noInit)), std::runtime_error);
|
||||
nanovdb::cuda::Buffer<int> buf(0, 8, nanovdb::cuda::noInit);
|
||||
cudaStream_t other = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&other), cudaSuccess);
|
||||
EXPECT_THROW(buf.resize(oversized, other), std::runtime_error);
|
||||
EXPECT_EQ(buf.size(), 8u); // failed resize leaves the buffer untouched --
|
||||
EXPECT_EQ(buf.stream(), cudaStream_t(0)); // including its retained stream
|
||||
ASSERT_EQ(cudaStreamDestroy(other), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(0), cudaSuccess);
|
||||
}
|
||||
|
||||
TEST(TestBuffer, ClearFreesAndEmpties)
|
||||
{
|
||||
Counters c;
|
||||
nanovdb::cuda::Buffer<float, CountingResource> buf(0, CountingResource{&c}, 64, nanovdb::cuda::noInit);
|
||||
ASSERT_EQ(c.allocs, 1);
|
||||
buf.clear();
|
||||
EXPECT_EQ(buf.data(), nullptr);
|
||||
EXPECT_EQ(buf.size(), 0u);
|
||||
EXPECT_TRUE(buf.empty());
|
||||
EXPECT_EQ(c.deallocs, 1);
|
||||
buf.clear(); // idempotent: no second free
|
||||
EXPECT_EQ(c.deallocs, 1);
|
||||
ASSERT_EQ(cudaStreamSynchronize(0), cudaSuccess);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// Stream retention: the destructor and resize free on the retained stream
|
||||
// (stream of the most recent allocation, or the one supplied via setStream)
|
||||
//======================================================================
|
||||
|
||||
TEST(TestBuffer, DestructorFreesOnAllocationStream)
|
||||
{
|
||||
cudaStream_t a = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&a), cudaSuccess);
|
||||
StreamLog log;
|
||||
{
|
||||
nanovdb::cuda::Buffer<float, StreamRecordingResource> buf(a, StreamRecordingResource{&log}, 32, nanovdb::cuda::noInit);
|
||||
EXPECT_EQ(buf.stream(), a);
|
||||
}
|
||||
ASSERT_EQ(log.allocStreams.size(), 1u);
|
||||
EXPECT_EQ(log.allocStreams.back(), a);
|
||||
ASSERT_EQ(log.deallocStreams.size(), 1u);
|
||||
EXPECT_EQ(log.deallocStreams.back(), a);
|
||||
ASSERT_EQ(cudaStreamSynchronize(a), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(a), cudaSuccess);
|
||||
}
|
||||
|
||||
TEST(TestBuffer, SetStreamRedirectsTheFree)
|
||||
{
|
||||
cudaStream_t a = nullptr, b = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&a), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamCreate(&b), cudaSuccess);
|
||||
StreamLog log;
|
||||
{
|
||||
nanovdb::cuda::Buffer<float, StreamRecordingResource> buf(a, StreamRecordingResource{&log}, 32, nanovdb::cuda::noInit);
|
||||
buf.setStream(b); // member update only, no synchronization
|
||||
EXPECT_EQ(buf.stream(), b);
|
||||
}
|
||||
ASSERT_EQ(log.allocStreams.size(), 1u);
|
||||
EXPECT_EQ(log.allocStreams.back(), a);
|
||||
ASSERT_EQ(log.deallocStreams.size(), 1u);
|
||||
EXPECT_EQ(log.deallocStreams.back(), b);
|
||||
ASSERT_EQ(cudaStreamSynchronize(a), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(b), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(a), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(b), cudaSuccess);
|
||||
}
|
||||
|
||||
TEST(TestBuffer, ResizePreservesPrefixAndRetainsNewStream)
|
||||
{
|
||||
cudaStream_t a = nullptr, c = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&a), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamCreate(&c), cudaSuccess);
|
||||
StreamLog log;
|
||||
const size_t n = 128, m = 256;
|
||||
{
|
||||
nanovdb::cuda::Buffer<int, StreamRecordingResource> buf(a, StreamRecordingResource{&log}, n, nanovdb::cuda::noInit);
|
||||
std::vector<int> pattern(n);
|
||||
for (size_t i = 0; i < n; ++i) pattern[i] = int(i) * 3 + 1;
|
||||
ASSERT_EQ(cudaMemcpyAsync(buf.data(), pattern.data(), n * sizeof(int), cudaMemcpyHostToDevice, a), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(a), cudaSuccess);
|
||||
|
||||
buf.resize(m, c); // grow on stream c
|
||||
EXPECT_EQ(buf.size(), m);
|
||||
EXPECT_EQ(buf.stream(), c);
|
||||
ASSERT_EQ(log.allocStreams.size(), 2u);
|
||||
EXPECT_EQ(log.allocStreams.back(), c); // new block allocated on c
|
||||
ASSERT_EQ(log.deallocStreams.size(), 1u);
|
||||
// The prefix copy on c is the old block's last use, so the old block is
|
||||
// freed on c as well; freeing it on a would let the allocator recycle
|
||||
// it concurrently with the copy.
|
||||
EXPECT_EQ(log.deallocStreams.back(), c);
|
||||
|
||||
std::vector<int> readback(m, -1);
|
||||
ASSERT_EQ(cudaMemcpyAsync(readback.data(), buf.data(), m * sizeof(int), cudaMemcpyDeviceToHost, c), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(c), cudaSuccess);
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
ASSERT_EQ(readback[i], pattern[i]) << "prefix element " << i << " lost across resize";
|
||||
|
||||
buf.resize(n / 2, c); // shrink: keeps the min(old,new) prefix
|
||||
std::vector<int> shrunk(n / 2, -1);
|
||||
ASSERT_EQ(cudaMemcpyAsync(shrunk.data(), buf.data(), (n / 2) * sizeof(int), cudaMemcpyDeviceToHost, c), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(c), cudaSuccess);
|
||||
for (size_t i = 0; i < n / 2; ++i)
|
||||
ASSERT_EQ(shrunk[i], pattern[i]) << "prefix element " << i << " lost across shrink";
|
||||
}
|
||||
EXPECT_EQ(log.allocStreams.size(), log.deallocStreams.size()); // every allocation freed
|
||||
ASSERT_EQ(cudaStreamSynchronize(a), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(c), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(a), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(c), cudaSuccess);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// Move semantics (Buffer is move-only) and explicit deep copy
|
||||
//======================================================================
|
||||
|
||||
TEST(TestBuffer, MoveConstructorSteals)
|
||||
{
|
||||
Counters c;
|
||||
{
|
||||
nanovdb::cuda::Buffer<float, CountingResource> src(0, CountingResource{&c}, 64, nanovdb::cuda::noInit);
|
||||
float* p = src.data();
|
||||
nanovdb::cuda::Buffer<float, CountingResource> dst(std::move(src));
|
||||
EXPECT_EQ(dst.data(), p); // stolen, not reallocated
|
||||
EXPECT_EQ(dst.size(), 64u);
|
||||
EXPECT_EQ(src.data(), nullptr); // source emptied
|
||||
EXPECT_EQ(src.size(), 0u);
|
||||
EXPECT_TRUE(src.empty());
|
||||
EXPECT_EQ(c.allocs, 1); // no allocation on move
|
||||
}
|
||||
EXPECT_EQ(c.allocs, c.deallocs); // exactly one free, no double free
|
||||
EXPECT_EQ(c.deallocs, 1);
|
||||
ASSERT_EQ(cudaStreamSynchronize(0), cudaSuccess);
|
||||
}
|
||||
|
||||
TEST(TestBuffer, MoveAssignFreesDestination)
|
||||
{
|
||||
Counters c;
|
||||
{
|
||||
nanovdb::cuda::Buffer<float, CountingResource> a(0, CountingResource{&c}, 64, nanovdb::cuda::noInit);
|
||||
nanovdb::cuda::Buffer<float, CountingResource> b(0, CountingResource{&c}, 32, nanovdb::cuda::noInit);
|
||||
ASSERT_EQ(c.allocs, 2);
|
||||
float* p = a.data();
|
||||
b = std::move(a); // frees b's old block first
|
||||
EXPECT_EQ(c.deallocs, 1);
|
||||
EXPECT_EQ(b.data(), p);
|
||||
EXPECT_EQ(b.size(), 64u);
|
||||
EXPECT_EQ(a.data(), nullptr);
|
||||
}
|
||||
EXPECT_EQ(c.allocs, c.deallocs); // both blocks freed exactly once
|
||||
ASSERT_EQ(cudaStreamSynchronize(0), cudaSuccess);
|
||||
}
|
||||
|
||||
TEST(TestBuffer, SelfMoveAssignIsSafe)
|
||||
{
|
||||
Counters c;
|
||||
{
|
||||
nanovdb::cuda::Buffer<float, CountingResource> buf(0, CountingResource{&c}, 16, nanovdb::cuda::noInit);
|
||||
auto& self = buf; // avoids the compiler warning on a literal self-move
|
||||
buf = std::move(self);
|
||||
EXPECT_NE(buf.data(), nullptr); // still owns its block
|
||||
EXPECT_EQ(buf.size(), 16u);
|
||||
EXPECT_EQ(c.deallocs, 0);
|
||||
}
|
||||
EXPECT_EQ(c.allocs, 1);
|
||||
EXPECT_EQ(c.deallocs, 1);
|
||||
ASSERT_EQ(cudaStreamSynchronize(0), cudaSuccess);
|
||||
}
|
||||
|
||||
TEST(TestBuffer, CopyIsDeep)
|
||||
{
|
||||
cudaStream_t s = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&s), cudaSuccess);
|
||||
Counters c;
|
||||
const size_t n = 96;
|
||||
{
|
||||
nanovdb::cuda::Buffer<int, CountingResource> src(s, CountingResource{&c}, n, nanovdb::cuda::noInit);
|
||||
std::vector<int> pattern(n);
|
||||
for (size_t i = 0; i < n; ++i) pattern[i] = int(i) - 7;
|
||||
ASSERT_EQ(cudaMemcpyAsync(src.data(), pattern.data(), n * sizeof(int), cudaMemcpyHostToDevice, s), cudaSuccess);
|
||||
|
||||
auto dup = src.copy(s);
|
||||
EXPECT_EQ(c.allocs, 2); // deep copy allocates its own block
|
||||
EXPECT_NE(dup.data(), src.data());
|
||||
EXPECT_EQ(dup.size(), n);
|
||||
EXPECT_EQ(dup.stream(), s);
|
||||
|
||||
std::vector<int> readback(n, 0);
|
||||
ASSERT_EQ(cudaMemcpyAsync(readback.data(), dup.data(), n * sizeof(int), cudaMemcpyDeviceToHost, s), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
|
||||
EXPECT_EQ(readback, pattern);
|
||||
}
|
||||
EXPECT_EQ(c.allocs, c.deallocs);
|
||||
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(s), cudaSuccess);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// CUDA graph capture: the async path (stream-ordered allocation, no hidden
|
||||
// synchronization or initialization) must be recordable in a graph. The
|
||||
// synchronous tier is excluded by construction -- it synchronizes.
|
||||
//======================================================================
|
||||
|
||||
TEST(TestBuffer, AsyncPathIsGraphCapturable)
|
||||
{
|
||||
cudaStream_t s = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&s), cudaSuccess);
|
||||
|
||||
cudaGraph_t graph = nullptr;
|
||||
ASSERT_EQ(cudaStreamBeginCapture(s, cudaStreamCaptureModeGlobal), cudaSuccess);
|
||||
{
|
||||
// Allocation, use, and free are all recorded as graph nodes; the
|
||||
// buffer is created and destroyed inside the capture so the graph
|
||||
// owns the allocation's whole lifetime.
|
||||
nanovdb::cuda::Buffer<int> buf(s, 256, nanovdb::cuda::noInit);
|
||||
ASSERT_NE(buf.data(), nullptr);
|
||||
ASSERT_EQ(cudaMemsetAsync(buf.data(), 0x5A, buf.size_bytes(), s), cudaSuccess);
|
||||
}
|
||||
ASSERT_EQ(cudaStreamEndCapture(s, &graph), cudaSuccess);
|
||||
ASSERT_NE(graph, nullptr);
|
||||
|
||||
cudaGraphExec_t exec = nullptr;
|
||||
ASSERT_EQ(cudaGraphInstantiate(&exec, graph, 0), cudaSuccess);
|
||||
ASSERT_EQ(cudaGraphLaunch(exec, s), cudaSuccess);
|
||||
ASSERT_EQ(cudaGraphLaunch(exec, s), cudaSuccess); // relaunchable
|
||||
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
|
||||
|
||||
ASSERT_EQ(cudaGraphExecDestroy(exec), cudaSuccess);
|
||||
ASSERT_EQ(cudaGraphDestroy(graph), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(s), cudaSuccess);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// Buffer over a synchronous resource (PinnedResource): no stream anywhere
|
||||
//======================================================================
|
||||
|
||||
template<class B, class = void>
|
||||
struct HasSetStream : std::false_type {};
|
||||
template<class B>
|
||||
struct HasSetStream<B, std::void_t<decltype(std::declval<B&>().setStream(cudaStream_t{0}))>> : std::true_type {};
|
||||
|
||||
template<class B, class = void>
|
||||
struct HasStreamGetter : std::false_type {};
|
||||
template<class B>
|
||||
struct HasStreamGetter<B, std::void_t<decltype(std::declval<const B&>().stream())>> : std::true_type {};
|
||||
|
||||
using PinnedBufferF = nanovdb::cuda::Buffer<float, nanovdb::cuda::PinnedResource>;
|
||||
using DeviceBufferF = nanovdb::cuda::Buffer<float, nanovdb::cuda::DeviceResource>;
|
||||
|
||||
// A Buffer over a synchronous resource exposes no stream API at all.
|
||||
static_assert(!HasSetStream<PinnedBufferF>::value, "sync-resource Buffer must not expose setStream");
|
||||
static_assert(!HasStreamGetter<PinnedBufferF>::value, "sync-resource Buffer must not expose stream()");
|
||||
static_assert(HasSetStream<DeviceBufferF>::value, "async-resource Buffer exposes setStream");
|
||||
static_assert(HasStreamGetter<DeviceBufferF>::value, "async-resource Buffer exposes stream()");
|
||||
|
||||
TEST(TestBuffer, PinnedBufferIsPageLocked)
|
||||
{
|
||||
const size_t n = 300;
|
||||
PinnedBufferF buf(n, nanovdb::cuda::noInit); // synchronous: no stream parameter
|
||||
ASSERT_NE(buf.data(), nullptr);
|
||||
EXPECT_EQ(buf.size(), n);
|
||||
|
||||
cudaPointerAttributes attr{};
|
||||
ASSERT_EQ(cudaPointerGetAttributes(&attr, buf.data()), cudaSuccess);
|
||||
EXPECT_EQ(attr.type, cudaMemoryTypeHost); // genuine page-locked host memory
|
||||
}
|
||||
|
||||
TEST(TestBuffer, PinnedBufferNoInitResizeAndCopy)
|
||||
{
|
||||
const size_t n = 64;
|
||||
nanovdb::cuda::Buffer<int, nanovdb::cuda::PinnedResource> buf(n, nanovdb::cuda::noInit);
|
||||
ASSERT_NE(buf.data(), nullptr);
|
||||
for (size_t i = 0; i < n; ++i) buf.data()[i] = int(i) + 11;
|
||||
|
||||
buf.resize(2 * n); // synchronous: no stream parameter
|
||||
EXPECT_EQ(buf.size(), 2 * n);
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
ASSERT_EQ(buf.data()[i], int(i) + 11) << "prefix element " << i << " lost across resize";
|
||||
|
||||
auto dup = buf.copy();
|
||||
ASSERT_NE(dup.data(), nullptr);
|
||||
EXPECT_NE(dup.data(), buf.data()); // deep copy
|
||||
EXPECT_EQ(dup.size(), buf.size());
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
ASSERT_EQ(dup.data()[i], int(i) + 11);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// Raw (untyped) form: Buffer<std::byte, DeviceResource>
|
||||
//======================================================================
|
||||
|
||||
TEST(TestBuffer, RawByteBufferRoundTrip)
|
||||
{
|
||||
cudaStream_t s = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&s), cudaSuccess);
|
||||
const size_t n = 1000;
|
||||
nanovdb::cuda::Buffer<std::byte> buf(s, n, nanovdb::cuda::noInit);
|
||||
EXPECT_EQ(buf.size(), n);
|
||||
EXPECT_EQ(buf.size_bytes(), n); // one byte per element
|
||||
|
||||
std::vector<std::byte> pattern(n);
|
||||
for (size_t i = 0; i < n; ++i) pattern[i] = std::byte(i % 251);
|
||||
ASSERT_EQ(cudaMemcpyAsync(buf.data(), pattern.data(), n, cudaMemcpyHostToDevice, s), cudaSuccess);
|
||||
std::vector<std::byte> readback(n);
|
||||
ASSERT_EQ(cudaMemcpyAsync(readback.data(), buf.data(), n, cudaMemcpyDeviceToHost, s), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
|
||||
EXPECT_EQ(readback, pattern);
|
||||
buf.clear();
|
||||
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(s), cudaSuccess);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// BufferView: non-owning typed view with span semantics
|
||||
//======================================================================
|
||||
|
||||
static_assert(std::is_trivially_copyable_v<nanovdb::cuda::BufferView<std::byte>>,
|
||||
"BufferView must be trivially copyable");
|
||||
static_assert(std::is_trivially_copyable_v<nanovdb::cuda::BufferView<const std::byte>>,
|
||||
"BufferView over const elements must be trivially copyable");
|
||||
|
||||
TEST(TestBuffer, BufferViewWrapsHostArray)
|
||||
{
|
||||
float host[5] = {0.f, 1.f, 2.f, 3.f, 4.f};
|
||||
|
||||
nanovdb::cuda::BufferView<float> empty;
|
||||
EXPECT_EQ(empty.data(), nullptr);
|
||||
EXPECT_EQ(empty.size(), 0u);
|
||||
EXPECT_TRUE(empty.empty());
|
||||
|
||||
nanovdb::cuda::BufferView<float> view(host, 5);
|
||||
EXPECT_EQ(view.data(), host);
|
||||
EXPECT_EQ(view.size(), 5u);
|
||||
EXPECT_EQ(view.size_bytes(), 5 * sizeof(float));
|
||||
EXPECT_FALSE(view.empty());
|
||||
view.data()[2] = 42.f; // writable through the view
|
||||
EXPECT_EQ(host[2], 42.f);
|
||||
|
||||
view.clear(); // detaches, does not free
|
||||
EXPECT_EQ(view.data(), nullptr);
|
||||
EXPECT_TRUE(view.empty());
|
||||
EXPECT_EQ(host[2], 42.f); // underlying storage untouched
|
||||
|
||||
// A null base pointer is only valid for an empty view.
|
||||
EXPECT_THROW((nanovdb::cuda::BufferView<float>(nullptr, 5)), std::runtime_error);
|
||||
EXPECT_NO_THROW((nanovdb::cuda::BufferView<float>(nullptr, 0)));
|
||||
}
|
||||
|
||||
TEST(TestBuffer, BufferViewConstElements)
|
||||
{
|
||||
const std::byte host[4] = {std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}};
|
||||
nanovdb::cuda::BufferView<const std::byte> view(host, 4);
|
||||
EXPECT_EQ(view.data(), host);
|
||||
EXPECT_EQ(view.size_bytes(), 4u);
|
||||
EXPECT_EQ(view.data()[3], std::byte{4}); // read-only access
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// Zero-copy GridHandle over a BufferView: a handle that indexes grids in
|
||||
// storage owned elsewhere
|
||||
//======================================================================
|
||||
|
||||
TEST(TestBuffer, GridHandleOverBufferViewIsZeroCopy)
|
||||
{
|
||||
// Build a small host grid with the default (owning) HostBuffer.
|
||||
auto owner = nanovdb::tools::createLevelSetSphere<float>(20.0, nanovdb::Vec3d(0), 1.0, 3.0, nanovdb::Vec3d(0), "sphere");
|
||||
ASSERT_NE(owner.data(), nullptr);
|
||||
const auto* grid = owner.grid<float>();
|
||||
ASSERT_NE(grid, nullptr);
|
||||
|
||||
{
|
||||
// Wrap the owning handle's storage in a non-owning view...
|
||||
nanovdb::cuda::BufferView<std::byte> view(static_cast<std::byte*>(owner.data()), owner.bufferSize());
|
||||
// ...and construct a second, zero-copy handle over the same bytes.
|
||||
nanovdb::GridHandle<nanovdb::cuda::BufferView<std::byte>> viewHandle(std::move(view));
|
||||
|
||||
EXPECT_EQ(viewHandle.gridCount(), owner.gridCount());
|
||||
EXPECT_EQ(viewHandle.data(), owner.data()); // same bytes, not a copy
|
||||
|
||||
const auto* viewGrid = viewHandle.grid<float>();
|
||||
ASSERT_NE(viewGrid, nullptr);
|
||||
EXPECT_EQ(viewGrid, grid); // zero copy: identical grid address
|
||||
EXPECT_EQ(viewGrid->gridType(), grid->gridType());
|
||||
EXPECT_EQ(viewGrid->activeVoxelCount(), grid->activeVoxelCount());
|
||||
EXPECT_EQ(viewGrid->worldBBox(), grid->worldBBox());
|
||||
EXPECT_STREQ(viewGrid->gridName(), grid->gridName());
|
||||
} // view handle destroyed first: detaches without freeing
|
||||
|
||||
// The owning handle is still intact and is the single owner of the bytes.
|
||||
ASSERT_NE(owner.data(), nullptr);
|
||||
EXPECT_NE(owner.grid<float>(), nullptr);
|
||||
EXPECT_EQ(owner.grid<float>()->activeVoxelCount(), grid->activeVoxelCount());
|
||||
} // owner destroyed here: the one and only free
|
||||
|
||||
} // unnamed namespace
|
||||
@@ -209,63 +209,4 @@ TEST(TestMemoryResource, PointsToGrid_InjectedInstanceSeam)
|
||||
ASSERT_EQ(cudaFree(d_coords), cudaSuccess);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
// A Point grid built through a custom (non-default) resource must still
|
||||
// encode its point coordinates. The encode path must not depend on the
|
||||
// resource type; only allocation does.
|
||||
//======================================================================
|
||||
|
||||
TEST(TestMemoryResource, PointsToGrid_PointEncodedWithCustomResource)
|
||||
{
|
||||
using BuildT = nanovdb::Point;
|
||||
using Vec3T = nanovdb::Vec3d;
|
||||
|
||||
const size_t pointCount = 256;
|
||||
std::vector<Vec3T> points;
|
||||
points.reserve(pointCount);
|
||||
std::srand(98765);
|
||||
const int max = 128, min = -max;
|
||||
auto op = [&]() { return double(std::rand() % (max - min) + min); };
|
||||
while (points.size() < pointCount) points.emplace_back(op(), op(), op());
|
||||
|
||||
Vec3T* d_points = nullptr;
|
||||
const size_t pointSize = points.size() * sizeof(Vec3T);
|
||||
ASSERT_EQ(cudaMalloc(&d_points, pointSize), cudaSuccess);
|
||||
ASSERT_EQ(cudaMemcpy(d_points, points.data(), pointSize, cudaMemcpyHostToDevice), cudaSuccess);
|
||||
|
||||
const double voxelSize = 8.0;
|
||||
CountingResource res;
|
||||
nanovdb::tools::cuda::PointsToGrid<BuildT, CountingResource> converter(voxelSize, nanovdb::Vec3d(0.0), 0, res);
|
||||
auto handle = converter.getHandle(d_points, pointCount);
|
||||
ASSERT_EQ(cudaStreamSynchronize(0), cudaSuccess);
|
||||
ASSERT_EQ(cudaFree(d_points), cudaSuccess);
|
||||
|
||||
EXPECT_TRUE(handle.deviceData());
|
||||
EXPECT_TRUE(handle.deviceGrid<BuildT>());
|
||||
|
||||
handle.deviceDownload();
|
||||
auto* grid = handle.grid<BuildT>();
|
||||
ASSERT_TRUE(grid);
|
||||
EXPECT_EQ(pointCount, grid->pointCount());
|
||||
|
||||
// Every input point must be recoverable from the encoded per-voxel point data.
|
||||
nanovdb::PointAccessor<Vec3T, BuildT> acc(*grid);
|
||||
ASSERT_TRUE(acc);
|
||||
for (size_t i = 0; i < points.size(); ++i) {
|
||||
const nanovdb::Coord ijk = grid->worldToIndex(points[i]).round();
|
||||
ASSERT_TRUE(acc.probeLeaf(ijk) != nullptr);
|
||||
ASSERT_TRUE(acc.isActive(ijk));
|
||||
const Vec3T *start = nullptr, *stop = nullptr;
|
||||
const uint64_t count = acc.voxelPoints(ijk, start, stop);
|
||||
ASSERT_TRUE(start);
|
||||
ASSERT_TRUE(stop);
|
||||
bool found = false;
|
||||
for (uint64_t j = 0; !found && j < count; ++j)
|
||||
found = nanovdb::math::isApproxZero<double>((points[i] - start[j]).lengthSqr());
|
||||
EXPECT_TRUE(found);
|
||||
}
|
||||
|
||||
EXPECT_GT(res.allocs, 0);
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
@@ -140,7 +140,7 @@ protected:
|
||||
mTimer.start("Reading grid from the file \"" + fileName + "\"");
|
||||
try {
|
||||
openvdb::io::File file(fileName);
|
||||
file.open();
|
||||
file.open(false); //disable delayed loading
|
||||
grid = openvdb::gridPtrCast<openvdb::FloatGrid>(file.readGrid(file.beginName().gridName()));
|
||||
} catch(const std::exception& e) {
|
||||
std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl;
|
||||
@@ -2860,7 +2860,7 @@ TEST_F(TestOpenVDB, LevelSetFiles)
|
||||
//mTimer.start("\nReading grid from the file \"" + fileName + "\"");
|
||||
try {
|
||||
openvdb::io::File file(fileName);
|
||||
file.open();
|
||||
file.open(false); //disable delayed loading
|
||||
auto srcGrid = openvdb::gridPtrCast<openvdb::FloatGrid>(file.readGrid(file.beginName().gridName()));
|
||||
|
||||
const size_t pos = fileName.find_last_of("/\\") + 1;
|
||||
@@ -2938,7 +2938,7 @@ TEST_F(TestOpenVDB, FogFiles)
|
||||
//mTimer.start("Reading grid from the file \"" + fileName + "\"");
|
||||
try {
|
||||
openvdb::io::File file(fileName);
|
||||
file.open();
|
||||
file.open(false); //disable delayed loading
|
||||
auto srcGrid = openvdb::gridPtrCast<openvdb::FloatGrid>(file.readGrid(file.beginName().gridName()));
|
||||
|
||||
const size_t pos = fileName.find_last_of("/\\") + 1;
|
||||
@@ -3008,7 +3008,7 @@ TEST_F(TestOpenVDB, PointFiles)
|
||||
//mTimer.start("Reading grid from the file \"" + fileName + "\"");
|
||||
try {
|
||||
openvdb::io::File file(fileName);
|
||||
file.open();
|
||||
file.open(false); //disable delayed loading
|
||||
|
||||
auto srcGrid = openvdb::gridPtrCast<openvdb::points::PointDataGrid>(file.readGrid(file.beginName().gridName()));
|
||||
//std::cerr << "Read PointDataGrid named \"" << srcGrid->getName() << "\"" << std::endl;
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// @file TestUtilCuda.cu
|
||||
///
|
||||
/// @brief Unit tests for the util::cuda allocation wrappers: the cached
|
||||
/// memory-pool capability query, and an allocate/use/free round-trip
|
||||
/// through whichever mode the build selects (stream-ordered by default,
|
||||
/// synchronous under NANOVDB_USE_SYNC_CUDA_MALLOC). This file is compiled
|
||||
/// into two test binaries -- one per mode -- so both are exercised. On a
|
||||
/// device without memory pools the default (async) wrappers fail by
|
||||
/// design; such a device must build with NANOVDB_USE_SYNC_CUDA_MALLOC,
|
||||
/// so the async round-trip skips when pools are absent.
|
||||
|
||||
#include <nanovdb/util/cuda/Util.h>
|
||||
|
||||
#include <cuda_runtime_api.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
TEST(TestUtilCuda, MemoryPoolsSupportedMatchesAttribute)
|
||||
{
|
||||
int count = 0;
|
||||
ASSERT_EQ(cudaGetDeviceCount(&count), cudaSuccess);
|
||||
ASSERT_GT(count, 0);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
int attr = 0;
|
||||
ASSERT_EQ(cudaDeviceGetAttribute(&attr, cudaDevAttrMemoryPoolsSupported, i), cudaSuccess);
|
||||
EXPECT_EQ(nanovdb::util::cuda::memoryPoolsSupported(i), attr != 0) << "device " << i;
|
||||
}
|
||||
// Out-of-range device ids are unsupported, not undefined.
|
||||
EXPECT_FALSE(nanovdb::util::cuda::memoryPoolsSupported(-1));
|
||||
EXPECT_FALSE(nanovdb::util::cuda::memoryPoolsSupported(count));
|
||||
}
|
||||
|
||||
TEST(TestUtilCuda, MallocAsyncRoundTrip)
|
||||
{
|
||||
// Allocate/use/free through the wrapper in whichever mode the build selects:
|
||||
// synchronous under the macro, stream-ordered otherwise. Without the macro
|
||||
// the async path requires memory pools, so skip when the device lacks them.
|
||||
int device = 0;
|
||||
ASSERT_EQ(cudaGetDevice(&device), cudaSuccess);
|
||||
#ifndef NANOVDB_USE_SYNC_CUDA_MALLOC
|
||||
if (!nanovdb::util::cuda::memoryPoolsSupported(device))
|
||||
GTEST_SKIP() << "device " << device << " lacks memory pools; the async wrappers fail by design "
|
||||
"(build with NANOVDB_USE_SYNC_CUDA_MALLOC)";
|
||||
#endif
|
||||
cudaStream_t s = nullptr;
|
||||
ASSERT_EQ(cudaStreamCreate(&s), cudaSuccess);
|
||||
|
||||
const size_t n = 4096;
|
||||
void* d = nullptr;
|
||||
ASSERT_EQ(nanovdb::util::cuda::mallocAsync(&d, n, s), cudaSuccess);
|
||||
ASSERT_NE(d, nullptr);
|
||||
ASSERT_EQ(cudaMemsetAsync(d, 0x3C, n, s), cudaSuccess);
|
||||
|
||||
std::vector<unsigned char> host(n, 0);
|
||||
ASSERT_EQ(cudaMemcpyAsync(host.data(), d, n, cudaMemcpyDeviceToHost, s), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
ASSERT_EQ(host[i], 0x3Cu) << "byte " << i;
|
||||
|
||||
ASSERT_EQ(nanovdb::util::cuda::freeAsync(d, s), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamSynchronize(s), cudaSuccess);
|
||||
ASSERT_EQ(cudaStreamDestroy(s), cudaSuccess);
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
#include <cuda.h>
|
||||
#include <cuda_runtime_api.h>
|
||||
#include <vector>
|
||||
#include <nanovdb/util/Util.h> // for stderr and NANOVDB_ASSERT
|
||||
|
||||
// change 1 -> 0 to only perform asserts during debug builds
|
||||
@@ -71,54 +70,23 @@ namespace nanovdb {// =========================================================
|
||||
namespace util::cuda {// ======================================================
|
||||
|
||||
//#define NANOVDB_USE_SYNC_CUDA_MALLOC
|
||||
// cudaMallocAsync and cudaFreeAsync were introduced in CUDA 11.2, and even on newer
|
||||
// toolkits a device may not expose stream-ordered memory pools at runtime — fractional
|
||||
// vGPU configurations commonly disable them, making cudaMallocAsync fail with
|
||||
// cudaErrorNotSupported. The wrappers below therefore behave in two modes: when
|
||||
// CUDA < 11.2 or NANOVDB_USE_SYNC_CUDA_MALLOC is defined they map to synchronous
|
||||
// cudaMalloc/cudaFree; otherwise they use cudaMallocAsync/cudaFreeAsync and, on a
|
||||
// device that lacks memory pools, fail with an actionable diagnostic rather than
|
||||
// silently substituting synchronous allocation (which would make an async resource
|
||||
// misrepresent its own semantics — the choice of a synchronous backend belongs to the
|
||||
// caller, not to a hidden runtime fallback). Callers select synchronous allocation by
|
||||
// defining NANOVDB_USE_SYNC_CUDA_MALLOC, which -- because the wrappers are inline
|
||||
// functions -- must be defined identically in every translation unit to avoid violating
|
||||
// the one-definition rule.
|
||||
|
||||
/// @brief Returns true if @c device supports stream-ordered memory pools, i.e.
|
||||
/// cudaMallocAsync/cudaFreeAsync. Queried once per process for all
|
||||
/// devices and cached; out-of-range device ids return false.
|
||||
inline bool memoryPoolsSupported(int device)
|
||||
{
|
||||
#if (CUDART_VERSION < 11020)
|
||||
(void)device;
|
||||
return false;
|
||||
#else
|
||||
static const auto supported = [] {
|
||||
int count = 0;
|
||||
if (cudaGetDeviceCount(&count) != cudaSuccess || count < 0) count = 0;
|
||||
std::vector<char> s(static_cast<size_t>(count), 0);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
int attr = 0;
|
||||
if (cudaDeviceGetAttribute(&attr, cudaDevAttrMemoryPoolsSupported, i) == cudaSuccess)
|
||||
s[static_cast<size_t>(i)] = char(attr != 0);
|
||||
}
|
||||
return s;
|
||||
}();
|
||||
return device >= 0 && static_cast<size_t>(device) < supported.size() && supported[static_cast<size_t>(device)] != 0;
|
||||
#endif
|
||||
}
|
||||
// cudaMallocAsync and cudaFreeAsync were introduced in CUDA 11.2 so we introduce
|
||||
// custom implementations that map to cudaMalloc and cudaFree below. If NANOVDB_USE_SYNC_CUDA_MALLOC
|
||||
// is defined these implementations will also be defined, which is useful in virtualized environments
|
||||
// that slice up the GPU and share it between instances as vGPU's. GPU unified memory is usually disabled
|
||||
// out of security considerations. Asynchronous CUDA malloc/free depends on GPU unified memory, so it
|
||||
// is not possible to use cudaMallocAsync and cudaFreeAsync in such environments.
|
||||
|
||||
#if (CUDART_VERSION < 11020) || defined(NANOVDB_USE_SYNC_CUDA_MALLOC) // 11.2 introduced cudaMallocAsync and cudaFreeAsync
|
||||
|
||||
/// @brief Wrapper forced to synchronous cudaMalloc; see the mode comment above.
|
||||
/// @brief Simple wrapper 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 mallocAsync(void** d_ptr, size_t size, cudaStream_t){return cudaMalloc(d_ptr, size);}
|
||||
|
||||
/// @brief Wrapper forced to synchronous cudaFree; see the mode comment above.
|
||||
/// @brief Simple wrapper 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
|
||||
@@ -126,43 +94,18 @@ inline cudaError_t freeAsync(void* d_ptr, cudaStream_t){return cudaFree(d_ptr);}
|
||||
|
||||
#else
|
||||
|
||||
/// @brief Wrapper that calls cudaMallocAsync. On a device without stream-ordered
|
||||
/// memory pools it emits an actionable diagnostic and returns
|
||||
/// cudaErrorNotSupported rather than silently allocating synchronously.
|
||||
/// @brief Simple wrapper that calls cudaMallocAsync
|
||||
/// @param d_ptr Device pointer to allocated device memory
|
||||
/// @param size Number of bytes to allocate
|
||||
/// @param stream The stream establishing the stream ordering contract and the memory pool to allocate from
|
||||
/// @return Cuda error code
|
||||
inline cudaError_t mallocAsync(void** d_ptr, size_t size, cudaStream_t stream)
|
||||
{
|
||||
int device = 0;
|
||||
const cudaError_t err = cudaGetDevice(&device);
|
||||
if (err != cudaSuccess) return err;
|
||||
if (!memoryPoolsSupported(device)) {
|
||||
fprintf(stderr,
|
||||
"NanoVDB: device %d does not support stream-ordered CUDA memory pools required by "
|
||||
"cudaMallocAsync. Define NANOVDB_USE_SYNC_CUDA_MALLOC when building to allocate "
|
||||
"synchronously with cudaMalloc/cudaFree instead.\n",
|
||||
device);
|
||||
return cudaErrorNotSupported;
|
||||
}
|
||||
return cudaMallocAsync(d_ptr, size, stream);
|
||||
}
|
||||
inline cudaError_t mallocAsync(void** d_ptr, size_t size, cudaStream_t stream){return cudaMallocAsync(d_ptr, size, stream);}
|
||||
|
||||
/// @brief Wrapper that calls cudaFreeAsync. Mirrors mallocAsync's pool check so a
|
||||
/// device without memory pools reports cudaErrorNotSupported rather than
|
||||
/// calling cudaFreeAsync where it cannot succeed.
|
||||
/// @brief Simple wrapper that calls cudaFreeAsync
|
||||
/// @param d_ptr Device pointer that will be freed
|
||||
/// @param stream The stream establishing the stream ordering promise
|
||||
/// @return Cuda error code
|
||||
inline cudaError_t freeAsync(void* d_ptr, cudaStream_t stream)
|
||||
{
|
||||
int device = 0;
|
||||
const cudaError_t err = cudaGetDevice(&device);
|
||||
if (err != cudaSuccess) return err;
|
||||
if (!memoryPoolsSupported(device)) return cudaErrorNotSupported;
|
||||
return cudaFreeAsync(d_ptr, stream);
|
||||
}
|
||||
inline cudaError_t freeAsync(void* d_ptr, cudaStream_t stream){return cudaFreeAsync(d_ptr, stream);}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -117,6 +117,19 @@ endif()
|
||||
|
||||
# Collect and configure lib dependencies
|
||||
|
||||
if(OPENVDB_USE_DELAYED_LOADING)
|
||||
find_package(Boost ${MINIMUM_BOOST_VERSION} REQUIRED COMPONENTS iostreams)
|
||||
|
||||
if(OPENVDB_FUTURE_DEPRECATION AND FUTURE_MINIMUM_BOOST_VERSION)
|
||||
# The X.Y.Z boost version value isn't available until CMake 3.14
|
||||
set(FULL_BOOST_VERSION "${Boost_MAJOR_VERSION}.${Boost_MINOR_VERSION}.${Boost_SUBMINOR_VERSION}")
|
||||
if(${FULL_BOOST_VERSION} VERSION_LESS FUTURE_MINIMUM_BOOST_VERSION)
|
||||
message(DEPRECATION "Support for Boost versions < ${FUTURE_MINIMUM_BOOST_VERSION} "
|
||||
"is deprecated and will be removed.")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
find_package(TBB ${MINIMUM_TBB_VERSION} REQUIRED COMPONENTS tbb)
|
||||
if(OPENVDB_FUTURE_DEPRECATION AND FUTURE_MINIMUM_TBB_VERSION)
|
||||
if(${TBB_VERSION} VERSION_LESS FUTURE_MINIMUM_TBB_VERSION)
|
||||
@@ -239,6 +252,27 @@ if(UNIX)
|
||||
list(APPEND OPENVDB_CORE_DEPENDENT_LIBS Threads::Threads)
|
||||
endif()
|
||||
|
||||
# Pull in Boost last as houdini's boost (hboost) is fully namespaced and libs
|
||||
# are renamed too. Boost can be pulled in at any time, but do it last so that,
|
||||
# if it's in a shared place (like /usr/local) it doesn't accidently pull in
|
||||
# other headers.
|
||||
|
||||
if(OPENVDB_USE_DELAYED_LOADING)
|
||||
list(APPEND OPENVDB_CORE_DEPENDENT_LIBS Boost::iostreams)
|
||||
if(WIN32)
|
||||
# Boost headers contain #pragma commands on Windows which causes Boost
|
||||
# libraries to be linked in automatically. Custom boost installations
|
||||
# may find that these naming conventions don't always match and can
|
||||
# cause linker errors. This option disables this feature of Boost. Note
|
||||
# -DBOOST_ALL_NO_LIB can also be provided manually.
|
||||
if(OPENVDB_DISABLE_BOOST_IMPLICIT_LINKING)
|
||||
list(APPEND OPENVDB_CORE_DEPENDENT_LIBS
|
||||
Boost::disable_autolinking # add -DBOOST_ALL_NO_LIB
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
##########################################################################
|
||||
|
||||
##### Core library configuration
|
||||
@@ -340,12 +374,13 @@ configure_file(version.h.in openvdb/version.h)
|
||||
set(OPENVDB_LIBRARY_SOURCE_FILES
|
||||
Grid.cc
|
||||
io/Archive.cc
|
||||
io/Codec.cc
|
||||
io/Compression.cc
|
||||
io/DelayedLoadMetadata.cc
|
||||
io/File.cc
|
||||
io/GridDescriptor.cc
|
||||
io/Queue.cc
|
||||
io/Stream.cc
|
||||
io/TempFile.cc
|
||||
math/Maps.cc
|
||||
math/Proximity.cc
|
||||
math/QuantizedUnitVec.cc
|
||||
@@ -376,28 +411,16 @@ set(OPENVDB_LIBRARY_INCLUDE_FILES
|
||||
TypeList.h
|
||||
)
|
||||
|
||||
set(OPENVDB_LIBRARY_CODECS_INCLUDE_FILES
|
||||
codecs/BoolCodec.h
|
||||
codecs/PointDataCodec.h
|
||||
codecs/PointIndexCodec.h
|
||||
codecs/ScalarCodec.h
|
||||
codecs/TopologyCodec.h
|
||||
codecs/ValueMaskCodec.h
|
||||
)
|
||||
|
||||
set(OPENVDB_LIBRARY_CODECS_IMPL_INCLUDE_FILES
|
||||
codecs/impl/ScalarLeafCodec.h
|
||||
)
|
||||
|
||||
set(OPENVDB_LIBRARY_IO_INCLUDE_FILES
|
||||
io/Archive.h
|
||||
io/Codec.h
|
||||
io/Compression.h
|
||||
io/DelayedLoadMetadata.h
|
||||
io/File.h
|
||||
io/GridDescriptor.h
|
||||
io/io.h
|
||||
io/Queue.h
|
||||
io/Stream.h
|
||||
io/TempFile.h
|
||||
)
|
||||
|
||||
set(OPENVDB_LIBRARY_MATH_INCLUDE_FILES
|
||||
@@ -438,7 +461,6 @@ set(OPENVDB_LIBRARY_POINTS_INCLUDE_FILES
|
||||
points/PointConversion.h
|
||||
points/PointCount.h
|
||||
points/PointDataGrid.h
|
||||
points/PointDataIO.h
|
||||
points/PointDelete.h
|
||||
points/PointGroup.h
|
||||
points/PointMask.h
|
||||
@@ -590,8 +612,6 @@ if(USE_EXPLICIT_INSTANTIATION)
|
||||
# inexpensive to generate in this case.
|
||||
set(OPENVDB_LIBRARY_INSTANTIATION_HEADERS
|
||||
${OPENVDB_LIBRARY_TOOLS_INCLUDE_FILES}
|
||||
${OPENVDB_LIBRARY_CODECS_INCLUDE_FILES}
|
||||
${OPENVDB_LIBRARY_CODECS_IMPL_INCLUDE_FILES}
|
||||
)
|
||||
|
||||
# A new source file is generated by CMake for every header
|
||||
@@ -666,6 +686,9 @@ endif()
|
||||
if(USE_LOG4CPLUS)
|
||||
list(APPEND OPENVDB_CORE_PUBLIC_DEFINES -DOPENVDB_USE_LOG4CPLUS)
|
||||
endif()
|
||||
if(OPENVDB_USE_DELAYED_LOADING)
|
||||
list(APPEND OPENVDB_CORE_PUBLIC_DEFINES -DOPENVDB_USE_DELAYED_LOADING)
|
||||
endif()
|
||||
|
||||
##########################################################################
|
||||
|
||||
@@ -776,8 +799,6 @@ endif()
|
||||
install(FILES ${OPENVDB_LIBRARY_INCLUDE_FILES} DESTINATION ${OPENVDB_INSTALL_INCLUDEDIR}/openvdb)
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/openvdb/version.h DESTINATION ${OPENVDB_INSTALL_INCLUDEDIR}/openvdb)
|
||||
install(FILES ${OPENVDB_LIBRARY_IO_INCLUDE_FILES} DESTINATION ${OPENVDB_INSTALL_INCLUDEDIR}/openvdb/io)
|
||||
install(FILES ${OPENVDB_LIBRARY_CODECS_INCLUDE_FILES} DESTINATION ${OPENVDB_INSTALL_INCLUDEDIR}/openvdb/codecs)
|
||||
install(FILES ${OPENVDB_LIBRARY_CODECS_IMPL_INCLUDE_FILES} DESTINATION ${OPENVDB_INSTALL_INCLUDEDIR}/openvdb/codecs/impl)
|
||||
install(FILES ${OPENVDB_LIBRARY_MATH_INCLUDE_FILES} DESTINATION ${OPENVDB_INSTALL_INCLUDEDIR}/openvdb/math)
|
||||
install(FILES ${OPENVDB_LIBRARY_POINTS_INCLUDE_FILES} DESTINATION ${OPENVDB_INSTALL_INCLUDEDIR}/openvdb/points)
|
||||
install(FILES ${OPENVDB_LIBRARY_POINTS_IMPL_INCLUDE_FILES} DESTINATION ${OPENVDB_INSTALL_INCLUDEDIR}/openvdb/points/impl)
|
||||
|
||||
+24
-36
@@ -457,12 +457,12 @@ public:
|
||||
virtual void readBuffers(std::istream&) = 0;
|
||||
/// Read all of this grid's data buffers that intersect the given index-space bounding box.
|
||||
virtual void readBuffers(std::istream&, const CoordBBox&) = 0;
|
||||
|
||||
#if OPENVDB_ABI_VERSION_NUMBER < 14
|
||||
OPENVDB_DEPRECATED_MESSAGE("This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
/// @brief Read all of this grid's data buffers that are not yet resident in memory
|
||||
/// (because delayed loading is in effect).
|
||||
/// @details If this grid was read from a memory-mapped file, this operation
|
||||
/// disconnects the grid from the file.
|
||||
/// @sa io::File::open, io::MappedFile
|
||||
virtual void readNonresidentBuffers() const = 0;
|
||||
#endif
|
||||
|
||||
/// Write out all data buffers for this grid.
|
||||
virtual void writeBuffers(std::ostream&) const = 0;
|
||||
|
||||
@@ -941,12 +941,12 @@ public:
|
||||
void readBuffers(std::istream&) override;
|
||||
/// Read all of this grid's data buffers that intersect the given index-space bounding box.
|
||||
void readBuffers(std::istream&, const CoordBBox&) override;
|
||||
|
||||
#if OPENVDB_ABI_VERSION_NUMBER < 14
|
||||
OPENVDB_DEPRECATED_MESSAGE("This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
void readNonresidentBuffers() const override { }
|
||||
#endif
|
||||
|
||||
/// @brief Read all of this grid's data buffers that are not yet resident in memory
|
||||
/// (because delayed loading is in effect).
|
||||
/// @details If this grid was read from a memory-mapped file, this operation
|
||||
/// disconnects the grid from the file.
|
||||
/// @sa io::File::open, io::MappedFile
|
||||
void readNonresidentBuffers() const override;
|
||||
/// Write out all data buffers for this grid.
|
||||
void writeBuffers(std::ostream&) const override;
|
||||
|
||||
@@ -1183,34 +1183,14 @@ struct TreeAdapter<tree::ValueAccessor<_TreeType> >
|
||||
////////////////////////////////////////
|
||||
|
||||
|
||||
namespace points {
|
||||
|
||||
template<typename T, Index Log2Dim> class PointDataLeafNode;
|
||||
|
||||
/// @brief Type trait that evaluates to true only for @c PointDataLeafNode instantiations.
|
||||
template<typename T>
|
||||
struct IsPointDataLeafNode : std::false_type {};
|
||||
|
||||
template<typename T, Index Log2Dim>
|
||||
struct IsPointDataLeafNode<PointDataLeafNode<T, Log2Dim>> : std::true_type {};
|
||||
|
||||
} // namespace points
|
||||
|
||||
|
||||
/// @brief Metafunction that specifies whether a given leaf node, tree, or grid type
|
||||
/// requires multiple passes to read and write voxel data.
|
||||
/// @details Multi-pass I/O allows leaf nodes to optimize their serialization layout
|
||||
/// for delayed-load access patterns. Only @c PointDataLeafNode supports multi-pass I/O.
|
||||
/// Defining a custom leaf node that inherits @c io::PointDataGridMultiPass is no longer permitted.
|
||||
/// @sa points::IsPointDataLeafNode
|
||||
/// requires multiple passes to read and write voxel data
|
||||
/// @details Multi-pass I/O allows one to optimize the data layout of leaf nodes
|
||||
/// for certain access patterns during delayed loading.
|
||||
/// @sa io::MultiPass
|
||||
template<typename LeafNodeType>
|
||||
struct HasMultiPassIO {
|
||||
static_assert(
|
||||
!std::is_base_of<io::PointDataGridMultiPass, LeafNodeType>::value
|
||||
|| points::IsPointDataLeafNode<LeafNodeType>::value,
|
||||
"Only PointDataLeafNode may inherit from io::PointDataGridMultiPass; "
|
||||
"use points::IsPointDataLeafNode to test for multi-pass I/O support.");
|
||||
static const bool value = points::IsPointDataLeafNode<LeafNodeType>::value;
|
||||
static const bool value = std::is_base_of<io::MultiPass, LeafNodeType>::value;
|
||||
};
|
||||
|
||||
// Partial specialization for Tree types
|
||||
@@ -1692,6 +1672,14 @@ Grid<TreeT>::readBuffers(std::istream& is, const CoordBBox& bbox)
|
||||
}
|
||||
|
||||
|
||||
template<typename TreeT>
|
||||
inline void
|
||||
Grid<TreeT>::readNonresidentBuffers() const
|
||||
{
|
||||
tree().readNonresidentBuffers();
|
||||
}
|
||||
|
||||
|
||||
template<typename TreeT>
|
||||
inline void
|
||||
Grid<TreeT>::writeBuffers(std::ostream& os) const
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef OPENVDB_IO_CODECS_BOOLCODEC_HAS_BEEN_INCLUDED
|
||||
#define OPENVDB_IO_CODECS_BOOLCODEC_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <openvdb/io/Codec.h>
|
||||
|
||||
#include <openvdb/openvdb.h>
|
||||
|
||||
#include "TopologyCodec.h"
|
||||
|
||||
namespace openvdb {
|
||||
OPENVDB_USE_VERSION_NAMESPACE
|
||||
namespace OPENVDB_VERSION_NAME {
|
||||
namespace codecs {
|
||||
namespace internal {
|
||||
|
||||
template <typename GridT>
|
||||
struct ReadBoolBuffersOp
|
||||
{
|
||||
using TreeT = typename GridT::TreeType;
|
||||
using RootT = typename TreeT::RootNodeType;
|
||||
using LeafT = typename TreeT::LeafNodeType;
|
||||
using ValueT = typename TreeT::ValueType;
|
||||
|
||||
ReadBoolBuffersOp(std::istream& _is, bool _saveFloatAsHalf,
|
||||
const ValueT& _background, const CoordBBox* _clipBBox)
|
||||
: is(_is)
|
||||
, saveFloatAsHalf(_saveFloatAsHalf)
|
||||
, background(_background)
|
||||
, clipBBox(_clipBBox) { }
|
||||
|
||||
void operator()(RootT& root, size_t)
|
||||
{
|
||||
// Clip root-level tiles and prune children that were clipped.
|
||||
if (clipBBox) {
|
||||
root.clip(*clipBBox);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename NodeT>
|
||||
void operator()(NodeT& node, size_t)
|
||||
{
|
||||
// Clip internal node tiles and prune children that were clipped.
|
||||
if (clipBBox) {
|
||||
node.clip(*clipBBox, background);
|
||||
}
|
||||
}
|
||||
|
||||
void operator()(LeafT& leaf, size_t)
|
||||
{
|
||||
// Read in the value mask.
|
||||
leaf.getValueMask().load(is);
|
||||
// Read in the origin.
|
||||
Coord origin;
|
||||
is.read(reinterpret_cast<char*>(&origin), sizeof(Coord::ValueType) * 3);
|
||||
leaf.setOrigin(origin);
|
||||
|
||||
// Read in the mask for the voxel values.
|
||||
typename LeafT::Buffer::NodeMaskType nodeMask;
|
||||
nodeMask.load(is);
|
||||
typename LeafT::Buffer temp(nodeMask);
|
||||
leaf.swap(temp);
|
||||
}
|
||||
|
||||
std::istream& is;
|
||||
const bool saveFloatAsHalf;
|
||||
const ValueT& background;
|
||||
const CoordBBox* clipBBox = nullptr;
|
||||
}; // struct ReadBoolBuffersOp
|
||||
|
||||
template <typename GridT>
|
||||
struct WriteBoolBuffersOp
|
||||
{
|
||||
using TreeT = typename GridT::TreeType;
|
||||
using LeafT = typename TreeT::LeafNodeType;
|
||||
|
||||
WriteBoolBuffersOp(std::ostream& _os, bool _saveFloatAsHalf)
|
||||
: os(_os)
|
||||
, saveFloatAsHalf(_saveFloatAsHalf) { }
|
||||
|
||||
template <typename NodeT>
|
||||
void operator()(const NodeT&, size_t) { }
|
||||
|
||||
void operator()(const LeafT& leaf, size_t)
|
||||
{
|
||||
// Write out the value mask.
|
||||
leaf.getValueMask().save(os);
|
||||
|
||||
// Write out the origin.
|
||||
os.write(reinterpret_cast<const char*>(&leaf.origin()), sizeof(Coord::ValueType) * 3);
|
||||
|
||||
// Write out the voxel values.
|
||||
leaf.buffer().storage().save(os);
|
||||
}
|
||||
|
||||
std::ostream& os;
|
||||
const bool saveFloatAsHalf;
|
||||
}; // struct WriteBoolBuffersOp
|
||||
|
||||
} // namespace internal
|
||||
|
||||
template <typename GridT>
|
||||
struct BoolCodec final: public TopologyCodec<GridT>
|
||||
{
|
||||
using Ptr = std::unique_ptr<BoolCodec<GridT>>;
|
||||
|
||||
~BoolCodec() noexcept = default;
|
||||
|
||||
static inline std::string name() { return GridT::gridType(); }
|
||||
|
||||
void readBuffers(std::istream& is, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics&) final
|
||||
{
|
||||
GridT& grid = static_cast<GridT&>(*data.grid);
|
||||
|
||||
if (grid.hasMultiPassIO()) {
|
||||
OPENVDB_THROW(IoError, "Multi-pass IO is not supported in BoolCodec");
|
||||
}
|
||||
|
||||
io::checkFormatVersion(is);
|
||||
|
||||
bool saveFloatAsHalf = grid.saveFloatAsHalf();
|
||||
|
||||
auto& tree = grid.tree();
|
||||
tree.clearAllAccessors();
|
||||
|
||||
std::unique_ptr<CoordBBox> clipIndexBBox;
|
||||
if (options.clipBBox.isSorted()) {
|
||||
clipIndexBBox = std::make_unique<CoordBBox>(grid.constTransform().worldToIndexNodeCentered(options.clipBBox));
|
||||
}
|
||||
|
||||
internal::ReadBoolBuffersOp<GridT> readBuffersOp(is, saveFloatAsHalf, tree.background(), clipIndexBBox.get());
|
||||
tools::visitNodesDepthFirst(grid.tree(), readBuffersOp, /*idx=*/0, /*topDown=*/false);
|
||||
}
|
||||
|
||||
void writeBuffers(std::ostream& os, const GridBase& gridBase, const io::WriteOptions&) final
|
||||
{
|
||||
const GridT& grid = static_cast<const GridT&>(gridBase);
|
||||
|
||||
if (grid.hasMultiPassIO()) {
|
||||
OPENVDB_THROW(IoError, "Multi-pass IO is not supported in BoolCodec");
|
||||
}
|
||||
|
||||
internal::WriteBoolBuffersOp<GridT> writeBuffersOp(os, grid.saveFloatAsHalf());
|
||||
tools::visitNodesDepthFirst(grid.tree(), writeBuffersOp);
|
||||
}
|
||||
}; // struct BoolCodec
|
||||
|
||||
} // namespace codecs
|
||||
} // namespace OPENVDB_VERSION_NAME
|
||||
} // namespace openvdb
|
||||
|
||||
#endif // OPENVDB_IO_CODECS_BOOLCODEC_HAS_BEEN_INCLUDED
|
||||
@@ -1,451 +0,0 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef OPENVDB_IO_CODECS_POINTDATACODEC_HAS_BEEN_INCLUDED
|
||||
#define OPENVDB_IO_CODECS_POINTDATACODEC_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <openvdb/io/Codec.h>
|
||||
|
||||
#include <openvdb/tools/Clip.h>
|
||||
|
||||
#include <openvdb/points/PointDataGrid.h>
|
||||
#include <openvdb/points/AttributeSet.h>
|
||||
#include <openvdb/points/StreamCompression.h>
|
||||
|
||||
#include "impl/ScalarLeafCodec.h"
|
||||
#include "TopologyCodec.h"
|
||||
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace openvdb {
|
||||
OPENVDB_USE_VERSION_NAMESPACE
|
||||
namespace OPENVDB_VERSION_NAME {
|
||||
namespace codecs {
|
||||
namespace internal {
|
||||
|
||||
/// Look up an existing paged stream or create a new one for the given attribute index
|
||||
template <typename PagedStreamPtrT>
|
||||
inline typename PagedStreamPtrT::element_type* getOrCreatePagedStream(
|
||||
std::map<Index, PagedStreamPtrT>& pagedStreams, Index attributeIndex)
|
||||
{
|
||||
auto it = pagedStreams.find(attributeIndex);
|
||||
if (it != pagedStreams.end()) return it->second.get();
|
||||
using PagedStreamT = typename PagedStreamPtrT::element_type;
|
||||
auto ptr = std::make_shared<PagedStreamT>();
|
||||
auto& stored = (pagedStreams[attributeIndex] = std::move(ptr));
|
||||
return stored.get();
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
// Read-side functions
|
||||
|
||||
template <typename LeafT>
|
||||
inline void readPointDataVoxelSizes(const std::vector<LeafT*>& leaves,
|
||||
std::istream& is, std::unordered_map<Coord, uint16_t>& voxelBufferSizes)
|
||||
{
|
||||
for (auto* leaf : leaves) {
|
||||
uint16_t voxelBufferSize;
|
||||
is.read(reinterpret_cast<char*>(&voxelBufferSize), sizeof(uint16_t));
|
||||
voxelBufferSizes[leaf->origin()] = voxelBufferSize;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LeafT>
|
||||
inline void readPointDataDescriptors(const std::vector<LeafT*>& leaves,
|
||||
std::istream& is)
|
||||
{
|
||||
points::AttributeSet::Descriptor::Ptr sharedDescriptor;
|
||||
for (auto* leaf : leaves) {
|
||||
points::AttributeSet::UniquePtr attrSet = leaf->stealAttributeSet();
|
||||
if (sharedDescriptor) {
|
||||
// Reuse shared descriptor from first leaf
|
||||
attrSet->resetDescriptor(sharedDescriptor, /*allowMismatchingDescriptors=*/true);
|
||||
}
|
||||
else {
|
||||
uint8_t header;
|
||||
is.read(reinterpret_cast<char*>(&header), sizeof(uint8_t));
|
||||
attrSet->readDescriptor(is);
|
||||
if (header & uint8_t(1)) {
|
||||
// Store descriptor for subsequent leaves
|
||||
sharedDescriptor = attrSet->descriptorPtr();
|
||||
}
|
||||
// a forwards-compatibility mechanism for future use,
|
||||
// if a 0x2 bit is set, read and skip over a specific number of bytes
|
||||
if (header & uint8_t(2)) {
|
||||
uint64_t bytesToSkip;
|
||||
is.read(reinterpret_cast<char*>(&bytesToSkip), sizeof(uint64_t));
|
||||
if (bytesToSkip > uint64_t(0)) {
|
||||
std::vector<uint8_t> tempData(bytesToSkip);
|
||||
is.read(reinterpret_cast<char*>(&tempData[0]), bytesToSkip);
|
||||
}
|
||||
}
|
||||
// this reader is only able to read headers with 0x1 and 0x2 bits set
|
||||
if (header > uint8_t(3)) {
|
||||
OPENVDB_THROW(IoError, "Unrecognised header flags in PointDataLeafNode");
|
||||
}
|
||||
}
|
||||
attrSet->readMetadata(is);
|
||||
leaf->replaceAttributeSet(attrSet.release(), /*allowMismatchingDescriptors=*/true);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LeafT>
|
||||
inline void readPointDataAttributeSizes(const std::vector<LeafT*>& leaves,
|
||||
std::istream& is, Index attributeIndex,
|
||||
std::map<Index, compression::PagedInputStream::Ptr>& pagedStreams)
|
||||
{
|
||||
for (auto* leaf : leaves) {
|
||||
points::AttributeArray* array = attributeIndex < leaf->attributeSet().size() ?
|
||||
&leaf->attributeArray(attributeIndex) : nullptr;
|
||||
if (array) {
|
||||
auto* pagedStream = getOrCreatePagedStream(pagedStreams, attributeIndex);
|
||||
pagedStream->setInputStream(is);
|
||||
pagedStream->setSizeOnly(true);
|
||||
array->readPagedBuffers(*pagedStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LeafT>
|
||||
inline void readPointDataVoxelData(const std::vector<LeafT*>& leaves,
|
||||
std::istream& is, bool saveFloatAsHalf,
|
||||
const typename LeafT::ValueType& background,
|
||||
[[maybe_unused]] const std::unordered_map<Coord, uint16_t>& voxelBufferSizes)
|
||||
{
|
||||
using BaseLeaf = typename LeafT::BaseLeaf;
|
||||
for (auto* leaf : leaves) {
|
||||
OPENVDB_ASSERT(voxelBufferSizes.find(leaf->origin()) != voxelBufferSizes.end());
|
||||
BaseLeaf& baseLeaf = static_cast<BaseLeaf&>(*leaf);
|
||||
readScalarLeafBuffers(baseLeaf, is, saveFloatAsHalf, background);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LeafT>
|
||||
inline void readPointDataAttributeData(const std::vector<LeafT*>& leaves,
|
||||
std::istream& is, Index attributeIndex,
|
||||
std::map<Index, compression::PagedInputStream::Ptr>& pagedStreams)
|
||||
{
|
||||
for (auto* leaf : leaves) {
|
||||
points::AttributeArray* array = attributeIndex < leaf->attributeSet().size() ?
|
||||
&leaf->attributeArray(attributeIndex) : nullptr;
|
||||
if (array) {
|
||||
auto* pagedStream = getOrCreatePagedStream(pagedStreams, attributeIndex);
|
||||
pagedStream->setInputStream(is);
|
||||
pagedStream->setSizeOnly(false);
|
||||
array->readPagedBuffers(*pagedStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LeafT>
|
||||
inline void skipPointDataAttributeSizes(const std::vector<LeafT*>& leaves,
|
||||
std::istream& is, Index attributeIndex,
|
||||
std::map<Index, compression::PagedInputStream::Ptr>& pagedStreams)
|
||||
{
|
||||
for (auto* leaf : leaves) {
|
||||
points::AttributeArray* array = attributeIndex < leaf->attributeSet().size() ?
|
||||
&leaf->attributeArray(attributeIndex) : nullptr;
|
||||
if (array) {
|
||||
auto* pagedStream = getOrCreatePagedStream(pagedStreams, attributeIndex);
|
||||
pagedStream->setInputStream(is);
|
||||
pagedStream->setSizeOnly(true);
|
||||
array->skipPagedBuffers(*pagedStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LeafT>
|
||||
inline void skipPointDataAttributeData(const std::vector<LeafT*>& leaves,
|
||||
std::istream& is, Index attributeIndex,
|
||||
std::map<Index, compression::PagedInputStream::Ptr>& pagedStreams)
|
||||
{
|
||||
for (auto* leaf : leaves) {
|
||||
points::AttributeArray* array = attributeIndex < leaf->attributeSet().size() ?
|
||||
&leaf->attributeArray(attributeIndex) : nullptr;
|
||||
if (array) {
|
||||
auto* pagedStream = getOrCreatePagedStream(pagedStreams, attributeIndex);
|
||||
pagedStream->setInputStream(is);
|
||||
pagedStream->setSizeOnly(false);
|
||||
array->skipPagedBuffers(*pagedStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
// Write-side functions
|
||||
|
||||
template <typename LeafT>
|
||||
inline Index countPointDataPasses(const std::vector<const LeafT*>& leaves)
|
||||
{
|
||||
Index maxRequiredPasses = 0;
|
||||
for (const auto* leaf : leaves) {
|
||||
const Index requiredPasses = leaf->buffers();
|
||||
if (requiredPasses > maxRequiredPasses) {
|
||||
maxRequiredPasses = requiredPasses;
|
||||
}
|
||||
}
|
||||
return maxRequiredPasses;
|
||||
}
|
||||
|
||||
template <typename LeafT>
|
||||
inline void writePointDataVoxelSizes(const std::vector<const LeafT*>& leaves,
|
||||
std::ostream& os, bool& matching,
|
||||
points::AttributeSet::Descriptor::Ptr& sharedDescriptor)
|
||||
{
|
||||
bool descriptorChecked = false;
|
||||
matching = true;
|
||||
for (const auto* leaf : leaves) {
|
||||
io::writeCompressedValuesSize(os, leaf->buffer().data(), LeafT::SIZE);
|
||||
|
||||
// Track descriptor matching
|
||||
const auto& descriptor = leaf->attributeSet().descriptorPtr();
|
||||
if (!descriptorChecked) {
|
||||
// First leaf - store descriptor
|
||||
descriptorChecked = true;
|
||||
sharedDescriptor = descriptor;
|
||||
}
|
||||
else if (matching && *sharedDescriptor != *descriptor) {
|
||||
matching = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LeafT>
|
||||
inline void writePointDataDescriptors(const std::vector<const LeafT*>& leaves,
|
||||
std::ostream& os, bool matching,
|
||||
const points::AttributeSet::Descriptor::Ptr&)
|
||||
{
|
||||
bool firstWrite = true;
|
||||
for (const auto* leaf : leaves) {
|
||||
const points::AttributeSet& attributeSet = leaf->attributeSet();
|
||||
if (matching) {
|
||||
// Shared descriptor - only write on first leaf
|
||||
if (firstWrite) {
|
||||
firstWrite = false;
|
||||
uint8_t header(1);
|
||||
os.write(reinterpret_cast<const char*>(&header), sizeof(uint8_t));
|
||||
attributeSet.writeDescriptor(os, /*transient=*/false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Non-shared descriptor - write on every leaf
|
||||
uint8_t header(0);
|
||||
os.write(reinterpret_cast<const char*>(&header), sizeof(uint8_t));
|
||||
attributeSet.writeDescriptor(os, /*transient=*/false);
|
||||
}
|
||||
attributeSet.writeMetadata(os, /*transient=*/false, /*paged=*/true);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LeafT>
|
||||
inline void writePointDataAttributeSizes(const std::vector<const LeafT*>& leaves,
|
||||
std::ostream& os, Index attributeIndex)
|
||||
{
|
||||
std::map<Index, compression::PagedOutputStream::Ptr> pagedStreams;
|
||||
for (const auto* leaf : leaves) {
|
||||
const points::AttributeArray* array = attributeIndex < leaf->attributeSet().size() ?
|
||||
leaf->attributeSet().getConst(attributeIndex) : nullptr;
|
||||
if (array) {
|
||||
auto* pagedStream = getOrCreatePagedStream(pagedStreams, attributeIndex);
|
||||
pagedStream->setOutputStream(os);
|
||||
pagedStream->setSizeOnly(true);
|
||||
array->writePagedBuffers(*pagedStream, /*outputTransient*/false);
|
||||
}
|
||||
}
|
||||
// Flush paged streams to write any remaining buffered page headers
|
||||
for (auto& pair : pagedStreams) {
|
||||
pair.second->flush();
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LeafT>
|
||||
inline void writePointDataVoxelData(const std::vector<const LeafT*>& leaves,
|
||||
std::ostream& os, bool saveFloatAsHalf)
|
||||
{
|
||||
using BaseLeaf = typename LeafT::BaseLeaf;
|
||||
for (const auto* leaf : leaves) {
|
||||
const BaseLeaf& baseLeaf = static_cast<const BaseLeaf&>(*leaf);
|
||||
writeScalarLeafBuffers(baseLeaf, os, saveFloatAsHalf);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LeafT>
|
||||
inline void writePointDataAttributeData(const std::vector<const LeafT*>& leaves,
|
||||
std::ostream& os, Index attributeIndex)
|
||||
{
|
||||
std::map<Index, compression::PagedOutputStream::Ptr> pagedStreams;
|
||||
for (const auto* leaf : leaves) {
|
||||
const points::AttributeArray* array = attributeIndex < leaf->attributeSet().size() ?
|
||||
leaf->attributeSet().getConst(attributeIndex) : nullptr;
|
||||
if (array) {
|
||||
auto* pagedStream = getOrCreatePagedStream(pagedStreams, attributeIndex);
|
||||
pagedStream->setOutputStream(os);
|
||||
pagedStream->setSizeOnly(false);
|
||||
array->writePagedBuffers(*pagedStream, /*outputTransient*/false);
|
||||
}
|
||||
}
|
||||
// Flush paged streams to write any remaining buffered page data
|
||||
for (auto& pair : pagedStreams) {
|
||||
pair.second->flush();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
/// Per-grid-type codec-specific options for PointDataCodec
|
||||
/// Contains point attribute filtering options
|
||||
struct OPENVDB_API PointDataCodecTypeData : public io::ReadTypedOptions
|
||||
{
|
||||
// Point Attribute Options - which attributes to read
|
||||
std::vector<std::string> pointAttributeNames;
|
||||
}; // struct PointDataCodecTypeData
|
||||
|
||||
template <typename GridT>
|
||||
struct PointDataCodec final: public TopologyCodec<GridT>
|
||||
{
|
||||
using Ptr = std::unique_ptr<PointDataCodec<GridT>>;
|
||||
|
||||
~PointDataCodec() noexcept = default;
|
||||
|
||||
static inline std::string name() { return GridT::gridType(); }
|
||||
|
||||
void readBuffers(std::istream& is, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics&) final
|
||||
{
|
||||
OPENVDB_ASSERT(dynamic_cast<GridT*>(data.grid.get()));
|
||||
|
||||
GridT& grid = static_cast<GridT&>(*data.grid);
|
||||
|
||||
std::vector<std::string> pointAttributeNames;
|
||||
|
||||
// Look up point-specific options if provided
|
||||
auto it = options.typeData.find(name());
|
||||
if (it != options.typeData.end()) {
|
||||
auto& pointTypeData = io::ReadTypedOptions::cast<PointDataCodecTypeData>(it->second);
|
||||
pointAttributeNames = pointTypeData.pointAttributeNames;
|
||||
}
|
||||
|
||||
io::checkFormatVersion(is);
|
||||
|
||||
bool saveFloatAsHalf = grid.saveFloatAsHalf();
|
||||
|
||||
auto& tree = grid.tree();
|
||||
tree.clearAllAccessors();
|
||||
|
||||
uint16_t numPasses = 1;
|
||||
is.read(reinterpret_cast<char*>(&numPasses), sizeof(uint16_t));
|
||||
const Index attributes = (numPasses - 4) / 2;
|
||||
|
||||
using LeafT = typename GridT::TreeType::LeafNodeType;
|
||||
std::vector<LeafT*> leaves;
|
||||
tree.getNodes(leaves);
|
||||
|
||||
// Pass 0: read voxel data sizes
|
||||
std::unordered_map<Coord, uint16_t> voxelBufferSizes;
|
||||
internal::readPointDataVoxelSizes(leaves, is, voxelBufferSizes);
|
||||
|
||||
// Pass 1: read descriptor and attribute metadata
|
||||
internal::readPointDataDescriptors(leaves, is);
|
||||
|
||||
// Build set of attribute indices to skip based on pointAttributeNames.
|
||||
// An empty pointAttributeNames means no filtering (read all attributes).
|
||||
std::set<Index> skipIndices;
|
||||
if (!pointAttributeNames.empty() && !leaves.empty()) {
|
||||
const auto& nameMap = leaves[0]->attributeSet().descriptor().map();
|
||||
const std::set<std::string> wantedNames(
|
||||
pointAttributeNames.begin(),
|
||||
pointAttributeNames.end());
|
||||
for (const auto& namePos : nameMap) {
|
||||
if (wantedNames.find(namePos.first) == wantedNames.end()) {
|
||||
skipIndices.insert(static_cast<Index>(namePos.second));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Passes 2..N+1: read attribute buffer sizes
|
||||
std::map<Index, compression::PagedInputStream::Ptr> pagedStreams;
|
||||
for (Index i = 0; i < attributes; ++i) {
|
||||
if (skipIndices.count(i)) {
|
||||
internal::skipPointDataAttributeSizes(leaves, is, i, pagedStreams);
|
||||
} else {
|
||||
internal::readPointDataAttributeSizes(leaves, is, i, pagedStreams);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass N+2: read voxel data
|
||||
internal::readPointDataVoxelData(leaves, is, saveFloatAsHalf,
|
||||
tree.background(), voxelBufferSizes);
|
||||
|
||||
// Passes N+3..2N+2: read attribute data buffers
|
||||
for (Index i = 0; i < attributes; ++i) {
|
||||
if (skipIndices.count(i)) {
|
||||
internal::skipPointDataAttributeData(leaves, is, i, pagedStreams);
|
||||
} else {
|
||||
internal::readPointDataAttributeData(leaves, is, i, pagedStreams);
|
||||
}
|
||||
}
|
||||
|
||||
// Drop skipped attributes from each leaf's AttributeSet
|
||||
if (!skipIndices.empty() && !leaves.empty()) {
|
||||
const std::vector<size_t> dropPositions(skipIndices.begin(), skipIndices.end());
|
||||
auto filteredDescriptor =
|
||||
leaves[0]->attributeSet().descriptorPtr()->duplicateDrop(dropPositions);
|
||||
for (auto* leaf : leaves) {
|
||||
leaf->dropAttributes(dropPositions,
|
||||
leaf->attributeSet().descriptor(), filteredDescriptor);
|
||||
}
|
||||
}
|
||||
|
||||
// PointDataGrid uses multiple passes, so clip after reading
|
||||
// the buffers if bbox is not infinite.
|
||||
if (options.clipBBox.isSorted()) {
|
||||
CoordBBox indexBBox =
|
||||
grid.constTransform().worldToIndexNodeCentered(options.clipBBox);
|
||||
grid.tree().root().clip(indexBBox);
|
||||
}
|
||||
}
|
||||
|
||||
void writeBuffers(std::ostream& os, const GridBase& gridBase, const io::WriteOptions&) final
|
||||
{
|
||||
const GridT& grid = static_cast<const GridT&>(gridBase);
|
||||
bool saveFloatAsHalf = grid.saveFloatAsHalf();
|
||||
|
||||
using LeafT = typename GridT::TreeType::LeafNodeType;
|
||||
std::vector<const LeafT*> leaves;
|
||||
grid.tree().getNodes(leaves);
|
||||
|
||||
// Determine how many leaf buffer passes are required for this grid
|
||||
uint16_t numPasses =
|
||||
static_cast<uint16_t>(internal::countPointDataPasses(leaves));
|
||||
os.write(reinterpret_cast<const char*>(&numPasses), sizeof(uint16_t));
|
||||
|
||||
const Index attributes = (numPasses - 4) / 2;
|
||||
|
||||
// Pass 0: write voxel data sizes + descriptor tracking
|
||||
bool matching = true;
|
||||
points::AttributeSet::Descriptor::Ptr sharedDescriptor;
|
||||
internal::writePointDataVoxelSizes(leaves, os, matching, sharedDescriptor);
|
||||
|
||||
// Pass 1: write descriptor and attribute metadata
|
||||
internal::writePointDataDescriptors(leaves, os, matching, sharedDescriptor);
|
||||
|
||||
// Passes 2..N+1: write attribute buffer sizes (page headers)
|
||||
for (Index i = 0; i < attributes; ++i) {
|
||||
internal::writePointDataAttributeSizes(leaves, os, i);
|
||||
}
|
||||
|
||||
// Pass N+2: write voxel data
|
||||
internal::writePointDataVoxelData(leaves, os, saveFloatAsHalf);
|
||||
|
||||
// Passes N+3..2N+2: write attribute data buffers (page data)
|
||||
for (Index i = 0; i < attributes; ++i) {
|
||||
internal::writePointDataAttributeData(leaves, os, i);
|
||||
}
|
||||
}
|
||||
}; // struct PointDataCodec
|
||||
|
||||
} // namespace codecs
|
||||
} // namespace OPENVDB_VERSION_NAME
|
||||
} // namespace openvdb
|
||||
|
||||
#endif // OPENVDB_IO_CODECS_POINTDATACODEC_HAS_BEEN_INCLUDED
|
||||
@@ -1,159 +0,0 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef OPENVDB_IO_CODECS_POINTINDEXCODEC_HAS_BEEN_INCLUDED
|
||||
#define OPENVDB_IO_CODECS_POINTINDEXCODEC_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <openvdb/io/Codec.h>
|
||||
|
||||
#include <openvdb/tools/PointIndexGrid.h>
|
||||
|
||||
#include "impl/ScalarLeafCodec.h"
|
||||
#include "TopologyCodec.h"
|
||||
|
||||
namespace openvdb {
|
||||
OPENVDB_USE_VERSION_NAMESPACE
|
||||
namespace OPENVDB_VERSION_NAME {
|
||||
namespace codecs {
|
||||
namespace internal {
|
||||
|
||||
template <typename GridT>
|
||||
struct ReadPointIndexBuffersOp
|
||||
{
|
||||
using TreeT = typename GridT::TreeType;
|
||||
using RootT = typename TreeT::RootNodeType;
|
||||
using LeafT = typename TreeT::LeafNodeType;
|
||||
using ValueT = typename TreeT::ValueType;
|
||||
|
||||
ReadPointIndexBuffersOp(std::istream& _is, bool _saveFloatAsHalf,
|
||||
const ValueT& _background)
|
||||
: is(_is)
|
||||
, saveFloatAsHalf(_saveFloatAsHalf)
|
||||
, background(_background) { }
|
||||
|
||||
template <typename NodeT>
|
||||
void operator()(NodeT&, size_t) { }
|
||||
|
||||
void operator()(LeafT& leaf, size_t)
|
||||
{
|
||||
using BaseLeaf = typename LeafT::BaseLeaf;
|
||||
|
||||
// Read the value mask and voxel data via base class
|
||||
BaseLeaf& baseLeaf = static_cast<BaseLeaf&>(leaf);
|
||||
readScalarLeafBuffers(baseLeaf, is, saveFloatAsHalf, background, /*skip=*/false, /*clipBBox=*/nullptr);
|
||||
|
||||
// Read the number of indices.
|
||||
Index64 numIndices = Index64(0);
|
||||
is.read(reinterpret_cast<char*>(&numIndices), sizeof(Index64));
|
||||
|
||||
// Read the indices data.
|
||||
leaf.indices().resize(size_t(numIndices));
|
||||
is.read(reinterpret_cast<char*>(leaf.indices().data()), numIndices * sizeof(ValueT));
|
||||
|
||||
// Reserved for future use.
|
||||
Index64 auxDataBytes = Index64(0);
|
||||
is.read(reinterpret_cast<char*>(&auxDataBytes), sizeof(Index64));
|
||||
if (auxDataBytes > 0) {
|
||||
// For now, read and discard any auxiliary data.
|
||||
std::unique_ptr<char[]> auxData{new char[auxDataBytes]};
|
||||
is.read(auxData.get(), auxDataBytes);
|
||||
}
|
||||
}
|
||||
|
||||
std::istream& is;
|
||||
const bool saveFloatAsHalf;
|
||||
const ValueT& background;
|
||||
}; // struct ReadPointIndexBuffersOp
|
||||
|
||||
template <typename GridT>
|
||||
struct WritePointIndexBuffersOp
|
||||
{
|
||||
using TreeT = typename GridT::TreeType;
|
||||
using LeafT = typename TreeT::LeafNodeType;
|
||||
using ValueT = typename TreeT::ValueType;
|
||||
|
||||
WritePointIndexBuffersOp(std::ostream& _os, bool _saveFloatAsHalf)
|
||||
: os(_os)
|
||||
, saveFloatAsHalf(_saveFloatAsHalf) { }
|
||||
|
||||
template <typename NodeT>
|
||||
void operator()(const NodeT&, size_t) { }
|
||||
|
||||
void operator()(const LeafT& leaf, size_t)
|
||||
{
|
||||
using BaseLeaf = typename LeafT::BaseLeaf;
|
||||
|
||||
// Write out the value mask and voxel values via base class
|
||||
const BaseLeaf& baseLeaf = static_cast<const BaseLeaf&>(leaf);
|
||||
writeScalarLeafBuffers(baseLeaf, os, saveFloatAsHalf);
|
||||
|
||||
// Write the number of indices.
|
||||
Index64 numIndices = Index64(leaf.indices().size());
|
||||
os.write(reinterpret_cast<const char*>(&numIndices), sizeof(Index64));
|
||||
|
||||
// Write the indices data.
|
||||
os.write(reinterpret_cast<const char*>(leaf.indices().data()), numIndices * sizeof(ValueT));
|
||||
|
||||
// Reserved for future use.
|
||||
const Index64 auxDataBytes = Index64(0);
|
||||
os.write(reinterpret_cast<const char*>(&auxDataBytes), sizeof(Index64));
|
||||
}
|
||||
|
||||
std::ostream& os;
|
||||
const bool saveFloatAsHalf;
|
||||
}; // struct WritePointIndexBuffersOp
|
||||
|
||||
} // namespace internal
|
||||
|
||||
template <typename GridT>
|
||||
struct PointIndexCodec final: public TopologyCodec<GridT>
|
||||
{
|
||||
using Ptr = std::unique_ptr<PointIndexCodec<GridT>>;
|
||||
|
||||
~PointIndexCodec() noexcept = default;
|
||||
|
||||
static inline std::string name() { return GridT::gridType(); }
|
||||
|
||||
void readBuffers(std::istream& is, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics& diagnostics) final
|
||||
{
|
||||
OPENVDB_ASSERT(dynamic_cast<GridT*>(data.grid.get()));
|
||||
|
||||
GridT& grid = static_cast<GridT&>(*data.grid);
|
||||
|
||||
if (grid.hasMultiPassIO()) {
|
||||
OPENVDB_THROW(IoError, "Multi-pass IO is not supported in PointIndexCodec");
|
||||
}
|
||||
|
||||
io::checkFormatVersion(is);
|
||||
|
||||
bool saveFloatAsHalf = grid.saveFloatAsHalf();
|
||||
|
||||
auto& tree = grid.tree();
|
||||
tree.clearAllAccessors();
|
||||
|
||||
if (options.clipBBox.isSorted()) {
|
||||
diagnostics.addWarning(grid.getName(), "bounding box clipping is not supported for PointIndexGrids");
|
||||
}
|
||||
|
||||
internal::ReadPointIndexBuffersOp<GridT> readBuffersOp(is, saveFloatAsHalf, tree.background());
|
||||
tools::visitNodesDepthFirst(grid.tree(), readBuffersOp, /*idx=*/0, /*topDown=*/false);
|
||||
}
|
||||
|
||||
void writeBuffers(std::ostream& os, const GridBase& gridBase, const io::WriteOptions&) final
|
||||
{
|
||||
const GridT& grid = static_cast<const GridT&>(gridBase);
|
||||
|
||||
if (grid.hasMultiPassIO()) {
|
||||
OPENVDB_THROW(IoError, "Multi-pass IO is not supported in PointIndexCodec");
|
||||
}
|
||||
|
||||
internal::WritePointIndexBuffersOp<GridT> writeBuffersOp(os, grid.saveFloatAsHalf());
|
||||
tools::visitNodesDepthFirst(grid.tree(), writeBuffersOp);
|
||||
}
|
||||
}; // struct PointIndexCodec
|
||||
|
||||
} // namespace codecs
|
||||
} // namespace OPENVDB_VERSION_NAME
|
||||
} // namespace openvdb
|
||||
|
||||
#endif // OPENVDB_IO_CODECS_POINTINDEXCODEC_HAS_BEEN_INCLUDED
|
||||
@@ -1,173 +0,0 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef OPENVDB_IO_CODECS_SCALARCODEC_HAS_BEEN_INCLUDED
|
||||
#define OPENVDB_IO_CODECS_SCALARCODEC_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <openvdb/openvdb.h>
|
||||
#include <openvdb/tree/Tree.h>
|
||||
#include <openvdb/tools/Clip.h>
|
||||
#include <openvdb/tools/NodeVisitor.h>
|
||||
#include <openvdb/io/Codec.h>
|
||||
|
||||
#include "impl/ScalarLeafCodec.h"
|
||||
#include "TopologyCodec.h"
|
||||
|
||||
namespace openvdb {
|
||||
OPENVDB_USE_VERSION_NAMESPACE
|
||||
namespace OPENVDB_VERSION_NAME {
|
||||
namespace codecs {
|
||||
namespace internal {
|
||||
|
||||
template <typename TreeT>
|
||||
struct WriteBuffersOp
|
||||
{
|
||||
using LeafT = typename TreeT::LeafNodeType;
|
||||
|
||||
WriteBuffersOp(std::ostream& _os, bool _saveFloatAsHalf)
|
||||
: os(_os)
|
||||
, saveFloatAsHalf(_saveFloatAsHalf) { }
|
||||
|
||||
template <typename NodeT>
|
||||
void operator()(const NodeT&, size_t) { }
|
||||
|
||||
void operator()(const LeafT& leaf, size_t)
|
||||
{
|
||||
writeScalarLeafBuffers(leaf, os, saveFloatAsHalf);
|
||||
}
|
||||
|
||||
std::ostream& os;
|
||||
const bool saveFloatAsHalf;
|
||||
}; // struct WriteBuffersOp
|
||||
|
||||
|
||||
template <typename TreeT, typename StorageTreeT>
|
||||
struct ReadBuffersOp
|
||||
{
|
||||
using RootT = typename TreeT::RootNodeType;
|
||||
using LeafT = typename TreeT::LeafNodeType;
|
||||
using ValueT = typename TreeT::ValueType;
|
||||
using StorageLeafT = typename StorageTreeT::LeafNodeType;
|
||||
|
||||
ReadBuffersOp(std::istream& _is, bool _saveFloatAsHalf, const ValueT& _background,
|
||||
const CoordBBox* _clipBBox)
|
||||
: is(_is)
|
||||
, saveFloatAsHalf(_saveFloatAsHalf)
|
||||
, background(_background)
|
||||
, clipBBox(_clipBBox) { }
|
||||
|
||||
void operator()(RootT& root, size_t)
|
||||
{
|
||||
if (clipBBox) {
|
||||
root.clip(*clipBBox);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename NodeT>
|
||||
void operator()(NodeT& node, size_t)
|
||||
{
|
||||
if (clipBBox) {
|
||||
node.clip(*clipBBox, background);
|
||||
}
|
||||
}
|
||||
|
||||
void operator()(LeafT& leaf, size_t)
|
||||
{
|
||||
readScalarLeafBuffers<LeafT, StorageLeafT>(leaf, is, saveFloatAsHalf, background, /*skip=*/false, clipBBox);
|
||||
}
|
||||
|
||||
std::istream& is;
|
||||
const bool saveFloatAsHalf;
|
||||
const ValueT& background;
|
||||
const CoordBBox* clipBBox = nullptr;
|
||||
}; // struct ReadBuffersOp
|
||||
|
||||
|
||||
// Free-standing function for both standard and conversion codec cases
|
||||
// Uses StorageGridT = GridT by default, but allows different storage type for conversions
|
||||
template<typename GridT, typename StorageGridT = GridT>
|
||||
void scalarCodecReadBuffers(GridT& grid, std::istream& is, const io::ReadOptions& options)
|
||||
{
|
||||
if (grid.hasMultiPassIO()) {
|
||||
OPENVDB_THROW(IoError, "Multi-pass IO is not supported in ScalarCodec");
|
||||
}
|
||||
|
||||
using TreeT = typename GridT::TreeType;
|
||||
using StorageTreeT = typename StorageGridT::TreeType;
|
||||
|
||||
io::checkFormatVersion(is);
|
||||
|
||||
bool saveFloatAsHalf = grid.saveFloatAsHalf();
|
||||
|
||||
auto& tree = grid.tree();
|
||||
tree.clearAllAccessors();
|
||||
|
||||
std::unique_ptr<CoordBBox> clipIndexBBox;
|
||||
if (options.clipBBox.isSorted()) {
|
||||
clipIndexBBox = std::make_unique<CoordBBox>(grid.constTransform().worldToIndexNodeCentered(options.clipBBox));
|
||||
}
|
||||
|
||||
// Works for both standard (TreeT == StorageTreeT) and conversion cases
|
||||
ReadBuffersOp<TreeT, StorageTreeT> readBuffersOp(is, saveFloatAsHalf, tree.background(),
|
||||
clipIndexBBox.get());
|
||||
tools::visitNodesDepthFirst(grid.tree(), readBuffersOp, /*idx=*/0, /*topDown=*/false);
|
||||
}
|
||||
|
||||
// Free-standing function for write case (no StorageGridT needed)
|
||||
template<typename GridT>
|
||||
void scalarCodecWriteBuffers(const GridT& grid, std::ostream& os)
|
||||
{
|
||||
using TreeType = typename GridT::TreeType;
|
||||
|
||||
if (grid.hasMultiPassIO()) {
|
||||
OPENVDB_THROW(IoError, "Multi-pass IO is not supported in ScalarCodec");
|
||||
}
|
||||
|
||||
WriteBuffersOp<TreeType> writeBuffersOp(os, grid.saveFloatAsHalf());
|
||||
tools::visitNodesDepthFirst(grid.tree(), writeBuffersOp);
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
template <typename GridT, typename StorageGridT = GridT, io::CodecMode Mode = io::CodecMode::ReadWrite>
|
||||
struct ScalarCodec final: public TopologyCodec<GridT, StorageGridT, Mode>
|
||||
{
|
||||
static_assert(GridT::TreeType::RootNodeType::template SameConfiguration<
|
||||
typename StorageGridT::TreeType::RootNodeType>::value,
|
||||
"GridT and StorageGridT must have the same configuration");
|
||||
|
||||
using Ptr = std::unique_ptr<ScalarCodec<GridT, StorageGridT, Mode>>;
|
||||
|
||||
~ScalarCodec() noexcept = default;
|
||||
|
||||
static inline std::string name()
|
||||
{
|
||||
if constexpr (std::is_same_v<GridT, StorageGridT>) {
|
||||
return GridT::gridType();
|
||||
} else {
|
||||
std::string buildType = typeNameAsString<typename GridT::BuildType>();
|
||||
return StorageGridT::gridType() + "_to_" + buildType;
|
||||
}
|
||||
}
|
||||
|
||||
void readBuffers(std::istream& is, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics&) final
|
||||
{
|
||||
GridT& grid = static_cast<GridT&>(*data.grid);
|
||||
internal::scalarCodecReadBuffers<GridT, StorageGridT>(grid, is, options);
|
||||
}
|
||||
|
||||
void writeBuffers(std::ostream& os, const GridBase& gridBase, const io::WriteOptions&) final
|
||||
{
|
||||
if constexpr (Mode == io::CodecMode::ReadOnly) return;
|
||||
|
||||
const GridT& grid = static_cast<const GridT&>(gridBase);
|
||||
internal::scalarCodecWriteBuffers(grid, os);
|
||||
}
|
||||
}; // struct ScalarCodec
|
||||
|
||||
|
||||
} // namespace codecs
|
||||
} // namespace OPENVDB_VERSION_NAME
|
||||
} // namespace openvdb
|
||||
|
||||
#endif // OPENVDB_IO_CODECS_SCALARCODEC_HAS_BEEN_INCLUDED
|
||||
@@ -1,341 +0,0 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef OPENVDB_IO_CODECS_TOPOLOGYCODEC_HAS_BEEN_INCLUDED
|
||||
#define OPENVDB_IO_CODECS_TOPOLOGYCODEC_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <openvdb/openvdb.h>
|
||||
#include <openvdb/tree/Tree.h>
|
||||
#include <openvdb/tree/NodeManager.h>
|
||||
#include <openvdb/tools/NodeVisitor.h>
|
||||
#include <openvdb/io/Codec.h>
|
||||
#include <openvdb/Grid.h>
|
||||
|
||||
namespace openvdb {
|
||||
OPENVDB_USE_VERSION_NAMESPACE
|
||||
namespace OPENVDB_VERSION_NAME {
|
||||
namespace codecs {
|
||||
namespace internal {
|
||||
|
||||
template <typename TreeT>
|
||||
struct WriteTopologyOp
|
||||
{
|
||||
using ValueT = typename TreeT::ValueType;
|
||||
using RootT = typename TreeT::RootNodeType;
|
||||
using LeafT = typename TreeT::LeafNodeType;
|
||||
|
||||
WriteTopologyOp(std::ostream& _os, bool _saveFloatAsHalf)
|
||||
: os(_os)
|
||||
, saveFloatAsHalf(_saveFloatAsHalf) { }
|
||||
|
||||
void operator()(const RootT& root, size_t)
|
||||
{
|
||||
int32_t bufferCount = 1;
|
||||
os.write(reinterpret_cast<char*>(&bufferCount), sizeof(int32_t));
|
||||
|
||||
background = &root.background();
|
||||
if (!saveFloatAsHalf) {
|
||||
os.write(reinterpret_cast<const char*>(background), sizeof(ValueT));
|
||||
} else {
|
||||
ValueT truncatedVal = io::truncateRealToHalf(*background);
|
||||
os.write(reinterpret_cast<const char*>(&truncatedVal), sizeof(ValueT));
|
||||
}
|
||||
io::setGridBackgroundValuePtr(os, background);
|
||||
|
||||
const Index numTiles = root.tileCount(), numChildren = root.childCount();
|
||||
os.write(reinterpret_cast<const char*>(&numTiles), sizeof(Index));
|
||||
os.write(reinterpret_cast<const char*>(&numChildren), sizeof(Index));
|
||||
|
||||
// Write tiles.
|
||||
for (auto iter = root.cbeginValueAll(); iter; ++iter) {
|
||||
const auto& ijk = iter.getCoord();
|
||||
os.write(reinterpret_cast<const char*>(ijk.asPointer()), 3 * sizeof(Int32));
|
||||
ValueT value;
|
||||
bool active = root.getTileValueUnsafe(ijk, value);
|
||||
os.write(reinterpret_cast<const char*>(&value), sizeof(ValueT));
|
||||
os.write(reinterpret_cast<const char*>(&active), sizeof(bool));
|
||||
}
|
||||
|
||||
rootChildLevel = RootT::LEVEL-1;
|
||||
}
|
||||
|
||||
template <typename NodeT>
|
||||
void operator()(const NodeT& node, size_t)
|
||||
{
|
||||
// Write origin for RootNode children only
|
||||
if (rootChildLevel == NodeT::LEVEL) {
|
||||
const auto& ijk = node.origin();
|
||||
os.write(reinterpret_cast<const char*>(ijk.asPointer()), 3 * sizeof(Int32));
|
||||
}
|
||||
|
||||
const auto& childMask = node.getChildMask();
|
||||
const auto& valueMask = node.getValueMask();
|
||||
|
||||
childMask.save(os);
|
||||
valueMask.save(os);
|
||||
|
||||
{
|
||||
// Copy all of this node's values into an array.
|
||||
std::unique_ptr<ValueT[]> valuePtr(new ValueT[NodeT::NUM_VALUES]);
|
||||
ValueT* values = valuePtr.get();
|
||||
const ValueT zero = zeroVal<ValueT>();
|
||||
for (Index i = 0; i < NodeT::NUM_VALUES; ++i) {
|
||||
values[i] = (node.isChildMaskOff(i) ? node.getValueUnsafe(i) : zero);
|
||||
}
|
||||
// Compress (optionally) and write out the contents of the array.
|
||||
io::writeCompressedValues(os, values, NodeT::NUM_VALUES, valueMask, childMask, saveFloatAsHalf);
|
||||
}
|
||||
}
|
||||
|
||||
void operator()(const LeafT& leaf, size_t)
|
||||
{
|
||||
leaf.getValueMask().save(os);
|
||||
}
|
||||
|
||||
std::ostream& os;
|
||||
const bool saveFloatAsHalf;
|
||||
const ValueT* background = nullptr;
|
||||
Index rootChildLevel = std::numeric_limits<Index>::max();
|
||||
}; // struct WriteTopologyOp
|
||||
|
||||
|
||||
template <typename TreeT, typename StorageTreeT>
|
||||
struct ReadTopologyOp
|
||||
{
|
||||
using ValueT = typename TreeT::ValueType;
|
||||
using RootT = typename TreeT::RootNodeType;
|
||||
using LeafT = typename TreeT::LeafNodeType;
|
||||
using StorageValueT = typename StorageTreeT::ValueType;
|
||||
|
||||
ReadTopologyOp(std::istream& _is, bool _saveFloatAsHalf, io::ReadDiagnostics& _diagnostics,
|
||||
const std::string& _gridName)
|
||||
: is(_is)
|
||||
, saveFloatAsHalf(_saveFloatAsHalf)
|
||||
, diagnostics(_diagnostics)
|
||||
, gridName(_gridName) { }
|
||||
|
||||
void operator()(RootT& root)
|
||||
{
|
||||
using ChildT = typename RootT::ChildNodeType;
|
||||
|
||||
int32_t bufferCount;
|
||||
is.read(reinterpret_cast<char*>(&bufferCount), sizeof(int32_t));
|
||||
if (bufferCount != 1) {
|
||||
diagnostics.addWarning(gridName, "multi-buffer trees are no longer supported");
|
||||
}
|
||||
|
||||
// Delete the existing tree.
|
||||
root.clear();
|
||||
|
||||
// Read a RootNode that was stored in the current format.
|
||||
|
||||
if constexpr (std::is_same_v<ValueT, StorageValueT>) {
|
||||
is.read(reinterpret_cast<char*>(&background), sizeof(ValueT));
|
||||
} else {
|
||||
StorageValueT _background;
|
||||
is.read(reinterpret_cast<char*>(&_background), sizeof(StorageValueT));
|
||||
background = static_cast<ValueT>(_background);
|
||||
}
|
||||
|
||||
root.setBackground(background, false);
|
||||
io::setGridBackgroundValuePtr(is, &root.background());
|
||||
|
||||
Index numTiles = 0, numChildren = 0;
|
||||
is.read(reinterpret_cast<char*>(&numTiles), sizeof(Index));
|
||||
is.read(reinterpret_cast<char*>(&numChildren), sizeof(Index));
|
||||
|
||||
Int32 vec[3];
|
||||
StorageValueT value;
|
||||
bool active;
|
||||
|
||||
// Read tiles.
|
||||
for (Index n = 0; n < numTiles; ++n) {
|
||||
is.read(reinterpret_cast<char*>(vec), 3 * sizeof(Int32));
|
||||
is.read(reinterpret_cast<char*>(&value), sizeof(StorageValueT));
|
||||
is.read(reinterpret_cast<char*>(&active), sizeof(bool));
|
||||
Coord origin(vec);
|
||||
if constexpr (std::is_same_v<ValueT, StorageValueT>) {
|
||||
root.addTile(origin, value, active);
|
||||
} else {
|
||||
root.addTile(origin, static_cast<ValueT>(value), active);
|
||||
}
|
||||
}
|
||||
|
||||
// Read child nodes.
|
||||
for (Index n = 0; n < numChildren; ++n) {
|
||||
is.read(reinterpret_cast<char*>(vec), 3 * sizeof(Int32));
|
||||
Coord origin(vec);
|
||||
ChildT* child = new ChildT(PartialCreate(), origin, background);
|
||||
(*this)(*child);
|
||||
root.addChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename NodeT>
|
||||
void operator()(NodeT& node)
|
||||
{
|
||||
using ChildT = typename NodeT::ChildNodeType;
|
||||
using NodeMaskT = typename NodeT::NodeMaskType;
|
||||
|
||||
NodeMaskT childMask, valueMask;
|
||||
childMask.load(is);
|
||||
valueMask.load(is);
|
||||
node.setValueMaskUnsafe(valueMask);
|
||||
|
||||
const bool oldVersion =
|
||||
(io::getFormatVersion(is) < OPENVDB_FILE_VERSION_NODE_MASK_COMPRESSION);
|
||||
const Index numValues = (oldVersion ? childMask.countOff() : NodeT::NUM_VALUES);
|
||||
{
|
||||
// Read in (and uncompress, if necessary) all of this node's values
|
||||
// into a contiguous array.
|
||||
std::unique_ptr<StorageValueT[]> valuePtr(new StorageValueT[numValues]);
|
||||
StorageValueT* values = valuePtr.get();
|
||||
io::readCompressedValues(is, values, numValues, valueMask, saveFloatAsHalf);
|
||||
|
||||
// Copy values from the array into this node's table.
|
||||
if (oldVersion) {
|
||||
Index n = 0;
|
||||
for (auto iter = node.beginValueAll(); iter; ++iter) {
|
||||
node.setValueOnlyUnsafe(iter.pos(), static_cast<ValueT>(values[n++]));
|
||||
}
|
||||
OPENVDB_ASSERT(n == numValues);
|
||||
} else {
|
||||
for (auto iter = node.beginValueAll(); iter; ++iter) {
|
||||
node.setValueOnlyUnsafe(iter.pos(), static_cast<ValueT>(values[iter.pos()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read in all child nodes and insert them into the table at their proper locations.
|
||||
for (auto iter = childMask.beginOn(); iter; ++iter) {
|
||||
Coord origin = node.offsetToGlobalCoord(iter.pos());
|
||||
auto* child = new ChildT(PartialCreate(), origin, background);
|
||||
(*this)(*child);
|
||||
node.setChildUnsafe(iter.pos(), child);
|
||||
}
|
||||
}
|
||||
|
||||
void operator()(LeafT& leaf)
|
||||
{
|
||||
typename LeafT::NodeMaskType valueMask;
|
||||
valueMask.load(is);
|
||||
leaf.setValueMask(valueMask);
|
||||
}
|
||||
|
||||
std::istream& is;
|
||||
bool saveFloatAsHalf;
|
||||
ValueT background;
|
||||
io::ReadDiagnostics& diagnostics;
|
||||
std::string gridName;
|
||||
}; // struct ReadTopologyOp
|
||||
|
||||
template <typename TreeT>
|
||||
struct SetTilesToBackgroundOp
|
||||
{
|
||||
using RootT = typename TreeT::RootNodeType;
|
||||
using LeafT = typename TreeT::LeafNodeType;
|
||||
using ValueT = typename TreeT::ValueType;
|
||||
|
||||
explicit SetTilesToBackgroundOp(const ValueT& background)
|
||||
: mBackground(background) { }
|
||||
|
||||
bool operator()(RootT& root, size_t) const
|
||||
{
|
||||
for (auto it = root.beginValueAll(); it; ++it) {
|
||||
it.setValue(mBackground);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename NodeT>
|
||||
bool operator()(NodeT& node, size_t) const
|
||||
{
|
||||
for (Index i = 0; i < NodeT::NUM_VALUES; ++i) {
|
||||
if (node.isChildMaskOff(i)) {
|
||||
node.setValueOnlyUnsafe(i, mBackground);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool operator()(LeafT&, size_t) const { return true; }
|
||||
|
||||
private:
|
||||
const ValueT mBackground;
|
||||
}; // struct SetTilesToBackgroundOp
|
||||
|
||||
template<typename TreeT>
|
||||
void setTilesToBackground(TreeT& tree)
|
||||
{
|
||||
const typename TreeT::ValueType& background = tree.root().background();
|
||||
SetTilesToBackgroundOp<TreeT> op(background);
|
||||
tree::DynamicNodeManager<TreeT> nodeManager(tree);
|
||||
nodeManager.foreachTopDown(op);
|
||||
}
|
||||
|
||||
// Free-standing function for read case (supports type conversion via StorageGridT)
|
||||
template<typename GridT, typename StorageGridT = GridT>
|
||||
void topologyCodecReadTopology(GridBase& gridBase, std::istream& is, const io::ReadOptions& options, io::ReadDiagnostics& diagnostics)
|
||||
{
|
||||
io::checkFormatVersion(is);
|
||||
|
||||
GridT& grid = static_cast<GridT&>(gridBase);
|
||||
grid.tree().clearAllAccessors();
|
||||
|
||||
internal::ReadTopologyOp<typename GridT::TreeType, typename StorageGridT::TreeType> readTopologyOp(is, grid.saveFloatAsHalf(), diagnostics, grid.getName());
|
||||
readTopologyOp(grid.tree().root());
|
||||
|
||||
if (options.readMode == io::ReadMode::TopologyOnly) {
|
||||
internal::setTilesToBackground(grid.tree());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Free-standing function for write case (no StorageGridT needed)
|
||||
template<typename GridT>
|
||||
void topologyCodecWriteTopology(const GridBase& gridBase, std::ostream& os)
|
||||
{
|
||||
const GridT& grid = static_cast<const GridT&>(gridBase);
|
||||
using TreeType = typename GridT::TreeType;
|
||||
|
||||
internal::WriteTopologyOp<TreeType> writeTopologyOp(os, grid.saveFloatAsHalf());
|
||||
tools::visitNodesDepthFirst(grid.tree(), writeTopologyOp);
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
template <typename GridT, typename StorageGridT = GridT, io::CodecMode Mode = io::CodecMode::ReadWrite>
|
||||
struct TopologyCodec : public io::Codec
|
||||
{
|
||||
using Ptr = std::unique_ptr<TopologyCodec<GridT, StorageGridT, Mode>>;
|
||||
|
||||
~TopologyCodec() noexcept = default;
|
||||
|
||||
io::CodecData::Ptr createData() override
|
||||
{
|
||||
auto data = std::make_unique<io::CodecData>();
|
||||
data->grid = GridT::create();
|
||||
return data;
|
||||
}
|
||||
|
||||
void readTopology(std::istream& is, io::CodecData& data, const io::ReadOptions& options,
|
||||
io::ReadDiagnostics& diagnostics) final
|
||||
{
|
||||
internal::topologyCodecReadTopology<GridT, StorageGridT>(*data.grid, is, options, diagnostics);
|
||||
}
|
||||
|
||||
void writeTopology(std::ostream& os, const GridBase& gridBase, const io::WriteOptions&) final
|
||||
{
|
||||
// disable implementation when read only
|
||||
if constexpr (Mode == io::CodecMode::ReadOnly) return;
|
||||
|
||||
internal::topologyCodecWriteTopology<GridT>(gridBase, os);
|
||||
}
|
||||
}; // struct TopologyCodec
|
||||
|
||||
|
||||
} // namespace codecs
|
||||
} // namespace OPENVDB_VERSION_NAME
|
||||
} // namespace openvdb
|
||||
|
||||
#endif // OPENVDB_IO_CODECS_TOPOLOGYCODEC_HAS_BEEN_INCLUDED
|
||||
@@ -1,146 +0,0 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef OPENVDB_IO_CODECS_VALUEMASKCODEC_HAS_BEEN_INCLUDED
|
||||
#define OPENVDB_IO_CODECS_VALUEMASKCODEC_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <openvdb/io/Codec.h>
|
||||
|
||||
#include <openvdb/openvdb.h>
|
||||
#include <openvdb/tools/Clip.h>
|
||||
|
||||
#include "TopologyCodec.h"
|
||||
|
||||
namespace openvdb {
|
||||
OPENVDB_USE_VERSION_NAMESPACE
|
||||
namespace OPENVDB_VERSION_NAME {
|
||||
namespace codecs {
|
||||
namespace internal {
|
||||
|
||||
template <typename GridT>
|
||||
struct ReadValueMaskBuffersOp
|
||||
{
|
||||
using TreeT = typename GridT::TreeType;
|
||||
using RootT = typename TreeT::RootNodeType;
|
||||
using LeafT = typename TreeT::LeafNodeType;
|
||||
using ValueT = typename TreeT::ValueType;
|
||||
|
||||
ReadValueMaskBuffersOp(std::istream& _is, bool _saveFloatAsHalf,
|
||||
const ValueT& _background, const CoordBBox* _clipBBox)
|
||||
: is(_is)
|
||||
, saveFloatAsHalf(_saveFloatAsHalf)
|
||||
, background(_background)
|
||||
, clipBBox(_clipBBox) { }
|
||||
|
||||
void operator()(RootT& root, size_t)
|
||||
{
|
||||
// Clip root-level tiles and prune children that were clipped.
|
||||
if (clipBBox) {
|
||||
root.clip(*clipBBox);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename NodeT>
|
||||
void operator()(NodeT& node, size_t)
|
||||
{
|
||||
// Clip internal node tiles and prune children that were clipped.
|
||||
if (clipBBox) {
|
||||
node.clip(*clipBBox, background);
|
||||
}
|
||||
}
|
||||
|
||||
void operator()(LeafT& leaf, size_t)
|
||||
{
|
||||
// Read in the value mask.
|
||||
leaf.getValueMask().load(is);
|
||||
// Read in the origin.
|
||||
Coord origin;
|
||||
origin.read(is);
|
||||
leaf.setOrigin(origin);
|
||||
}
|
||||
|
||||
std::istream& is;
|
||||
const bool saveFloatAsHalf;
|
||||
const ValueT& background;
|
||||
const CoordBBox* clipBBox = nullptr;
|
||||
}; // struct ReadValueMaskBuffersOp
|
||||
|
||||
template <typename GridT>
|
||||
struct WriteValueMaskBuffersOp
|
||||
{
|
||||
using TreeT = typename GridT::TreeType;
|
||||
using LeafT = typename TreeT::LeafNodeType;
|
||||
|
||||
WriteValueMaskBuffersOp(std::ostream& _os, bool _saveFloatAsHalf)
|
||||
: os(_os)
|
||||
, saveFloatAsHalf(_saveFloatAsHalf) { }
|
||||
|
||||
template <typename NodeT>
|
||||
void operator()(const NodeT&, size_t) { }
|
||||
|
||||
void operator()(const LeafT& leaf, size_t)
|
||||
{
|
||||
// Write out the value mask.
|
||||
leaf.getValueMask().save(os);
|
||||
|
||||
// Write out the origin.
|
||||
leaf.origin().write(os);
|
||||
}
|
||||
|
||||
std::ostream& os;
|
||||
const bool saveFloatAsHalf;
|
||||
}; // struct WriteValueMaskBuffersOp
|
||||
|
||||
} // namespace internal
|
||||
|
||||
template <typename GridT>
|
||||
struct ValueMaskCodec final: public TopologyCodec<GridT>
|
||||
{
|
||||
using Ptr = std::unique_ptr<ValueMaskCodec<GridT>>;
|
||||
|
||||
~ValueMaskCodec() noexcept = default;
|
||||
|
||||
static inline std::string name() { return GridT::gridType(); }
|
||||
|
||||
void readBuffers(std::istream& is, io::CodecData& data, const io::ReadOptions& options, io::ReadDiagnostics&) final
|
||||
{
|
||||
GridT& grid = static_cast<GridT&>(*data.grid);
|
||||
|
||||
if (grid.hasMultiPassIO()) {
|
||||
OPENVDB_THROW(IoError, "Multi-pass IO is not supported in ValueMaskCodec");
|
||||
}
|
||||
|
||||
io::checkFormatVersion(is);
|
||||
|
||||
bool saveFloatAsHalf = grid.saveFloatAsHalf();
|
||||
|
||||
auto& tree = grid.tree();
|
||||
tree.clearAllAccessors();
|
||||
|
||||
std::unique_ptr<CoordBBox> clipIndexBBox;
|
||||
if (options.clipBBox.isSorted()) {
|
||||
clipIndexBBox = std::make_unique<CoordBBox>(grid.constTransform().worldToIndexNodeCentered(options.clipBBox));
|
||||
}
|
||||
|
||||
internal::ReadValueMaskBuffersOp<GridT> readBuffersOp(is, saveFloatAsHalf, tree.background(), clipIndexBBox.get());
|
||||
tools::visitNodesDepthFirst(grid.tree(), readBuffersOp, /*idx=*/0, /*topDown=*/false);
|
||||
}
|
||||
|
||||
void writeBuffers(std::ostream& os, const GridBase& gridBase, const io::WriteOptions&) final
|
||||
{
|
||||
const GridT& grid = static_cast<const GridT&>(gridBase);
|
||||
|
||||
if (grid.hasMultiPassIO()) {
|
||||
OPENVDB_THROW(IoError, "Multi-pass IO is not supported in ValueMaskCodec");
|
||||
}
|
||||
|
||||
internal::WriteValueMaskBuffersOp<GridT> writeBuffersOp(os, grid.saveFloatAsHalf());
|
||||
tools::visitNodesDepthFirst(grid.tree(), writeBuffersOp);
|
||||
}
|
||||
}; // struct ValueMaskCodec
|
||||
|
||||
} // namespace codecs
|
||||
} // namespace OPENVDB_VERSION_NAME
|
||||
} // namespace openvdb
|
||||
|
||||
#endif // OPENVDB_IO_CODECS_VALUEMASKCODEC_HAS_BEEN_INCLUDED
|
||||
@@ -1,123 +0,0 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef OPENVDB_IO_CODECS_IMPL_SCALARLEAFCODEC_HAS_BEEN_INCLUDED
|
||||
#define OPENVDB_IO_CODECS_IMPL_SCALARLEAFCODEC_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <openvdb/io/Compression.h>
|
||||
|
||||
namespace openvdb {
|
||||
OPENVDB_USE_VERSION_NAMESPACE
|
||||
namespace OPENVDB_VERSION_NAME {
|
||||
namespace codecs {
|
||||
namespace internal {
|
||||
|
||||
template <typename LeafT>
|
||||
void writeScalarLeafBuffers(const LeafT& leaf, std::ostream& os, bool saveFloatAsHalf)
|
||||
{
|
||||
using NodeMaskT = typename LeafT::NodeMaskType;
|
||||
|
||||
// Write out the value mask.
|
||||
leaf.getValueMask().save(os);
|
||||
|
||||
leaf.buffer().data(); // load values
|
||||
|
||||
io::writeCompressedValues(os, leaf.buffer().data(), LeafT::SIZE,
|
||||
leaf.getValueMask(), /*childMask=*/NodeMaskT(), saveFloatAsHalf);
|
||||
}
|
||||
|
||||
|
||||
template <typename LeafT, typename StorageLeafT = LeafT>
|
||||
void readScalarLeafBuffers(LeafT& leaf, std::istream& is, bool saveFloatAsHalf,
|
||||
const typename LeafT::ValueType& background, bool skip = false,
|
||||
const math::CoordBBox* clipBBox = nullptr)
|
||||
{
|
||||
using ValueT = typename LeafT::ValueType;
|
||||
using NodeMaskT = typename LeafT::NodeMaskType;
|
||||
using StorageBufferT = typename StorageLeafT::Buffer;
|
||||
using StorageValueT = typename StorageLeafT::ValueType;
|
||||
|
||||
constexpr Index SIZE = LeafT::SIZE;
|
||||
|
||||
SharedPtr<io::StreamMetadata> meta = io::getStreamMetadataPtr(is);
|
||||
const bool seekable = meta && meta->seekable();
|
||||
|
||||
auto& valueMask = leaf.getValueMask();
|
||||
|
||||
// Load or seek the value mask
|
||||
if (seekable) valueMask.seek(is);
|
||||
else valueMask.load(is);
|
||||
|
||||
// Pre-node-mask-compression format stored the origin and buffer count
|
||||
// inline after the value mask.
|
||||
int8_t numBuffers = 1;
|
||||
if (io::getFormatVersion(is) < OPENVDB_FILE_VERSION_NODE_MASK_COMPRESSION) {
|
||||
Coord origin;
|
||||
is.read(reinterpret_cast<char*>(&origin), sizeof(Coord::ValueType) * 3);
|
||||
leaf.setOrigin(origin);
|
||||
|
||||
is.read(reinterpret_cast<char*>(&numBuffers), sizeof(int8_t));
|
||||
|
||||
if (numBuffers > 1) {
|
||||
OPENVDB_THROW(IoError, "Old file format 221 (FLOAT_FRUSTUM_BBOX) with multiple buffers is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
if (skip) {
|
||||
if (seekable) {
|
||||
io::readCompressedValues<StorageValueT, NodeMaskT>(is, nullptr, SIZE, valueMask, saveFloatAsHalf);
|
||||
} else {
|
||||
StorageBufferT storageTemp;
|
||||
io::readCompressedValues(is, storageTemp.data(), SIZE, valueMask, saveFloatAsHalf);
|
||||
}
|
||||
// Clear the value mask so that the skipped leaf has no active
|
||||
// voxels. Without this, the leaf retains its on-disk active
|
||||
// topology even though no data was read into its buffer.
|
||||
valueMask.setOff();
|
||||
return;
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<typename LeafT::BuildType, ValueMask>) {
|
||||
// ValueMask leaf: value == active state, already captured in the value mask above.
|
||||
// Seek/consume past the storage buffer without populating any separate leaf buffer.
|
||||
if (seekable) {
|
||||
io::readCompressedValues<StorageValueT, NodeMaskT>(is, nullptr, SIZE, valueMask, saveFloatAsHalf);
|
||||
} else {
|
||||
StorageBufferT storageTemp;
|
||||
io::readCompressedValues(is, storageTemp.data(), SIZE, valueMask, saveFloatAsHalf);
|
||||
}
|
||||
} else if constexpr (std::is_same_v<ValueT, bool>) {
|
||||
// Bool leaf: must read storage values regardless of seekability, then convert.
|
||||
if constexpr (std::is_same_v<StorageValueT, bool>) {
|
||||
io::readCompressedValues(is, leaf.buffer().data(), SIZE, valueMask, saveFloatAsHalf);
|
||||
} else {
|
||||
StorageBufferT storageTemp;
|
||||
io::readCompressedValues(is, storageTemp.data(), SIZE, valueMask, saveFloatAsHalf);
|
||||
for (Index i = 0; i < SIZE; ++i) {
|
||||
leaf.buffer().setValue(i, static_cast<bool>(storageTemp.getValue(i)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
leaf.buffer().allocate();
|
||||
if constexpr (std::is_same_v<StorageValueT, ValueT>) {
|
||||
io::readCompressedValues(is, leaf.buffer().data(), SIZE, valueMask, saveFloatAsHalf);
|
||||
} else {
|
||||
StorageBufferT storageTemp;
|
||||
io::readCompressedValues(is, storageTemp.data(), SIZE, valueMask, saveFloatAsHalf);
|
||||
for (Index i = 0; i < SIZE; ++i) {
|
||||
leaf.buffer().setValue(i, static_cast<ValueT>(storageTemp.getValue(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (clipBBox) {
|
||||
leaf.clip(*clipBBox, background);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
} // namespace codecs
|
||||
} // namespace OPENVDB_VERSION_NAME
|
||||
} // namespace openvdb
|
||||
|
||||
#endif // OPENVDB_IO_CODECS_IMPL_SCALARLEAFCODEC_HAS_BEEN_INCLUDED
|
||||
+336
-130
@@ -4,6 +4,7 @@
|
||||
#include "Archive.h"
|
||||
|
||||
#include "GridDescriptor.h"
|
||||
#include "DelayedLoadMetadata.h"
|
||||
#include "io.h"
|
||||
|
||||
#include <openvdb/Exceptions.h>
|
||||
@@ -12,6 +13,35 @@
|
||||
#include <openvdb/util/logging.h>
|
||||
#include <openvdb/openvdb.h>
|
||||
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
// Boost.Interprocess uses a header-only portion of Boost.DateTime
|
||||
#ifdef __clang__
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunused-macros"
|
||||
#endif
|
||||
#define BOOST_DATE_TIME_NO_LIB
|
||||
#ifdef __clang__
|
||||
#pragma clang diagnostic pop
|
||||
#endif
|
||||
#include <boost/interprocess/file_mapping.hpp>
|
||||
#include <boost/interprocess/mapped_region.hpp>
|
||||
#include <boost/iostreams/device/array.hpp>
|
||||
#include <boost/iostreams/stream.hpp>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <boost/interprocess/detail/os_file_functions.hpp> // open_existing_file(), close_file()
|
||||
extern "C" __declspec(dllimport) bool __stdcall GetFileTime(
|
||||
void* fh, void* ctime, void* atime, void* mtime);
|
||||
// boost::interprocess::detail was renamed to boost::interprocess::ipcdetail in Boost 1.48.
|
||||
// Ensure that both namespaces exist.
|
||||
namespace boost { namespace interprocess { namespace detail {} namespace ipcdetail {} } }
|
||||
#else
|
||||
#include <sys/types.h> // for struct stat
|
||||
#include <sys/stat.h> // for stat()
|
||||
#include <unistd.h> // for unlink()
|
||||
#endif
|
||||
#endif // OPENVDB_USE_DELAYED_LOADING
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include <algorithm> // for std::find_if()
|
||||
@@ -179,7 +209,7 @@ struct StreamMetadata::Impl
|
||||
uint32_t mPass = 0;
|
||||
MetaMap mGridMetadata;
|
||||
AuxDataMap mAuxData;
|
||||
bool mDelayedLoadMeta = false;
|
||||
bool mDelayedLoadMeta = DelayedLoadMetadata::isRegisteredType();
|
||||
uint64_t mLeaf = 0;
|
||||
uint32_t mTest = 0; // for testing only
|
||||
}; // struct StreamMetadata
|
||||
@@ -244,6 +274,7 @@ bool StreamMetadata::seekable() const { return mImpl->mSeekabl
|
||||
bool StreamMetadata::delayedLoadMeta() const { return mImpl->mDelayedLoadMeta; }
|
||||
bool StreamMetadata::countingPasses() const { return mImpl->mCountingPasses; }
|
||||
uint32_t StreamMetadata::pass() const { return mImpl->mPass; }
|
||||
uint64_t StreamMetadata::leaf() const { return mImpl->mLeaf; }
|
||||
MetaMap& StreamMetadata::gridMetadata() { return mImpl->mGridMetadata; }
|
||||
const MetaMap& StreamMetadata::gridMetadata() const { return mImpl->mGridMetadata; }
|
||||
uint32_t StreamMetadata::__test() const { return mImpl->mTest; }
|
||||
@@ -261,6 +292,7 @@ void StreamMetadata::setWriteGridStats(bool b) { mImpl->mWriteGridStats
|
||||
void StreamMetadata::setSeekable(bool b) { mImpl->mSeekable = b; }
|
||||
void StreamMetadata::setCountingPasses(bool b) { mImpl->mCountingPasses = b; }
|
||||
void StreamMetadata::setPass(uint32_t i) { mImpl->mPass = i; }
|
||||
void StreamMetadata::setLeaf(uint64_t i) { mImpl->mLeaf = i; }
|
||||
void StreamMetadata::__setTest(uint32_t t) { mImpl->mTest = t; }
|
||||
|
||||
std::string
|
||||
@@ -274,6 +306,7 @@ StreamMetadata::str() const
|
||||
ostr << "compression: " << compressionToString(compression()) << "\n";
|
||||
ostr << "half_float: " << halfFloat() << "\n";
|
||||
ostr << "seekable: " << seekable() << "\n";
|
||||
ostr << "delayed_load_meta: " << delayedLoadMeta() << "\n";
|
||||
ostr << "pass: " << pass() << "\n";
|
||||
ostr << "counting_passes: " << countingPasses() << "\n";
|
||||
ostr << "write_grid_stats_metadata: " << writeGridStats() << "\n";
|
||||
@@ -306,6 +339,73 @@ writeAsType(std::ostream& os, const std::any& val)
|
||||
return false;
|
||||
}
|
||||
|
||||
struct PopulateDelayedLoadMetadataOp
|
||||
{
|
||||
DelayedLoadMetadata& metadata;
|
||||
uint32_t compression;
|
||||
|
||||
PopulateDelayedLoadMetadataOp(DelayedLoadMetadata& _metadata, uint32_t _compression)
|
||||
: metadata(_metadata)
|
||||
, compression(_compression) { }
|
||||
|
||||
template<typename GridT>
|
||||
void operator()(const GridT& grid) const
|
||||
{
|
||||
using TreeT = typename GridT::TreeType;
|
||||
using ValueT = typename TreeT::ValueType;
|
||||
using LeafT = typename TreeT::LeafNodeType;
|
||||
using MaskT = typename LeafT::NodeMaskType;
|
||||
|
||||
const TreeT& tree = grid.constTree();
|
||||
const Index64 leafCount = tree.leafCount();
|
||||
|
||||
// early exit if not leaf nodes
|
||||
if (leafCount == Index64(0)) return;
|
||||
|
||||
metadata.resizeMask(leafCount);
|
||||
|
||||
if (compression & (COMPRESS_BLOSC | COMPRESS_ZIP)) {
|
||||
metadata.resizeCompressedSize(leafCount);
|
||||
}
|
||||
|
||||
const auto background = tree.background();
|
||||
const bool saveFloatAsHalf = grid.saveFloatAsHalf();
|
||||
|
||||
tree::LeafManager<const TreeT> leafManager(tree);
|
||||
|
||||
leafManager.foreach(
|
||||
[&](const LeafT& leaf, size_t idx) {
|
||||
// set mask value
|
||||
MaskCompress<ValueT, MaskT> maskCompressData(
|
||||
leaf.valueMask(), /*childMask=*/MaskT(), leaf.buffer().data(), background);
|
||||
metadata.setMask(idx, maskCompressData.metadata);
|
||||
|
||||
if (compression & (COMPRESS_BLOSC | COMPRESS_ZIP)) {
|
||||
// set compressed size value
|
||||
size_t sizeBytes(8);
|
||||
size_t compressedSize = io::writeCompressedValuesSize(
|
||||
leaf.buffer().data(), LeafT::SIZE,
|
||||
leaf.valueMask(), maskCompressData.metadata, saveFloatAsHalf, compression);
|
||||
metadata.setCompressedSize(idx, compressedSize+sizeBytes);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
bool populateDelayedLoadMetadata(DelayedLoadMetadata& metadata,
|
||||
const GridBase& gridBase, uint32_t compression)
|
||||
{
|
||||
PopulateDelayedLoadMetadataOp op(metadata, compression);
|
||||
|
||||
using AllowedTypes = TypeList<
|
||||
Int32Grid, Int64Grid,
|
||||
FloatGrid, DoubleGrid,
|
||||
Vec3IGrid, Vec3SGrid, Vec3DGrid>;
|
||||
|
||||
return gridBase.apply<AllowedTypes>(op);
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
std::ostream&
|
||||
@@ -343,6 +443,117 @@ operator<<(std::ostream& os, const StreamMetadata::AuxDataMap& auxData)
|
||||
////////////////////////////////////////
|
||||
|
||||
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
|
||||
|
||||
// Memory-mapping a VDB file permits threaded input (and output, potentially,
|
||||
// though that might not be practical for compressed files or files containing
|
||||
// multiple grids). In particular, a memory-mapped file can be loaded lazily,
|
||||
// meaning that the voxel buffers of the leaf nodes of a grid's tree are not allocated
|
||||
// until they are actually accessed. When access to its buffer is requested,
|
||||
// a leaf node allocates memory for the buffer and then streams in (and decompresses)
|
||||
// its contents from the memory map, starting from a stream offset that was recorded
|
||||
// at the time the node was constructed. The memory map must persist as long as
|
||||
// there are unloaded leaf nodes; this is ensured by storing a shared pointer
|
||||
// to the map in each unloaded node.
|
||||
|
||||
class MappedFile::Impl
|
||||
{
|
||||
public:
|
||||
Impl(const std::string& filename, bool autoDelete)
|
||||
: mMap(filename.c_str(), boost::interprocess::read_only)
|
||||
, mRegion(mMap, boost::interprocess::read_only)
|
||||
, mAutoDelete(autoDelete)
|
||||
{
|
||||
if (mAutoDelete) {
|
||||
#ifndef _WIN32
|
||||
// On Unix systems, unlink the file so that it gets deleted once it is closed.
|
||||
::unlink(mMap.get_name());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
~Impl()
|
||||
{
|
||||
std::string filename;
|
||||
if (const char* s = mMap.get_name()) filename = s;
|
||||
OPENVDB_LOG_DEBUG_RUNTIME("closing memory-mapped file " << filename);
|
||||
if (mNotifier) mNotifier(filename);
|
||||
if (mAutoDelete) {
|
||||
if (!boost::interprocess::file_mapping::remove(filename.c_str())) {
|
||||
if (errno != ENOENT) {
|
||||
// Warn if the file exists but couldn't be removed.
|
||||
std::string mesg = getErrorString();
|
||||
if (!mesg.empty()) mesg = " (" + mesg + ")";
|
||||
OPENVDB_LOG_WARN("failed to remove temporary file " << filename << mesg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boost::interprocess::file_mapping mMap;
|
||||
boost::interprocess::mapped_region mRegion;
|
||||
bool mAutoDelete;
|
||||
Notifier mNotifier;
|
||||
#if OPENVDB_ABI_VERSION_NUMBER <= 12
|
||||
mutable std::atomic<Index64> mLastWriteTime;
|
||||
#endif
|
||||
|
||||
private:
|
||||
Impl(const Impl&); // not copyable
|
||||
Impl& operator=(const Impl&); // not copyable
|
||||
};
|
||||
|
||||
|
||||
MappedFile::MappedFile(const std::string& filename, bool autoDelete):
|
||||
mImpl(new Impl(filename, autoDelete))
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
MappedFile::~MappedFile()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
MappedFile::filename() const
|
||||
{
|
||||
std::string result;
|
||||
if (const char* s = mImpl->mMap.get_name()) result = s;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
SharedPtr<std::streambuf>
|
||||
MappedFile::createBuffer() const
|
||||
{
|
||||
return SharedPtr<std::streambuf>{
|
||||
new boost::iostreams::stream_buffer<boost::iostreams::array_source>{
|
||||
static_cast<const char*>(mImpl->mRegion.get_address()), mImpl->mRegion.get_size()}};
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
MappedFile::setNotifier(const Notifier& notifier)
|
||||
{
|
||||
mImpl->mNotifier = notifier;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
MappedFile::clearNotifier()
|
||||
{
|
||||
mImpl->mNotifier = nullptr;
|
||||
}
|
||||
|
||||
|
||||
#endif // OPENVDB_USE_DELAYED_LOADING
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
|
||||
|
||||
std::string
|
||||
getErrorString(int errorNum)
|
||||
{
|
||||
@@ -384,35 +595,6 @@ Archive::copy() const
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Archive::enableReadDiagnostics()
|
||||
{
|
||||
mReadDiagnostics.enable();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Archive::disableReadDiagnostics()
|
||||
{
|
||||
mReadDiagnostics.disable();
|
||||
mReadDiagnostics.clear();
|
||||
}
|
||||
|
||||
|
||||
const ReadDiagnostics&
|
||||
Archive::readDiagnostics() const
|
||||
{
|
||||
return mReadDiagnostics;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Archive::clearReadDiagnostics()
|
||||
{
|
||||
mReadDiagnostics.clear();
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
|
||||
|
||||
@@ -706,6 +888,25 @@ setGridBackgroundValuePtr(std::ios_base& strm, const void* background)
|
||||
}
|
||||
|
||||
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
MappedFile::Ptr
|
||||
getMappedFilePtr(std::ios_base& strm)
|
||||
{
|
||||
if (const void* ptr = strm.pword(GetSteamState().mappedFile)) {
|
||||
return *static_cast<const MappedFile::Ptr*>(ptr);
|
||||
}
|
||||
return MappedFile::Ptr();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
setMappedFilePtr(std::ios_base& strm, io::MappedFile::Ptr& mappedFile)
|
||||
{
|
||||
strm.pword(GetSteamState().mappedFile) = &mappedFile;
|
||||
}
|
||||
#endif // OPENVDB_USE_DELAYED_LOADING
|
||||
|
||||
|
||||
StreamMetadata::Ptr
|
||||
getStreamMetadataPtr(std::ios_base& strm)
|
||||
{
|
||||
@@ -894,27 +1095,6 @@ Archive::readGridCount(std::istream& is)
|
||||
////////////////////////////////////////
|
||||
|
||||
|
||||
io::Codec*
|
||||
Archive::findCodec(const std::string& gridType, const io::ReadOptions& options)
|
||||
{
|
||||
// if readMode is Half, then search for a codec that converts
|
||||
// from the storage grid type to the grid type
|
||||
if (options.readMode == ReadMode::Half) {
|
||||
return io::CodecRegistry::get(gridType + "_to_half");
|
||||
} else if (options.readMode == ReadMode::Bool) {
|
||||
return io::CodecRegistry::get(gridType + "_to_bool");
|
||||
} else if (options.readMode == ReadMode::Mask) {
|
||||
return io::CodecRegistry::get(gridType + "_to_mask");
|
||||
}
|
||||
|
||||
// Determine the I/O codec to use to read this grid
|
||||
return io::CodecRegistry::get(gridType);
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
|
||||
|
||||
void
|
||||
Archive::connectInstance(const GridDescriptor& gd, const NamedGridMap& grids) const
|
||||
{
|
||||
@@ -946,12 +1126,35 @@ Archive::connectInstance(const GridDescriptor& gd, const NamedGridMap& grids) co
|
||||
////////////////////////////////////////
|
||||
|
||||
|
||||
GridBase::Ptr
|
||||
Archive::readGrid(const GridDescriptor& gd, std::istream& is, const io::ReadOptions& readOptions, ReadDiagnostics& diagnostics)
|
||||
//static
|
||||
bool
|
||||
Archive::isDelayedLoadingEnabled()
|
||||
{
|
||||
// Read the compression settings for this grid and tag the stream with them
|
||||
// so that downstream functions can reference them.
|
||||
readGridCompression(is);
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
return (nullptr == std::getenv("OPENVDB_DISABLE_DELAYED_LOAD"));
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
struct NoBBox {};
|
||||
|
||||
template<typename BoxType>
|
||||
void
|
||||
doReadGrid(GridBase::Ptr grid, const GridDescriptor& gd, std::istream& is, const BoxType& bbox)
|
||||
{
|
||||
struct Local {
|
||||
static void readBuffers(GridBase& g, std::istream& istrm, NoBBox) { g.readBuffers(istrm); }
|
||||
static void readBuffers(GridBase& g, std::istream& istrm, const CoordBBox& indexBBox) {
|
||||
g.readBuffers(istrm, indexBBox);
|
||||
}
|
||||
static void readBuffers(GridBase& g, std::istream& istrm, const BBoxd& worldBBox) {
|
||||
g.readBuffers(istrm, g.constTransform().worldToIndexNodeCentered(worldBBox));
|
||||
}
|
||||
};
|
||||
|
||||
// Restore the file-level stream metadata on exit.
|
||||
struct OnExit {
|
||||
@@ -962,27 +1165,6 @@ Archive::readGrid(const GridDescriptor& gd, std::istream& is, const io::ReadOpti
|
||||
};
|
||||
OnExit restore(is);
|
||||
|
||||
// Find the codec for the grid type and options.
|
||||
io::Codec* codec = findCodec(gd.gridType(), readOptions);
|
||||
|
||||
GridBase::Ptr grid;
|
||||
io::CodecData::Ptr codecData;
|
||||
|
||||
// Create the grid.
|
||||
if (codec) {
|
||||
codecData = codec->createData();
|
||||
if (codecData->grid) grid = codecData->grid;
|
||||
} else {
|
||||
if (!GridBase::isRegistered(gd.gridType())) {
|
||||
OPENVDB_THROW(KeyError, "Cannot read grid "
|
||||
<< GridDescriptor::nameAsString(gd.uniqueName())
|
||||
<< ": grid type " << gd.gridType() << " is not registered");
|
||||
}
|
||||
|
||||
grid = GridBase::createGrid(gd.gridType());
|
||||
}
|
||||
grid->setSaveFloatAsHalf(gd.saveFloatAsHalf());
|
||||
|
||||
// Stream metadata varies per grid, and it needs to persist
|
||||
// in case delayed load is in effect.
|
||||
io::StreamMetadata::Ptr streamMetadata;
|
||||
@@ -1000,47 +1182,75 @@ Archive::readGrid(const GridDescriptor& gd, std::istream& is, const io::ReadOpti
|
||||
|
||||
grid->readMeta(is);
|
||||
|
||||
// Delayed loading is no longer supported - always remove metadata related to delayed loading if it exists
|
||||
if ((*grid)[GridBase::META_FILE_DELAYED_LOAD]) {
|
||||
grid->removeMeta(GridBase::META_FILE_DELAYED_LOAD);
|
||||
// Add a description of the compression settings to the grid as metadata.
|
||||
/// @todo Would this be useful?
|
||||
//const uint32_t c = getDataCompression(is);
|
||||
//grid->insertMeta(GridBase::META_FILE_COMPRESSION,
|
||||
// StringMetadata(compressionToString(c)));
|
||||
|
||||
const VersionId version = getLibraryVersion(is);
|
||||
if (version.first < 6 || (version.first == 6 && version.second <= 1)) {
|
||||
// If delay load metadata exists, but the file format version does not support
|
||||
// delay load metadata, this likely means the original grid was read and then
|
||||
// written using a prior version of OpenVDB and ABI>=5 where unknown metadata
|
||||
// can be blindly copied. This means that it is possible for the metadata to
|
||||
// no longer be in sync with the grid, so we remove it to ensure correctness.
|
||||
|
||||
if ((*grid)[GridBase::META_FILE_DELAYED_LOAD]) {
|
||||
grid->removeMeta(GridBase::META_FILE_DELAYED_LOAD);
|
||||
}
|
||||
}
|
||||
|
||||
streamMetadata->gridMetadata() = static_cast<MetaMap&>(*grid);
|
||||
const GridClass gridClass = grid->getGridClass();
|
||||
io::setGridClass(is, gridClass);
|
||||
|
||||
grid->readTransform(is);
|
||||
if (readOptions.readMode != io::ReadMode::TopologyOnly && !gd.isInstance()) {
|
||||
// read topology
|
||||
if (codec) {
|
||||
codec->readTopology(is, *codecData, readOptions, diagnostics);
|
||||
} else {
|
||||
grid->readTopology(is);
|
||||
}
|
||||
// read buffers
|
||||
if (codec) {
|
||||
codec->readBuffers(is, *codecData, readOptions, diagnostics);
|
||||
} else {
|
||||
const auto& worldBBox = readOptions.clipBBox;
|
||||
const bool clip = worldBBox.isSorted();
|
||||
if (clip) {
|
||||
const auto indexBBox = grid->constTransform().worldToIndexNodeCentered(worldBBox);
|
||||
grid->readBuffers(is, indexBBox);
|
||||
} else {
|
||||
grid->readBuffers(is);
|
||||
}
|
||||
// reset leaf value to zero
|
||||
streamMetadata->setLeaf(0);
|
||||
|
||||
// drop DelayedLoadMetadata from the grid as it is only useful for IO
|
||||
// a stream metadata non-zero value disables this behaviour for testing
|
||||
|
||||
if (streamMetadata->__test() == uint32_t(0)) {
|
||||
if ((*grid)[GridBase::META_FILE_DELAYED_LOAD]) {
|
||||
grid->removeMeta(GridBase::META_FILE_DELAYED_LOAD);
|
||||
}
|
||||
}
|
||||
|
||||
return grid;
|
||||
grid->readTransform(is);
|
||||
if (!gd.isInstance()) {
|
||||
grid->readTopology(is);
|
||||
Local::readBuffers(*grid, is, bbox);
|
||||
}
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
GridBase::Ptr
|
||||
Archive::readGrid(const GridDescriptor& gd, std::istream& is, const io::ReadOptions& readOptions)
|
||||
|
||||
void
|
||||
Archive::readGrid(GridBase::Ptr grid, const GridDescriptor& gd, std::istream& is)
|
||||
{
|
||||
ReadDiagnostics nullDiagnostics;
|
||||
return readGrid(gd, is, readOptions, nullDiagnostics);
|
||||
// Read the compression settings for this grid and tag the stream with them
|
||||
// so that downstream functions can reference them.
|
||||
readGridCompression(is);
|
||||
|
||||
doReadGrid(grid, gd, is, NoBBox());
|
||||
}
|
||||
|
||||
void
|
||||
Archive::readGrid(GridBase::Ptr grid, const GridDescriptor& gd,
|
||||
std::istream& is, const BBoxd& worldBBox)
|
||||
{
|
||||
readGridCompression(is);
|
||||
doReadGrid(grid, gd, is, worldBBox);
|
||||
}
|
||||
|
||||
void
|
||||
Archive::readGrid(GridBase::Ptr grid, const GridDescriptor& gd,
|
||||
std::istream& is, const CoordBBox& indexBBox)
|
||||
{
|
||||
readGridCompression(is);
|
||||
doReadGrid(grid, gd, is, indexBBox);
|
||||
}
|
||||
|
||||
|
||||
@@ -1049,15 +1259,15 @@ Archive::readGrid(const GridDescriptor& gd, std::istream& is, const io::ReadOpti
|
||||
|
||||
void
|
||||
Archive::write(std::ostream& os, const GridPtrVec& grids, bool seekable,
|
||||
const MetaMap& metadata, const io::WriteOptions& writeOptions) const
|
||||
const MetaMap& metadata) const
|
||||
{
|
||||
this->write(os, GridCPtrVec(grids.begin(), grids.end()), seekable, metadata, writeOptions);
|
||||
this->write(os, GridCPtrVec(grids.begin(), grids.end()), seekable, metadata);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Archive::write(std::ostream& os, const GridCPtrVec& grids, bool seekable,
|
||||
const MetaMap& metadata, const io::WriteOptions& writeOptions) const
|
||||
const MetaMap& metadata) const
|
||||
{
|
||||
// Set stream flags so that downstream functions can reference them.
|
||||
io::StreamMetadata::Ptr streamMetadata = io::getStreamMetadataPtr(os);
|
||||
@@ -1129,7 +1339,7 @@ Archive::write(std::ostream& os, const GridCPtrVec& grids, bool seekable,
|
||||
// Get the name of the other grid.
|
||||
gd.setInstanceParentName(mapIter->second.uniqueName());
|
||||
// Write out this grid's descriptor and metadata, but not its tree.
|
||||
writeGridInstance(gd, grid, os, seekable, writeOptions);
|
||||
writeGridInstance(gd, grid, os, seekable);
|
||||
|
||||
OPENVDB_LOG_DEBUG_RUNTIME("io::Archive::write(): "
|
||||
<< GridDescriptor::nameAsString(gd.uniqueName())
|
||||
@@ -1138,7 +1348,7 @@ Archive::write(std::ostream& os, const GridCPtrVec& grids, bool seekable,
|
||||
<< GridDescriptor::nameAsString(gd.instanceParentName()));
|
||||
} else {
|
||||
// Write out the grid descriptor and its associated grid.
|
||||
writeGrid(gd, grid, os, seekable, writeOptions);
|
||||
writeGrid(gd, grid, os, seekable);
|
||||
// Record the grid's tree pointer so that the tree doesn't get written
|
||||
// more than once.
|
||||
treeMap[treePtr] = gd;
|
||||
@@ -1154,7 +1364,7 @@ Archive::write(std::ostream& os, const GridCPtrVec& grids, bool seekable,
|
||||
|
||||
void
|
||||
Archive::writeGrid(GridDescriptor& gd, GridBase::ConstPtr grid,
|
||||
std::ostream& os, bool seekable, const io::WriteOptions& writeOptions) const
|
||||
std::ostream& os, bool seekable) const
|
||||
{
|
||||
// Restore file-level stream metadata on exit.
|
||||
struct OnExit {
|
||||
@@ -1165,9 +1375,6 @@ Archive::writeGrid(GridDescriptor& gd, GridBase::ConstPtr grid,
|
||||
};
|
||||
OnExit restore(os);
|
||||
|
||||
// Find the codec for the grid type and options.
|
||||
io::Codec* codec = findCodec(gd.gridType());
|
||||
|
||||
// Stream metadata varies per grid, so make a copy of the file-level stream metadata.
|
||||
io::StreamMetadata::Ptr streamMetadata;
|
||||
if (io::StreamMetadata::Ptr meta = io::getStreamMetadataPtr(os)) {
|
||||
@@ -1196,36 +1403,35 @@ Archive::writeGrid(GridDescriptor& gd, GridBase::ConstPtr grid,
|
||||
// Save the compression settings for this grid.
|
||||
setGridCompression(os, *grid);
|
||||
|
||||
// copy grid and add delay load metadata
|
||||
const auto copyOfGrid = grid->copyGrid(); // shallow copy
|
||||
const auto nonConstCopyOfGrid = ConstPtrCast<GridBase>(copyOfGrid);
|
||||
nonConstCopyOfGrid->insertMeta(GridBase::META_FILE_DELAYED_LOAD,
|
||||
DelayedLoadMetadata());
|
||||
DelayedLoadMetadata::Ptr delayLoadMeta =
|
||||
nonConstCopyOfGrid->getMetadata<DelayedLoadMetadata>(GridBase::META_FILE_DELAYED_LOAD);
|
||||
if (!populateDelayedLoadMetadata(*delayLoadMeta, *grid, compression())) {
|
||||
nonConstCopyOfGrid->removeMeta(GridBase::META_FILE_DELAYED_LOAD);
|
||||
}
|
||||
|
||||
// Save the grid's metadata and transform.
|
||||
if (getWriteGridStatsMetadata(os)) {
|
||||
// Compute and add grid statistics metadata.
|
||||
const auto copyOfGrid = grid->copyGrid(); // shallow copy
|
||||
const auto nonConstCopyOfGrid = ConstPtrCast<GridBase>(copyOfGrid);
|
||||
nonConstCopyOfGrid->addStatsMetadata();
|
||||
nonConstCopyOfGrid->insertMeta(GridBase::META_FILE_COMPRESSION,
|
||||
StringMetadata(compressionToString(getDataCompression(os))));
|
||||
copyOfGrid->writeMeta(os);
|
||||
} else {
|
||||
grid->writeMeta(os);
|
||||
}
|
||||
copyOfGrid->writeMeta(os);
|
||||
grid->writeTransform(os);
|
||||
|
||||
// Save the grid's structure.
|
||||
if (codec) {
|
||||
codec->writeTopology(os, *grid, writeOptions);
|
||||
} else {
|
||||
grid->writeTopology(os);
|
||||
}
|
||||
grid->writeTopology(os);
|
||||
|
||||
// Now we know the grid block storage position.
|
||||
if (seekable) gd.setBlockPos(os.tellp());
|
||||
|
||||
// Save out the data blocks of the grid.
|
||||
if (codec) {
|
||||
codec->writeBuffers(os, *grid, writeOptions);
|
||||
} else {
|
||||
grid->writeBuffers(os);
|
||||
}
|
||||
grid->writeBuffers(os);
|
||||
|
||||
// Now we know the end position of this grid.
|
||||
if (seekable) gd.setEndPos(os.tellp());
|
||||
@@ -1244,7 +1450,7 @@ Archive::writeGrid(GridDescriptor& gd, GridBase::ConstPtr grid,
|
||||
|
||||
void
|
||||
Archive::writeGridInstance(GridDescriptor& gd, GridBase::ConstPtr grid,
|
||||
std::ostream& os, bool seekable, const io::WriteOptions&) const
|
||||
std::ostream& os, bool seekable) const
|
||||
{
|
||||
// Write out the Descriptor's header information (grid name, type
|
||||
// and instance parent name).
|
||||
|
||||
@@ -5,14 +5,11 @@
|
||||
#define OPENVDB_IO_ARCHIVE_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <openvdb/version.h>
|
||||
#include "Compression.h" // for COMPRESS_ZIP, etc.
|
||||
#include <openvdb/Grid.h>
|
||||
#include <openvdb/MetaMap.h>
|
||||
#include <openvdb/Platform.h>
|
||||
#include <openvdb/version.h> // for VersionId
|
||||
|
||||
#include "Codec.h"
|
||||
#include "Compression.h" // for COMPRESS_ZIP, etc.
|
||||
|
||||
#include <cstdint>
|
||||
#include <iosfwd>
|
||||
#include <map>
|
||||
@@ -92,23 +89,14 @@ public:
|
||||
void setGridStatsMetadataEnabled(bool b) { mEnableGridStats = b; }
|
||||
|
||||
/// @brief Write the grids in the given container to this archive's output stream.
|
||||
virtual void write(const GridCPtrVec&, const MetaMap& = MetaMap(),
|
||||
const io::WriteOptions& = io::WriteOptions{}) const {}
|
||||
virtual void write(const GridCPtrVec&, const MetaMap& = MetaMap()) const {}
|
||||
|
||||
/// @brief Return @c false (delayed loading has been removed).
|
||||
static bool isDelayedLoadingEnabled() { return false; }
|
||||
|
||||
/// @brief Enable collection of read diagnostics (warnings, etc.) during I/O operations.
|
||||
void enableReadDiagnostics();
|
||||
|
||||
/// @brief Disable collection of read diagnostics.
|
||||
void disableReadDiagnostics();
|
||||
|
||||
/// @brief Return a const reference to the diagnostics collector.
|
||||
const ReadDiagnostics& readDiagnostics() const;
|
||||
|
||||
/// @brief Clear the diagnostics list while keeping collection active.
|
||||
void clearReadDiagnostics();
|
||||
/// @brief Return @c true if delayed loading is enabled.
|
||||
/// @details If enabled, delayed loading can be disabled for individual files,
|
||||
/// but not vice-versa.
|
||||
/// @note Define the environment variable @c OPENVDB_DISABLE_DELAYED_LOAD
|
||||
/// to disable delayed loading unconditionally.
|
||||
static bool isDelayedLoadingEnabled();
|
||||
|
||||
protected:
|
||||
/// @brief Return @c true if the input stream contains grid offsets
|
||||
@@ -143,15 +131,14 @@ protected:
|
||||
/// Read in and return the number of grids on the input stream.
|
||||
static int32_t readGridCount(std::istream&);
|
||||
|
||||
/// @brief Find the codec for the given grid type and options.
|
||||
static io::Codec* findCodec(const std::string& gridType, const io::ReadOptions& options = io::ReadOptions{});
|
||||
|
||||
/// @brief Read in and create the grid represented by the given grid descriptor using the
|
||||
/// given input stream, using the provided options if given.
|
||||
static GridBase::Ptr readGrid(const GridDescriptor&, std::istream&,
|
||||
const io::ReadOptions& readOptions, ReadDiagnostics& diagnostics);
|
||||
static GridBase::Ptr readGrid(const GridDescriptor&, std::istream&,
|
||||
const io::ReadOptions& readOptions = io::ReadOptions{});
|
||||
/// Populate the given grid from the input stream.
|
||||
static void readGrid(GridBase::Ptr, const GridDescriptor&, std::istream&);
|
||||
/// @brief Populate the given grid from the input stream, but only where it
|
||||
/// intersects the given world-space bounding box.
|
||||
static void readGrid(GridBase::Ptr, const GridDescriptor&, std::istream&, const BBoxd&);
|
||||
/// @brief Populate the given grid from the input stream, but only where it
|
||||
/// intersects the given index-space bounding box.
|
||||
static void readGrid(GridBase::Ptr, const GridDescriptor&, std::istream&, const CoordBBox&);
|
||||
|
||||
using NamedGridMap = std::map<Name /*uniqueName*/, GridBase::Ptr>;
|
||||
|
||||
@@ -162,14 +149,13 @@ protected:
|
||||
/// Write the given grid descriptor and grid to an output stream
|
||||
/// and update the GridDescriptor offsets.
|
||||
/// @param seekable if true, the output stream supports seek operations
|
||||
void writeGrid(GridDescriptor&, GridBase::ConstPtr, std::ostream&, bool seekable,
|
||||
const io::WriteOptions& writeOptions = io::WriteOptions{}) const;
|
||||
void writeGrid(GridDescriptor&, GridBase::ConstPtr, std::ostream&, bool seekable) const;
|
||||
/// Write the given grid descriptor and grid metadata to an output stream
|
||||
/// and update the GridDescriptor offsets, but don't write the grid's tree,
|
||||
/// since it is shared with another grid.
|
||||
/// @param seekable if true, the output stream supports seek operations
|
||||
void writeGridInstance(GridDescriptor&, GridBase::ConstPtr,
|
||||
std::ostream&, bool seekable, const io::WriteOptions& writeOptions = io::WriteOptions{}) const;
|
||||
std::ostream&, bool seekable) const;
|
||||
|
||||
/// @brief Read the magic number, version numbers, UUID, etc. from the given input stream.
|
||||
/// @return @c true if the input UUID differs from the previously-read UUID.
|
||||
@@ -181,15 +167,10 @@ protected:
|
||||
|
||||
//@{
|
||||
/// Write the given grids to an output stream.
|
||||
void write(std::ostream&, const GridPtrVec&, bool seekable, const MetaMap&,
|
||||
const io::WriteOptions& writeOptions = io::WriteOptions{}) const;
|
||||
void write(std::ostream&, const GridCPtrVec&, bool seekable, const MetaMap&,
|
||||
const io::WriteOptions& writeOptions = io::WriteOptions{}) const;
|
||||
void write(std::ostream&, const GridPtrVec&, bool seekable, const MetaMap& = MetaMap()) const;
|
||||
void write(std::ostream&, const GridCPtrVec&, bool seekable, const MetaMap& = MetaMap()) const;
|
||||
//@}
|
||||
|
||||
/// Diagnostics collector for read operations (always valid; enabled/disabled via flag)
|
||||
mutable ReadDiagnostics mReadDiagnostics;
|
||||
|
||||
private:
|
||||
friend class ::TestFile;
|
||||
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include "Codec.h"
|
||||
|
||||
#include <openvdb/codecs/ScalarCodec.h>
|
||||
#include <openvdb/codecs/BoolCodec.h>
|
||||
#include <openvdb/codecs/PointDataCodec.h>
|
||||
#include <openvdb/codecs/PointIndexCodec.h>
|
||||
#include <openvdb/codecs/ValueMaskCodec.h>
|
||||
|
||||
#include <openvdb/openvdb.h>
|
||||
|
||||
namespace openvdb {
|
||||
OPENVDB_USE_VERSION_NAMESPACE
|
||||
namespace OPENVDB_VERSION_NAME {
|
||||
namespace io {
|
||||
|
||||
namespace {
|
||||
|
||||
using CodecFactoryMap = std::map<std::string, Codec::Ptr>;
|
||||
using CodecFactoryMapCIter = CodecFactoryMap::const_iterator;
|
||||
|
||||
struct LockedCodecRegistry {
|
||||
LockedCodecRegistry() {}
|
||||
~LockedCodecRegistry() {}
|
||||
std::mutex mMutex;
|
||||
CodecFactoryMap mMap;
|
||||
};
|
||||
|
||||
// Global function for accessing the registry
|
||||
static LockedCodecRegistry*
|
||||
getCodecRegistry()
|
||||
{
|
||||
static LockedCodecRegistry registry;
|
||||
return ®istry;
|
||||
}
|
||||
|
||||
} // unnamed namespace
|
||||
|
||||
|
||||
namespace internal {
|
||||
|
||||
template <typename GridT>
|
||||
struct RegisterCodec { inline void operator()() { CodecRegistry::registerCodec<codecs::ScalarCodec<GridT>>(); } };
|
||||
|
||||
template <typename GridT>
|
||||
struct RegisterConvertCodec {
|
||||
inline void operator()()
|
||||
{
|
||||
CodecRegistry::registerCodec<codecs::ScalarCodec<MaskGrid, GridT, CodecMode::ReadOnly>>();
|
||||
CodecRegistry::registerCodec<codecs::ScalarCodec<BoolGrid, GridT, CodecMode::ReadOnly>>();
|
||||
}
|
||||
}; // struct RegisterConvertCodec
|
||||
|
||||
void initialize()
|
||||
{
|
||||
NumericGridTypes::foreach<RegisterCodec>();
|
||||
Vec3GridTypes::foreach<RegisterCodec>();
|
||||
|
||||
CodecRegistry::registerCodec<codecs::BoolCodec<BoolGrid>>();
|
||||
CodecRegistry::registerCodec<codecs::ValueMaskCodec<MaskGrid>>();
|
||||
CodecRegistry::registerCodec<codecs::PointIndexCodec<tools::PointIndexGrid>>();
|
||||
CodecRegistry::registerCodec<codecs::PointDataCodec<points::PointDataGrid>>();
|
||||
|
||||
// register the plugin that converts from scalar to mask/bool
|
||||
NumericGridTypes::foreach<RegisterConvertCodec>();
|
||||
|
||||
// register the plugin that converts from float to half
|
||||
CodecRegistry::registerCodec<codecs::ScalarCodec<HalfGrid, FloatGrid, CodecMode::ReadOnly>>();
|
||||
}
|
||||
|
||||
void uninitialize()
|
||||
{
|
||||
CodecRegistry::clear();
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
|
||||
|
||||
bool
|
||||
CodecRegistry::isRegistered(const std::string& name)
|
||||
{
|
||||
LockedCodecRegistry* registry = getCodecRegistry();
|
||||
std::lock_guard<std::mutex> lock(registry->mMutex);
|
||||
|
||||
return (registry->mMap.find(name) != registry->mMap.end());
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
CodecRegistry::registerCodecByName(const std::string& name, Codec::Ptr&& codec)
|
||||
{
|
||||
LockedCodecRegistry* registry = getCodecRegistry();
|
||||
std::lock_guard<std::mutex> lock(registry->mMutex);
|
||||
|
||||
if (registry->mMap.find(name) != registry->mMap.end()) {
|
||||
OPENVDB_THROW(KeyError,
|
||||
"Cannot register codec " << name << ". Codec is already registered");
|
||||
}
|
||||
|
||||
registry->mMap[name] = std::move(codec);
|
||||
}
|
||||
|
||||
|
||||
Codec*
|
||||
CodecRegistry::get(const std::string& name)
|
||||
{
|
||||
LockedCodecRegistry* registry = getCodecRegistry();
|
||||
std::lock_guard<std::mutex> lock(registry->mMutex);
|
||||
|
||||
CodecFactoryMapCIter iter = registry->mMap.find(name);
|
||||
|
||||
return (iter != registry->mMap.end()) ? iter->second.get() : nullptr;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
CodecRegistry::clear()
|
||||
{
|
||||
LockedCodecRegistry* registry = getCodecRegistry();
|
||||
std::lock_guard<std::mutex> lock(registry->mMutex);
|
||||
|
||||
registry->mMap.clear();
|
||||
}
|
||||
|
||||
|
||||
} // namespace io
|
||||
} // namespace OPENVDB_VERSION_NAME
|
||||
} // namespace openvdb
|
||||
|
||||
@@ -1,473 +0,0 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef OPENVDB_IO_CODEC_HAS_BEEN_INCLUDED
|
||||
#define OPENVDB_IO_CODEC_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
|
||||
#include <openvdb/Types.h>
|
||||
|
||||
namespace openvdb {
|
||||
OPENVDB_USE_VERSION_NAMESPACE
|
||||
namespace OPENVDB_VERSION_NAME {
|
||||
|
||||
// Forward declaration
|
||||
class GridBase;
|
||||
|
||||
namespace io {
|
||||
|
||||
namespace internal {
|
||||
|
||||
// Global registration of codecs. These initialization functions are NOT intended
|
||||
// to be called directly by the user.
|
||||
// While they are thread-safe, they lack the early-exit logic of openvdb::initialize() -
|
||||
// this can result in exceptions being thrown when attempting to register a codec that is
|
||||
// already registered.
|
||||
// They are also used extensively in unit tests.
|
||||
|
||||
OPENVDB_API void initialize();
|
||||
OPENVDB_API void uninitialize();
|
||||
|
||||
} // namespace internal
|
||||
|
||||
////////////////////////////////////////
|
||||
|
||||
/// @brief Controls which operations a codec exposes.
|
||||
///
|
||||
/// Passed to codec registration to restrict how the codec may be used at
|
||||
/// runtime. A @c ReadOnly codec can still be used to read existing files but
|
||||
/// will not be selected as the write codec for new grids.
|
||||
enum class CodecMode {
|
||||
/// Both @c readTopology()/@c readBuffers() and
|
||||
/// @c writeTopology()/@c writeBuffers() are enabled. This is the default.
|
||||
ReadWrite,
|
||||
/// Only @c readTopology() and @c readBuffers() are enabled; the codec
|
||||
/// will not be offered as a write target.
|
||||
ReadOnly
|
||||
};
|
||||
|
||||
/// @brief Controls how grid data is read.
|
||||
///
|
||||
/// Passed via @c ReadOptions::readMode to influence which portions of a grid
|
||||
/// are deserialized and whether any on-the-fly conversion is performed during read.
|
||||
/// Codecs are expected to honour this setting in their @c readTopology() and
|
||||
/// @c readBuffers() implementations.
|
||||
enum class ReadMode {
|
||||
/// Deserialize both topology and all value buffers. This is the default
|
||||
/// behaviour and produces a fully populated grid identical to the one
|
||||
/// that was written.
|
||||
Original,
|
||||
/// Deserialize topology and value buffers, converting each voxel value
|
||||
/// to half-precision floating point on the fly. Codecs that do not
|
||||
/// support native half conversion should fall back to @c Original and
|
||||
/// record a @c ReadDiagnostic warning.
|
||||
Half,
|
||||
/// Deserialize topology and value buffers, converting each voxel value
|
||||
/// to @c bool (non-zero to @c true, zero to @c false). Produces a
|
||||
/// @c BoolGrid whose active set matches the source grid's active set.
|
||||
Bool,
|
||||
/// Deserialize topology only and promote the result to a mask grid,
|
||||
/// discarding all value data. Equivalent to reading @c TopologyOnly and
|
||||
/// then constructing a @c MaskGrid from the active-voxel set, but may
|
||||
/// be performed more efficiently inside the codec.
|
||||
Mask,
|
||||
/// Deserialize topology only; value buffers are skipped. The resulting
|
||||
/// grid has a valid tree structure (active/inactive state, node
|
||||
/// hierarchy) but leaf buffer data is left at its default (background)
|
||||
/// value. Useful when only the active-voxel mask is needed and
|
||||
/// avoiding the cost of reading large value buffers is desirable.
|
||||
TopologyOnly
|
||||
};
|
||||
|
||||
/// @brief Base class for per-grid-type, codec-specific read options.
|
||||
///
|
||||
/// Codecs that require type-specific read configuration should derive from
|
||||
/// this class and store an instance in @c ReadOptions::typeData, keyed by
|
||||
/// the grid type string (e.g. @c "Vec3SGrid").
|
||||
///
|
||||
/// @par Example
|
||||
/// @code
|
||||
/// struct MyCodecReadOptions : public ReadTypedOptions {
|
||||
/// bool someFlag = false;
|
||||
/// };
|
||||
///
|
||||
/// ReadOptions options;
|
||||
/// auto data = std::make_shared<MyCodecReadOptions>();
|
||||
/// data->someFlag = true;
|
||||
/// options.typeData["Vec3SGrid"] = data;
|
||||
/// @endcode
|
||||
struct OPENVDB_API ReadTypedOptions
|
||||
{
|
||||
using Ptr = std::shared_ptr<ReadTypedOptions>;
|
||||
|
||||
virtual ~ReadTypedOptions() = default;
|
||||
|
||||
/// @brief Downcast a @c ReadTypedOptions::Ptr to a concrete derived type.
|
||||
///
|
||||
/// Performs a @c static_cast to @c T after verifying via @c dynamic_cast
|
||||
/// (when @c OPENVDB_ASSERT is enabled) that @p data actually holds an
|
||||
/// instance of @c T. Calling this with a mismatched type is undefined
|
||||
/// behaviour in release builds.
|
||||
///
|
||||
/// @tparam T The concrete @c ReadTypedOptions subclass to cast to.
|
||||
/// @param data A shared pointer to the base @c ReadTypedOptions object.
|
||||
/// @return A reference to the underlying @c T instance.
|
||||
template<typename T>
|
||||
static T& cast(const Ptr& data) {
|
||||
OPENVDB_ASSERT(dynamic_cast<T*>(data.get()) != nullptr);
|
||||
return *static_cast<T*>(data.get());
|
||||
}
|
||||
}; // struct ReadTypedOptions
|
||||
|
||||
/// @brief Global read configuration passed to every codec during deserialization.
|
||||
///
|
||||
/// An instance of this struct is threaded through all codec @c readTopology() and
|
||||
/// @c readBuffers() calls so that site-wide policy (clipping, read mode) and any
|
||||
/// type-specific overrides are available in a single place.
|
||||
///
|
||||
/// @par Clipping
|
||||
/// When @c clipBBox is non-empty (i.e. @c !clipBBox.empty()), codecs should
|
||||
/// restrict the voxel data they load to the region that intersects that world-space
|
||||
/// bounding box. An empty bbox (the default) means no clipping is applied.
|
||||
///
|
||||
/// @par Read mode
|
||||
/// @c readMode controls the granularity and on-the-fly conversion applied
|
||||
/// during deserialization. The default, @c ReadMode::Original, loads both
|
||||
/// topology and value buffers without modification.
|
||||
/// @c ReadMode::Half, @c ReadMode::Bool, and @c ReadMode::Mask request
|
||||
/// in-place type conversion as data is read.
|
||||
/// @c ReadMode::TopologyOnly skips value buffers entirely, which can be
|
||||
/// significantly faster when only the active-voxel mask is needed.
|
||||
///
|
||||
/// @par Per-type options
|
||||
/// @c typeData allows callers to attach codec-specific configuration for
|
||||
/// individual grid types. Entries are keyed by the grid type string
|
||||
/// (e.g. @c "Vec3SGrid") and hold a @c ReadTypedOptions-derived object.
|
||||
/// Codecs retrieve their entry via @c ReadTypedOptions::cast<T>().
|
||||
struct OPENVDB_API ReadOptions
|
||||
{
|
||||
/// World-space bounding box used to spatially clip the read.
|
||||
/// An empty bbox (the default) disables clipping.
|
||||
BBoxd clipBBox = BBoxd();
|
||||
|
||||
/// Controls which portions of each grid are deserialized.
|
||||
/// Defaults to @c ReadMode::Original (full topology + value buffers).
|
||||
ReadMode readMode = ReadMode::Original;
|
||||
|
||||
/// Optional per-grid-type codec configuration, keyed by grid type string.
|
||||
/// Values are @c ReadTypedOptions subclass instances; use
|
||||
/// @c ReadTypedOptions::cast<T>() to retrieve the concrete type.
|
||||
std::unordered_map<std::string, ReadTypedOptions::Ptr> typeData;
|
||||
}; // struct ReadOptions
|
||||
|
||||
/// @brief Global write configuration passed to every codec during serialization.
|
||||
///
|
||||
/// Currently carries no fields, but is provided for forward compatibility:
|
||||
/// future write-time options (e.g. compression hints, metadata policies) can
|
||||
/// be added here without changing the codec interface.
|
||||
struct OPENVDB_API WriteOptions
|
||||
{
|
||||
}; // struct WriteOptions
|
||||
|
||||
/// @brief Severity level for a read diagnostic.
|
||||
/// @note Currently only @c Warning is defined.
|
||||
enum class DiagnosticSeverity { Warning };
|
||||
|
||||
/// @brief A single read diagnostic message.
|
||||
/// @note @c context is typically the grid name or another identifier that
|
||||
/// locates the source of the message.
|
||||
struct ReadDiagnostic {
|
||||
DiagnosticSeverity severity;
|
||||
std::string context;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
/// @brief Thread-safe collection of read diagnostics accumulated during a codec read.
|
||||
///
|
||||
/// @details Diagnostics report situations where a requested read option could
|
||||
/// not be honoured by the codec — for example, clipping that was not applied
|
||||
/// natively and had to fall back to a post-process, or an option that the codec
|
||||
/// ignores entirely. The object must be explicitly enabled before any messages
|
||||
/// are recorded; it is disabled by default.
|
||||
struct OPENVDB_API ReadDiagnostics {
|
||||
ReadDiagnostics() = default;
|
||||
ReadDiagnostics(const ReadDiagnostics& other) {
|
||||
std::lock_guard<std::mutex> lock(other.mMutex);
|
||||
mEnabled = other.mEnabled;
|
||||
mDiagnostics = other.mDiagnostics;
|
||||
}
|
||||
ReadDiagnostics& operator=(const ReadDiagnostics& other) {
|
||||
if (this != &other) {
|
||||
std::scoped_lock lock(mMutex, other.mMutex);
|
||||
mEnabled = other.mEnabled;
|
||||
mDiagnostics = other.mDiagnostics;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
/// @brief Append a warning diagnostic. No-op if the object is disabled.
|
||||
/// @note This method is thread-safe.
|
||||
void addWarning(const std::string& context, const std::string& message) {
|
||||
if (!mEnabled) return;
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mDiagnostics.push_back({DiagnosticSeverity::Warning, context, message});
|
||||
}
|
||||
/// @brief Remove all recorded diagnostics.
|
||||
/// @note This method is thread-safe.
|
||||
void clear() {
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mDiagnostics.clear();
|
||||
}
|
||||
/// @brief Enable diagnostic recording.
|
||||
void enable() { mEnabled = true; }
|
||||
/// @brief Disable diagnostic recording.
|
||||
void disable() { mEnabled = false; }
|
||||
/// @brief Return @c true if diagnostic recording is enabled.
|
||||
bool enabled() const { return mEnabled; }
|
||||
/// @brief Return the list of recorded diagnostics.
|
||||
const std::vector<ReadDiagnostic>& diagnostics() const { return mDiagnostics; }
|
||||
/// @brief Return @c true if no diagnostics have been recorded.
|
||||
bool empty() const { return mDiagnostics.empty(); }
|
||||
private:
|
||||
bool mEnabled = false;
|
||||
std::vector<ReadDiagnostic> mDiagnostics;
|
||||
mutable std::mutex mMutex;
|
||||
}; // struct ReadDiagnostics
|
||||
|
||||
/// @brief Mutable per-operation state created by @c Codec::createData() and
|
||||
/// passed into every read and write call on a given codec.
|
||||
///
|
||||
/// @details The main purpose of this class is to provide a mechanism for passing
|
||||
/// additional state between the different methods in the codec. @c ReadOptions is
|
||||
/// immutable and cannot carry per-operation state; the codec itself is stateless
|
||||
/// by design (to allow sharing across threads) and therefore also cannot be used
|
||||
/// for this purpose.
|
||||
///
|
||||
/// The base class provides the single field that every codec requires:
|
||||
/// a shared pointer to the @c GridBase being populated (on read) or inspected
|
||||
/// (on write). Derived codecs that need additional transient state should
|
||||
/// subclass @c CodecData and add those fields.
|
||||
///
|
||||
/// A @c CodecData instance is created once per grid per read or write operation
|
||||
/// by @c Codec::createData(), which also allocates the concrete @c GridBase
|
||||
/// subclass and stores it in @c grid.
|
||||
///
|
||||
/// @par Deriving from CodecData
|
||||
/// @code
|
||||
/// struct MyCodecData : public io::CodecData {
|
||||
/// // Additional per-operation state goes here
|
||||
/// SomeCache intermediateBuffer;
|
||||
/// };
|
||||
/// @endcode
|
||||
///
|
||||
/// Inside @c Codec::createData(), the derived type is allocated and returned:
|
||||
/// @code
|
||||
/// io::CodecData::Ptr createData() override {
|
||||
/// auto data = std::make_unique<MyCodecData>();
|
||||
/// data->grid = MyGridType::create();
|
||||
/// return data;
|
||||
/// }
|
||||
/// @endcode
|
||||
///
|
||||
/// In the read and write methods, downcast the reference back to the concrete
|
||||
/// type to access the extra fields:
|
||||
/// @code
|
||||
/// void readBuffers(std::istream& is, io::CodecData& data, ...) override {
|
||||
/// auto& myData = static_cast<MyCodecData&>(data);
|
||||
/// MyGridType& grid = static_cast<MyGridType&>(*myData.grid);
|
||||
/// // Use myData.intermediateBuffer, grid, etc.
|
||||
/// }
|
||||
/// @endcode
|
||||
struct OPENVDB_API CodecData
|
||||
{
|
||||
using Ptr = std::unique_ptr<CodecData>;
|
||||
|
||||
virtual ~CodecData() = default;
|
||||
|
||||
/// The grid being populated on read, or being serialized on write.
|
||||
SharedPtr<GridBase> grid;
|
||||
}; // struct CodecData
|
||||
|
||||
/// @brief Abstract base class for grid I/O codecs.
|
||||
///
|
||||
/// @details A codec encapsulates the serialization and deserialization logic
|
||||
/// for a specific grid type (or family of grid types). The I/O subsystem
|
||||
/// selects a codec by name at runtime, calls @c createData() to allocate
|
||||
/// per-operation state, and then dispatches to the appropriate read or write
|
||||
/// methods.
|
||||
///
|
||||
/// Codecs must be stateless: all mutable per-operation state is stored in a
|
||||
/// @c CodecData object (see above) that is created fresh for each grid read
|
||||
/// or write. This ensures that a single registered codec instance can safely
|
||||
/// handle concurrent I/O operations.
|
||||
///
|
||||
/// @par Implementing a new codec
|
||||
///
|
||||
/// 1. **Declare the codec struct**, deriving publicly from @c Codec (or from a
|
||||
/// convenience intermediate that already handles topology, if one exists for
|
||||
/// the grid family in question):
|
||||
/// @code
|
||||
/// struct MyCodec : public io::Codec { ... };
|
||||
/// @endcode
|
||||
///
|
||||
/// 2. **Provide a static @c name() method** that returns the unique string
|
||||
/// identifier under which the codec will be registered. Any globally
|
||||
/// unique ASCII string is acceptable:
|
||||
/// @code
|
||||
/// static std::string name() { return "mycodec"; }
|
||||
/// @endcode
|
||||
///
|
||||
/// 3. **Override @c createData()** (the only pure-virtual method). Allocate a
|
||||
/// @c CodecData (or a derived subclass if extra per-operation state is
|
||||
/// needed), create the appropriate @c GridBase subclass, assign it to
|
||||
/// @c CodecData::grid, and return the object:
|
||||
/// @code
|
||||
/// io::CodecData::Ptr createData() override {
|
||||
/// auto data = std::make_unique<io::CodecData>();
|
||||
/// data->grid = MyGridType::create();
|
||||
/// return data;
|
||||
/// }
|
||||
/// @endcode
|
||||
///
|
||||
/// 4. **Override the read and/or write methods** as required. All four
|
||||
/// methods have default no-op implementations, so override only those that
|
||||
/// the codec actually uses. The I/O layer separates topology (the tree
|
||||
/// structure and active/inactive voxel mask) from value buffers (the actual
|
||||
/// voxel data), allowing callers to request topology-only reads via
|
||||
/// @c ReadOptions::readMode. Implementations should honour that setting:
|
||||
/// @code
|
||||
/// void readTopology(std::istream& is, io::CodecData& data,
|
||||
/// const io::ReadOptions& options,
|
||||
/// io::ReadDiagnostics& diagnostics) override
|
||||
/// {
|
||||
/// MyGridType& grid = static_cast<MyGridType&>(*data.grid);
|
||||
/// // Deserialize the tree structure into grid...
|
||||
/// }
|
||||
///
|
||||
/// void readBuffers(std::istream& is, io::CodecData& data,
|
||||
/// const io::ReadOptions& options,
|
||||
/// io::ReadDiagnostics& diagnostics) override
|
||||
/// {
|
||||
/// if (options.readMode == io::ReadMode::TopologyOnly) return;
|
||||
/// MyGridType& grid = static_cast<MyGridType&>(*data.grid);
|
||||
/// // Deserialize voxel values into grid...
|
||||
/// }
|
||||
/// @endcode
|
||||
/// If a requested option (e.g. spatial clipping) cannot be honoured natively,
|
||||
/// record a warning via @c diagnostics.addWarning() rather than silently ignoring it.
|
||||
///
|
||||
/// 5. **Register the codec** once at start-up, typically from the library's
|
||||
/// @c initialize() function, using @c CodecRegistry::registerCodec<MyCodec>():
|
||||
/// @code
|
||||
/// io::CodecRegistry::registerCodec<MyCodec>();
|
||||
/// @endcode
|
||||
///
|
||||
/// @note The codec struct itself is never copied; the registry takes ownership
|
||||
/// of a single heap-allocated instance via @c Codec::Ptr
|
||||
/// (@c std::unique_ptr<Codec>).
|
||||
struct OPENVDB_API Codec
|
||||
{
|
||||
using Ptr = std::unique_ptr<Codec>;
|
||||
|
||||
virtual ~Codec() = default;
|
||||
|
||||
/// @brief Allocate per-operation codec state, including the target grid.
|
||||
///
|
||||
/// This is the only pure-virtual method. Implementations must create a
|
||||
/// concrete @c GridBase subclass appropriate for the codec, store it in
|
||||
/// @c CodecData::grid, and return the @c CodecData (or a derived subclass
|
||||
/// carrying additional state). The returned object is passed by reference
|
||||
/// to every subsequent read or write call for the same grid.
|
||||
///
|
||||
/// @return A fully initialized @c CodecData whose @c grid field is non-null.
|
||||
virtual CodecData::Ptr createData() = 0;
|
||||
|
||||
/// @brief Deserialize the grid topology (tree structure and active-voxel
|
||||
/// mask) from @a is into the grid held by @a data.
|
||||
virtual void readTopology(std::istream& /*is*/, CodecData& /*data*/,
|
||||
const ReadOptions& /*options*/, ReadDiagnostics& /*diagnostics*/) { }
|
||||
|
||||
/// @brief Deserialize all voxel-value buffers from @a is into the grid
|
||||
/// held by @a data.
|
||||
///
|
||||
/// The default implementation is a no-op. Override to populate leaf-node
|
||||
/// value buffers after topology has been established. When
|
||||
/// @c options.readMode is @c ReadMode::TopologyOnly this method will not
|
||||
/// be called by the I/O layer, so implementations may also guard against
|
||||
/// that mode internally for safety. If @c options.clipBBox is non-empty,
|
||||
/// restrict the loaded data to the region that intersects it; if the codec
|
||||
/// cannot honour clipping natively, fall back to a post-process and record
|
||||
/// a warning via @a diagnostics.
|
||||
virtual void readBuffers(std::istream& /*is*/, CodecData& /*data*/,
|
||||
const ReadOptions& /*options*/, ReadDiagnostics& /*diagnostics*/) { }
|
||||
|
||||
/// @brief Serialize the grid topology (tree structure and active-voxel
|
||||
/// mask) from @a grid to @a os.
|
||||
///
|
||||
/// The default implementation is a no-op. Override when the codec stores
|
||||
/// topology as a distinct section that precedes the value buffers in the
|
||||
/// stream.
|
||||
virtual void writeTopology(std::ostream& /*os*/, const GridBase& /*grid*/,
|
||||
const WriteOptions& /*options*/) { }
|
||||
|
||||
/// @brief Serialize all voxel-value buffers from @a grid to @a os.
|
||||
///
|
||||
/// The default implementation is a no-op. Override to write the leaf-node
|
||||
/// value buffers that follow the topology section.
|
||||
virtual void writeBuffers(std::ostream& /*os*/, const GridBase& /*grid*/,
|
||||
const WriteOptions& /*options*/) { }
|
||||
}; // struct Codec
|
||||
|
||||
/// @brief A thread-safe, process-global registry that maps codec names to
|
||||
/// @c Codec instances.
|
||||
///
|
||||
/// Codecs are identified by a unique string name (typically provided by @c Codec::name())
|
||||
/// and stored as @c Codec::Ptr (i.e. @c std::unique_ptr<Codec>).
|
||||
///
|
||||
/// @par Typical usage
|
||||
/// @code
|
||||
/// // Register a custom codec once at start-up (e.g. from initialize()):
|
||||
/// CodecRegistry::registerCodec<MyCodec>();
|
||||
/// @endcode
|
||||
///
|
||||
/// @note Attempting to register a codec whose name is already present throws
|
||||
/// @c openvdb::KeyError. Call @c isRegistered() first when the caller
|
||||
/// cannot guarantee uniqueness.
|
||||
struct OPENVDB_API CodecRegistry
|
||||
{
|
||||
/// Return @c true if a codec with the given @a name has been registered.
|
||||
static bool isRegistered(const std::string& name);
|
||||
|
||||
/// Register a codec under the explicit string @a name, transferring
|
||||
/// ownership of @a codec to the registry.
|
||||
/// @throw KeyError if @a name is already registered.
|
||||
static void registerCodecByName(const std::string& name, Codec::Ptr&& codec);
|
||||
|
||||
/// Convenience wrapper that registers @c CodecT using the name returned
|
||||
/// by @c CodecT::name(), constructing the instance internally.
|
||||
/// @throw KeyError if the codec is already registered.
|
||||
template <typename CodecT>
|
||||
static void registerCodec()
|
||||
{
|
||||
registerCodecByName(CodecT::name(), std::make_unique<CodecT>());
|
||||
}
|
||||
|
||||
/// Return a raw (non-owning) pointer to the codec registered under
|
||||
/// @a name, or @c nullptr if no such codec exists.
|
||||
/// The returned pointer remains valid for the lifetime of the registry.
|
||||
static Codec* get(const std::string& name);
|
||||
|
||||
/// Deregister all codecs and reset the registry to an empty state.
|
||||
static void clear();
|
||||
}; // struct CodecRegistry
|
||||
|
||||
} // namespace io
|
||||
} // namespace OPENVDB_VERSION_NAME
|
||||
} // namespace openvdb
|
||||
|
||||
#endif // OPENVDB_IO_CODEC_HAS_BEEN_INCLUDED
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <openvdb/math/Math.h> // for negative()
|
||||
#include <openvdb/util/Assert.h>
|
||||
#include "io.h" // for getDataCompression(), etc.
|
||||
#include "DelayedLoadMetadata.h"
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
@@ -235,20 +236,28 @@ OPENVDB_API void bloscFromStream(std::istream&, char* data, size_t numBytes);
|
||||
/// @param count the number of elements to read in
|
||||
/// @param compression whether and how the data is compressed (either COMPRESS_NONE,
|
||||
/// COMPRESS_ZIP, COMPRESS_ACTIVE_MASK or COMPRESS_BLOSC)
|
||||
/// @param metadata optional pointer to a DelayedLoadMetadata object that stores
|
||||
/// the size of the compressed buffer
|
||||
/// @param metadataOffset offset into DelayedLoadMetadata, ignored if pointer is null
|
||||
/// @throw IoError if @a compression is COMPRESS_BLOSC but OpenVDB was compiled
|
||||
/// without Blosc support.
|
||||
/// @details This default implementation is instantiated only for types
|
||||
/// whose size can be determined by the sizeof() operator.
|
||||
template<typename T>
|
||||
inline void
|
||||
readData(std::istream& is, T* data, Index count, uint32_t compression)
|
||||
readData(std::istream& is, T* data, Index count, uint32_t compression,
|
||||
DelayedLoadMetadata* metadata = nullptr, size_t metadataOffset = size_t(0))
|
||||
{
|
||||
const bool seek = data == nullptr;
|
||||
if (seek) {
|
||||
OPENVDB_ASSERT(!getStreamMetadataPtr(is) || getStreamMetadataPtr(is)->seekable());
|
||||
}
|
||||
const bool hasCompression = compression & (COMPRESS_BLOSC | COMPRESS_ZIP);
|
||||
|
||||
if (compression & COMPRESS_BLOSC) {
|
||||
if (metadata && seek && hasCompression) {
|
||||
size_t compressedSize = metadata->getCompressedSize(metadataOffset);
|
||||
is.seekg(compressedSize, std::ios_base::cur);
|
||||
} else if (compression & COMPRESS_BLOSC) {
|
||||
bloscFromStream(is, reinterpret_cast<char*>(data), sizeof(T) * count);
|
||||
} else if (compression & COMPRESS_ZIP) {
|
||||
unzipFromStream(is, reinterpret_cast<char*>(data), sizeof(T) * count);
|
||||
@@ -262,7 +271,8 @@ readData(std::istream& is, T* data, Index count, uint32_t compression)
|
||||
/// Specialization for std::string input
|
||||
template<>
|
||||
inline void
|
||||
readData<std::string>(std::istream& is, std::string* data, Index count, uint32_t /*compression*/)
|
||||
readData<std::string>(std::istream& is, std::string* data, Index count, uint32_t /*compression*/,
|
||||
DelayedLoadMetadata* /*metadata*/, size_t /*metadataOffset*/)
|
||||
{
|
||||
for (Index i = 0; i < count; ++i) {
|
||||
size_t len = 0;
|
||||
@@ -284,22 +294,25 @@ template<bool IsReal, typename T> struct HalfReader;
|
||||
/// Partial specialization for non-floating-point types (no half to float promotion)
|
||||
template<typename T>
|
||||
struct HalfReader</*IsReal=*/false, T> {
|
||||
static inline void read(std::istream& is, T* data, Index count, uint32_t compression) {
|
||||
readData(is, data, count, compression);
|
||||
static inline void read(std::istream& is, T* data, Index count, uint32_t compression,
|
||||
DelayedLoadMetadata* metadata = nullptr, size_t metadataOffset = size_t(0)) {
|
||||
readData(is, data, count, compression, metadata, metadataOffset);
|
||||
}
|
||||
};
|
||||
/// Partial specialization for floating-point types
|
||||
template<typename T>
|
||||
struct HalfReader</*IsReal=*/true, T> {
|
||||
using HalfT = typename RealToHalf<T>::HalfT;
|
||||
static inline void read(std::istream& is, T* data, Index count, uint32_t compression) {
|
||||
static inline void read(std::istream& is, T* data, Index count, uint32_t compression,
|
||||
DelayedLoadMetadata* metadata = nullptr, size_t metadataOffset = size_t(0)) {
|
||||
if (count < 1) return;
|
||||
if (data == nullptr) {
|
||||
// seek mode - pass through null pointer
|
||||
readData<HalfT>(is, nullptr, count, compression);
|
||||
readData<HalfT>(is, nullptr, count, compression, metadata, metadataOffset);
|
||||
} else {
|
||||
std::vector<HalfT> halfData(count); // temp buffer into which to read half float values
|
||||
readData<HalfT>(is, reinterpret_cast<HalfT*>(&halfData[0]), count, compression);
|
||||
readData<HalfT>(is, reinterpret_cast<HalfT*>(&halfData[0]), count, compression,
|
||||
metadata, metadataOffset);
|
||||
// Copy half float values from the temporary buffer to the full float output array.
|
||||
std::copy(halfData.begin(), halfData.end(), data);
|
||||
}
|
||||
@@ -463,6 +476,16 @@ readCompressedValues(std::istream& is, ValueT* destBuf, Index destCount,
|
||||
const bool seek = (destBuf == nullptr);
|
||||
OPENVDB_ASSERT(!seek || (!meta || meta->seekable()));
|
||||
|
||||
// Get delayed load metadata if it exists
|
||||
|
||||
DelayedLoadMetadata::Ptr delayLoadMeta;
|
||||
uint64_t leafIndex(0);
|
||||
if (seek && meta && meta->delayedLoadMeta()) {
|
||||
delayLoadMeta =
|
||||
meta->gridMetadata().getMetadata<DelayedLoadMetadata>("file_delayed_load");
|
||||
leafIndex = meta->leaf();
|
||||
}
|
||||
|
||||
int8_t metadata = NO_MASK_AND_ALL_VALS;
|
||||
|
||||
if (getFormatVersion(is) >= OPENVDB_FILE_VERSION_NODE_MASK_COMPRESSION) {
|
||||
@@ -470,6 +493,9 @@ readCompressedValues(std::istream& is, ValueT* destBuf, Index destCount,
|
||||
// (selection mask and/or inactive value(s)) is saved.
|
||||
if (seek && !maskCompressed) {
|
||||
is.seekg(/*bytes=*/1, std::ios_base::cur);
|
||||
} else if (seek && delayLoadMeta) {
|
||||
metadata = delayLoadMeta->getMask(leafIndex);
|
||||
is.seekg(/*bytes=*/1, std::ios_base::cur);
|
||||
} else {
|
||||
is.read(reinterpret_cast<char*>(&metadata), /*bytes=*/1);
|
||||
}
|
||||
@@ -536,10 +562,10 @@ readCompressedValues(std::istream& is, ValueT* destBuf, Index destCount,
|
||||
// Read in the buffer.
|
||||
if (fromHalf) {
|
||||
HalfReader<RealToHalf<ValueT>::isReal, ValueT>::read(
|
||||
is, (seek ? nullptr : tempBuf), tempCount, compression);
|
||||
is, (seek ? nullptr : tempBuf), tempCount, compression, delayLoadMeta.get(), leafIndex);
|
||||
} else {
|
||||
readData<ValueT>(
|
||||
is, (seek ? nullptr : tempBuf), tempCount, compression);
|
||||
is, (seek ? nullptr : tempBuf), tempCount, compression, delayLoadMeta.get(), leafIndex);
|
||||
}
|
||||
|
||||
// If mask compression is enabled and the number of active values read into
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include "DelayedLoadMetadata.h"
|
||||
|
||||
#include <openvdb/points/StreamCompression.h>
|
||||
#include <openvdb/util/Assert.h>
|
||||
|
||||
#ifdef OPENVDB_USE_BLOSC
|
||||
#include <blosc.h>
|
||||
|
||||
namespace {
|
||||
|
||||
inline size_t padMask(size_t bytes)
|
||||
{
|
||||
return size_t(std::ceil(static_cast<float>(bytes+1) /
|
||||
sizeof(openvdb::io::DelayedLoadMetadata::MaskType)));
|
||||
}
|
||||
|
||||
inline size_t padCompressedSize(size_t bytes)
|
||||
{
|
||||
return size_t(std::ceil(static_cast<float>(bytes+1) /
|
||||
sizeof(openvdb::io::DelayedLoadMetadata::CompressedSizeType)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif
|
||||
|
||||
namespace openvdb {
|
||||
OPENVDB_USE_VERSION_NAMESPACE
|
||||
namespace OPENVDB_VERSION_NAME {
|
||||
namespace io {
|
||||
|
||||
DelayedLoadMetadata::DelayedLoadMetadata(const DelayedLoadMetadata& other)
|
||||
: Metadata()
|
||||
, mMask(other.mMask)
|
||||
, mCompressedSize(other.mCompressedSize)
|
||||
{
|
||||
}
|
||||
|
||||
Name DelayedLoadMetadata::typeName() const
|
||||
{
|
||||
return DelayedLoadMetadata::staticTypeName();
|
||||
}
|
||||
|
||||
Metadata::Ptr DelayedLoadMetadata::copy() const
|
||||
{
|
||||
Metadata::Ptr metadata(new DelayedLoadMetadata());
|
||||
metadata->copy(*this);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
void DelayedLoadMetadata::copy(const Metadata& other)
|
||||
{
|
||||
const DelayedLoadMetadata* t = dynamic_cast<const DelayedLoadMetadata*>(&other);
|
||||
if (t == nullptr) OPENVDB_THROW(TypeError, "Incompatible type during copy");
|
||||
mMask = t->mMask;
|
||||
mCompressedSize = t->mCompressedSize;
|
||||
}
|
||||
|
||||
std::string DelayedLoadMetadata::str() const
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
bool DelayedLoadMetadata::asBool() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Index32 DelayedLoadMetadata::size() const
|
||||
{
|
||||
if (mMask.empty() && mCompressedSize.empty()) return Index32(0);
|
||||
|
||||
// count
|
||||
size_t size = sizeof(Index32);
|
||||
|
||||
{ // mask
|
||||
size += sizeof(Index32);
|
||||
size_t compressedSize = compression::bloscCompressedSize(
|
||||
reinterpret_cast<const char*>(mMask.data()), mMask.size()*sizeof(MaskType));
|
||||
|
||||
if (compressedSize > 0) size += compressedSize;
|
||||
else size += mMask.size()*sizeof(MaskType);
|
||||
}
|
||||
{ // compressed size
|
||||
size += sizeof(Index32);
|
||||
if (!mCompressedSize.empty()) {
|
||||
size_t compressedSize = compression::bloscCompressedSize(
|
||||
reinterpret_cast<const char*>(mCompressedSize.data()), mCompressedSize.size()*sizeof(CompressedSizeType));
|
||||
|
||||
if (compressedSize > 0) size += compressedSize;
|
||||
else size += mCompressedSize.size()*sizeof(CompressedSizeType);
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<Index32>(size);
|
||||
}
|
||||
|
||||
void DelayedLoadMetadata::clear()
|
||||
{
|
||||
mMask.clear();
|
||||
mCompressedSize.clear();
|
||||
}
|
||||
|
||||
bool DelayedLoadMetadata::empty() const
|
||||
{
|
||||
return mMask.empty() && mCompressedSize.empty();
|
||||
}
|
||||
|
||||
void DelayedLoadMetadata::resizeMask(size_t size)
|
||||
{
|
||||
mMask.resize(size);
|
||||
}
|
||||
|
||||
void DelayedLoadMetadata::resizeCompressedSize(size_t size)
|
||||
{
|
||||
mCompressedSize.resize(size);
|
||||
}
|
||||
|
||||
DelayedLoadMetadata::MaskType DelayedLoadMetadata::getMask(size_t index) const
|
||||
{
|
||||
OPENVDB_ASSERT(DelayedLoadMetadata::isRegisteredType());
|
||||
OPENVDB_ASSERT(index < mMask.size());
|
||||
return mMask[index];
|
||||
}
|
||||
|
||||
void DelayedLoadMetadata::setMask(size_t index, const MaskType& value)
|
||||
{
|
||||
OPENVDB_ASSERT(index < mMask.size());
|
||||
mMask[index] = value;
|
||||
}
|
||||
|
||||
DelayedLoadMetadata::CompressedSizeType DelayedLoadMetadata::getCompressedSize(size_t index) const
|
||||
{
|
||||
OPENVDB_ASSERT(DelayedLoadMetadata::isRegisteredType());
|
||||
OPENVDB_ASSERT(index < mCompressedSize.size());
|
||||
return mCompressedSize[index];
|
||||
}
|
||||
|
||||
void DelayedLoadMetadata::setCompressedSize(size_t index, const CompressedSizeType& value)
|
||||
{
|
||||
OPENVDB_ASSERT(index < mCompressedSize.size());
|
||||
mCompressedSize[index] = value;
|
||||
}
|
||||
|
||||
void DelayedLoadMetadata::readValue(std::istream& is, Index32 numBytes)
|
||||
{
|
||||
if (numBytes == 0) return;
|
||||
|
||||
// initial header size
|
||||
size_t total = sizeof(Index32);
|
||||
|
||||
Index32 count = 0;
|
||||
is.read(reinterpret_cast<char*>(&count), sizeof(Index32));
|
||||
total += sizeof(Index32);
|
||||
|
||||
Index32 bytes = 0;
|
||||
is.read(reinterpret_cast<char*>(&bytes), sizeof(Index32));
|
||||
total += sizeof(Index32);
|
||||
|
||||
if (bytes > Index32(0)) {
|
||||
std::unique_ptr<char[]> compressedBuffer(new char[bytes]);
|
||||
is.read(reinterpret_cast<char*>(compressedBuffer.get()), bytes);
|
||||
|
||||
total += bytes;
|
||||
|
||||
#ifdef OPENVDB_USE_BLOSC
|
||||
// pad to include BLOSC_MAX_OVERHEAD
|
||||
size_t uncompressedBytes = openvdb::compression::bloscUncompressedSize(compressedBuffer.get());
|
||||
const size_t paddedCount = padMask(uncompressedBytes + BLOSC_MAX_OVERHEAD);
|
||||
|
||||
mMask.reserve(paddedCount);
|
||||
mMask.resize(count);
|
||||
|
||||
// resize should never modify capacity for smaller vector sizes
|
||||
OPENVDB_ASSERT(mMask.capacity() >= paddedCount);
|
||||
|
||||
compression::bloscDecompress(reinterpret_cast<char*>(mMask.data()), count*sizeof(MaskType), mMask.capacity()*sizeof(MaskType), compressedBuffer.get());
|
||||
#endif
|
||||
} else {
|
||||
mMask.resize(count);
|
||||
is.read(reinterpret_cast<char*>(mMask.data()), count*sizeof(MaskType));
|
||||
total += count*sizeof(MaskType);
|
||||
}
|
||||
|
||||
is.read(reinterpret_cast<char*>(&bytes), sizeof(Index32));
|
||||
|
||||
if (bytes != std::numeric_limits<Index32>::max()) {
|
||||
if (bytes > Index32(0)) {
|
||||
std::unique_ptr<char[]> compressedBuffer(new char[bytes]);
|
||||
is.read(reinterpret_cast<char*>(compressedBuffer.get()), bytes);
|
||||
|
||||
total += size_t(bytes);
|
||||
|
||||
#ifdef OPENVDB_USE_BLOSC
|
||||
// pad to include BLOSC_MAX_OVERHEAD
|
||||
size_t uncompressedBytes = openvdb::compression::bloscUncompressedSize(compressedBuffer.get());
|
||||
const size_t paddedCount = padCompressedSize(uncompressedBytes + BLOSC_MAX_OVERHEAD);
|
||||
|
||||
mCompressedSize.reserve(paddedCount);
|
||||
mCompressedSize.resize(count);
|
||||
|
||||
// resize should never modify capacity for smaller vector sizes
|
||||
OPENVDB_ASSERT(mCompressedSize.capacity() >= paddedCount);
|
||||
|
||||
compression::bloscDecompress(reinterpret_cast<char*>(mCompressedSize.data()), count*sizeof(CompressedSizeType), mCompressedSize.capacity()*sizeof(CompressedSizeType), compressedBuffer.get());
|
||||
#endif
|
||||
} else {
|
||||
mCompressedSize.resize(count);
|
||||
is.read(reinterpret_cast<char*>(mCompressedSize.data()), count*sizeof(CompressedSizeType));
|
||||
total += count*sizeof(CompressedSizeType);
|
||||
}
|
||||
}
|
||||
|
||||
Index32 totalBytes = static_cast<Index32>(total);
|
||||
|
||||
if (totalBytes < numBytes) {
|
||||
// Read and discard any unknown bytes at the end of the metadata for forwards-compatibility
|
||||
// (without seeking, because the stream might not be seekable).
|
||||
const size_t BUFFER_SIZE = 1024;
|
||||
std::vector<char> buffer(BUFFER_SIZE);
|
||||
for (Index32 bytesRemaining = numBytes - totalBytes; bytesRemaining > 0; ) {
|
||||
const Index32 bytesToSkip = std::min<Index32>(bytesRemaining, BUFFER_SIZE);
|
||||
is.read(&buffer[0], bytesToSkip);
|
||||
bytesRemaining -= bytesToSkip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DelayedLoadMetadata::writeValue(std::ostream& os) const
|
||||
{
|
||||
// metadata has a limit of 2^32 bytes
|
||||
OPENVDB_ASSERT(mMask.size() < std::numeric_limits<Index32>::max());
|
||||
OPENVDB_ASSERT(mCompressedSize.size() < std::numeric_limits<Index32>::max());
|
||||
|
||||
if (mMask.empty() && mCompressedSize.empty()) return;
|
||||
|
||||
OPENVDB_ASSERT(mCompressedSize.empty() || (mMask.size() == mCompressedSize.size()));
|
||||
|
||||
Index32 count = static_cast<Index32>(mMask.size());
|
||||
os.write(reinterpret_cast<const char*>(&count), sizeof(Index32));
|
||||
|
||||
const Index32 zeroSize(0);
|
||||
const Index32 maxSize(std::numeric_limits<Index32>::max());
|
||||
|
||||
{ // mask buffer
|
||||
size_t compressedBytes(0);
|
||||
std::unique_ptr<char[]> compressedBuffer;
|
||||
if (compression::bloscCanCompress()) {
|
||||
compressedBuffer = compression::bloscCompress(
|
||||
reinterpret_cast<const char*>(mMask.data()),
|
||||
mMask.size()*sizeof(MaskType), compressedBytes, /*resize=*/false);
|
||||
}
|
||||
|
||||
if (compressedBuffer) {
|
||||
OPENVDB_ASSERT(compressedBytes < std::numeric_limits<Index32>::max());
|
||||
Index32 bytes(static_cast<Index32>(compressedBytes));
|
||||
os.write(reinterpret_cast<const char*>(&bytes), sizeof(Index32));
|
||||
os.write(reinterpret_cast<const char*>(compressedBuffer.get()), compressedBytes);
|
||||
}
|
||||
else {
|
||||
os.write(reinterpret_cast<const char*>(&zeroSize), sizeof(Index32));
|
||||
os.write(reinterpret_cast<const char*>(mMask.data()),
|
||||
mMask.size()*sizeof(MaskType));
|
||||
}
|
||||
}
|
||||
|
||||
// compressed size buffer
|
||||
|
||||
if (mCompressedSize.empty()) {
|
||||
// write out maximum Index32 value to denote no compressed sizes stored
|
||||
os.write(reinterpret_cast<const char*>(&maxSize), sizeof(Index32));
|
||||
} else {
|
||||
size_t compressedBytes(0);
|
||||
std::unique_ptr<char[]> compressedBuffer;
|
||||
if (compression::bloscCanCompress()) {
|
||||
compressedBuffer = compression::bloscCompress(
|
||||
reinterpret_cast<const char*>(mCompressedSize.data()),
|
||||
mCompressedSize.size()*sizeof(CompressedSizeType), compressedBytes, /*resize=*/false);
|
||||
}
|
||||
|
||||
if (compressedBuffer) {
|
||||
OPENVDB_ASSERT(compressedBytes < std::numeric_limits<Index32>::max());
|
||||
Index32 bytes(static_cast<Index32>(compressedBytes));
|
||||
os.write(reinterpret_cast<const char*>(&bytes), sizeof(Index32));
|
||||
os.write(reinterpret_cast<const char*>(compressedBuffer.get()), compressedBytes);
|
||||
}
|
||||
else {
|
||||
os.write(reinterpret_cast<const char*>(&zeroSize), sizeof(Index32));
|
||||
os.write(reinterpret_cast<const char*>(mCompressedSize.data()),
|
||||
mCompressedSize.size()*sizeof(CompressedSizeType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace io
|
||||
} // namespace OPENVDB_VERSION_NAME
|
||||
} // namespace openvdb
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef OPENVDB_DELAYED_LOAD_METADATA_HAS_BEEN_INCLUDED
|
||||
#define OPENVDB_DELAYED_LOAD_METADATA_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <openvdb/Metadata.h>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
||||
namespace openvdb {
|
||||
OPENVDB_USE_VERSION_NAMESPACE
|
||||
namespace OPENVDB_VERSION_NAME {
|
||||
namespace io {
|
||||
|
||||
/// @brief Store a buffer of data that can be optionally used
|
||||
/// during reading for faster delayed-load I/O performance
|
||||
class OPENVDB_API DelayedLoadMetadata: public Metadata
|
||||
{
|
||||
public:
|
||||
using Ptr = SharedPtr<DelayedLoadMetadata>;
|
||||
using ConstPtr = SharedPtr<const DelayedLoadMetadata>;
|
||||
using MaskType = int8_t;
|
||||
using CompressedSizeType = int64_t;
|
||||
|
||||
DelayedLoadMetadata() = default;
|
||||
DelayedLoadMetadata(const DelayedLoadMetadata& other);
|
||||
~DelayedLoadMetadata() override = default;
|
||||
|
||||
Name typeName() const override;
|
||||
Metadata::Ptr copy() const override;
|
||||
void copy(const Metadata&) override;
|
||||
std::string str() const override;
|
||||
bool asBool() const override;
|
||||
Index32 size() const override;
|
||||
|
||||
static Name staticTypeName() { return "__delayedload"; }
|
||||
|
||||
static Metadata::Ptr createMetadata()
|
||||
{
|
||||
Metadata::Ptr ret(new DelayedLoadMetadata);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void registerType()
|
||||
{
|
||||
Metadata::registerType(DelayedLoadMetadata::staticTypeName(),
|
||||
DelayedLoadMetadata::createMetadata);
|
||||
}
|
||||
|
||||
static void unregisterType()
|
||||
{
|
||||
Metadata::unregisterType(DelayedLoadMetadata::staticTypeName());
|
||||
}
|
||||
|
||||
static bool isRegisteredType()
|
||||
{
|
||||
return Metadata::isRegisteredType(DelayedLoadMetadata::staticTypeName());
|
||||
}
|
||||
|
||||
/// @brief Delete the contents of the mask and compressed size arrays
|
||||
void clear();
|
||||
/// @brief Return @c true if both arrays are empty
|
||||
bool empty() const;
|
||||
|
||||
/// @brief Resize the mask array
|
||||
void resizeMask(size_t size);
|
||||
/// @brief Resize the compressed size array
|
||||
void resizeCompressedSize(size_t size);
|
||||
|
||||
/// @brief Return the mask value for a specific index
|
||||
/// @note throws if index is out-of-range or DelayedLoadMask not registered
|
||||
MaskType getMask(size_t index) const;
|
||||
/// @brief Set the mask value for a specific index
|
||||
/// @note throws if index is out-of-range
|
||||
void setMask(size_t index, const MaskType& value);
|
||||
|
||||
/// @brief Return the compressed size value for a specific index
|
||||
/// @note throws if index is out-of-range or DelayedLoadMask not registered
|
||||
CompressedSizeType getCompressedSize(size_t index) const;
|
||||
/// @brief Set the compressed size value for a specific index
|
||||
/// @note throws if index is out-of-range
|
||||
void setCompressedSize(size_t index, const CompressedSizeType& value);
|
||||
|
||||
protected:
|
||||
void readValue(std::istream&, Index32 numBytes) override;
|
||||
void writeValue(std::ostream&) const override;
|
||||
|
||||
private:
|
||||
std::vector<MaskType> mMask;
|
||||
std::vector<CompressedSizeType> mCompressedSize;
|
||||
}; // class DelayedLoadMetadata
|
||||
|
||||
|
||||
} // namespace io
|
||||
} // namespace OPENVDB_VERSION_NAME
|
||||
} // namespace openvdb
|
||||
|
||||
#endif // OPENVDB_DELAYED_LOAD_METADATA_HAS_BEEN_INCLUDED
|
||||
+415
-150
@@ -5,11 +5,20 @@
|
||||
|
||||
#include "File.h"
|
||||
|
||||
#include "TempFile.h"
|
||||
#include <openvdb/Exceptions.h>
|
||||
#include <openvdb/util/logging.h>
|
||||
#include <openvdb/util/Assert.h>
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
#include <boost/iostreams/copy.hpp>
|
||||
#ifndef _WIN32
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#endif // OPENVDB_USE_DELAYED_LOADING
|
||||
|
||||
#include <sys/stat.h> // stat()
|
||||
|
||||
#include <cstdlib> // for getenv(), strtoul()
|
||||
@@ -25,24 +34,106 @@ OPENVDB_USE_VERSION_NAMESPACE
|
||||
namespace OPENVDB_VERSION_NAME {
|
||||
namespace io {
|
||||
|
||||
|
||||
File::File(const std::string& filename)
|
||||
: Archive()
|
||||
, mFilename(filename)
|
||||
// Implementation details of the File class
|
||||
struct File::Impl
|
||||
{
|
||||
enum { DEFAULT_COPY_MAX_BYTES = 500000000 }; // 500 MB
|
||||
|
||||
struct NoBBox {};
|
||||
|
||||
// Common implementation of the various File::readGrid() overloads,
|
||||
// with and without bounding box clipping
|
||||
template<typename BoxType>
|
||||
static GridBase::Ptr readGrid(const File& file, const GridDescriptor& gd, const BoxType& bbox)
|
||||
{
|
||||
// This method should not be called for files that don't contain grid offsets.
|
||||
OPENVDB_ASSERT(file.inputHasGridOffsets());
|
||||
|
||||
GridBase::Ptr grid = file.createGrid(gd);
|
||||
gd.seekToGrid(file.inputStream());
|
||||
unarchive(file, grid, gd, bbox);
|
||||
return grid;
|
||||
}
|
||||
|
||||
static void unarchive(const File& file, GridBase::Ptr& grid,
|
||||
const GridDescriptor& gd, NoBBox)
|
||||
{
|
||||
file.Archive::readGrid(grid, gd, file.inputStream());
|
||||
}
|
||||
|
||||
static void unarchive(const File& file, GridBase::Ptr& grid,
|
||||
const GridDescriptor& gd, const CoordBBox& indexBBox)
|
||||
{
|
||||
file.Archive::readGrid(grid, gd, file.inputStream(), indexBBox);
|
||||
}
|
||||
|
||||
static void unarchive(const File& file, GridBase::Ptr& grid,
|
||||
const GridDescriptor& gd, const BBoxd& worldBBox)
|
||||
{
|
||||
file.Archive::readGrid(grid, gd, file.inputStream(), worldBBox);
|
||||
}
|
||||
|
||||
static Index64 getDefaultCopyMaxBytes()
|
||||
{
|
||||
Index64 result = DEFAULT_COPY_MAX_BYTES;
|
||||
if (const char* s = std::getenv("OPENVDB_DELAYED_LOAD_COPY_MAX_BYTES")) {
|
||||
char* endptr = nullptr;
|
||||
result = std::strtoul(s, &endptr, /*base=*/10);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string mFilename;
|
||||
// The file-level metadata
|
||||
MetaMap::Ptr mMeta;
|
||||
// The file stream that is open for reading
|
||||
std::unique_ptr<std::istream> mInStream;
|
||||
// File-level stream metadata (file format, compression, etc.)
|
||||
StreamMetadata::Ptr mStreamMetadata;
|
||||
// Flag indicating if we have read in the global information (header,
|
||||
// metadata, and grid descriptors) for this VDB file
|
||||
bool mIsOpen;
|
||||
// Grid descriptors for all grids stored in the file, indexed by grid name
|
||||
NameMap mGridDescriptors;
|
||||
// All grids, indexed by unique name (used only when mHasGridOffsets is false)
|
||||
Archive::NamedGridMap mNamedGrids;
|
||||
// All grids stored in the file (used only when mHasGridOffsets is false)
|
||||
GridPtrVecPtr mGrids;
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
// The memory-mapped file
|
||||
MappedFile::Ptr mFileMapping;
|
||||
// The buffer for the input stream, if it is a memory-mapped file
|
||||
SharedPtr<std::streambuf> mStreamBuf;
|
||||
// File size limit for copying during delayed loading
|
||||
Index64 mCopyMaxBytes;
|
||||
#endif
|
||||
}; // class File::Impl
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
|
||||
|
||||
File::File(const std::string& filename): mImpl(new Impl)
|
||||
{
|
||||
mImpl->mFilename = filename;
|
||||
mImpl->mIsOpen = false;
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
mImpl->mCopyMaxBytes = Impl::getDefaultCopyMaxBytes();
|
||||
#endif
|
||||
setInputHasGridOffsets(true);
|
||||
}
|
||||
|
||||
|
||||
File::~File()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
File::File(const File& other)
|
||||
: Archive(other)
|
||||
, mFilename(other.mFilename)
|
||||
, mMeta(other.mMeta)
|
||||
, mIsOpen(false)
|
||||
, mGridDescriptors(other.mGridDescriptors)
|
||||
, mNamedGrids(other.mNamedGrids)
|
||||
, mGrids(other.mGrids)
|
||||
, mImpl(new Impl)
|
||||
{
|
||||
*this = other;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,12 +142,16 @@ File::operator=(const File& other)
|
||||
{
|
||||
if (&other != this) {
|
||||
Archive::operator=(other);
|
||||
mFilename = other.mFilename;
|
||||
mMeta = other.mMeta;
|
||||
mIsOpen = false; // don't want two file objects reading from the same stream
|
||||
mGridDescriptors = other.mGridDescriptors;
|
||||
mNamedGrids = other.mNamedGrids;
|
||||
mGrids = other.mGrids;
|
||||
const Impl& otherImpl = *other.mImpl;
|
||||
mImpl->mFilename = otherImpl.mFilename;
|
||||
mImpl->mMeta = otherImpl.mMeta;
|
||||
mImpl->mIsOpen = false; // don't want two file objects reading from the same stream
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
mImpl->mCopyMaxBytes = otherImpl.mCopyMaxBytes;
|
||||
#endif
|
||||
mImpl->mGridDescriptors = otherImpl.mGridDescriptors;
|
||||
mImpl->mNamedGrids = otherImpl.mNamedGrids;
|
||||
mImpl->mGrids = otherImpl.mGrids;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
@@ -75,43 +170,43 @@ File::copy() const
|
||||
const std::string&
|
||||
File::filename() const
|
||||
{
|
||||
return mFilename;
|
||||
return mImpl->mFilename;
|
||||
}
|
||||
|
||||
|
||||
MetaMap::Ptr
|
||||
File::fileMetadata()
|
||||
{
|
||||
return mMeta;
|
||||
return mImpl->mMeta;
|
||||
}
|
||||
|
||||
MetaMap::ConstPtr
|
||||
File::fileMetadata() const
|
||||
{
|
||||
return mMeta;
|
||||
return mImpl->mMeta;
|
||||
}
|
||||
|
||||
|
||||
const File::NameMap&
|
||||
File::gridDescriptors() const
|
||||
{
|
||||
return mGridDescriptors;
|
||||
return mImpl->mGridDescriptors;
|
||||
}
|
||||
|
||||
File::NameMap&
|
||||
File::gridDescriptors()
|
||||
{
|
||||
return mGridDescriptors;
|
||||
return mImpl->mGridDescriptors;
|
||||
}
|
||||
|
||||
|
||||
std::istream&
|
||||
File::inputStream() const
|
||||
{
|
||||
if (!mInStream) {
|
||||
OPENVDB_THROW(IoError, mFilename << " is not open for reading");
|
||||
if (!mImpl->mInStream) {
|
||||
OPENVDB_THROW(IoError, filename() << " is not open for reading");
|
||||
}
|
||||
return *mInStream;
|
||||
return *mImpl->mInStream;
|
||||
}
|
||||
|
||||
|
||||
@@ -127,11 +222,11 @@ File::getSize() const
|
||||
|
||||
Index64 result = std::numeric_limits<Index64>::max();
|
||||
|
||||
std::string mesg = "could not get size of file " + mFilename;
|
||||
std::string mesg = "could not get size of file " + filename();
|
||||
|
||||
#ifdef _WIN32
|
||||
// Get the file size by seeking to the end of the file.
|
||||
std::ifstream fstrm(mFilename);
|
||||
std::ifstream fstrm(filename());
|
||||
if (fstrm) {
|
||||
fstrm.seekg(0, fstrm.end);
|
||||
result = static_cast<Index64>(fstrm.tellg());
|
||||
@@ -141,7 +236,7 @@ File::getSize() const
|
||||
#else
|
||||
// Get the file size using the stat() system call.
|
||||
struct stat info;
|
||||
if (0 != ::stat(mFilename.c_str(), &info)) {
|
||||
if (0 != ::stat(filename().c_str(), &info)) {
|
||||
std::string s = getErrorString();
|
||||
if (!s.empty()) mesg += " (" + s + ")";
|
||||
OPENVDB_THROW(IoError, mesg);
|
||||
@@ -157,31 +252,96 @@ File::getSize() const
|
||||
}
|
||||
|
||||
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
Index64
|
||||
File::copyMaxBytes() const
|
||||
{
|
||||
return mImpl->mCopyMaxBytes;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
File::setCopyMaxBytes(Index64 bytes)
|
||||
{
|
||||
mImpl->mCopyMaxBytes = bytes;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
|
||||
|
||||
bool
|
||||
File::isOpen() const
|
||||
{
|
||||
return mIsOpen;
|
||||
return mImpl->mIsOpen;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
File::open()
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
File::open(bool delayLoad, const MappedFile::Notifier& notifier)
|
||||
#else
|
||||
File::open(bool /*delayLoad = true*/)
|
||||
#endif // OPENVDB_USE_DELAYED_LOADING
|
||||
{
|
||||
if (mIsOpen) {
|
||||
OPENVDB_THROW(IoError, mFilename << " is already open");
|
||||
if (isOpen()) {
|
||||
OPENVDB_THROW(IoError, filename() << " is already open");
|
||||
}
|
||||
mInStream.reset();
|
||||
mImpl->mInStream.reset();
|
||||
|
||||
// Open the file using standard I/O (delayed loading has been removed)
|
||||
// Open the file.
|
||||
std::unique_ptr<std::istream> newStream;
|
||||
newStream.reset(new std::ifstream(
|
||||
mFilename.c_str(), std::ios_base::in | std::ios_base::binary));
|
||||
SharedPtr<std::streambuf> newStreamBuf;
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
MappedFile::Ptr newFileMapping;
|
||||
if (!delayLoad || !Archive::isDelayedLoadingEnabled()) {
|
||||
#endif
|
||||
newStream.reset(new std::ifstream(
|
||||
filename().c_str(), std::ios_base::in | std::ios_base::binary));
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
} else {
|
||||
bool isTempFile = false;
|
||||
std::string fname = filename();
|
||||
if (getSize() < copyMaxBytes()) {
|
||||
// If the file is not too large, make a temporary private copy of it
|
||||
// and open the copy instead. The original file can then be modified
|
||||
// or removed without affecting delayed load.
|
||||
try {
|
||||
TempFile tempFile;
|
||||
std::ifstream fstrm(filename().c_str(),
|
||||
std::ios_base::in | std::ios_base::binary);
|
||||
boost::iostreams::copy(fstrm, tempFile);
|
||||
fname = tempFile.filename();
|
||||
isTempFile = true;
|
||||
} catch (std::exception& e) {
|
||||
std::string mesg;
|
||||
if (e.what()) mesg = std::string(" (") + e.what() + ")";
|
||||
OPENVDB_LOG_WARN("failed to create a temporary copy of " << filename()
|
||||
<< " for delayed loading" << mesg
|
||||
<< "; will read directly from " << filename() << " instead");
|
||||
}
|
||||
}
|
||||
|
||||
// While the file is open, its mapping, stream buffer and stream
|
||||
// must all be maintained. Once the file is closed, the buffer and
|
||||
// the stream can be discarded, but the mapping needs to persist
|
||||
// if any grids were lazily loaded.
|
||||
try {
|
||||
newFileMapping.reset(new MappedFile(fname, /*autoDelete=*/isTempFile));
|
||||
newStreamBuf = newFileMapping->createBuffer();
|
||||
newStream.reset(new std::istream(newStreamBuf.get()));
|
||||
} catch (std::exception& e) {
|
||||
std::ostringstream ostr;
|
||||
ostr << "could not open file " << filename();
|
||||
if (e.what() != nullptr) ostr << " (" << e.what() << ")";
|
||||
OPENVDB_THROW(IoError, ostr.str());
|
||||
}
|
||||
}
|
||||
#endif // OPENVDB_USE_DELAYED_LOADING
|
||||
|
||||
if (newStream->fail()) {
|
||||
OPENVDB_THROW(IoError, "could not open file " << mFilename);
|
||||
OPENVDB_THROW(IoError, "could not open file " << filename());
|
||||
}
|
||||
|
||||
// Read in the file header.
|
||||
@@ -191,67 +351,63 @@ File::open()
|
||||
} catch (IoError& e) {
|
||||
if (e.what() && std::string("not a VDB file") == e.what()) {
|
||||
// Rethrow, adding the filename.
|
||||
OPENVDB_THROW(IoError, mFilename << " is not a VDB file");
|
||||
OPENVDB_THROW(IoError, filename() << " is not a VDB file");
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
mInStream.swap(newStream);
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
mImpl->mFileMapping = newFileMapping;
|
||||
if (mImpl->mFileMapping) mImpl->mFileMapping->setNotifier(notifier);
|
||||
mImpl->mStreamBuf = newStreamBuf;
|
||||
#endif
|
||||
mImpl->mInStream.swap(newStream);
|
||||
|
||||
// Tag the input stream with the file format and library version numbers
|
||||
// and other metadata.
|
||||
mStreamMetadata.reset(new StreamMetadata);
|
||||
mStreamMetadata->setSeekable(true);
|
||||
io::setStreamMetadataPtr(inputStream(), mStreamMetadata, /*transfer=*/false);
|
||||
mImpl->mStreamMetadata.reset(new StreamMetadata);
|
||||
mImpl->mStreamMetadata->setSeekable(true);
|
||||
io::setStreamMetadataPtr(inputStream(), mImpl->mStreamMetadata, /*transfer=*/false);
|
||||
Archive::setFormatVersion(inputStream());
|
||||
Archive::setLibraryVersion(inputStream());
|
||||
Archive::setDataCompression(inputStream());
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
io::setMappedFilePtr(inputStream(), mImpl->mFileMapping);
|
||||
#endif
|
||||
|
||||
// Read in the VDB metadata.
|
||||
mMeta = MetaMap::Ptr(new MetaMap);
|
||||
mMeta->readMeta(inputStream());
|
||||
mImpl->mMeta = MetaMap::Ptr(new MetaMap);
|
||||
mImpl->mMeta->readMeta(inputStream());
|
||||
|
||||
if (!inputHasGridOffsets()) {
|
||||
OPENVDB_LOG_DEBUG_RUNTIME("file " << mFilename << " does not support partial reading");
|
||||
OPENVDB_LOG_DEBUG_RUNTIME("file " << filename() << " does not support partial reading");
|
||||
|
||||
mGrids.reset(new GridPtrVec);
|
||||
mNamedGrids.clear();
|
||||
mImpl->mGrids.reset(new GridPtrVec);
|
||||
mImpl->mNamedGrids.clear();
|
||||
|
||||
// Stream in the entire contents of the file and append all grids to mGrids.
|
||||
const int32_t gridCount = readGridCount(inputStream());
|
||||
for (int32_t i = 0; i < gridCount; ++i) {
|
||||
GridDescriptor gd;
|
||||
gd.readHeader(inputStream());
|
||||
gd.readStreamPos(inputStream());
|
||||
gd.read(inputStream());
|
||||
|
||||
GridBase::Ptr grid = Archive::readGrid(gd, inputStream(), io::ReadOptions{});
|
||||
GridBase::Ptr grid = createGrid(gd);
|
||||
Archive::readGrid(grid, gd, inputStream());
|
||||
|
||||
mGridDescriptors.insert(std::make_pair(gd.gridName(), gd));
|
||||
mGrids->push_back(grid);
|
||||
mNamedGrids[gd.uniqueName()] = grid;
|
||||
gridDescriptors().insert(std::make_pair(gd.gridName(), gd));
|
||||
mImpl->mGrids->push_back(grid);
|
||||
mImpl->mNamedGrids[gd.uniqueName()] = grid;
|
||||
}
|
||||
// Connect instances (grids that share trees with other grids).
|
||||
for (NameMapCIter it = mGridDescriptors.begin(); it != mGridDescriptors.end(); ++it) {
|
||||
Archive::connectInstance(it->second, mNamedGrids);
|
||||
for (NameMapCIter it = gridDescriptors().begin(); it != gridDescriptors().end(); ++it) {
|
||||
Archive::connectInstance(it->second, mImpl->mNamedGrids);
|
||||
}
|
||||
} else {
|
||||
mGridDescriptors.clear();
|
||||
|
||||
for (int32_t i = 0, N = readGridCount(inputStream()); i < N; ++i) {
|
||||
// Read the grid descriptor.
|
||||
GridDescriptor gd;
|
||||
gd.readHeader(inputStream());
|
||||
gd.readStreamPos(inputStream());
|
||||
|
||||
// Add the descriptor to the dictionary.
|
||||
mGridDescriptors.insert(std::make_pair(gd.gridName(), gd));
|
||||
|
||||
// Skip forward to the next descriptor.
|
||||
gd.seekToEnd(inputStream());
|
||||
}
|
||||
// Read in just the grid descriptors.
|
||||
readGridDescriptors(inputStream());
|
||||
}
|
||||
|
||||
mIsOpen = true;
|
||||
mImpl->mIsOpen = true;
|
||||
return newFile; // true if file is not identical to opened file
|
||||
}
|
||||
|
||||
@@ -260,14 +416,18 @@ void
|
||||
File::close()
|
||||
{
|
||||
// Reset all data.
|
||||
mMeta.reset();
|
||||
mGridDescriptors.clear();
|
||||
mGrids.reset();
|
||||
mNamedGrids.clear();
|
||||
mInStream.reset();
|
||||
mStreamMetadata.reset();
|
||||
mImpl->mMeta.reset();
|
||||
mImpl->mGridDescriptors.clear();
|
||||
mImpl->mGrids.reset();
|
||||
mImpl->mNamedGrids.clear();
|
||||
mImpl->mInStream.reset();
|
||||
mImpl->mStreamMetadata.reset();
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
mImpl->mStreamBuf.reset();
|
||||
mImpl->mFileMapping.reset();
|
||||
#endif
|
||||
|
||||
mIsOpen = false;
|
||||
mImpl->mIsOpen = false;
|
||||
setInputHasGridOffsets(true);
|
||||
}
|
||||
|
||||
@@ -278,54 +438,52 @@ File::close()
|
||||
bool
|
||||
File::hasGrid(const Name& name) const
|
||||
{
|
||||
if (!mIsOpen) {
|
||||
OPENVDB_THROW(IoError, mFilename << " is not open for reading");
|
||||
if (!isOpen()) {
|
||||
OPENVDB_THROW(IoError, filename() << " is not open for reading");
|
||||
}
|
||||
return (findDescriptor(name) != mGridDescriptors.end());
|
||||
return (findDescriptor(name) != gridDescriptors().end());
|
||||
}
|
||||
|
||||
|
||||
MetaMap::Ptr
|
||||
File::getMetadata() const
|
||||
{
|
||||
if (!mIsOpen) {
|
||||
OPENVDB_THROW(IoError, mFilename << " is not open for reading");
|
||||
if (!isOpen()) {
|
||||
OPENVDB_THROW(IoError, filename() << " is not open for reading");
|
||||
}
|
||||
// Return a deep copy of the file-level metadata, which was read
|
||||
// when the file was opened.
|
||||
return MetaMap::Ptr(new MetaMap(*mMeta));
|
||||
return MetaMap::Ptr(new MetaMap(*mImpl->mMeta));
|
||||
}
|
||||
|
||||
|
||||
GridPtrVecPtr
|
||||
File::getGrids(const io::ReadOptions& readOptions) const
|
||||
File::getGrids() const
|
||||
{
|
||||
if (!mIsOpen) {
|
||||
OPENVDB_THROW(IoError, mFilename << " is not open for reading");
|
||||
if (!isOpen()) {
|
||||
OPENVDB_THROW(IoError, filename() << " is not open for reading");
|
||||
}
|
||||
|
||||
GridPtrVecPtr ret;
|
||||
if (!inputHasGridOffsets()) {
|
||||
// If the input file doesn't have grid offsets, then all of the grids
|
||||
// have already been streamed in and stored in mGrids.
|
||||
ret = mGrids;
|
||||
ret = mImpl->mGrids;
|
||||
} else {
|
||||
ret.reset(new GridPtrVec);
|
||||
|
||||
Archive::NamedGridMap namedGrids;
|
||||
|
||||
// Read all grids represented by the GridDescriptors.
|
||||
for (NameMapCIter i = mGridDescriptors.begin(), e = mGridDescriptors.end(); i != e; ++i) {
|
||||
for (NameMapCIter i = gridDescriptors().begin(), e = gridDescriptors().end(); i != e; ++i) {
|
||||
const GridDescriptor& gd = i->second;
|
||||
// Seek to the grid in the file.
|
||||
gd.seekToGrid(inputStream());
|
||||
GridBase::Ptr grid = Archive::readGrid(gd, inputStream(), readOptions, mReadDiagnostics);
|
||||
GridBase::Ptr grid = readGrid(gd);
|
||||
ret->push_back(grid);
|
||||
namedGrids[gd.uniqueName()] = grid;
|
||||
}
|
||||
|
||||
// Connect instances (grids that share trees with other grids).
|
||||
for (NameMapCIter i = mGridDescriptors.begin(), e = mGridDescriptors.end(); i != e; ++i) {
|
||||
for (NameMapCIter i = gridDescriptors().begin(), e = gridDescriptors().end(); i != e; ++i) {
|
||||
Archive::connectInstance(i->second, namedGrids);
|
||||
}
|
||||
}
|
||||
@@ -345,11 +503,11 @@ File::retrieveCachedGrid(const Name& name) const
|
||||
|
||||
// Search by unique name.
|
||||
Archive::NamedGridMap::const_iterator it =
|
||||
mNamedGrids.find(GridDescriptor::stringAsUniqueName(name));
|
||||
mImpl->mNamedGrids.find(GridDescriptor::stringAsUniqueName(name));
|
||||
// If not found, search by grid name.
|
||||
if (it == mNamedGrids.end()) it = mNamedGrids.find(name);
|
||||
if (it == mNamedGrids.end()) {
|
||||
OPENVDB_THROW(KeyError, mFilename << " has no grid named \"" << name << "\"");
|
||||
if (it == mImpl->mNamedGrids.end()) it = mImpl->mNamedGrids.find(name);
|
||||
if (it == mImpl->mNamedGrids.end()) {
|
||||
OPENVDB_THROW(KeyError, filename() << " has no grid named \"" << name << "\"");
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
@@ -361,8 +519,8 @@ File::retrieveCachedGrid(const Name& name) const
|
||||
GridPtrVecPtr
|
||||
File::readAllGridMetadata()
|
||||
{
|
||||
if (!mIsOpen) {
|
||||
OPENVDB_THROW(IoError, mFilename << " is not open for reading");
|
||||
if (!isOpen()) {
|
||||
OPENVDB_THROW(IoError, filename() << " is not open for reading");
|
||||
}
|
||||
|
||||
if (fileVersion() < OPENVDB_FILE_VERSION_FLOAT_FRUSTUM_BBOX) {
|
||||
@@ -375,22 +533,18 @@ File::readAllGridMetadata()
|
||||
if (!inputHasGridOffsets()) {
|
||||
// If the input file doesn't have grid offsets, then all of the grids
|
||||
// have already been streamed in and stored in mGrids.
|
||||
for (size_t i = 0, N = mGrids->size(); i < N; ++i) {
|
||||
for (size_t i = 0, N = mImpl->mGrids->size(); i < N; ++i) {
|
||||
// Return copies of the grids, but with empty trees.
|
||||
ret->push_back((*mGrids)[i]->copyGridWithNewTree());
|
||||
ret->push_back((*mImpl->mGrids)[i]->copyGridWithNewTree());
|
||||
}
|
||||
} else {
|
||||
// Read just the metadata and transforms for all grids.
|
||||
for (NameMapCIter i = mGridDescriptors.begin(), e = mGridDescriptors.end(); i != e; ++i) {
|
||||
for (NameMapCIter i = gridDescriptors().begin(), e = gridDescriptors().end(); i != e; ++i) {
|
||||
const GridDescriptor& gd = i->second;
|
||||
// Seek to the grid in the file.
|
||||
gd.seekToGrid(inputStream());
|
||||
io::ReadOptions readOptions;
|
||||
readOptions.readMode = io::ReadMode::TopologyOnly;
|
||||
GridBase::ConstPtr grid = Archive::readGrid(gd, inputStream(), readOptions);
|
||||
GridBase::ConstPtr grid = readGridPartial(gd, /*readTopology=*/false);
|
||||
// Return copies of the grids, but with empty trees.
|
||||
// (As of 0.98.0, at least, it would suffice to just const cast
|
||||
// the grid pointers returned by readGrid(partial=true), but shallow
|
||||
// the grid pointers returned by readGridPartial(), but shallow
|
||||
// copying the grids helps to ensure future compatibility.)
|
||||
ret->push_back(grid->copyGridWithNewTree());
|
||||
}
|
||||
@@ -402,8 +556,8 @@ File::readAllGridMetadata()
|
||||
GridBase::Ptr
|
||||
File::readGridMetadata(const Name& name)
|
||||
{
|
||||
if (!mIsOpen) {
|
||||
OPENVDB_THROW(IoError, mFilename << " is not open for reading.");
|
||||
if (!isOpen()) {
|
||||
OPENVDB_THROW(IoError, filename() << " is not open for reading.");
|
||||
}
|
||||
|
||||
if (fileVersion() < OPENVDB_FILE_VERSION_FLOAT_FRUSTUM_BBOX) {
|
||||
@@ -418,16 +572,13 @@ File::readGridMetadata(const Name& name)
|
||||
ret = readGrid(name);
|
||||
} else {
|
||||
NameMapCIter it = findDescriptor(name);
|
||||
if (it == mGridDescriptors.end()) {
|
||||
OPENVDB_THROW(KeyError, mFilename << " has no grid named \"" << name << "\"");
|
||||
if (it == gridDescriptors().end()) {
|
||||
OPENVDB_THROW(KeyError, filename() << " has no grid named \"" << name << "\"");
|
||||
}
|
||||
|
||||
// Seek to and read in the grid from the file.
|
||||
const GridDescriptor& gd = it->second;
|
||||
gd.seekToGrid(inputStream());
|
||||
io::ReadOptions readOptions;
|
||||
readOptions.readMode = io::ReadMode::TopologyOnly;
|
||||
ret = Archive::readGrid(gd, inputStream(), readOptions);
|
||||
ret = readGridPartial(gd, /*readTopology=*/false);
|
||||
}
|
||||
return ret->copyGridWithNewTree();
|
||||
}
|
||||
@@ -437,28 +588,33 @@ File::readGridMetadata(const Name& name)
|
||||
|
||||
|
||||
GridBase::Ptr
|
||||
File::readGrid(const Name& name, const BBoxd& bbox)
|
||||
File::readGrid(const Name& name)
|
||||
{
|
||||
io::ReadOptions readOptions;
|
||||
readOptions.clipBBox = bbox;
|
||||
return readGrid(name, readOptions);
|
||||
return readGridByName(name, BBoxd());
|
||||
}
|
||||
|
||||
|
||||
GridBase::Ptr
|
||||
File::readGrid(const Name& name, const io::ReadOptions& readOptions)
|
||||
File::readGrid(const Name& name, const BBoxd& bbox)
|
||||
{
|
||||
if (!mIsOpen) {
|
||||
OPENVDB_THROW(IoError, mFilename << " is not open for reading.");
|
||||
return readGridByName(name, bbox);
|
||||
}
|
||||
|
||||
|
||||
GridBase::Ptr
|
||||
File::readGridByName(const Name& name, const BBoxd& bbox)
|
||||
{
|
||||
if (!isOpen()) {
|
||||
OPENVDB_THROW(IoError, filename() << " is not open for reading.");
|
||||
}
|
||||
|
||||
const bool clip = bbox.isSorted();
|
||||
|
||||
// If a grid with the given name was already read and cached
|
||||
// (along with the entire contents of the file, because the file
|
||||
// doesn't support random access), retrieve and return it.
|
||||
GridBase::Ptr grid = retrieveCachedGrid(name);
|
||||
if (grid) {
|
||||
const auto& bbox = readOptions.clipBBox;
|
||||
const bool clip = bbox.isSorted();
|
||||
if (clip) {
|
||||
grid = grid->deepCopyGrid();
|
||||
grid->clipGrid(bbox);
|
||||
@@ -467,33 +623,32 @@ File::readGrid(const Name& name, const io::ReadOptions& readOptions)
|
||||
}
|
||||
|
||||
NameMapCIter it = findDescriptor(name);
|
||||
if (it == mGridDescriptors.end()) {
|
||||
OPENVDB_THROW(KeyError, mFilename << " has no grid named \"" << name << "\"");
|
||||
if (it == gridDescriptors().end()) {
|
||||
OPENVDB_THROW(KeyError, filename() << " has no grid named \"" << name << "\"");
|
||||
}
|
||||
|
||||
// Seek to and read in the grid from the file.
|
||||
const GridDescriptor& gd = it->second;
|
||||
// This method should not be called for files that don't contain grid offsets.
|
||||
OPENVDB_ASSERT(inputHasGridOffsets());
|
||||
// Seek to the grid in the file.
|
||||
gd.seekToGrid(inputStream());
|
||||
grid = Archive::readGrid(gd, inputStream(), readOptions, mReadDiagnostics);
|
||||
grid = (clip ? readGrid(gd, bbox) : readGrid(gd));
|
||||
|
||||
if (gd.isInstance()) {
|
||||
/// @todo Refactor to share code with Archive::connectInstance()?
|
||||
NameMapCIter parentIt =
|
||||
findDescriptor(GridDescriptor::nameAsString(gd.instanceParentName()));
|
||||
if (parentIt == mGridDescriptors.end()) {
|
||||
if (parentIt == gridDescriptors().end()) {
|
||||
OPENVDB_THROW(KeyError, "missing instance parent \""
|
||||
<< GridDescriptor::nameAsString(gd.instanceParentName())
|
||||
<< "\" for grid " << GridDescriptor::nameAsString(gd.uniqueName())
|
||||
<< " in file " << mFilename);
|
||||
<< " in file " << filename());
|
||||
}
|
||||
|
||||
GridBase::Ptr parent;
|
||||
OPENVDB_ASSERT(inputHasGridOffsets());
|
||||
parentIt->second.seekToGrid(inputStream());
|
||||
parent = Archive::readGrid(parentIt->second, inputStream(), readOptions, mReadDiagnostics);
|
||||
if (clip) {
|
||||
const CoordBBox indexBBox = grid->constTransform().worldToIndexNodeCentered(bbox);
|
||||
parent = readGrid(parentIt->second, indexBBox);
|
||||
} else {
|
||||
parent = readGrid(parentIt->second);
|
||||
}
|
||||
if (parent) grid->setTree(parent->baseTreePtr());
|
||||
}
|
||||
return grid;
|
||||
@@ -504,29 +659,53 @@ File::readGrid(const Name& name, const io::ReadOptions& readOptions)
|
||||
|
||||
|
||||
void
|
||||
File::writeGrids(const GridCPtrVec& grids, const MetaMap& meta, const io::WriteOptions& writeOptions) const
|
||||
File::writeGrids(const GridCPtrVec& grids, const MetaMap& meta) const
|
||||
{
|
||||
if (mIsOpen) {
|
||||
if (isOpen()) {
|
||||
OPENVDB_THROW(IoError,
|
||||
mFilename << " cannot be written because it is open for reading");
|
||||
filename() << " cannot be written because it is open for reading");
|
||||
}
|
||||
|
||||
// Create a file stream and write it out.
|
||||
std::ofstream file;
|
||||
file.open(mFilename.c_str(),
|
||||
file.open(filename().c_str(),
|
||||
std::ios_base::out | std::ios_base::binary | std::ios_base::trunc);
|
||||
|
||||
if (file.fail()) {
|
||||
OPENVDB_THROW(IoError, "could not open " << mFilename << " for writing");
|
||||
OPENVDB_THROW(IoError, "could not open " << filename() << " for writing");
|
||||
}
|
||||
|
||||
// Write out the vdb.
|
||||
Archive::write(file, grids, /*seekable=*/true, meta, writeOptions);
|
||||
Archive::write(file, grids, /*seekable=*/true, meta);
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
|
||||
|
||||
void
|
||||
File::readGridDescriptors(std::istream& is)
|
||||
{
|
||||
// This method should not be called for files that don't contain grid offsets.
|
||||
OPENVDB_ASSERT(inputHasGridOffsets());
|
||||
|
||||
gridDescriptors().clear();
|
||||
|
||||
for (int32_t i = 0, N = readGridCount(is); i < N; ++i) {
|
||||
// Read the grid descriptor.
|
||||
GridDescriptor gd;
|
||||
gd.read(is);
|
||||
|
||||
// Add the descriptor to the dictionary.
|
||||
gridDescriptors().insert(std::make_pair(gd.gridName(), gd));
|
||||
|
||||
// Skip forward to the next descriptor.
|
||||
gd.seekToEnd(is);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
|
||||
@@ -537,20 +716,20 @@ File::findDescriptor(const Name& name) const
|
||||
const Name uniqueName = GridDescriptor::stringAsUniqueName(name);
|
||||
|
||||
// Find all descriptors with the given grid name.
|
||||
std::pair<NameMapCIter, NameMapCIter> range = mGridDescriptors.equal_range(name);
|
||||
std::pair<NameMapCIter, NameMapCIter> range = gridDescriptors().equal_range(name);
|
||||
|
||||
if (range.first == range.second) {
|
||||
// If no descriptors were found with the given grid name, the name might have
|
||||
// a suffix ("name[N]"). In that case, remove the "[N]" suffix and search again.
|
||||
range = mGridDescriptors.equal_range(GridDescriptor::stripSuffix(uniqueName));
|
||||
range = gridDescriptors().equal_range(GridDescriptor::stripSuffix(uniqueName));
|
||||
}
|
||||
|
||||
const size_t count = size_t(std::distance(range.first, range.second));
|
||||
if (count > 1 && name == uniqueName) {
|
||||
OPENVDB_LOG_WARN(mFilename << " has more than one grid named \"" << name << "\"");
|
||||
OPENVDB_LOG_WARN(filename() << " has more than one grid named \"" << name << "\"");
|
||||
}
|
||||
|
||||
NameMapCIter ret = mGridDescriptors.end();
|
||||
NameMapCIter ret = gridDescriptors().end();
|
||||
|
||||
if (count > 0) {
|
||||
if (name == uniqueName) {
|
||||
@@ -576,23 +755,109 @@ File::findDescriptor(const Name& name) const
|
||||
////////////////////////////////////////
|
||||
|
||||
|
||||
GridBase::Ptr
|
||||
File::createGrid(const GridDescriptor& gd) const
|
||||
{
|
||||
// Create the grid.
|
||||
if (!GridBase::isRegistered(gd.gridType())) {
|
||||
OPENVDB_THROW(KeyError, "Cannot read grid "
|
||||
<< GridDescriptor::nameAsString(gd.uniqueName())
|
||||
<< " from " << filename() << ": grid type "
|
||||
<< gd.gridType() << " is not registered");
|
||||
}
|
||||
|
||||
GridBase::Ptr grid = GridBase::createGrid(gd.gridType());
|
||||
if (grid) grid->setSaveFloatAsHalf(gd.saveFloatAsHalf());
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
|
||||
GridBase::ConstPtr
|
||||
File::readGridPartial(const GridDescriptor& gd, bool readTopology) const
|
||||
{
|
||||
// This method should not be called for files that don't contain grid offsets.
|
||||
OPENVDB_ASSERT(inputHasGridOffsets());
|
||||
|
||||
GridBase::Ptr grid = createGrid(gd);
|
||||
|
||||
// Seek to grid.
|
||||
gd.seekToGrid(inputStream());
|
||||
|
||||
// Read the grid partially.
|
||||
readGridPartial(grid, inputStream(), gd.isInstance(), readTopology);
|
||||
|
||||
// Promote to a const grid.
|
||||
GridBase::ConstPtr constGrid = grid;
|
||||
|
||||
return constGrid;
|
||||
}
|
||||
|
||||
|
||||
GridBase::Ptr
|
||||
File::readGrid(const GridDescriptor& gd) const
|
||||
{
|
||||
return Impl::readGrid(*this, gd, Impl::NoBBox());
|
||||
}
|
||||
|
||||
|
||||
GridBase::Ptr
|
||||
File::readGrid(const GridDescriptor& gd, const BBoxd& bbox) const
|
||||
{
|
||||
return Impl::readGrid(*this, gd, bbox);
|
||||
}
|
||||
|
||||
|
||||
GridBase::Ptr
|
||||
File::readGrid(const GridDescriptor& gd, const CoordBBox& bbox) const
|
||||
{
|
||||
return Impl::readGrid(*this, gd, bbox);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
File::readGridPartial(GridBase::Ptr grid, std::istream& is,
|
||||
bool isInstance, bool readTopology) const
|
||||
{
|
||||
// This method should not be called for files that don't contain grid offsets.
|
||||
OPENVDB_ASSERT(inputHasGridOffsets());
|
||||
|
||||
// This code needs to stay in sync with io::Archive::readGrid(), in terms of
|
||||
// the order of operations.
|
||||
readGridCompression(is);
|
||||
grid->readMeta(is);
|
||||
|
||||
// drop DelayedLoadMetadata from the grid as it is only useful for IO
|
||||
if ((*grid)[GridBase::META_FILE_DELAYED_LOAD]) {
|
||||
grid->removeMeta(GridBase::META_FILE_DELAYED_LOAD);
|
||||
}
|
||||
|
||||
grid->readTransform(is);
|
||||
if (!isInstance && readTopology) {
|
||||
grid->readTopology(is);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
|
||||
|
||||
File::NameIterator
|
||||
File::beginName() const
|
||||
{
|
||||
if (!mIsOpen) {
|
||||
OPENVDB_THROW(IoError, mFilename << " is not open for reading");
|
||||
if (!isOpen()) {
|
||||
OPENVDB_THROW(IoError, filename() << " is not open for reading");
|
||||
}
|
||||
return File::NameIterator(mGridDescriptors.begin());
|
||||
return File::NameIterator(gridDescriptors().begin());
|
||||
}
|
||||
|
||||
|
||||
File::NameIterator
|
||||
File::endName() const
|
||||
{
|
||||
return File::NameIterator(mGridDescriptors.end());
|
||||
return File::NameIterator(gridDescriptors().end());
|
||||
}
|
||||
|
||||
|
||||
} // namespace io
|
||||
} // namespace OPENVDB_VERSION_NAME
|
||||
} // namespace openvdb
|
||||
|
||||
+66
-41
@@ -34,7 +34,7 @@ public:
|
||||
using NameMapCIter = NameMap::const_iterator;
|
||||
|
||||
explicit File(const std::string& filename);
|
||||
~File() override { }
|
||||
~File() override;
|
||||
|
||||
/// @brief Copy constructor
|
||||
/// @details The copy will be closed and will not reference the same
|
||||
@@ -54,16 +54,20 @@ public:
|
||||
/// @details The file does not necessarily exist on disk yet.
|
||||
const std::string& filename() const;
|
||||
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
/// @brief Open the file, read the file header and the file-level metadata,
|
||||
/// and populate the grid descriptors, but do not load any grids into memory.
|
||||
/// @details If @a delayLoad is true, map the file into memory and enable delayed loading
|
||||
/// of grids, and if a notifier is provided, call it when the file gets unmapped.
|
||||
/// @note Define the environment variable @c OPENVDB_DISABLE_DELAYED_LOAD to disable
|
||||
/// delayed loading unconditionally.
|
||||
/// @throw IoError if the file is not a valid VDB file.
|
||||
/// @return @c true if the file's UUID has changed since it was last read.
|
||||
bool open();
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("Use File::open() instead.This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
bool open(bool /*delayLoad*/) { return open(); }
|
||||
OPENVDB_DEPRECATED_MESSAGE("Use File::open() instead. This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
bool open(bool /*delayLoad*/, const MappedFile::Notifier& /*notifier*/) { return open(); }
|
||||
/// @see setCopyMaxBytes
|
||||
bool open(bool delayLoad = true, const MappedFile::Notifier& = MappedFile::Notifier());
|
||||
#else
|
||||
bool open(bool /*delayLoad*/ = false);
|
||||
#endif
|
||||
|
||||
/// Return @c true if the file has been opened for reading.
|
||||
bool isOpen() const;
|
||||
@@ -75,10 +79,23 @@ public:
|
||||
/// @throw IoError if the file size cannot be determined.
|
||||
Index64 getSize() const;
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("Always returns 0. This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
Index64 copyMaxBytes() const { return 0; }
|
||||
OPENVDB_DEPRECATED_MESSAGE("This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
void setCopyMaxBytes(Index64 /*bytes*/) { }
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
/// @brief Return the size in bytes above which this file will not be
|
||||
/// automatically copied during delayed loading.
|
||||
Index64 copyMaxBytes() const;
|
||||
/// @brief If this file is opened with delayed loading enabled, make a private copy
|
||||
/// of the file if its size in bytes is less than the specified value.
|
||||
/// @details Making a private copy ensures that the file can't change on disk
|
||||
/// before it has been fully read.
|
||||
/// @warning If the file is larger than this size, it is the user's responsibility
|
||||
/// to ensure that it does not change on disk before it has been fully read.
|
||||
/// Undefined behavior and/or a crash might result otherwise.
|
||||
/// @note Copying is enabled by default, but it can be disabled for individual files
|
||||
/// by setting the maximum size to zero bytes. A default size limit can be specified
|
||||
/// by setting the environment variable @c OPENVDB_DELAYED_LOAD_COPY_MAX_BYTES
|
||||
/// to the desired number of bytes.
|
||||
void setCopyMaxBytes(Index64 bytes);
|
||||
#endif
|
||||
|
||||
/// Return @c true if a grid of the given name exists in this file.
|
||||
bool hasGrid(const Name&) const;
|
||||
@@ -87,7 +104,7 @@ public:
|
||||
MetaMap::Ptr getMetadata() const;
|
||||
|
||||
/// Read the entire contents of the file and return a list of grid pointers.
|
||||
GridPtrVecPtr getGrids(const io::ReadOptions& readOptions = io::ReadOptions{}) const;
|
||||
GridPtrVecPtr getGrids() const;
|
||||
|
||||
/// @brief Read just the grid metadata and transforms from the file and return a list
|
||||
/// of pointers to grids that are empty except for their metadata and transforms.
|
||||
@@ -100,8 +117,8 @@ public:
|
||||
/// @throw KeyError if no grid with the given name exists in this file.
|
||||
GridBase::Ptr readGridMetadata(const Name&);
|
||||
|
||||
/// Read an entire grid, including all of its data blocks, using the provided options if given.
|
||||
GridBase::Ptr readGrid(const Name&, const io::ReadOptions& readOptions = io::ReadOptions{});
|
||||
/// Read an entire grid, including all of its data blocks.
|
||||
GridBase::Ptr readGrid(const Name&);
|
||||
/// @brief Read a grid, including its data blocks, but only where it
|
||||
/// intersects the given world-space bounding box.
|
||||
GridBase::Ptr readGrid(const Name&, const BBoxd&);
|
||||
@@ -110,14 +127,12 @@ public:
|
||||
|
||||
/// @brief Write the grids in the given container to the file whose name
|
||||
/// was given in the constructor.
|
||||
void write(const GridCPtrVec&, const MetaMap& = MetaMap(),
|
||||
const io::WriteOptions& = io::WriteOptions{}) const override;
|
||||
void write(const GridCPtrVec&, const MetaMap& = MetaMap()) const override;
|
||||
|
||||
/// @brief Write the grids in the given container to the file whose name
|
||||
/// was given in the constructor.
|
||||
template<typename GridPtrContainerT>
|
||||
void write(const GridPtrContainerT&, const MetaMap& = MetaMap(),
|
||||
const io::WriteOptions& = io::WriteOptions{}) const;
|
||||
void write(const GridPtrContainerT&, const MetaMap& = MetaMap()) const;
|
||||
|
||||
/// A const iterator that iterates over all names in the file. This is only
|
||||
/// valid once the file has been opened.
|
||||
@@ -148,16 +163,42 @@ public:
|
||||
NameIterator endName() const;
|
||||
|
||||
private:
|
||||
/// Read in all grid descriptors that are stored in the given stream.
|
||||
void readGridDescriptors(std::istream&);
|
||||
|
||||
/// @brief Return an iterator to the descriptor for the grid with the given name.
|
||||
/// If the name is non-unique, return an iterator to the first matching descriptor.
|
||||
NameMapCIter findDescriptor(const Name&) const;
|
||||
|
||||
/// Return a newly created, empty grid of the type specified by the given grid descriptor.
|
||||
GridBase::Ptr createGrid(const GridDescriptor&) const;
|
||||
|
||||
/// @brief Read a grid, including its data blocks, but only where it
|
||||
/// intersects the given world-space bounding box.
|
||||
GridBase::Ptr readGridByName(const Name&, const BBoxd&);
|
||||
|
||||
/// Read in and return the partially-populated grid specified by the given grid descriptor.
|
||||
GridBase::ConstPtr readGridPartial(const GridDescriptor&, bool readTopology) const;
|
||||
|
||||
/// Read in and return the grid specified by the given grid descriptor.
|
||||
GridBase::Ptr readGrid(const GridDescriptor&) const;
|
||||
/// Read in and return the region of the grid specified by the given grid descriptor
|
||||
/// that intersects the given world-space bounding box.
|
||||
GridBase::Ptr readGrid(const GridDescriptor&, const BBoxd&) const;
|
||||
/// Read in and return the region of the grid specified by the given grid descriptor
|
||||
/// that intersects the given index-space bounding box.
|
||||
GridBase::Ptr readGrid(const GridDescriptor&, const CoordBBox&) const;
|
||||
|
||||
/// @brief Partially populate the given grid by reading its metadata and transform and,
|
||||
/// if the grid is not an instance, its tree structure, but not the tree's leaf nodes.
|
||||
void readGridPartial(GridBase::Ptr, std::istream&, bool isInstance, bool readTopology) const;
|
||||
|
||||
/// @brief Retrieve a grid from @c mNamedGrids. Return a null pointer
|
||||
/// if @c mNamedGrids was not populated (because this file is random-access).
|
||||
/// @throw KeyError if no grid with the given name exists in this file.
|
||||
GridBase::Ptr retrieveCachedGrid(const Name&) const;
|
||||
|
||||
void writeGrids(const GridCPtrVec&, const MetaMap&, const io::WriteOptions&) const;
|
||||
void writeGrids(const GridCPtrVec&, const MetaMap&) const;
|
||||
|
||||
MetaMap::Ptr fileMetadata();
|
||||
MetaMap::ConstPtr fileMetadata() const;
|
||||
@@ -170,22 +211,8 @@ private:
|
||||
friend class ::TestFile;
|
||||
friend class ::TestStream;
|
||||
|
||||
std::string mFilename;
|
||||
// The file-level metadata
|
||||
MetaMap::Ptr mMeta;
|
||||
// The file stream that is open for reading
|
||||
std::unique_ptr<std::istream> mInStream;
|
||||
// File-level stream metadata (file format, compression, etc.)
|
||||
StreamMetadata::Ptr mStreamMetadata;
|
||||
// Flag indicating if we have read in the global information (header,
|
||||
// metadata, and grid descriptors) for this VDB file
|
||||
bool mIsOpen = false;
|
||||
// Grid descriptors for all grids stored in the file, indexed by grid name
|
||||
NameMap mGridDescriptors;
|
||||
// All grids, indexed by unique name (used only when mHasGridOffsets is false)
|
||||
Archive::NamedGridMap mNamedGrids;
|
||||
// All grids stored in the file (used only when mHasGridOffsets is false)
|
||||
GridPtrVecPtr mGrids;
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> mImpl;
|
||||
};
|
||||
|
||||
|
||||
@@ -193,21 +220,19 @@ private:
|
||||
|
||||
|
||||
inline void
|
||||
File::write(const GridCPtrVec& grids, const MetaMap& meta,
|
||||
const io::WriteOptions& writeOptions) const
|
||||
File::write(const GridCPtrVec& grids, const MetaMap& meta) const
|
||||
{
|
||||
this->writeGrids(grids, meta, writeOptions);
|
||||
this->writeGrids(grids, meta);
|
||||
}
|
||||
|
||||
|
||||
template<typename GridPtrContainerT>
|
||||
inline void
|
||||
File::write(const GridPtrContainerT& container, const MetaMap& meta,
|
||||
const io::WriteOptions& writeOptions) const
|
||||
File::write(const GridPtrContainerT& container, const MetaMap& meta) const
|
||||
{
|
||||
GridCPtrVec grids;
|
||||
std::copy(container.begin(), container.end(), std::back_inserter(grids));
|
||||
this->writeGrids(grids, meta, writeOptions);
|
||||
this->writeGrids(grids, meta);
|
||||
}
|
||||
|
||||
} // namespace io
|
||||
|
||||
@@ -69,8 +69,8 @@ GridDescriptor::writeStreamPos(std::ostream &os) const
|
||||
os.write(reinterpret_cast<const char*>(&mEndPos), sizeof(int64_t));
|
||||
}
|
||||
|
||||
void
|
||||
GridDescriptor::readHeader(std::istream &is)
|
||||
GridBase::Ptr
|
||||
GridDescriptor::read(std::istream &is)
|
||||
{
|
||||
checkFormatVersion(is);
|
||||
|
||||
@@ -86,28 +86,21 @@ GridDescriptor::readHeader(std::istream &is)
|
||||
}
|
||||
|
||||
mInstanceParentName = readString(is);
|
||||
}
|
||||
|
||||
void
|
||||
GridDescriptor::readStreamPos(std::istream &is)
|
||||
{
|
||||
is.read(reinterpret_cast<char*>(&mGridPos), sizeof(int64_t));
|
||||
is.read(reinterpret_cast<char*>(&mBlockPos), sizeof(int64_t));
|
||||
is.read(reinterpret_cast<char*>(&mEndPos), sizeof(int64_t));
|
||||
}
|
||||
|
||||
GridBase::Ptr
|
||||
GridDescriptor::read(std::istream &is)
|
||||
{
|
||||
readHeader(is);
|
||||
readStreamPos(is);
|
||||
|
||||
// Create the grid of the type if it has been registered.
|
||||
if (!GridBase::isRegistered(mGridType)) {
|
||||
OPENVDB_THROW(LookupError, "Cannot read grid." <<
|
||||
" Grid type " << mGridType << " is not registered.");
|
||||
}
|
||||
// else
|
||||
GridBase::Ptr grid = GridBase::createGrid(mGridType);
|
||||
if (grid) grid->setSaveFloatAsHalf(mSaveFloatAsHalf);
|
||||
|
||||
// Read in the offsets.
|
||||
is.read(reinterpret_cast<char*>(&mGridPos), sizeof(int64_t));
|
||||
is.read(reinterpret_cast<char*>(&mBlockPos), sizeof(int64_t));
|
||||
is.read(reinterpret_cast<char*>(&mEndPos), sizeof(int64_t));
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,18 +61,8 @@ public:
|
||||
/// written out separately.
|
||||
void writeStreamPos(std::ostream&) const;
|
||||
|
||||
/// @brief Read this descriptor's header information (all data except for
|
||||
/// stream offsets) from the given stream.
|
||||
void readHeader(std::istream&);
|
||||
|
||||
/// @brief Read stream positions (grid, block, and end offsets) from the
|
||||
/// given stream.
|
||||
void readStreamPos(std::istream&);
|
||||
|
||||
/// @brief Read a grid descriptor from the given stream.
|
||||
/// @return an empty grid of the type specified by the grid descriptor.
|
||||
/// @deprecated Use readHeader() followed by readStreamPos() instead.
|
||||
OPENVDB_DEPRECATED_MESSAGE("Use readHeader() followed by readStreamPos() instead.")
|
||||
GridBase::Ptr read(std::istream&);
|
||||
|
||||
/// @brief Append the number @a n to the given name (separated by an ASCII
|
||||
|
||||
+159
-56
@@ -5,9 +5,14 @@
|
||||
|
||||
#include "File.h" ///< @todo refactor
|
||||
#include "GridDescriptor.h"
|
||||
#include "TempFile.h"
|
||||
#include <openvdb/Exceptions.h>
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
#include <boost/iostreams/copy.hpp>
|
||||
#endif
|
||||
|
||||
#include <cstdio> // for remove()
|
||||
#include <functional> // for std::bind()
|
||||
#include <iostream>
|
||||
@@ -19,62 +24,140 @@ OPENVDB_USE_VERSION_NAMESPACE
|
||||
namespace OPENVDB_VERSION_NAME {
|
||||
namespace io {
|
||||
|
||||
struct Stream::Impl
|
||||
{
|
||||
Impl(): mOutputStream{nullptr} {}
|
||||
Impl(const Impl& other) { *this = other; }
|
||||
Impl& operator=(const Impl& other)
|
||||
{
|
||||
if (&other != this) {
|
||||
mMeta = other.mMeta; ///< @todo deep copy?
|
||||
mGrids = other.mGrids; ///< @todo deep copy?
|
||||
mOutputStream = other.mOutputStream;
|
||||
mFile.reset();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
Stream::Stream(std::istream& is)
|
||||
MetaMap::Ptr mMeta;
|
||||
GridPtrVecPtr mGrids;
|
||||
std::ostream* mOutputStream;
|
||||
std::unique_ptr<File> mFile;
|
||||
};
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
|
||||
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
|
||||
namespace {
|
||||
|
||||
/// @todo Use MappedFile auto-deletion instead.
|
||||
void
|
||||
removeTempFile(const std::string expectedFilename, const std::string& filename)
|
||||
{
|
||||
if (filename == expectedFilename) {
|
||||
if (0 != std::remove(filename.c_str())) {
|
||||
std::string mesg = getErrorString();
|
||||
if (!mesg.empty()) mesg = " (" + mesg + ")";
|
||||
OPENVDB_LOG_WARN("failed to remove temporary file " << filename << mesg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif // OPENVDB_USE_DELAYED_LOADING
|
||||
|
||||
|
||||
Stream::Stream(std::istream& is, bool delayLoad): mImpl(new Impl)
|
||||
{
|
||||
if (!is) return;
|
||||
|
||||
// Delayed loading has been removed - always read directly from the stream
|
||||
readHeader(is);
|
||||
(void) delayLoad;
|
||||
|
||||
// Tag the input stream with the library and file format version numbers
|
||||
// and the compression options specified in the header.
|
||||
StreamMetadata::Ptr streamMetadata(new StreamMetadata);
|
||||
io::setStreamMetadataPtr(is, streamMetadata, /*transfer=*/false);
|
||||
io::setVersion(is, libraryVersion(), fileVersion());
|
||||
io::setDataCompression(is, compression());
|
||||
|
||||
// Read in the VDB metadata.
|
||||
mMeta.reset(new MetaMap);
|
||||
mMeta->readMeta(is);
|
||||
|
||||
// Read in the number of grids.
|
||||
const int32_t gridCount = readGridCount(is);
|
||||
|
||||
// Read in all grids and insert them into mGrids.
|
||||
mGrids.reset(new GridPtrVec);
|
||||
std::vector<GridDescriptor> descriptors;
|
||||
descriptors.reserve(gridCount);
|
||||
Archive::NamedGridMap namedGrids;
|
||||
for (int32_t i = 0; i < gridCount; ++i) {
|
||||
GridDescriptor gd;
|
||||
gd.readHeader(is);
|
||||
gd.readStreamPos(is);
|
||||
descriptors.push_back(gd);
|
||||
GridBase::Ptr grid = Archive::readGrid(gd, is, io::ReadOptions{});
|
||||
mGrids->push_back(grid);
|
||||
namedGrids[gd.uniqueName()] = grid;
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
if (delayLoad && Archive::isDelayedLoadingEnabled()) {
|
||||
// Copy the contents of the stream to a temporary private file
|
||||
// and open the file instead.
|
||||
std::unique_ptr<TempFile> tempFile;
|
||||
try {
|
||||
tempFile.reset(new TempFile);
|
||||
} catch (std::exception& e) {
|
||||
std::string mesg;
|
||||
if (e.what()) mesg = std::string(" (") + e.what() + ")";
|
||||
OPENVDB_LOG_WARN("failed to create a temporary file for delayed loading" << mesg
|
||||
<< "; will read directly from the input stream instead");
|
||||
}
|
||||
if (tempFile) {
|
||||
boost::iostreams::copy(is, *tempFile);
|
||||
const std::string& filename = tempFile->filename();
|
||||
mImpl->mFile.reset(new File(filename));
|
||||
mImpl->mFile->setCopyMaxBytes(0); // don't make a copy of the temporary file
|
||||
/// @todo Need to pass auto-deletion flag to MappedFile.
|
||||
mImpl->mFile->open(delayLoad,
|
||||
std::bind(&removeTempFile, filename, std::placeholders::_1));
|
||||
}
|
||||
}
|
||||
#endif // OPENVDB_USE_DELAYED_LOADING
|
||||
|
||||
// Connect instances (grids that share trees with other grids).
|
||||
for (size_t i = 0, N = descriptors.size(); i < N; ++i) {
|
||||
Archive::connectInstance(descriptors[i], namedGrids);
|
||||
if (!mImpl->mFile) {
|
||||
readHeader(is);
|
||||
|
||||
// Tag the input stream with the library and file format version numbers
|
||||
// and the compression options specified in the header.
|
||||
StreamMetadata::Ptr streamMetadata(new StreamMetadata);
|
||||
io::setStreamMetadataPtr(is, streamMetadata, /*transfer=*/false);
|
||||
io::setVersion(is, libraryVersion(), fileVersion());
|
||||
io::setDataCompression(is, compression());
|
||||
|
||||
// Read in the VDB metadata.
|
||||
mImpl->mMeta.reset(new MetaMap);
|
||||
mImpl->mMeta->readMeta(is);
|
||||
|
||||
// Read in the number of grids.
|
||||
const int32_t gridCount = readGridCount(is);
|
||||
|
||||
// Read in all grids and insert them into mGrids.
|
||||
mImpl->mGrids.reset(new GridPtrVec);
|
||||
std::vector<GridDescriptor> descriptors;
|
||||
descriptors.reserve(gridCount);
|
||||
Archive::NamedGridMap namedGrids;
|
||||
for (int32_t i = 0; i < gridCount; ++i) {
|
||||
GridDescriptor gd;
|
||||
gd.read(is);
|
||||
descriptors.push_back(gd);
|
||||
GridBase::Ptr grid = readGrid(gd, is);
|
||||
mImpl->mGrids->push_back(grid);
|
||||
namedGrids[gd.uniqueName()] = grid;
|
||||
}
|
||||
|
||||
// Connect instances (grids that share trees with other grids).
|
||||
for (size_t i = 0, N = descriptors.size(); i < N; ++i) {
|
||||
Archive::connectInstance(descriptors[i], namedGrids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Stream::Stream(std::ostream& os)
|
||||
: Archive()
|
||||
, mOutputStream(&os)
|
||||
Stream::Stream(): mImpl(new Impl)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
Stream::Stream(const Stream& other)
|
||||
: Archive(other)
|
||||
, mMeta(other.mMeta)
|
||||
, mGrids(other.mGrids)
|
||||
, mOutputStream(other.mOutputStream)
|
||||
Stream::Stream(std::ostream& os): mImpl(new Impl)
|
||||
{
|
||||
mImpl->mOutputStream = &os;
|
||||
}
|
||||
|
||||
|
||||
Stream::~Stream()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
Stream::Stream(const Stream& other): Archive(other), mImpl(new Impl(*other.mImpl))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -83,9 +166,7 @@ Stream&
|
||||
Stream::operator=(const Stream& other)
|
||||
{
|
||||
if (&other != this) {
|
||||
mMeta = other.mMeta;
|
||||
mGrids = other.mGrids;
|
||||
mOutputStream = other.mOutputStream;
|
||||
mImpl.reset(new Impl(*other.mImpl));
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
@@ -101,22 +182,39 @@ Stream::copy() const
|
||||
////////////////////////////////////////
|
||||
|
||||
|
||||
void
|
||||
Stream::write(const GridCPtrVec& grids, const MetaMap& metadata,
|
||||
const io::WriteOptions& writeOptions) const
|
||||
GridBase::Ptr
|
||||
Stream::readGrid(const GridDescriptor& gd, std::istream& is) const
|
||||
{
|
||||
if (mOutputStream == nullptr) {
|
||||
OPENVDB_THROW(ValueError, "no output stream was specified");
|
||||
GridBase::Ptr grid;
|
||||
|
||||
if (!GridBase::isRegistered(gd.gridType())) {
|
||||
OPENVDB_THROW(TypeError, "can't read grid \""
|
||||
<< GridDescriptor::nameAsString(gd.uniqueName()) <<
|
||||
"\" from input stream because grid type " << gd.gridType() << " is unknown");
|
||||
} else {
|
||||
grid = GridBase::createGrid(gd.gridType());
|
||||
if (grid) grid->setSaveFloatAsHalf(gd.saveFloatAsHalf());
|
||||
|
||||
Archive::readGrid(grid, gd, is);
|
||||
}
|
||||
this->writeGrids(*mOutputStream, grids, metadata, writeOptions);
|
||||
return grid;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Stream::writeGrids(std::ostream& os, const GridCPtrVec& grids, const MetaMap& metadata,
|
||||
const io::WriteOptions& writeOptions) const
|
||||
Stream::write(const GridCPtrVec& grids, const MetaMap& metadata) const
|
||||
{
|
||||
Archive::write(os, grids, /*seekable=*/false, metadata, writeOptions);
|
||||
if (mImpl->mOutputStream == nullptr) {
|
||||
OPENVDB_THROW(ValueError, "no output stream was specified");
|
||||
}
|
||||
this->writeGrids(*mImpl->mOutputStream, grids, metadata);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Stream::writeGrids(std::ostream& os, const GridCPtrVec& grids, const MetaMap& metadata) const
|
||||
{
|
||||
Archive::write(os, grids, /*seekable=*/false, metadata);
|
||||
}
|
||||
|
||||
|
||||
@@ -127,10 +225,12 @@ MetaMap::Ptr
|
||||
Stream::getMetadata() const
|
||||
{
|
||||
MetaMap::Ptr result;
|
||||
if (mMeta) {
|
||||
if (mImpl->mFile) {
|
||||
result = mImpl->mFile->getMetadata();
|
||||
} else if (mImpl->mMeta) {
|
||||
// Return a deep copy of the file-level metadata
|
||||
// that was read when this object was constructed.
|
||||
result.reset(new MetaMap(*mMeta));
|
||||
result.reset(new MetaMap(*mImpl->mMeta));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -139,7 +239,10 @@ Stream::getMetadata() const
|
||||
GridPtrVecPtr
|
||||
Stream::getGrids()
|
||||
{
|
||||
return mGrids;
|
||||
if (mImpl->mFile) {
|
||||
return mImpl->mFile->getGrids();
|
||||
}
|
||||
return mImpl->mGrids;
|
||||
}
|
||||
|
||||
} // namespace io
|
||||
|
||||
+20
-19
@@ -22,21 +22,21 @@ class OPENVDB_API Stream: public Archive
|
||||
{
|
||||
public:
|
||||
/// @brief Read grids from an input stream.
|
||||
/// @param is The input stream to read from
|
||||
explicit Stream(std::istream&);
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("Use Stream(std::istream&) instead. This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
Stream(std::istream& is, bool /*delayLoad*/) : Stream(is) { }
|
||||
/// @details If @a delayLoad is true, map the contents of the input stream
|
||||
/// into memory and enable delayed loading of grids.
|
||||
/// @note Define the environment variable @c OPENVDB_DISABLE_DELAYED_LOAD
|
||||
/// to disable delayed loading unconditionally.
|
||||
explicit Stream(std::istream&, bool delayLoad = true);
|
||||
|
||||
/// Construct an archive for stream output.
|
||||
Stream() = default;
|
||||
Stream();
|
||||
/// Construct an archive for output to the given stream.
|
||||
explicit Stream(std::ostream&);
|
||||
|
||||
Stream(const Stream&);
|
||||
Stream& operator=(const Stream&);
|
||||
|
||||
~Stream() override { }
|
||||
~Stream() override;
|
||||
|
||||
/// @brief Return a copy of this archive.
|
||||
Archive::Ptr copy() const override;
|
||||
@@ -49,22 +49,24 @@ public:
|
||||
|
||||
/// @brief Write the grids in the given container to this archive's output stream.
|
||||
/// @throw ValueError if this archive was constructed without specifying an output stream.
|
||||
void write(const GridCPtrVec&, const MetaMap& = MetaMap(),
|
||||
const io::WriteOptions& = io::WriteOptions{}) const override;
|
||||
void write(const GridCPtrVec&, const MetaMap& = MetaMap()) const override;
|
||||
|
||||
/// @brief Write the grids in the given container to this archive's output stream.
|
||||
/// @throw ValueError if this archive was constructed without specifying an output stream.
|
||||
template<typename GridPtrContainerT>
|
||||
void write(const GridPtrContainerT&, const MetaMap& = MetaMap(),
|
||||
const io::WriteOptions& = io::WriteOptions{}) const;
|
||||
void write(const GridPtrContainerT&, const MetaMap& = MetaMap()) const;
|
||||
|
||||
private:
|
||||
void writeGrids(std::ostream&, const GridCPtrVec&, const MetaMap&,
|
||||
const io::WriteOptions&) const;
|
||||
/// Create a new grid of the type specified by the given descriptor,
|
||||
/// then populate the grid from the given input stream.
|
||||
/// @return the newly created grid.
|
||||
GridBase::Ptr readGrid(const GridDescriptor&, std::istream&) const;
|
||||
|
||||
MetaMap::Ptr mMeta;
|
||||
GridPtrVecPtr mGrids;
|
||||
std::ostream* mOutputStream = nullptr;
|
||||
void writeGrids(std::ostream&, const GridCPtrVec&, const MetaMap&) const;
|
||||
|
||||
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> mImpl;
|
||||
};
|
||||
|
||||
|
||||
@@ -73,12 +75,11 @@ private:
|
||||
|
||||
template<typename GridPtrContainerT>
|
||||
inline void
|
||||
Stream::write(const GridPtrContainerT& container, const MetaMap& metadata,
|
||||
const io::WriteOptions& writeOptions) const
|
||||
Stream::write(const GridPtrContainerT& container, const MetaMap& metadata) const
|
||||
{
|
||||
GridCPtrVec grids;
|
||||
std::copy(container.begin(), container.end(), std::back_inserter(grids));
|
||||
this->write(grids, metadata, writeOptions);
|
||||
this->write(grids, metadata);
|
||||
}
|
||||
|
||||
} // namespace io
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// @file TempFile.cc
|
||||
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
|
||||
#include "TempFile.h"
|
||||
|
||||
#include <openvdb/Exceptions.h>
|
||||
#ifndef _WIN32
|
||||
#include <boost/iostreams/stream.hpp>
|
||||
#include <boost/iostreams/device/file_descriptor.hpp>
|
||||
#include <cstdlib> // for std::getenv(), mkstemp()
|
||||
#include <sys/types.h> // for mode_t
|
||||
#include <sys/stat.h> // for mkdir(), umask()
|
||||
#include <unistd.h> // for access()
|
||||
#else
|
||||
#include <fstream> // for std::filebuf
|
||||
#endif
|
||||
#include <cstdio> // for std::tmpnam(), L_tmpnam, P_tmpdir
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
||||
namespace openvdb {
|
||||
OPENVDB_USE_VERSION_NAMESPACE
|
||||
namespace OPENVDB_VERSION_NAME {
|
||||
namespace io {
|
||||
|
||||
struct TempFile::TempFileImpl
|
||||
{
|
||||
const std::string& filename() const { return mPath; }
|
||||
|
||||
bool is_open() const { return mBuffer.is_open(); }
|
||||
|
||||
/// @internal boost::filesystem::unique_path(), etc. might be useful here,
|
||||
/// but as of 9/2014, Houdini ships without the Boost.Filesystem library,
|
||||
/// which makes it much less convenient to use that library.
|
||||
#ifndef _WIN32
|
||||
TempFileImpl(std::ostream& os): mFileDescr(-1) { this->init(os); }
|
||||
|
||||
void init(std::ostream& os)
|
||||
{
|
||||
std::string fn = this->getTempDir() + "/openvdb_temp_XXXXXX";
|
||||
std::vector<char> fnbuf(fn.begin(), fn.end());
|
||||
fnbuf.push_back(char(0));
|
||||
|
||||
//const mode_t savedMode = ::umask(~(S_IRUSR | S_IWUSR));
|
||||
mFileDescr = ::mkstemp(&fnbuf[0]);
|
||||
//::umask(savedMode);
|
||||
if (mFileDescr < 0) {
|
||||
OPENVDB_THROW(IoError, "failed to generate temporary file");
|
||||
}
|
||||
|
||||
mPath.assign(&fnbuf[0]);
|
||||
|
||||
mDevice = DeviceType(mFileDescr, boost::iostreams::never_close_handle);
|
||||
mBuffer.open(mDevice);
|
||||
os.rdbuf(&mBuffer);
|
||||
|
||||
if (!os.good()) {
|
||||
OPENVDB_THROW(IoError, "failed to open temporary file " + mPath);
|
||||
}
|
||||
}
|
||||
|
||||
void close() { mBuffer.close(); if (mFileDescr >= 0) ::close(mFileDescr); }
|
||||
|
||||
static std::string getTempDir()
|
||||
{
|
||||
if (const char* dir = std::getenv("OPENVDB_TEMP_DIR")) {
|
||||
if (0 != ::access(dir, F_OK)) {
|
||||
#ifdef _WIN32
|
||||
::mkdir(dir);
|
||||
#else
|
||||
::mkdir(dir, S_IRUSR | S_IWUSR | S_IXUSR);
|
||||
#endif
|
||||
if (0 != ::access(dir, F_OK)) {
|
||||
OPENVDB_THROW(IoError,
|
||||
"failed to create OPENVDB_TEMP_DIR (" + std::string(dir) + ")");
|
||||
}
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
if (const char* dir = std::getenv("TMPDIR")) return dir;
|
||||
return P_tmpdir;
|
||||
}
|
||||
|
||||
using DeviceType = boost::iostreams::file_descriptor_sink;
|
||||
using BufferType = boost::iostreams::stream_buffer<boost::iostreams::file_descriptor_sink>;
|
||||
|
||||
std::string mPath;
|
||||
DeviceType mDevice;
|
||||
BufferType mBuffer;
|
||||
int mFileDescr;
|
||||
#else // _WIN32
|
||||
// Use only standard library routines; no POSIX.
|
||||
|
||||
TempFileImpl(std::ostream& os) { this->init(os); }
|
||||
|
||||
void init(std::ostream& os)
|
||||
{
|
||||
char fnbuf[L_tmpnam];
|
||||
const char* filename = std::tmpnam(fnbuf);
|
||||
if (!filename) {
|
||||
OPENVDB_THROW(IoError, "failed to generate name for temporary file");
|
||||
}
|
||||
/// @todo This is not safe, since another process could open a file
|
||||
/// with this name before we do. Unfortunately, there is no safe,
|
||||
/// portable way to create a temporary file.
|
||||
mPath = filename;
|
||||
|
||||
const std::ios_base::openmode mode = (std::ios_base::out | std::ios_base::binary);
|
||||
os.rdbuf(mBuffer.open(mPath.c_str(), mode));
|
||||
if (!os.good()) {
|
||||
OPENVDB_THROW(IoError, "failed to open temporary file " + mPath);
|
||||
}
|
||||
}
|
||||
|
||||
void close() { mBuffer.close(); }
|
||||
|
||||
std::string mPath;
|
||||
std::filebuf mBuffer;
|
||||
#endif // _WIN32
|
||||
|
||||
private:
|
||||
TempFileImpl(const TempFileImpl&); // disable copying
|
||||
TempFileImpl& operator=(const TempFileImpl&); // disable assignment
|
||||
};
|
||||
|
||||
|
||||
TempFile::TempFile(): std::ostream(nullptr), mImpl(new TempFileImpl(*this)) {}
|
||||
TempFile::~TempFile() { this->close(); }
|
||||
const std::string& TempFile::filename() const { return mImpl->filename(); }
|
||||
bool TempFile::is_open() const { return mImpl->is_open(); }
|
||||
void TempFile::close() { mImpl->close(); }
|
||||
|
||||
} // namespace io
|
||||
} // namespace OPENVDB_VERSION_NAME
|
||||
} // namespace openvdb
|
||||
|
||||
#endif // OPENVDB_USE_DELAYED_LOADING
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright Contributors to the OpenVDB Project
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// @file TempFile.h
|
||||
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
|
||||
#ifndef OPENVDB_IO_TEMPFILE_HAS_BEEN_INCLUDED
|
||||
#define OPENVDB_IO_TEMPFILE_HAS_BEEN_INCLUDED
|
||||
|
||||
#include <openvdb/version.h>
|
||||
#include <memory>
|
||||
#include <ostream>
|
||||
|
||||
|
||||
namespace openvdb {
|
||||
OPENVDB_USE_VERSION_NAMESPACE
|
||||
namespace OPENVDB_VERSION_NAME {
|
||||
namespace io {
|
||||
|
||||
/// Output stream to a unique temporary file
|
||||
class OPENVDB_API TempFile: public std::ostream
|
||||
{
|
||||
public:
|
||||
/// @brief Create and open a unique file.
|
||||
/// @details On UNIX systems, the file is created in the directory specified by
|
||||
/// the environment variable @c OPENVDB_TEMP_DIR, if that variable is defined,
|
||||
/// or else in the directory specified by @c TMPDIR, if that variable is defined.
|
||||
/// Otherwise (and on non-UNIX systems), the file is created in the system default
|
||||
/// temporary directory.
|
||||
TempFile();
|
||||
~TempFile();
|
||||
|
||||
/// Return the path to the temporary file.
|
||||
const std::string& filename() const;
|
||||
|
||||
/// Return @c true if the file is open for writing.
|
||||
bool is_open() const;
|
||||
|
||||
/// Close the file.
|
||||
void close();
|
||||
|
||||
private:
|
||||
struct TempFileImpl;
|
||||
std::unique_ptr<TempFileImpl> mImpl;
|
||||
};
|
||||
|
||||
} // namespace io
|
||||
} // namespace OPENVDB_VERSION_NAME
|
||||
} // namespace openvdb
|
||||
|
||||
#endif // OPENVDB_IO_TEMPFILE_HAS_BEEN_INCLUDED
|
||||
|
||||
#endif // OPENVDB_USE_DELAYED_LOADING
|
||||
+46
-25
@@ -76,6 +76,9 @@ public:
|
||||
uint32_t pass() const;
|
||||
void setPass(uint32_t);
|
||||
|
||||
uint64_t leaf() const;
|
||||
void setLeaf(uint64_t);
|
||||
|
||||
//@{
|
||||
/// @brief Return a (reference to a) copy of the metadata of the grid
|
||||
/// currently being read or written.
|
||||
@@ -116,9 +119,9 @@ std::ostream& operator<<(std::ostream&, const StreamMetadata::AuxDataMap&);
|
||||
////////////////////////////////////////
|
||||
|
||||
|
||||
/// @brief Tag for multi-pass I/O. Only points::PointDataLeafNode may inherit from this.
|
||||
/// @sa Grid::hasMultiPassIO(), points::IsPointDataLeafNode
|
||||
struct PointDataGridMultiPass {};
|
||||
/// @brief Leaf nodes that require multi-pass I/O must inherit from this struct.
|
||||
/// @sa Grid::hasMultiPassIO()
|
||||
struct MultiPass {};
|
||||
|
||||
|
||||
////////////////////////////////////////
|
||||
@@ -126,37 +129,50 @@ struct PointDataGridMultiPass {};
|
||||
|
||||
class File;
|
||||
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
|
||||
// This class is deprecated and will be removed. Delayed loading is no longer supported.
|
||||
// It is retained for API backwards compatibility only, but all methods are no-ops.
|
||||
/// @brief Handle to control the lifetime of a memory-mapped .vdb file
|
||||
class OPENVDB_API MappedFile
|
||||
{
|
||||
public:
|
||||
using Ptr = SharedPtr<MappedFile>;
|
||||
using Notifier = std::function<void(std::string /*filename*/)>;
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("This class is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
explicit MappedFile(const std::string& /*filename*/, bool /*autoDelete*/ = false) {}
|
||||
|
||||
/// @brief Destructor
|
||||
~MappedFile() = default;
|
||||
|
||||
MappedFile(const MappedFile&) = delete;
|
||||
~MappedFile();
|
||||
MappedFile(const MappedFile&) = delete; // not copyable
|
||||
MappedFile& operator=(const MappedFile&) = delete;
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("Always returns an empty string. This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
std::string filename() const { return std::string(); }
|
||||
/// Return the filename of the mapped file.
|
||||
std::string filename() const;
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("Always returns a null stream buffer pointer. This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
SharedPtr<std::streambuf> createBuffer() const { return SharedPtr<std::streambuf>(); }
|
||||
/// @brief Return a new stream buffer for the mapped file.
|
||||
/// @details Typical usage is
|
||||
/// @code
|
||||
/// openvdb::io::MappedFile::Ptr mappedFile = ...;
|
||||
/// auto buf = mappedFile->createBuffer();
|
||||
/// std::istream istrm{buf.get()};
|
||||
/// // Read from istrm...
|
||||
/// @endcode
|
||||
/// The buffer must persist as long as the stream is open.
|
||||
SharedPtr<std::streambuf> createBuffer() const;
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
void setNotifier(const Notifier&) {}
|
||||
using Notifier = std::function<void(std::string /*filename*/)>;
|
||||
/// @brief Register a function that will be called with this file's name
|
||||
/// when the file is unmapped.
|
||||
void setNotifier(const Notifier&);
|
||||
/// Deregister the notifier.
|
||||
void clearNotifier();
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
void clearNotifier() {}
|
||||
private:
|
||||
friend class File;
|
||||
friend class ::TestMappedFile;
|
||||
|
||||
explicit MappedFile(const std::string& filename, bool autoDelete = false);
|
||||
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> mImpl;
|
||||
}; // class MappedFile
|
||||
|
||||
#endif // OPENVDB_USE_DELAYED_LOADING
|
||||
|
||||
////////////////////////////////////////
|
||||
|
||||
@@ -233,10 +249,15 @@ OPENVDB_API bool getWriteGridStatsMetadata(std::ios_base&);
|
||||
/// and store them as grid metadata when writing to the given stream.
|
||||
OPENVDB_API void setWriteGridStatsMetadata(std::ios_base&, bool writeGridStats);
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("Always returns a null pointer. This function is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
inline SharedPtr<MappedFile> getMappedFilePtr(std::ios_base&) { return SharedPtr<MappedFile>(); }
|
||||
OPENVDB_DEPRECATED_MESSAGE("This function is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
inline void setMappedFilePtr(std::ios_base&, SharedPtr<MappedFile>&) { }
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
/// @brief Return a shared pointer to the memory-mapped file with which the given stream
|
||||
/// is associated, or a null pointer if the stream is not associated with a memory-mapped file.
|
||||
OPENVDB_API SharedPtr<MappedFile> getMappedFilePtr(std::ios_base&);
|
||||
/// @brief Associate the given stream with (a shared pointer to) a memory-mapped file.
|
||||
/// @note The shared pointer object (not just the io::MappedFile object to which it points)
|
||||
/// must remain valid until the file is closed.
|
||||
OPENVDB_API void setMappedFilePtr(std::ios_base&, SharedPtr<MappedFile>&);
|
||||
#endif // OPENVDB_USE_DELAYED_LOADING
|
||||
|
||||
/// @brief Return a shared pointer to an object that stores metadata (file format,
|
||||
/// compression scheme, etc.) for use when reading from or writing to the given stream.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include "openvdb.h"
|
||||
#include "io/Codec.h"
|
||||
#include "io/DelayedLoadMetadata.h"
|
||||
#include "points/PointDataGrid.h"
|
||||
#include "tools/PointIndexGrid.h"
|
||||
#include "util/logging.h"
|
||||
@@ -71,8 +71,6 @@ initialize()
|
||||
|
||||
logging::initialize();
|
||||
|
||||
io::internal::initialize();
|
||||
|
||||
// Register metadata.
|
||||
Metadata::clearRegistry();
|
||||
MetaTypes::foreach<RegisterMeta>();
|
||||
@@ -133,7 +131,6 @@ __pragma(warning(default:1711))
|
||||
Metadata::clearRegistry();
|
||||
GridBase::clearRegistry();
|
||||
math::MapRegistry::clear();
|
||||
io::internal::uninitialize();
|
||||
points::internal::uninitialize();
|
||||
|
||||
#ifdef OPENVDB_USE_BLOSC
|
||||
|
||||
@@ -46,6 +46,9 @@ OPENVDB_API void initialize();
|
||||
OPENVDB_API void uninitialize();
|
||||
|
||||
|
||||
// foward declare some default types
|
||||
namespace io { class DelayedLoadMetadata; }
|
||||
|
||||
/// Common tree types
|
||||
using BoolTree = tree::Tree4<bool, 5, 4, 3>::Type;
|
||||
using DoubleTree = tree::Tree4<double, 5, 4, 3>::Type;
|
||||
@@ -219,7 +222,8 @@ using MetaTypes = TypeList<
|
||||
Vec4SMetadata,
|
||||
Vec4DMetadata,
|
||||
Mat4SMetadata,
|
||||
Mat4DMetadata>;
|
||||
Mat4DMetadata,
|
||||
io::DelayedLoadMetadata>;
|
||||
|
||||
|
||||
} // namespace OPENVDB_VERSION_NAME
|
||||
|
||||
@@ -61,6 +61,7 @@ AttributeArray::AttributeArray(const AttributeArray& rhs, const tbb::spin_mutex:
|
||||
: mIsUniform(rhs.mIsUniform)
|
||||
, mFlags(rhs.mFlags)
|
||||
, mUsePagedRead(rhs.mUsePagedRead)
|
||||
, mOutOfCore(rhs.mOutOfCore.load())
|
||||
, mPageHandle()
|
||||
{
|
||||
if (mFlags & PARTIALREAD) mCompressedBytes = rhs.mCompressedBytes;
|
||||
@@ -77,6 +78,7 @@ AttributeArray::operator=(const AttributeArray& rhs)
|
||||
mIsUniform = rhs.mIsUniform;
|
||||
mFlags = rhs.mFlags;
|
||||
mUsePagedRead = rhs.mUsePagedRead;
|
||||
mOutOfCore.store(rhs.mOutOfCore);
|
||||
if (mFlags & PARTIALREAD) mCompressedBytes = rhs.mCompressedBytes;
|
||||
else if (rhs.mPageHandle) mPageHandle = rhs.mPageHandle->copy();
|
||||
else mPageHandle.reset();
|
||||
@@ -188,6 +190,9 @@ AttributeArray::setConstantStride(bool state)
|
||||
bool
|
||||
AttributeArray::operator==(const AttributeArray& other) const
|
||||
{
|
||||
this->loadData();
|
||||
other.loadData();
|
||||
|
||||
if (this->mUsePagedRead != other.mUsePagedRead ||
|
||||
this->mFlags != other.mFlags) return false;
|
||||
return this->isEqual(other);
|
||||
|
||||
@@ -130,7 +130,7 @@ public:
|
||||
|
||||
template <typename ValueType, typename CodecType> friend class AttributeHandle;
|
||||
|
||||
AttributeArray(): mPageHandle() { }
|
||||
AttributeArray(): mPageHandle() { mOutOfCore = 0; }
|
||||
virtual ~AttributeArray()
|
||||
{
|
||||
// if this AttributeArray has been partially read, zero the compressed bytes,
|
||||
@@ -189,7 +189,10 @@ public:
|
||||
/// Return the number of bytes of memory used by this attribute.
|
||||
virtual size_t memUsage() const = 0;
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("Use memUsage() instead. This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
/// Return the number of bytes of memory used by this attribute array once it
|
||||
/// has been deserialized (this may be different to memUsage() if delay-loading
|
||||
/// is in use). Note that this method does NOT consider the fact that a
|
||||
/// uniform attribute could be expanded and only deals with delay-loading.
|
||||
virtual size_t memUsageIfLoaded() const = 0;
|
||||
|
||||
/// Create a new attribute array of the given (registered) type, length and stride.
|
||||
@@ -308,17 +311,14 @@ public:
|
||||
|
||||
/// Read attribute buffers from a paged stream.
|
||||
virtual void readPagedBuffers(compression::PagedInputStream&) = 0;
|
||||
/// Skip attribute buffers in a paged stream without reading or
|
||||
/// allocating any data.
|
||||
void skipPagedBuffers(compression::PagedInputStream&);
|
||||
/// Write attribute buffers to a paged stream.
|
||||
/// @param outputTransient if true, write out transient attributes
|
||||
virtual void writePagedBuffers(compression::PagedOutputStream&, bool outputTransient) const = 0;
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
/// Ensures all data is in-core
|
||||
virtual void loadData() const = 0;
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
/// Return @c true if all data has been loaded
|
||||
virtual bool isDataLoaded() const = 0;
|
||||
|
||||
/// Check the compressed bytes and flags. If they are equal, perform a deeper
|
||||
@@ -366,12 +366,10 @@ protected:
|
||||
mutable tbb::spin_mutex mMutex;
|
||||
uint8_t mFlags = 0;
|
||||
uint8_t mUsePagedRead = 0;
|
||||
#if OPENVDB_ABI_VERSION_NUMBER < 14
|
||||
std::atomic<Index32> mOutOfCore{0}; // interpreted as bool
|
||||
#endif
|
||||
std::atomic<Index32> mOutOfCore; // interpreted as bool
|
||||
/// used for out-of-core, paged reading
|
||||
union {
|
||||
std::unique_ptr<compression::PageHandle> mPageHandle;
|
||||
compression::PageHandle::Ptr mPageHandle;
|
||||
size_t mCompressedBytes;
|
||||
};
|
||||
}; // class AttributeArray
|
||||
@@ -613,8 +611,11 @@ public:
|
||||
/// Return the number of bytes of memory used by this attribute.
|
||||
size_t memUsage() const override;
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("Use memUsage() instead. This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
size_t memUsageIfLoaded() const override { return memUsage(); }
|
||||
/// Return the number of bytes of memory used by this attribute array once it
|
||||
/// has been deserialized (this may be different to memUsage() if delay-loading
|
||||
/// is in use). Note that this method does NOT consider the fact that a
|
||||
/// uniform attribute could be expanded and only deals with delay-loading.
|
||||
size_t memUsageIfLoaded() const override;
|
||||
|
||||
/// Return the value at index @a n (assumes in-core)
|
||||
ValueType getUnsafe(Index n) const;
|
||||
@@ -695,14 +696,14 @@ public:
|
||||
/// @param outputTransient if true, write out transient attributes
|
||||
void writePagedBuffers(compression::PagedOutputStream& os, bool outputTransient) const override;
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("Always returns false. This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
bool isOutOfCore() const { return false; }
|
||||
/// Return @c true if this buffer's values have not yet been read from disk.
|
||||
inline bool isOutOfCore() const;
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
void loadData() const override { }
|
||||
/// Ensures all data is in-core
|
||||
void loadData() const override;
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("Always returns true. This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
bool isDataLoaded() const override { return true; }
|
||||
/// Return @c true if all data has been loaded
|
||||
bool isDataLoaded() const override;
|
||||
|
||||
/// Return the raw data buffer
|
||||
inline const StorageType* constData() const { return this->data(); }
|
||||
@@ -715,13 +716,18 @@ protected:
|
||||
inline const StorageType* data() const { OPENVDB_ASSERT(validData()); return mData.get(); }
|
||||
|
||||
/// Verify that data is not out-of-core or in a partially-read state
|
||||
inline bool validData() const { return !((flags() & PARTIALREAD)); }
|
||||
inline bool validData() const { return !(isOutOfCore() || (flags() & PARTIALREAD)); }
|
||||
|
||||
private:
|
||||
friend class ::TestAttributeArray;
|
||||
|
||||
TypedAttributeArray(const TypedAttributeArray&, const tbb::spin_mutex::scoped_lock&);
|
||||
|
||||
/// Load data from memory-mapped file.
|
||||
inline void doLoad() const;
|
||||
/// Load data from memory-mapped file (unsafe as this function is not protected by a mutex).
|
||||
inline void doLoadUnsafe() const;
|
||||
|
||||
/// Toggle out-of-core state
|
||||
inline void setOutOfCore(const bool);
|
||||
|
||||
@@ -956,6 +962,8 @@ void AttributeArray::doCopyValues(const AttributeArray& sourceArray, const IterT
|
||||
{
|
||||
// ensure both arrays have float-float or integer-integer value types
|
||||
OPENVDB_ASSERT(sourceArray.valueTypeIsFloatingPoint() == this->valueTypeIsFloatingPoint());
|
||||
// ensure both arrays have been loaded from disk (if delay-loaded)
|
||||
OPENVDB_ASSERT(sourceArray.isDataLoaded() && this->isDataLoaded());
|
||||
// ensure storage size * stride matches on both arrays
|
||||
OPENVDB_ASSERT(this->storageTypeSize()*this->stride() ==
|
||||
sourceArray.storageTypeSize()*sourceArray.stride());
|
||||
@@ -1016,6 +1024,10 @@ void AttributeArray::copyValues(const AttributeArray& sourceArray, const IterT&
|
||||
OPENVDB_THROW(TypeError, "Cannot copy array data due to mis-match in storage type sizes.");
|
||||
}
|
||||
|
||||
// ensure both arrays have been loaded from disk
|
||||
sourceArray.loadData();
|
||||
this->loadData();
|
||||
|
||||
// if the target array is uniform, expand it first
|
||||
this->expand();
|
||||
|
||||
@@ -1194,6 +1206,8 @@ template<typename ValueType_, typename Codec_>
|
||||
size_t
|
||||
TypedAttributeArray<ValueType_, Codec_>::arrayMemUsage() const
|
||||
{
|
||||
if (this->isOutOfCore()) return 0;
|
||||
|
||||
return (mIsUniform ? 1 : this->dataSize()) * sizeof(StorageType);
|
||||
}
|
||||
|
||||
@@ -1218,6 +1232,11 @@ template<typename ValueType_, typename Codec_>
|
||||
void
|
||||
TypedAttributeArray<ValueType_, Codec_>::deallocate()
|
||||
{
|
||||
// detach from file if delay-loaded
|
||||
if (this->isOutOfCore()) {
|
||||
this->setOutOfCore(false);
|
||||
this->mPageHandle.reset();
|
||||
}
|
||||
if (mData) mData.reset();
|
||||
}
|
||||
|
||||
@@ -1285,6 +1304,14 @@ TypedAttributeArray<ValueType_, Codec_>::memUsage() const
|
||||
}
|
||||
|
||||
|
||||
template<typename ValueType_, typename Codec_>
|
||||
size_t
|
||||
TypedAttributeArray<ValueType_, Codec_>::memUsageIfLoaded() const
|
||||
{
|
||||
return sizeof(*this) + (mIsUniform ? 1 : this->dataSize()) * sizeof(StorageType);
|
||||
}
|
||||
|
||||
|
||||
template<typename ValueType_, typename Codec_>
|
||||
typename TypedAttributeArray<ValueType_, Codec_>::ValueType
|
||||
TypedAttributeArray<ValueType_, Codec_>::getUnsafe(Index n) const
|
||||
@@ -1302,6 +1329,7 @@ typename TypedAttributeArray<ValueType_, Codec_>::ValueType
|
||||
TypedAttributeArray<ValueType_, Codec_>::get(Index n) const
|
||||
{
|
||||
if (n >= this->dataSize()) OPENVDB_THROW(IndexError, "Out-of-range access.");
|
||||
if (this->isOutOfCore()) this->doLoad();
|
||||
|
||||
return this->getUnsafe(n);
|
||||
}
|
||||
@@ -1338,6 +1366,7 @@ void
|
||||
TypedAttributeArray<ValueType_, Codec_>::setUnsafe(Index n, const ValueType& val)
|
||||
{
|
||||
OPENVDB_ASSERT(n < this->dataSize());
|
||||
OPENVDB_ASSERT(!this->isOutOfCore());
|
||||
OPENVDB_ASSERT(!this->isUniform());
|
||||
|
||||
// this unsafe method assumes the data is not uniform, however if it is, this redirects the index
|
||||
@@ -1352,6 +1381,7 @@ void
|
||||
TypedAttributeArray<ValueType_, Codec_>::set(Index n, const ValueType& val)
|
||||
{
|
||||
if (n >= this->dataSize()) OPENVDB_THROW(IndexError, "Out-of-range access.");
|
||||
if (this->isOutOfCore()) this->doLoad();
|
||||
if (this->isUniform()) this->expand();
|
||||
|
||||
this->setUnsafe(n, val);
|
||||
@@ -1456,6 +1486,12 @@ template<typename ValueType_, typename Codec_>
|
||||
void
|
||||
TypedAttributeArray<ValueType_, Codec_>::fill(const ValueType& value)
|
||||
{
|
||||
if (this->isOutOfCore()) {
|
||||
tbb::spin_mutex::scoped_lock lock(mMutex);
|
||||
this->deallocate();
|
||||
this->allocate();
|
||||
}
|
||||
|
||||
const Index size = mIsUniform ? 1 : this->dataSize();
|
||||
for (Index i = 0; i < size; ++i) {
|
||||
Codec::encode(value, this->data()[i]);
|
||||
@@ -1471,6 +1507,54 @@ TypedAttributeArray<ValueType_, Codec_>::fill(AttributeArray* array, const Value
|
||||
}
|
||||
|
||||
|
||||
template<typename ValueType_, typename Codec_>
|
||||
bool
|
||||
TypedAttributeArray<ValueType_, Codec_>::isOutOfCore() const
|
||||
{
|
||||
return mOutOfCore;
|
||||
}
|
||||
|
||||
|
||||
template<typename ValueType_, typename Codec_>
|
||||
void
|
||||
TypedAttributeArray<ValueType_, Codec_>::setOutOfCore(const bool b)
|
||||
{
|
||||
mOutOfCore = b;
|
||||
}
|
||||
|
||||
|
||||
template<typename ValueType_, typename Codec_>
|
||||
void
|
||||
TypedAttributeArray<ValueType_, Codec_>::doLoad() const
|
||||
{
|
||||
if (!(this->isOutOfCore())) return;
|
||||
|
||||
TypedAttributeArray<ValueType_, Codec_>* self =
|
||||
const_cast<TypedAttributeArray<ValueType_, Codec_>*>(this);
|
||||
|
||||
// This lock will be contended at most once, after which this buffer
|
||||
// will no longer be out-of-core.
|
||||
tbb::spin_mutex::scoped_lock lock(self->mMutex);
|
||||
this->doLoadUnsafe();
|
||||
}
|
||||
|
||||
|
||||
template<typename ValueType_, typename Codec_>
|
||||
void
|
||||
TypedAttributeArray<ValueType_, Codec_>::loadData() const
|
||||
{
|
||||
this->doLoad();
|
||||
}
|
||||
|
||||
|
||||
template<typename ValueType_, typename Codec_>
|
||||
bool
|
||||
TypedAttributeArray<ValueType_, Codec_>::isDataLoaded() const
|
||||
{
|
||||
return !this->isOutOfCore();
|
||||
}
|
||||
|
||||
|
||||
template<typename ValueType_, typename Codec_>
|
||||
void
|
||||
TypedAttributeArray<ValueType_, Codec_>::read(std::istream& is)
|
||||
@@ -1540,6 +1624,8 @@ TypedAttributeArray<ValueType_, Codec_>::readBuffers(std::istream& is)
|
||||
OPENVDB_THROW(IoError, "Cannot read paged AttributeArray buffers.");
|
||||
}
|
||||
|
||||
tbb::spin_mutex::scoped_lock lock(mMutex);
|
||||
|
||||
this->deallocate();
|
||||
|
||||
uint8_t bloscCompressed(0);
|
||||
@@ -1577,6 +1663,13 @@ TypedAttributeArray<ValueType_, Codec_>::readPagedBuffers(compression::PagedInpu
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
// If this array is being read from a memory-mapped file, delay loading of its data
|
||||
// until the data is actually accessed.
|
||||
io::MappedFile::Ptr mappedFile = io::getMappedFilePtr(is.getInputStream());
|
||||
const bool delayLoad = (mappedFile.get() != nullptr);
|
||||
#endif
|
||||
|
||||
if (is.sizeOnly())
|
||||
{
|
||||
size_t compressedBytes(mCompressedBytes);
|
||||
@@ -1589,49 +1682,26 @@ TypedAttributeArray<ValueType_, Codec_>::readPagedBuffers(compression::PagedInpu
|
||||
|
||||
OPENVDB_ASSERT(mPageHandle);
|
||||
|
||||
tbb::spin_mutex::scoped_lock lock(mMutex);
|
||||
|
||||
this->deallocate();
|
||||
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
this->setOutOfCore(delayLoad);
|
||||
is.read(mPageHandle, std::streamsize(mPageHandle->size()), delayLoad);
|
||||
#else
|
||||
is.read(mPageHandle, std::streamsize(mPageHandle->size()), false);
|
||||
std::unique_ptr<char[]> buffer = mPageHandle->read();
|
||||
mData.reset(reinterpret_cast<StorageType*>(buffer.release()));
|
||||
mPageHandle.reset();
|
||||
#endif // OPENVDB_USE_DELAYED_LOADING
|
||||
|
||||
// clear page state
|
||||
|
||||
mUsePagedRead = 0;
|
||||
}
|
||||
|
||||
|
||||
inline void
|
||||
AttributeArray::skipPagedBuffers(compression::PagedInputStream& is)
|
||||
{
|
||||
if (!mUsePagedRead) {
|
||||
if (!is.sizeOnly()) {
|
||||
// for non-paged data, seek past the raw data in the stream
|
||||
std::istream& inputStream = is.getInputStream();
|
||||
uint8_t bloscCompressed(0);
|
||||
if (!mIsUniform) inputStream.read(reinterpret_cast<char*>(&bloscCompressed), sizeof(uint8_t));
|
||||
inputStream.seekg(mCompressedBytes, std::ios_base::cur);
|
||||
mCompressedBytes = 0;
|
||||
mFlags = static_cast<uint8_t>(mFlags & ~PARTIALREAD);
|
||||
}
|
||||
return;
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
if (!delayLoad) {
|
||||
#endif
|
||||
std::unique_ptr<char[]> buffer = mPageHandle->read();
|
||||
mData.reset(reinterpret_cast<StorageType*>(buffer.release()));
|
||||
mPageHandle.reset();
|
||||
#ifdef OPENVDB_USE_DELAYED_LOADING
|
||||
}
|
||||
|
||||
if (is.sizeOnly())
|
||||
{
|
||||
size_t compressedBytes(mCompressedBytes);
|
||||
mCompressedBytes = 0;
|
||||
mFlags = static_cast<uint8_t>(mFlags & ~PARTIALREAD);
|
||||
OPENVDB_ASSERT(!mPageHandle);
|
||||
mPageHandle = is.createHandle(compressedBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
OPENVDB_ASSERT(mPageHandle);
|
||||
|
||||
is.skip(mPageHandle, std::streamsize(mPageHandle->size()));
|
||||
mPageHandle.reset();
|
||||
#endif
|
||||
|
||||
// clear page state
|
||||
|
||||
@@ -1674,6 +1744,9 @@ TypedAttributeArray<ValueType_, Codec_>::writeMetadata(std::ostream& os, bool ou
|
||||
|
||||
bool bloscCompression = io::getDataCompression(os) & io::COMPRESS_BLOSC;
|
||||
|
||||
// any compressed data needs to be loaded if out-of-core
|
||||
if (bloscCompression) this->doLoad();
|
||||
|
||||
size_t compressedBytes = 0;
|
||||
|
||||
if (!strideOfOne)
|
||||
@@ -1722,6 +1795,8 @@ TypedAttributeArray<ValueType_, Codec_>::writeBuffers(std::ostream& os, bool out
|
||||
OPENVDB_THROW(IoError, "Cannot write out a partially-read AttributeArray.");
|
||||
}
|
||||
|
||||
this->doLoad();
|
||||
|
||||
if (this->isUniform()) {
|
||||
os.write(reinterpret_cast<const char*>(this->data()), sizeof(StorageType));
|
||||
}
|
||||
@@ -1769,10 +1844,37 @@ TypedAttributeArray<ValueType_, Codec_>::writePagedBuffers(compression::PagedOut
|
||||
OPENVDB_THROW(IoError, "Cannot write out a partially-read AttributeArray.");
|
||||
}
|
||||
|
||||
this->doLoad();
|
||||
|
||||
os.write(reinterpret_cast<const char*>(this->data()), this->arrayMemUsage());
|
||||
}
|
||||
|
||||
|
||||
template<typename ValueType_, typename Codec_>
|
||||
void
|
||||
TypedAttributeArray<ValueType_, Codec_>::doLoadUnsafe() const
|
||||
{
|
||||
if (!(this->isOutOfCore())) return;
|
||||
|
||||
// this function expects the mutex to already be locked
|
||||
|
||||
auto* self = const_cast<TypedAttributeArray<ValueType_, Codec_>*>(this);
|
||||
|
||||
OPENVDB_ASSERT(self->mPageHandle);
|
||||
OPENVDB_ASSERT(!(self->mFlags & PARTIALREAD));
|
||||
|
||||
std::unique_ptr<char[]> buffer = self->mPageHandle->read();
|
||||
|
||||
self->mData.reset(reinterpret_cast<StorageType*>(buffer.release()));
|
||||
|
||||
self->mPageHandle.reset();
|
||||
|
||||
// clear all write and out-of-core flags
|
||||
|
||||
self->mOutOfCore = false;
|
||||
}
|
||||
|
||||
|
||||
template<typename ValueType_, typename Codec_>
|
||||
AttributeArray::AccessorBasePtr
|
||||
TypedAttributeArray<ValueType_, Codec_>::getAccessor() const
|
||||
@@ -1799,6 +1901,9 @@ TypedAttributeArray<ValueType_, Codec_>::isEqual(const AttributeArray& other) co
|
||||
this->mIsUniform != otherT->mIsUniform ||
|
||||
this->attributeType() != this->attributeType()) return false;
|
||||
|
||||
this->doLoad();
|
||||
otherT->doLoad();
|
||||
|
||||
const StorageType *target = this->data(), *source = otherT->data();
|
||||
if (!target && !source) return true;
|
||||
if (!target || !source) return false;
|
||||
@@ -1890,6 +1995,10 @@ AttributeHandle<ValueType, CodecType>::AttributeHandle(const AttributeArray& arr
|
||||
OPENVDB_THROW(TypeError, "Cannot bind handle due to incompatible type of AttributeArray.");
|
||||
}
|
||||
|
||||
// load data if delay-loaded
|
||||
|
||||
mArray->loadData();
|
||||
|
||||
// bind getter and setter methods
|
||||
|
||||
AttributeArray::AccessorBasePtr accessor = mArray->getAccessor();
|
||||
|
||||
@@ -23,6 +23,10 @@ GroupHandle::GroupHandle(const GroupAttributeArray& array, const GroupType& offs
|
||||
, mBitMask(static_cast<GroupType>(1 << offset))
|
||||
{
|
||||
OPENVDB_ASSERT(isGroup(mArray));
|
||||
|
||||
// load data if delay-loaded
|
||||
|
||||
mArray.loadData();
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +36,10 @@ GroupHandle::GroupHandle(const GroupAttributeArray& array, const GroupType& bitM
|
||||
, mBitMask(bitMask)
|
||||
{
|
||||
OPENVDB_ASSERT(isGroup(mArray));
|
||||
|
||||
// load data if delay-loaded
|
||||
|
||||
mArray.loadData();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -140,6 +140,17 @@ AttributeSet::memUsage() const
|
||||
}
|
||||
|
||||
|
||||
size_t
|
||||
AttributeSet::memUsageIfLoaded() const
|
||||
{
|
||||
size_t bytes = sizeof(*this) + mDescr->memUsage();
|
||||
for (const auto& attr : mAttrs) {
|
||||
bytes += attr->memUsageIfLoaded();
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
|
||||
size_t
|
||||
AttributeSet::find(const std::string& name) const
|
||||
{
|
||||
|
||||
@@ -114,8 +114,10 @@ public:
|
||||
/// Return the number of bytes of memory used by this attribute set.
|
||||
size_t memUsage() const;
|
||||
|
||||
OPENVDB_DEPRECATED_MESSAGE("Use memUsage() instead. This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
size_t memUsageIfLoaded() const { return memUsage(); }
|
||||
/// Return the number of bytes of memory used by this attribute set once it
|
||||
/// has been deserialized (this may be different to memUsage() if delay-loading
|
||||
/// is in use).
|
||||
size_t memUsageIfLoaded() const;
|
||||
|
||||
/// @brief Return the position of the attribute array whose name is @a name,
|
||||
/// or @c INVALID_POS if no match is found.
|
||||
|
||||
@@ -134,6 +134,7 @@ populateAttribute( PointDataTreeT& tree,
|
||||
/// @param pointOffsets a vector of cumulative point offsets for each leaf
|
||||
/// @param startOffset a value to shift all the point offsets by
|
||||
/// @param filter an index filter
|
||||
/// @param inCoreOnly true if out-of-core leaf nodes are to be ignored
|
||||
///
|
||||
|
||||
template <typename PositionAttribute, typename PointDataGridT, typename FilterT = NullFilter>
|
||||
@@ -142,17 +143,8 @@ convertPointDataGridPosition( PositionAttribute& positionAttribute,
|
||||
const PointDataGridT& grid,
|
||||
const std::vector<Index64>& pointOffsets,
|
||||
const Index64 startOffset,
|
||||
const FilterT& filter = NullFilter());
|
||||
|
||||
template <typename PositionAttribute, typename PointDataGridT, typename FilterT = NullFilter>
|
||||
OPENVDB_DEPRECATED_MESSAGE("Use convertPointDataGridPosition() without inCoreOnly parameter instead. This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
inline void
|
||||
convertPointDataGridPosition( PositionAttribute& positionAttribute,
|
||||
const PointDataGridT& grid,
|
||||
const std::vector<Index64>& pointOffsets,
|
||||
const Index64 startOffset,
|
||||
const FilterT& filter,
|
||||
const bool /*inCoreOnly*/);
|
||||
const FilterT& filter = NullFilter(),
|
||||
const bool inCoreOnly = false);
|
||||
|
||||
|
||||
/// @brief Convert the attribute from a PointDataGrid
|
||||
@@ -164,6 +156,7 @@ convertPointDataGridPosition( PositionAttribute& positionAttribute,
|
||||
/// @param arrayIndex the index in the Descriptor of the array to be converted.
|
||||
/// @param stride the stride of the attribute
|
||||
/// @param filter an index filter
|
||||
/// @param inCoreOnly true if out-of-core leaf nodes are to be ignored
|
||||
template <typename TypedAttribute, typename PointDataTreeT, typename FilterT = NullFilter>
|
||||
inline void
|
||||
convertPointDataGridAttribute( TypedAttribute& attribute,
|
||||
@@ -172,19 +165,8 @@ convertPointDataGridAttribute( TypedAttribute& attribute,
|
||||
const Index64 startOffset,
|
||||
const unsigned arrayIndex,
|
||||
const Index stride = 1,
|
||||
const FilterT& filter = NullFilter());
|
||||
|
||||
template <typename TypedAttribute, typename PointDataTreeT, typename FilterT = NullFilter>
|
||||
OPENVDB_DEPRECATED_MESSAGE("Use convertPointDataGridAttribute() without inCoreOnly parameter instead. This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
inline void
|
||||
convertPointDataGridAttribute( TypedAttribute& attribute,
|
||||
const PointDataTreeT& tree,
|
||||
const std::vector<Index64>& pointOffsets,
|
||||
const Index64 startOffset,
|
||||
const unsigned arrayIndex,
|
||||
const Index stride,
|
||||
const FilterT& filter,
|
||||
const bool /*inCoreOnly*/);
|
||||
const FilterT& filter = NullFilter(),
|
||||
const bool inCoreOnly = false);
|
||||
|
||||
|
||||
/// @brief Convert the group from a PointDataGrid
|
||||
@@ -195,26 +177,18 @@ convertPointDataGridAttribute( TypedAttribute& attribute,
|
||||
/// @param startOffset a value to shift all the point offsets by
|
||||
/// @param index the group index to be converted.
|
||||
/// @param filter an index filter
|
||||
/// @param inCoreOnly true if out-of-core leaf nodes are to be ignored
|
||||
///
|
||||
template <typename Group, typename PointDataTreeT, typename FilterT = NullFilter>
|
||||
inline void
|
||||
convertPointDataGridGroup( Group& group,
|
||||
const PointDataTreeT& tree,
|
||||
const std::vector<Index64>& pointOffsets,
|
||||
const Index64 startOffset,
|
||||
const AttributeSet::Descriptor::GroupIndex index,
|
||||
const FilterT& filter = NullFilter());
|
||||
|
||||
template <typename Group, typename PointDataTreeT, typename FilterT = NullFilter>
|
||||
OPENVDB_DEPRECATED_MESSAGE("Use convertPointDataGridGroup() without inCoreOnly parameter instead. This method is deprecated and will be removed. Delayed loading is no longer supported.")
|
||||
inline void
|
||||
convertPointDataGridGroup( Group& group,
|
||||
const PointDataTreeT& tree,
|
||||
const std::vector<Index64>& pointOffsets,
|
||||
const Index64 startOffset,
|
||||
const AttributeSet::Descriptor::GroupIndex index,
|
||||
const FilterT& filter,
|
||||
const bool /*inCoreOnly*/);
|
||||
const FilterT& filter = NullFilter(),
|
||||
const bool inCoreOnly = false);
|
||||
|
||||
// for internal use only - this traits class extracts T::value_type if defined,
|
||||
// otherwise falls back to using Vec3R
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user