Compare commits

...
Author SHA1 Message Date
Jonathan Swartz 3296fd06e2 add CUDA DLL path to Windows examples
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-07-31 11:12:12 +12:00
Jonathan Swartz 9102630227 fix Windows example DLL search
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-07-31 09:32:40 +12:00
Jonathan Swartz e72bf3e86f diagnose Windows example crashes
Enable Python's fault handler and unbuffered output for example subprocesses so Windows access violations report the active Python frame and preserve output leading up to the crash.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-07-30 21:41:57 +12:00
Jonathan Swartz 4a97f065af fix Windows example test startup
Register dependent DLL directories through a test-only sitecustomize module so example smoke tests can run as normal Python scripts. This avoids the native access violations caused by executing them through runpy while preserving subprocess isolation.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-07-30 17:10:53 +12:00
Jonathan Swartz 0bc279c387 fix example DLL lookup on Windows
Run example smoke tests through a Windows-specific child launcher that
  registers dependent DLL directories before importing nanovdb. Keep the
  DLL directory handles alive while preserving subprocess isolation and
  normal script execution behavior.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-07-30 15:58:34 +12:00
Jonathan Swartz 6ce0707aae Merge branch 'master' into feature/nanovdb_python 2026-07-30 14:10:21 +12:00
Jonathan Swartz 75ed0cd57a Merge pull request #2217 from swahtz/docs_qol_improvements
Add docs quality CI gates and clear the doxygen warning backlog
2026-07-30 09:44:39 +12:00
Jonathan SwartzandClaude Opus 5 1f885014b8 Restore full historical signatures in changes.txt release notes
Per review feedback, the converted release-note entries now spell out the
complete historical signature (including parameters) rather than a bare
name, so readers still see exactly what each change referred to.

Uses <TT> rather than @c for these, since @c only formats the next
whitespace-delimited word and cannot span a signature with arguments.
Follows the existing <TT> precedent in this file.

Note that argument-qualified @vdblink/@link targets do not resolve under
the versioned inline namespace even when the API still exists, so plain
text is required here regardless of whether the symbol was removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-07-29 23:04:37 +12:00
Ken MusethandAndre Pradhana b7fc4fc7af Fix read accessor (#2260)
* addressed bug in nanovdb::ReadAccesor<0,1,2>

Signed-off-by: Ken Museth <ken.museth@gmail.com>

* fixed build of vdb_tool with OpenEXR on OSX

Signed-off-by: Ken Museth <ken.museth@gmail.com>

* changed default to off

Signed-off-by: Ken Museth <ken.museth@gmail.com>

* updated nanovdb.txt

Signed-off-by: Ken Museth <ken.museth@gmail.com>

* improved openvdb_cmd/vdb_tool/CMakeLists.txt

Signed-off-by: Ken Museth <ken.museth@gmail.com>

* Clean up vdb_tool OpenEXR/Imath CMake logic.

Signed-off-by: Andre Pradhana <andre.pradhana@gmail.com>

* Simplify Old ReadAccessor CMake option and ensures users who include NanoVDB.h directly get the new ReadAccessor behavior by default.

Signed-off-by: Andre Pradhana <andre.pradhana@gmail.com>

* reviewer feedback

Signed-off-by: Ken Museth <ken.museth@gmail.com>

* improved writing

Signed-off-by: Ken Museth <ken.museth@gmail.com>

---------

Signed-off-by: Ken Museth <ken.museth@gmail.com>
Signed-off-by: Andre Pradhana <andre.pradhana@gmail.com>
Co-authored-by: Andre Pradhana <andre.pradhana@gmail.com>
2026-07-27 16:43:55 -07:00
Jonathan Swartz 56287f697a Fix typo of quads/quad parameter
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-07-25 13:40:41 +12:00
Jonathan Swartz 32dac53ce1 Merge branch 'master' into docs_qol_improvements 2026-07-25 13:38:23 +12:00
Jonathan Swartz 8e5b1bc07e Updated all 26 affected historical references in doc/changes.txt.
- Removed APIs now use @c with fully qualified historical names, such as @c
    openvdb::Grid::signedFloodFill().

  - Restored names omitted entirely by the PR, including the Transform methods and
    Filter::laplacian().

  - Kept createLevelSetSphere() as a corrected @vdblink because it still exists.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-07-24 16:56:17 +12:00
apradhana 332a57d9d6 Merge pull request #2221 from apradhana/ci/skip-drafts-pr
Skip build CI if a PR is a draft.
2026-07-22 21:30:26 -07:00
mmustafasenoglu 50e1563078 fix: add else to constexpr-if chain in evalFirstCached to eliminate MSVC C4702 warning (#2252)
When either constexpr-if condition in the evalFirstCached lambda
evaluates to true, the remaining code including the return statement
is unreachable. MSVC emits warning C4702 (unreachable code) in this
case.

Add else keywords to form a proper constexpr-if / else-if / else
chain so that the unreachable code path is excluded from the
instantiated function body.

Fixes #2239

Signed-off-by: Mustafa Senoglu <mmustafasenoglu0@gmail.com>
2026-07-20 20:28:43 -07:00
Ken Museth 4775fa6079 Changed cmake for vdb_tool to use OpenEXR v3 vs v2 (#2253)
* fixed cmake for vdb_tool to use OpenEXR v3

Signed-off-by: Ken Museth <ken.museth@gmail.com>

* added min version to cmake for imath

Signed-off-by: Ken Museth <ken.museth@gmail.com>

---------

Signed-off-by: Ken Museth <ken.museth@gmail.com>
2026-07-17 23:32:36 -07:00
Ken Museth fb4516e3e9 Squash NanoVDB and vdb_tool contributions (#2241)
Signed-off-by: Ken Museth <ken.museth@gmail.com>
2026-07-15 15:45:42 -07:00
Jonathan Swartz a532de5526 Fix vdb_view Windows build: include windows.h before GL/glu.h (#2247)
GL/glu.h uses APIENTRY without defining it. This previously compiled because
oneTBB (<= 2022.3) transitively included windows.h from its public headers;
oneTBB 2023.0 removed that include, so APIENTRY is no longer defined when
RenderModules.cc includes GL/glu.h. GL/glew.h cannot provide it either: it
undefines APIENTRY at the end of the header when it was the one defining it
(and the vcpkg glew package additionally patches out glew.h's internal
GL/glu.h include). Include windows.h explicitly before GL/glu.h instead of
relying on a transitive include.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-07-09 14:54:49 -07:00
Andrew acca53b4f8 Adding semantics for VoxelBVH. (#2227)
* Adding semantics for VoxelBVH.

Signed-off-by: Andrew Reidmeyer <areidmeyer@nvidia.com>

* Adding range semantic.

Signed-off-by: Andrew Reidmeyer <areidmeyer@nvidia.com>

* Support voxelbvh value sidecar.

Signed-off-by: Andrew Reidmeyer <areidmeyer@nvidia.com>

---------

Signed-off-by: Andrew Reidmeyer <areidmeyer@nvidia.com>
2026-07-07 09:10:12 -07:00
Jonathan Swartz 7c172d99a4 nanovdb python: port host C++ examples to Python, smoke-test all examples
Nine new scripts under python/examples/, each a Python equivalent of a
host-capable C++ example (or its host code path):

- io_roundtrip.py: ex_write_nanovdb_grids + ex_read_nanovdb_sphere(_accessor)
  as a self-contained write/read round trip, plus splitGrids and zero-copy
  point positions via getBlindData.
- make_funny_nanovdb.py: ex_make_funny_nanovdb via the functor-based
  createFloatGrid factory (reduced domain — one Python call per voxel).
- make_typed_grids.py: ex_make_typed_grids across seven tools.build grid
  types with polymorphic re-read.
- raytrace_level_set.py / raytrace_fog_volume.py: the host render paths,
  re-expressed with the bound samplers (sphere tracing + zeroCrossing +
  gradient shading) and accessor marching respectively; PGM output.
- collide_level_set.py: the host particle-collision path using
  worldToIndexF, narrow-band isActive, and sampler.gradient normals.
- index_grid_channels.py: host half of ex_index_grid_cuda extended with
  ChannelAccessor read-back and CreateNanoGrid blind-data authoring.
- node_manager.py: host half of ex_nodemanager_cuda.
- openvdb_interop.py: ex_openvdb_to_nanovdb(_accessor); self-skips unless
  built with NANOVDB_USE_OPENVDB and openvdb imports.

Not ported: the pool-buffer examples (HostBuffer pool API unbound), the
MagicaVoxel converter (external parser/asset), device-only examples, and
a dedicated PointAccessor example (host point primitives bake UInt32
PointData grids; Point{Index,Data}Accessor require GridType.PointIndex).

test/TestExamples.py smoke-runs all 14 example scripts in subprocesses;
wired as ctest pytest_nanovdb_examples (~5 s) next to pytest_nanovdb,
gated on NANOVDB_BUILD_PYTHON_UNITTESTS.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-07-07 04:05:52 +00:00
Jonathan Swartz 86e975ba58 Merge remote-tracking branch 'upstream/master' into feature/nanovdb_python
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-07-06 21:49:24 +00:00
Jonathan Swartz 1a3a7b7b10 nanovdb python: index-grid completeness, transform aliases, sampler gradients
Close the residual gaps between the Python bindings and the C++ host API:

- Bind Grid.valueCount() on Index/OnIndex grids and Grid.pointCount() on
  PointGrid, gated per BuildT like the C++ SFINAE.
- Add tools.CreateNanoGrid mirroring the C++ converter class, exposing
  addBlindData() so blind-data channels can be authored from Python and
  filled through the writable getBlindData() NumPy view. Authored
  channels are zero-filled (the C++ path leaves them uninitialized).
- Bind ChannelAccessor<ChannelT, IndexT> for Index/OnIndex x
  float/double/int32/Vec3f, plus a polymorphic createChannelAccessor()
  dispatching on the channel's recorded dataType.
- Add the worldToIndex/indexToWorld convenience names (Dir/Grad/F
  variants included) as aliases on the type-erased Grid base.
- Bind gradient() and zeroCrossing() on the host samplers for the
  orders/BuildTs where the C++ templates compile (trilinear gradient,
  trilinear+triquadratic zeroCrossing, floating-point only).
- Fix a pre-existing bug: getBlindData() and the PointAccessor views
  built ndarrays without the nb::numpy framework tag, so the def-site
  keep_alive raised 'could not create a weak reference' whenever a
  non-empty view was returned.
- Paper cuts: restore the empty error messages in the sphere/torus
  primitive dispatchers, drop the unused baseName parameter from
  defineStats, and bind Checksum.isEmpty/isHalf/isFull/mode.

TestNanoVDB.py grows 139 -> 161 cases, covering the new surface plus the
previously untested populated blind-data and channel-accessor paths.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-07-03 02:54:47 +00:00
Mark HarrisandClaude Opus 4.8 ca21615c13 NanoVDB: add stream-ordered async memory-resource seam (CUDA) (#2231)
* NanoVDB: add stream-ordered async memory-resource seam (CUDA)

Introduce an instance-based async memory-resource concept on NanoVDB's CUDA
path so downstream code can inject its own allocator without forking headers,
with zero change to existing call sites at the default.

- cuda::DeviceResource: add instance allocate_async/deallocate_async alongside
  the existing static methods; add default_resource<R>() (a program-lifetime
  singleton) and the is_async_resource<R> trait.
- cuda::PinnedResource: new host-pinned resource modelling the same concept
  (default impl wraps cudaMallocHost/cudaFreeHost).
- cuda::TempPool: route allocations through a resource instance and free on the
  stream of the most recent reallocate() at destruction, not the null stream.
- tools::cuda::PointsToGrid: route internal scratch and mDeviceData through an
  injected resource instance via a defaulted ResourceT& constructor argument.
- unittest: add TestMemoryResource.cu (TestMemoryResource suite) with
  resource-concept, pinned page-lock, stream-retained-free, no-leak, and
  PointsToGrid instance-injection tests.

TopologyBuilder/DistributedPointsToGrid routing and the dual-buffer pinned-host
fix allocate via cuda::DeviceBuffer and are deferred to a follow-up that
introduces a resource-parameterized buffer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Mark Harris <mharris@nvidia.com>

* NanoVDB: make cuda::PinnedResource synchronous; document is_async_resource

cudaMallocHost / cudaFreeHost are synchronous, so PinnedResource now
models the synchronous Resource concept -- allocate(bytes, alignment) /
deallocate(ptr, bytes, alignment), no stream -- instead of carrying the
stream-ordered AsyncResource shape with an ignored stream argument. The
static allocateAsync / deallocateAsync helpers are removed; the type is
allocator-stateless, so the instance methods call CUDA directly.

is_async_resource<PinnedResource> is now false; the unit test asserts
that and exercises the synchronous allocate / deallocate API.

Add a @code example to is_async_resource documenting the if-constexpr
dispatch between a stream-ordered allocate_async and a synchronous
allocate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Mark Harris <mharris@nvidia.com>

---------

Signed-off-by: Mark Harris <mharris@nvidia.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 21:06:33 -07:00
Jonathan Swartz cf2f1493b9 Merge pull request #2238 from swahtz/nanovdb_n-ary_mergegrids
NanoVDB: generalize `tools::cuda::MergeGrids` to an N-ary union
2026-06-26 10:26:42 +12:00
Jonathan Swartz b94efdd07d Merge pull request #2198 from swahtz/fix/nanovdb_python_write_grids_drop
nanovdb/python: fix writeGrids dropping all but the last grid
2026-06-25 08:30:02 +12:00
Jonathan Swartz e332cc54d6 Merge branch 'master' into docs_qol_improvements 2026-06-24 14:13:02 +12:00
AndrewandJonathan Swartz 90c02c309d Bugfix for GridHandle::read(). (#2237)
* Bugfix for GridHandle::read().

Signed-off-by: Andrew Reidmeyer <areidmeyer@nvidia.com>

* Simplify.

Signed-off-by: Andrew Reidmeyer <areidmeyer@nvidia.com>

* Adding pendingchanges file.

Signed-off-by: Andrew Reidmeyer <areidmeyer@nvidia.com>

* Tweak wording.

Signed-off-by: Andrew Reidmeyer <areidmeyer@nvidia.com>

* Add a test

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* Requested change.

Signed-off-by: Andrew Reidmeyer <areidmeyer@nvidia.com>

---------

Signed-off-by: Andrew Reidmeyer <areidmeyer@nvidia.com>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
Co-authored-by: Jonathan Swartz <jonathan@jswartz.info>
2026-06-23 18:18:56 -07:00
Dan Bailey 63a052f4b4 Merge pull request #2222 from Idclip/ax_ast_doxy
Suppress some doxygen warings in AST.h
2026-06-17 21:17:29 -07:00
Dan Bailey c1b7dd251a Merge pull request #2234 from AcademySoftwareFoundation/feature/fix_windows_static_ci
Fix Windows Static CI
2026-06-17 21:15:32 -07:00
Dan Bailey 0dda5483b5 Add changes
Signed-off-by: Dan Bailey <danbailey@ilm.com>
2026-06-17 21:14:23 -07:00
jmlait 4ed2573179 Merge pull request #2233 from sideeffects/sendupstream_h22sync
Houdini 22 Sync
2026-06-17 16:33:23 -04:00
jmlait a6a9dcca29 Merge pull request #2235 from sideeffects/sendupstream_221support
Fix missing 221 file support
2026-06-17 16:05:17 -04:00
Jonathan Swartz d8c9ee5c5a nanovdb: generalize cuda MergeGrids to an N-ary union
tools::cuda::MergeGrids merged exactly two device OnIndex grids, but its
algorithm is already an accumulate-into-shared-masks union, so it extends
to any number of inputs without changing the kernels.

- Add an N-ary constructor taking std::vector<const GridT*>; the binary
  (grid1, grid2) constructor now delegates to it, leaving current callers
  unchanged. Loop over the inputs in mergeRoot / mergeInternalNodes /
  mergeLeafNodes instead of handling two grids explicitly; guard the
  empty list.
- A single N-ary pass uses one accumulator and reads each input once,
  versus materializing log(N) intermediate grids when chaining binaries.

Add TestNanoVDBCUDA.MergeGridsNary_ValueOnIndex: verify a 3-grid merge
matches chaining the binary form (and edge cases: single-element copy,
empty throws).

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-06-16 23:12:24 +00:00
Andre Pradhana ff3709c618 Run CI in nanovdb if it is push OR non-draft PR or matching manual run.
Signed-off-by: Andre Pradhana <andre.pradhana@gmail.com>
2026-06-15 20:55:46 -07:00
Jeff Lait c58736a68b Pending change for 221 fix.
Signed-off-by: Jeff Lait <jlait@andorra.sidefx.com>
2026-06-12 14:11:55 -04:00
Jeff Lait 31a6a29a6f Must use getFormatVersion as readGridCompression is static.
Signed-off-by: Jeff Lait <jlait@andorra.sidefx.com>
2026-06-12 14:11:03 -04:00
Jeff Lait defd4de2cf Fix the missing NODE_MASK_COMPRESSION tests in Archive.cc, restoring
the code from the earlier versions

Signed-off-by: Jeff Lait <jlait@andorra.sidefx.com>
2026-06-12 14:10:50 -04:00
Jeff Lait ecd3f084d6 Add pending changes.
Signed-off-by: Jeff Lait <jlait@andorra.sidefx.com>
2026-06-11 15:35:34 -04:00
Jeff Lait d257946b18 Synchnronization with Houdini 22.
- UT_CameraParms changes
- Assume Uniform option in Filter to prevent any anistropy being
  introduced.
- format override to pretty print ut types
- Sync of the reference files

Signed-off-by: Jeff Lait <jlait@andorra.sidefx.com>
2026-06-11 15:31:13 -04:00
Dan Bailey bce1c3aaa1 Fix an issue with blosc zlib on Windows static lib (use zlib that OpenVDB links against directly)
Signed-off-by: Dan Bailey <danbailey@ilm.com>
2026-06-10 22:41:46 -07:00
Jonathan Swartz 0130f7a20b Comment update
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-06-01 21:30:10 +00:00
Jonathan Swartz 6d65d9e840 Gate docs CI on Doxygen's native WARN_AS_ERROR; drop the warning filter
With the changes.txt, Half.h and ComputePrimitives.h warnings fixed, the
per-pattern suppression layer has served its purpose. Switch the CI docs build
to Doxygen's native FAIL_ON_WARNINGS so any net-new warning fails the PR
directly, and remove the filter scaffolding:

* .github/workflows/docs.yml passes -DOPENVDB_DOXYGEN_WARN_AS_ERROR=ON and
  drops the check_doxygen_warnings step. The warning log is still uploaded as
  an artifact (FAIL_ON_WARNINGS writes it before exiting non-zero).
* ci/check_doxygen_warnings.py and doc/.doxygen_warning_suppress.txt removed.
* doc/CMakeLists.txt option comment updated to note CI now uses the gate.

NOTE: the docs job stays red until the remaining openvdb_ax/ast/AST.h
INLINE_INHERITED_MEMB warning (handled separately) is merged into this branch
-- it is the only warning the build still emits.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-06-01 21:28:22 +00:00
Jonathan Swartz c157c94c14 Replace cxx14::make_index_sequence shim with std:: in ComputePrimitives.h
The cxx14 namespace was a C++11 forward-compat reimplementation of
std::index_sequence / std::make_index_sequence. NanoVDB requires C++17
(MINIMUM_CXX_STANDARD 17), so the standard versions in <utility> (already
included) are guaranteed present, and openvdb/Types.h already uses them.

Switching to std:: deletes the recursive-inheritance template
(make_index_sequence<N> : make_index_sequence<N-1, ...>) that Doxygen's
class-relation analyser flagged as a "potential recursive class relation".
Doxygen documents template entities rather than instantiations, so the
recursive base resolves back to the same class and looks like a self-loop;
it can't see that the <0, Is...> specialization terminates the recursion.
Removing the shim eliminates the warning at the source for every compiler,
not just the CI Doxygen, so its suppression entry is dropped too. The only
remaining backlog entry is the AST.h INLINE_INHERITED_MEMB interaction.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-06-01 21:19:48 +00:00
Jonathan Swartz 4d923768fa nanovdb python: release the GIL around VoxelBlockManager compute kernels
Addresses review feedback on PR #2219.

buildVoxelBlockManager, decodeBlock, and decodeInverseMaps all run
non-trivial C++ (the build may parallelize internally via
util::forEach; the decode sweeps multiple leaves) while holding the
GIL, blocking unrelated Python threads.

Release the GIL around the pure-C++ kernels only, matching the
established pattern in PyCreateNanoGrid.cc (tryIndexify / tryQuantizeFpX):

- pyDecodeInverseMapsImpl (shared by decodeBlock and the free
  decodeInverseMaps): scope a gil_scoped_release around the
  VBM::decodeInverseMaps call. The surrounding grid cast, bounds checks
  (which throw Python exceptions), and the NumPy array / capsule / tuple
  construction stay under the GIL.
- buildVoxelBlockManager: scope a gil_scoped_release around the
  buildVoxelBlockManager<LBW> call only.

Deliberately not a blanket nb::call_guard<nb::gil_scoped_release>() as
suggested: these entry points take an nb::handle and call
castOnIndexGrid (isinstance/cast) plus allocate NumPy arrays inside the
function body, so dropping the GIL across the whole call would touch the
Python C API without the GIL. The scoped release covers exactly the
GIL-free compute.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-05-29 11:52:07 +00:00
Jonathan Swartz 7a1e66396d nanovdb python: harden mergeGrids for empty input and use gridData(n)
Addresses review feedback on PR #2219.

Empty input: mergeGrids([]) — or a sequence of only-empty handles —
produced totalGrids == 0 and called BufferT::create(0). For HostBuffer
that yields a buffer whose data() is non-null over a zero-byte region,
so the GridHandle(buffer&&) ctor then reads a full GridData header out
of it (heap-overflow read; in practice an opaque "invalid host buffer"
throw). Return an empty handle up front when there's nothing to merge.

Per-grid source pointer: copy from h->gridData(n) — the authoritative
start pointer that applies mMetaData[n].offset — instead of walking a
raw data() pointer advanced by gridSize(n). The two are equivalent for
the current tightly-packed layout (offsets are a running sum of grid
sizes), but the accessor form doesn't bake in that assumption and drops
the manual pointer arithmetic.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-05-29 11:32:37 +00:00
Nick Avramoussis f9754140ba Merge pull request #2205 from Idclip/ax_unit_test_fixes
Minor AX unit test fix
2026-05-29 18:04:12 +12:00
Jonathan Swartz 2ea583eafd Clear the changes.txt and Half.h doxygen warning suppressions
Whittles two more entries off doc/.doxygen_warning_suppress.txt by fixing
the underlying warnings instead of suppressing them.

doc/changes.txt (26 unresolved-link warnings -> 0):
* Historical Release Notes \link references to APIs that were since renamed
  or removed are delinked to @c <name> (the file's existing inline-code
  style), or dropped to plain prose where the link text was an English word
  woven into a sentence. createLevelSetSphere() is requalified tools:: (it
  only lacked the namespace, matching the working link elsewhere in the file).

doc/CMakeLists.txt:
* EXCLUDE_SYMBOLS no longer carries a capital-I "*Internal*" glob: it also
  matched the public tree::InternalNode / nanovdb::InternalNode classes,
  silently dropping them from the docs and breaking InternalNode::childCount
  in changes.txt while its RootNode/LeafNode siblings resolved. Both classes
  are now documented and the link resolves.
* OPENVDB_API is predefined empty so Doxygen stops folding the export macro
  into the name of `enum OPENVDB_API FromBitsTag` in the vendored Half.h.

ext/imath/openvdb/math/Half.h (vendored, OpenVDB-adapted Imath fork):
* @file half.h -> @file Half.h (the file was renamed during vendoring).
* The internal half-float limit/config macros are wrapped in @cond/@endcond:
  Doxygen auto-links the literal `#define` token on documented active macros,
  and these are implementation details (the half type lives in the excluded
  internal namespace).
* The file-level description now renders cleanly: its ASCII bit-layout
  diagrams, formulas and example tables are wrapped in <pre>, and its
  double-backtick code spans are reduced to single backticks (the double
  form broke Doxygen 1.14's markdown paragraphing and collapsed the prose).

doc/.doxygen_warning_suppress.txt:
* Drop the changes.txt and Half.h entries; both are now warning-clean.
  Remaining backlog: the ComputePrimitives.h recursive-template false
  positive and the AST.h INLINE_INHERITED_MEMB interaction.

Verified against Doxygen 1.14 (the CI version): 0 surviving warnings.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-05-29 06:03:06 +00:00
Jonathan Swartz 88a4f90569 nanovdb python: bounds-check GridHandle grid accessors; fix stale docstring
Addresses review feedback on PR #2219.

GridHandle::gridSize(n) and gridType(n) index mMetaData[n] without a
bounds check (the C++ API documents "assumed to be less than
gridCount()"), so calling handle.gridSize()/gridType()/gridData() with
an out-of-range n — including any n on an empty handle — was undefined
behaviour reachable straight from Python. Wrap all three in
bounds-checked lambdas that raise IndexError. gridData() is guarded too:
gridData(n) itself returns nullptr for bad n, but the binding passes
gridSize(n) alongside it, and that call is the one that reads
mMetaData[n] out of bounds.

Also refresh the createOnIndexGrid docstring: it claimed the broader
createNanoGrid<SrcGridT, DstBuildT> surface "lands in a later phase",
but tools.createNanoGridOnIndex already ships in this PR
(PyCreateNanoGrid.cc). Point users at the canonical binding instead.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-05-29 04:27:11 +00:00
Nick Avramoussis c7fee6b08a Suppress some doxygen warings in AST.h
Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com>
2026-05-28 11:55:42 +01:00
Andre Pradhana 83c29033f2 Skip build CI if a PR is a draft.
Signed-off-by: Andre Pradhana <andre.pradhana@gmail.com>
2026-05-27 12:07:01 -07:00
Eftychios SifakisandClaude Sonnet 4.6 153c7ce5b1 Add MeshToGrid: CUDA triangle mesh to NanoVDB UDF/IndexGrid converter (#2178)
* Add MeshToGrid: CUDA triangle mesh to NanoVDB IndexGrid/UDF converter

Introduces nanovdb::tools::cuda::MeshToGrid<BuildT>, a GPU-accelerated
rasterizer that converts a triangle soup (vertex list + index list) into
a sparse NanoVDB grid on the device. Two output modes are implemented:

  - getHandle()         — produces a ValueOnIndex (topology-only) grid
  - getHandleAndUDF()   — produces the same grid plus a device sidecar
                          buffer of per-active-voxel unsigned distances
                          (world-space UDF, ordered by active voxel index)

SDF (signed distance field) output is planned future work.

The pipeline uses a hierarchical top-down subdivision approach:
1. transformTriangles()       — vertices transformed to NanoVDB index space
                                via nanovdb::Map::applyInverseMap()
2. processRootTrianglePairs() — enumerates (root tile, triangle) pairs whose
                                padded AABBs overlap, via CUB prefix-sum +
                                scatter (no atomics)
3. processLeafTrianglePairs() — 3-level 8x subdivision (4096→512→64→8),
                                refining the pair list at each level using
                                triangle-AABB / SAT intersection tests
                                (evaluateAndCountSubBoxesKernel: 1 CTA per
                                pair, 512 threads, warp ballot + CUB reduce)
4. Topology assembly          — unique leaf origins extracted, NanoVDB
                                ValueOnIndex tree built via TopologyBuilder,
                                empty leaves pruned via PruneGrid
5. UDF computation (optional) — ComputeUDFFunctor computes per-voxel
                                point-to-triangle distances via atomic-min,
                                finalized to world-space in a second pass

The example driver (ex_mesh_to_grid_cuda) reads an OBJ file, builds an
OpenVDB UDF as reference, and runs a thorough correctness validation:
  - Active voxel set comparison (false negatives, false positive rate)
  - Per-voxel UDF error histogram vs. OpenVDB reference (in voxel units)
  - CPU brute-force ground-truth check on the worst-error voxels

New files:
  nanovdb/nanovdb/tools/cuda/MeshToGrid.cuh
  nanovdb/nanovdb/math/Proximity.h         — point/triangle proximity utils
  nanovdb/nanovdb/util/cuda/Rasterization.cuh — ComputeUDFFunctor and helpers

Modified files:
  nanovdb/nanovdb/tools/cuda/TopologyBuilder.cuh — adds InitGridTreeRootFunctor
    for constructing grids from scratch (no source grid to copy metadata from)
  nanovdb/nanovdb/util/cuda/Util.h — adds operatorKernelInstance for launching
    pre-constructed device functors carrying data members
  nanovdb/nanovdb/examples/CMakeLists.txt — registers ex_mesh_to_grid_cuda

Signed-off-by: Efty Sifakis <esifakis@nvidia.com>

* Formatting changes

Signed-off-by: Efty Sifakis <esifakis@nvidia.com>

* Remove trailing whitespace in MeshToGrid.cuh and mesh_to_grid_cuda.cpp

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Efty Sifakis <esifakis@nvidia.com>

* Formatting changes in Rasterization.cuh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Efty Sifakis <esifakis@nvidia.com>

* Handle empty triangle list gracefully in MeshToGrid

Replace throws-on-zero with early returns throughout the pipeline so
that getHandle() and getHandleAndUDF() return a valid empty grid when
triangleCount == 0. Kernel launches over null sets are explicitly
guarded before any buffer pointer is dereferenced.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Efty Sifakis <esifakis@nvidia.com>

* Add unit tests for MeshToGrid: empty mesh and unit tetrahedron

MeshToGrid_EmptyMesh: verifies that getHandle() returns a valid grid
with zero active voxels when the triangle list is empty.

MeshToGrid_UnitTetrahedron: constructs a unit tetrahedron (4 vertices,
4 faces) with voxel size 0.1. Tests both getHandle() (topology-only)
and getHandleAndUDF() (with sidecar). Checks:
  - Topology: checksum from getHandle() equals that from getHandleAndUDF(),
    confirming both paths produce identical grids.
  - Per-voxel UDF: sidecar values match CPU brute-force (index-space
    pointToTriangleDistSqr * voxelSize) within 1e-3 voxels.
  - No false positives: every active voxel lies within the narrow band.
  - No false negatives: every voxel with CPU UDF strictly inside the band
    is active (checked via leaf-iteration active set).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Efty Sifakis <esifakis@nvidia.com>

* Slim down example driver: cleanup and leaner OpenVDB comparison

mesh_to_grid_cuda.cpp:
  - Remove unused #include <openvdb/tools/Morphology.h>
  - Fix cout label "openvdb_triangles" -> "triangles"
  - Replace atof(argv[3]) with std::stof
  - Remove spurious blank lines

mesh_to_grid_cuda_kernels.cu:
  - Remove unused #include <array> (leftover from std::array<Vec3f,3> era)
  - Collapse triple blank line after getHandleAndUDF() to single
  - Replace the detailed per-voxel histogram, top-N worst-voxel heap,
    and inline Ericson brute-force with a compact summary: active voxel
    counts, false negative/positive counts, and a single UDF quality
    line (max error + count exceeding 0.1 voxels vs OpenVDB reference)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Efty Sifakis <esifakis@nvidia.com>

* MeshToGrid: remove verbose>=2 diagnostic prints

All mVerbose>=2 guarded timer restarts and printf diagnostics have been
removed. Verbosity level 1 (function-scale start/stop timing) is retained.

Signed-off-by: Efty Sifakis <esifakis@nvidia.com>

* MeshToGrid: rename pool/gridPool params to buffer; remove diagnostic accessors

- getHandle: parameter renamed from pool to buffer, matching the
  convention used throughout NanoVDB tools (PointsToGrid, DilateGrid, etc.)
- getHandleAndUDF: parameters renamed from gridPool/sidecarPool to
  buffer/sidecarBuffer for the same reason
- Local raw-buffer variables renamed to gridBuffer to avoid shadowing
  the (now renamed) buffer parameter
- Remove getPairCount(), getDevicePairs(), getRootTileCount(), and
  getDeviceRootOrigins() — unused diagnostic-only accessors

Signed-off-by: Efty Sifakis <esifakis@nvidia.com>

* ci: trigger CI

Signed-off-by: Efty Sifakis <esifakis@nvidia.com>

* MeshToGrid: fix leaf-local X↔Z swap in CUDA rasterization kernels

Four kernels were decoding the per-thread leaf offset with the wrong bit
ordering (lx = vi & 7, lz = vi >> 6), transposing X and Z relative to
NanoVDB's Mask<3> convention. The kernels were self-consistent, so the
generated grid was a topologically valid X↔Z mirror of the input mesh's
UDF rather than a crash. Replace all four inline decodes with
NanoLeaf<BuildT>::OffsetToLocalCoord:

  - Rasterization.cuh: RasterizeLeafNodesFunctor, ComputeUDFFunctor
  - MeshToGrid.cuh:    evaluateAndCountSubBoxesKernel, ScatterChildPairsFunctor

Also fix the matching inline decodes in the unit test and example
verifiers, which were silently cancelling the bug on the consumer side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Efty Sifakis <esifakis@nvidia.com>

* Rasterization.cuh: qualify ::atomicOr to bypass namespace collision

The RasterizeLeafNodesFunctor lives in nanovdb::util::rasterization::cuda.
Unqualified atomicOr lookup from inside an innermost namespace named cuda
confuses nvcc's name resolution on CUDA 12.6 (clang/gcc on Linux): it
picks up an atomicOr(uint64_t*, uint64_t) overload from libcu++/cuda::
and then rejects the reinterpret_cast<unsigned long long*> argument with
"argument of type unsigned long long * is incompatible with parameter
of type uint64_t * (aka unsigned long *)". Qualifying as ::atomicOr
forces global-namespace lookup and resolves to the canonical CUDA
builtin atomicOr(unsigned long long*, unsigned long long).

Also pull the reinterpret_cast onto its own line into a typed pointer
variable, matching the pattern used by Morphology.cuh.

Mask::setOnAtomic in NanoVDB.h is unaffected because it lives directly
in nanovdb, not in a nested cuda namespace.

Signed-off-by: Efty Sifakis <esifakis@nvidia.com>

---------

Signed-off-by: Efty Sifakis <esifakis@nvidia.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 08:41:00 -07:00
Jonathan Swartz 03ed5c8f00 nanovdb python: examples + docstring coverage sweep (#2218)
* nanovdb python: Phase 6 — docs, migration guide, examples

Closes Phase 6 of the NanoVDB Python bindings restructure (#2208).
Ships:

* doc/nanovdb/PythonMigration.md — porting guide for users on the
  pre-Phase-1 typed-accessor API. Covers the polymorphic
  handle.grid(n) replacement of floatGrid() / doubleGrid() / etc.,
  the removal of the nanovdb.math.cuda submodule (sampleFromVoxels
  is now in nanovdb.tools.cuda), the GridHandle.__bool__ + enum
  __repr__ behavior changes, and a survey of every new surface that
  used to require dropping to C++ (GridMetaData, blind data,
  PointAccessor, tree/node walking, NodeManager, leaf_values,
  VoxelBlockManager, tools.build.Grid, stats, validation,
  primitives, quantized createNanoGrid). Closes with a mechanical
  before/after replacement table.

* doc/nanovdb/PythonAPI.md — narrative API reference for the final
  surface. One section per submodule (nanovdb root, math, io,
  tools, tools.build, tools.cuda, cuda) listing the bound classes
  and functions with one-line descriptions. Points readers at
  help(x) for full signatures.

* nanovdb/nanovdb/python/examples/ — five runnable .py scripts:
  - load_inspect.py: polymorphic handle.grid(n), GridMetaData
    introspection, mixed-type handle via mergeGrids.
  - build_grid.py: tools.build.FloatGrid setValue / ValueAccessor /
    WriteAccessor / .to_nanovdb() round-trip.
  - bulk_leaf_numpy.py: zero-copy (N_leaves, 512) NumPy view via
    grid.leaf_values(), with global-stats reduction and in-place
    mutation demonstrating the no-copy semantics.
  - quantize.py: createNanoGridFp{4,8,16} fixed-width + createNanoGridFpN
    with AbsDiff and RelDiff oracles. Prints the per-format size
    so users see the compression tradeoff at a glance.
  - validate.py: validateGrid / validateGrids / checkGrid / isValid
    plus evalChecksum / updateChecksum / validateChecksum.

* nanovdb/nanovdb/python/examples/README.md — index of the above
  with one-line summaries.

* nanovdb/nanovdb/Readme.md — added a "Python bindings" section
  linking the API reference, migration guide, and examples.

Each example is self-contained (builds its own input data,
prints a small stdout summary) and was smoke-tested locally
against the post-Phase-5 binding. The bulk_leaf_numpy example is
the only one requiring NumPy; it prints a friendly skip message
otherwise.

API reference docs via Sphinx / mkdocs are deferred to a follow-up;
the existing docstrings already make help() useful and the .pyi
type stubs (from Phase 0) cover IDE / type-checker integration.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: drop end-user docs, scrub refactor framing from examples

Two changes in response to review feedback:

* Remove doc/nanovdb/PythonAPI.md and doc/nanovdb/PythonMigration.md.
  End users don't need a separate narrative reference (help() and
  the .pyi stubs cover the same ground) and don't need a migration
  guide framed around the multi-phase refactor history.

* Rewrite the docstring blurb at the top of each example
  (load_inspect.py, build_grid.py, bulk_leaf_numpy.py, quantize.py,
  validate.py) so the prose describes what the example demonstrates
  about the current API, with no mention of which phase of the
  refactor added the feature. Same scrub on
  python/examples/README.md (drop the "bindings restructure" framing
  and the now-dead links to the deleted docs) and on
  nanovdb/nanovdb/Readme.md (drop the new "Python bindings" section
  header — just leave the single examples link alongside the existing
  Examples link).

Examples themselves remain unchanged in behavior; all five still
run cleanly against the current binding.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: docstring coverage sweep across the binding surface

Add a short prose docstring to every nanobind .def() and nb::class_
call that didn't already carry one. The audit dropped from 374
distinct undocumented call sites (3278 raw, counting per-BuildT
template duplicates) to 0.

Style:
* One short imperative sentence per method, focused on what it does
  in Python terms.
* For methods that exactly mirror a C++ member of the same name, a
  "See nanovdb::<Class>::<method> in NanoVDB.h." tail so users can
  find the canonical reference without us re-stating it.
* Class-level docstrings on every bound class (Grid, GridHandle,
  GridMetaData, Tree, Root, Upper, Lower, Leaf, NodeManager, the
  per-BuildT accessor subclasses, Map, Coord, BBox*, samplers,
  build::Grid + ValueAccessor + WriteAccessor, Extrema / Stats,
  oracle classes, Checksum, etc.).
* Two-to-three sentence blurbs on the entry-point classes that
  genuinely need orientation.

Files touched: every Py*.cc / Py*.h binding file under
nanovdb/nanovdb/python/ plus the three cuda/ binding files. No code
changes — only the trailing const char* docstring argument on .def
calls and the third arg of nb::class_. The auto-generated .pyi stub
output picks the new prose up automatically; help() shows the same
text at runtime.

Also drop the few remaining "Phase N" / "from Phase N follow-up"
references that survived in internal C++ comments in
PyCreateNanoGrid.cc, PyPrimitives.cc, and PyVoxelBlockManager.cc,
keeping the source consistent with the no-refactor-history policy
already applied to the public docstrings, tests, and examples.

Test plan: full pytest_nanovdb green locally (138/140; the two
errors are pre-existing BLOSC-disabled errors). Module compiles
cleanly with CUDA + nanobind stub generation enabled.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: address Copilot review on #2218

Two items from the review:

1. The Readme link to the Python examples directory used the wrong
   relative path. From nanovdb/nanovdb/Readme.md, the examples live
   at python/examples/ (sibling), not nanovdb/python/examples/
   (which would resolve to nanovdb/nanovdb/nanovdb/python/examples/).

2. The Map.applyMap / applyJacobian / applyInverseMap /
   applyInverseJacobian docstrings used the words "double precision"
   and "single precision" to describe the variant suffix. That
   wording is misleading: the F-suffixed variants use 32-bit math
   internally, the unsuffixed variants use 64-bit math, but BOTH
   return a vector whose dtype matches the input (Vec3f -> Vec3f,
   Vec3d -> Vec3d). Switching to "uses 64-bit math" / "uses 32-bit
   math" with an explicit "returns a vector of the same dtype as
   the input" tail makes the semantics unambiguous. Same fix on the
   Jacobian variants (which previously didn't mention precision at
   all on the 64-bit overloads, only on the 32-bit ones — also
   confusing).

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: clarify Grid.gridName docstring

Copilot caught that the Grid.gridName docstring described it as "the
in-header buffer; truncated if very long" — which is actually the
semantics of shortGridName(). gridName() reads the LONG-form name
from blind data when the HasLongGridName flag is set, falling back
to the in-header buffer otherwise. Reword the docstring to match
and explicitly point at shortGridName() for the truncated form.

(The other comment in the same review pass flagged the Examples
table in python/examples/README.md as having "double leading pipes"
but the actual file uses single-pipe GitHub-flavored markdown
syntax — verified via per-character inspection. The table renders
correctly on GitHub; no change needed there.)

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: clarify VBM offset and Root.bbox docstrings

Two more Copilot review items:

* VoxelBlockManagerHandle.firstOffset / lastOffset docstrings said
  "Linear leaf offset of the first/last block..." but these are
  sequential VOXEL indices, not leaf IDs. Switch the wording to
  "Sequential voxel index of the first/last active voxel covered
  by this handle" so users aren't confused into thinking the value
  is a leaf number.

* RootT.bbox docstring said "Index-space bounding box of every
  active tile" but RootNode::bbox() returns the bounding box of
  every active VALUE in the tree, not just the tile entries in the
  root table. Drop the misleading "tile" wording.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: remove dead code in bulk_leaf_numpy example

Copilot caught that bulk_leaf_numpy.py computed n_active_after via a
per-voxel sum over leaf.isActive() but never used the result — dead
code that linters would flag. Replace with leaf.getFirstValue(),
which is actually informative for what the surrounding comment is
trying to show (the values changed in place via the zero-copy
NumPy write) and prints in the same line.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

---------

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-05-22 01:00:46 +12:00
Jonathan Swartz f22bf6a8f7 nanovdb python: Phase 5 — primitives + quantized/index createNanoGrid (#2216)
* nanovdb python: Phase 5 — primitives + quantized/index createNanoGrid

Closes out Phase 5 of the NanoVDB Python bindings restructure (#2208).
Lands all three sub-phases (5a primitives, 5b quantized createNanoGrid
overloads, 5c generic createNanoGrid from build::Grid) in one PR.

Phase 5a — Host primitives
==========================

Bind the nine primitives that didn't ship with Phase 0:

* tools.createLevelSetBox(gridType, width, height, depth, ...) — narrow-
  band level set of a solid axis-aligned box.
* tools.createLevelSetBBox(gridType, width, height, depth, thickness, ...) —
  narrow-band level set of a hollow box wireframe.
* tools.createLevelSetOctahedron(gridType, scale, ...) — narrow-band
  level set of an octahedron.
* tools.createFogVolumeBox(...) and tools.createFogVolumeOctahedron(...) —
  the fog-volume counterparts of the above.
* tools.createPointSphere / createPointTorus / createPointBox(gridType,
  pointsPerVoxel, ...) — PointDataGrids scattered on the surface of each
  primitive shape.
* tools.createPointScatter(srcGrid, pointsPerVoxel, ...) — scatter a
  PointDataGrid into the active voxels of a NanoGrid<float> level set or
  fog volume. The C++ template also accepts double sources; the binding
  is float-only for simplicity (the runtime grid pointer carries the
  source BuildT and adding a per-type dispatch is mechanical follow-up).

Each non-point primitive is instantiated for float and double via the
same runtime-GridType switch the existing createLevelSetSphere et al.
already use. The FpN overloads of these primitives are deliberately
not bound here — quantization is reachable via Phase 5b's generic
createNanoGrid* path with explicit oracle and dither parameters.

Phase 5b — Quantized createNanoGrid overloads
=============================================

* tools.AbsDiff(tolerance=-1.0) and tools.RelDiff(tolerance=-1.0) —
  compression-oracle classes used by FpN. Both expose getTolerance /
  setTolerance and a truthy __bool__ that returns True iff the
  tolerance has been initialized (>= 0). The default tolerance of -1
  matches the C++ "uninitialized — fill in via init()" sentinel.

* tools.createNanoGridFp4 / Fp8 / Fp16(src, sMode, cMode, ditherOn,
  verbose) — quantize a float source into a fixed-bit-width grid.
  ditherOn adds sub-quantum noise to break up banding.

* tools.createNanoGridFpN(src, oracle, sMode, cMode, ditherOn, verbose) —
  variable-bit-width quantization. Two overloads: one accepting an
  AbsDiff oracle (the default), one accepting a RelDiff oracle. Python
  picks the right overload from the oracle argument's type.

The C++ Fp{4,8,16,N} preProcess templates static_assert SrcValueT ==
float, so the binding rejects double sources with a TypeError instead
of letting the compile-time assertion trip a hard abort.

Phase 5c — Generic createNanoGrid from build::Grid
==================================================

Each conversion entry accepts both NanoGrid<SrcBuildT> and
tools::build::Grid<SrcBuildT> as its source. Internally a small
template helper tries each source-side BuildT in turn (matching the
existing tryCreateOnIndexGrid pattern from the Phase 3 follow-up) and
dispatches via nb::isinstance.

* tools.createNanoGridIndex(src, channels=0, includeStats=True,
  includeTiles=True, verbose=0) — NEW. Bake any supported source into
  a NanoGrid<ValueIndex> with all voxels (active and inactive) given a
  uint64 sequential index. Original values can be carried as blind
  data when channels > 0.

* tools.createNanoGridOnIndex(src, ...) — same as Index but only the
  active voxels get a sequential index. Supersedes the Phase 3
  follow-up's tools.createOnIndexGrid test scaffold (which is kept
  alive in PyVoxelBlockManager.cc for backwards compatibility with
  the VBM tests; new code should prefer createNanoGridOnIndex).

Source types accepted by the index path are NanoGrid<float | double |
int32 | Vec3f> and tools::build::Grid<float | double | int32 | Vec3f>.
The same-type bake of build::Grid<T> -> NanoGrid<T> remains served by
the Phase 4a .to_nanovdb() method.

Test plan
=========

Three new TestCase classes covering: every primitive (TestNewPrimitives,
11 cases), every quantization path including double-source rejection
and oracle defaults (TestCreateNanoGridQuantized, 8 cases), and every
index/onindex source type including the build::Grid path
(TestCreateNanoGridIndex, 6 cases). All 25 new cases pass locally; the
full pytest_nanovdb suite stays green except for the two pre-existing
BLOSC-disabled errors in my local build.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: address Copilot review on #2216

Four items from the Phase 5 review, plus a follow-on segfault fix
caught while addressing the third item:

1. openToNanoVDB binding accidentally dropped the sMode argument and
   gave `base` a default of tools::StatsMode::Default — nonsense both
   semantically (base is an OpenVDB GridBase::Ptr) and structurally
   (the C++ template expects 4 args, the binding declared 3 keyword
   args). The CI build broke for openvdb-enabled configs with a
   "number of nb::arg annotations must match the argument count"
   static_assert from nanobind. Restore the correct signature: base
   has no default, sMode/cMode/verbose carry their defaults.

2. Octahedron primitive default name strings carried the upstream C++
   typo "octadedron" (sic). The C++ side keeps the misspelling for
   binary compatibility with old grids; correct it on the Python
   side since the default propagates into help() output and grid
   metadata where end-users see it.

3. The point primitives took a `gridType` argument that was
   misleading — it controls the intermediate level-set's precision
   the scatter starts from, not the returned PointDataGrid's type
   (which is always UInt32). While investigating the rename, I found
   that the createPointSphere<double> / createPointTorus<double> /
   createPointBox<double> code paths actually segfault during scatter
   with the current C++ implementation (only the float path is
   exercised by the C++ unit tests). Take Copilot's "remove it and
   always use float" alternative: drop the argument entirely. The
   binding now always uses the float intermediate level-set.
   createPointSphere / Torus / Box no longer have the parameter;
   their docstrings explicitly note "always returns a UInt32
   PointDataGrid".

4. The defineCreateNanoGridConversions doc-comment claimed sources
   were accepted for "every scalar/vector BuildT in BuildTypes.def
   that tools::createNanoGrid supports", which was wishful — the
   implementation actually tries a narrower set per destination kind.
   Spell out the actual matrix: quantized Fp{4,8,16,N} paths accept
   float only (the C++ preProcess static-asserts SrcValueT == float);
   the createNanoGridIndex / OnIndex paths accept float / double /
   int32 / Vec3f. Adding more source types is a one-line extension
   of the explicit try-each-SrcBuildT chains.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: address more Copilot review on #2216 + CI GPU gating

Four items in this round, three from Copilot plus a CI failure caught
on the linux-clang++ runner.

1. test_validateGrid_on_device_handle was gated only on the cuda
   submodule being importable, but the linux-clang++ CI runner has a
   CUDA toolkit installed (so the submodule loads) but no GPU
   driver. The test then crashed inside the C++ helper with
   "CUDA error 35: CUDA driver version is insufficient for CUDA
   runtime version". Switch the gate to the project's existing pair
   of helpers — nanovdb.isCudaAvailable() (build-time CUDA support)
   AND nanovdb.isGpuAvailable() (runtime device probe) — matching
   how TestPointsToGrid / TestSignedFloodFill / TestSampleFromPoints
   already gate. The test skips cleanly on CPU-only runners and
   driverless CUDA runners alike.

2. The Phase 5b/5c conversion helpers (tryQuantizeFpX, tryQuantizeFpN,
   tryIndexify) ran the heavy tools::createNanoGrid traversal while
   holding the Python GIL. Restructure each helper so the GIL is
   held only for the isinstance / cast dispatch, then released
   around the conversion itself (the source data lives in stable
   C++ storage anchored by the Python wrapper passed in via py_src,
   so it's safe to read without the GIL). Other Python threads can
   now make progress during large-grid quantization / indexing.

3. AbsDiff's class docstring said "pass an explicit positive value"
   for the tolerance, but the C++ operator bool() actually treats
   any non-negative value (including 0.0) as initialized. Reword to
   match what the API actually does.

4. The createNanoGridIndex/OnIndex source-rejection test was named
   test_index_rejects_unsupported_source with a comment about
   "bool / Boolean grids" but really only exercised the None case.
   Split into two cases with single, accurate purposes:
   - test_index_rejects_none — passes None; matches neither the
     NanoGrid nor the build::Grid isinstance arms.
   - test_index_rejects_unsupported_buildt — builds a
     tools.build.Vec3dGrid (structurally valid but a BuildT outside
     the float/double/int32/Vec3f source set) and confirms the
     try-each-SrcBuildT chain falls through to a TypeError.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: correct createPointScatter docs + add fog-rejection test

Copilot noted the createPointScatter binding docstring claimed the
source could be a "level set or fog volume", but the C++ implementation
explicitly checks srcGrid.isLevelSet() at line 1679 of
tools/CreatePrimitives.h and throws std::runtime_error("Expected a
level set grid") otherwise. Match the actual behavior: the docstring
now says the source must satisfy srcGrid.isLevelSet() and that
non-level-set sources (e.g. fog volumes) raise RuntimeError.

Add test_create_point_scatter_rejects_fog_volume to lock in the
behavior (build a fog volume sphere, confirm createPointScatter raises
RuntimeError).

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: fully-qualify nanovdb.tools.build.* in 5b/5c docs

Copilot noted the Phase 5b/5c TypeError messages and per-function
docstrings referred to the mutable build grids as "tools.build.FloatGrid",
but the actual Python import path is "nanovdb.tools.build.FloatGrid".
Replace every occurrence so help() / autocomplete / error messages
point at a path users can actually import.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

---------

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-05-21 23:28:42 +12:00
Jonathan Swartz 0e9bccd205 Add docs quality CI gates and clear the doxygen warning backlog
Motivated by issue #2161 (docs drift away from code), this change wires
in a layered set of gates so the same class of rot fails on the PR that
introduces it.

CI gates (new files):

* ci/validate_docs_references.py runs on every PR via a new validate_docs
  job. It scans doc/**/*.md and doc/**/*.txt and verifies that every
  #include <...> inside a fenced code block (Markdown or Doxygen @code)
  resolves to a real header in this checkout, and that every
  github.com/AcademySoftwareFoundation/openvdb/blob/<ref>/<path> URL
  points at a file on disk. Catches the exact pattern from #2161 (e.g.
  nanovdb/util/CudaDeviceBuffer.h -> nanovdb/cuda/DeviceBuffer.h).

* ci/check_doxygen_warnings.py filters the Doxygen warning log against
  doc/.doxygen_warning_suppress.txt and fails the build on any surviving
  warning. Replaces Doxygen's all-or-nothing WARN_AS_ERROR with a
  per-pattern gate that honours a documented backlog without blocking
  new regressions. Reports suppression hit counts so dead patterns are
  flagged automatically.

* doc/.doxygen_warning_suppress.txt is the explicit triage backlog: four
  narrowly-scoped, heavily-commented regex patterns (doc/changes.txt
  historical \link rot, vendored ext/imath/Half.h, a Doxygen recursive
  variadic template false positive in ex_util/ComputePrimitives.h, and
  the INLINE_INHERITED_MEMB + unnamed-override-param interaction in
  openvdb_ax/ast/AST.h).

* .github/workflows/docs.yml runs the validator first, then doxygen,
  then the warning check (which fails the job on any unsuppressed
  warning), and always uploads the doxygen.warnings.log as an artifact.
  Drops '**.md' from paths-ignore so doc-only PRs are still gated.

Single-source-of-truth fix for the bug in #2161:

* doc/nanovdb/HelloWorld.md and four backing example sources
  (ex_openvdb_to_nanovdb_accessor, ex_read_nanovdb_sphere_accessor,
  ex_read_nanovdb_sphere_accessor_cuda) now use Doxygen's \snippet
  mechanism instead of duplicating code into markdown. The Hello World
  rendered on openvdb.org will always match what is built and tested in
  CI. The exact namespace drift from #2161 is now structurally
  impossible to reintroduce.

* doc/CMakeLists.txt sets DOXYGEN_EXAMPLE_PATH/EXAMPLE_RECURSIVE so
  \snippet paths resolve, captures DOXYGEN_WARN_LOGFILE, and adds an
  OPENVDB_DOXYGEN_WARN_AS_ERROR option for local opt-in to Doxygen's
  native all-or-nothing gate.

Doxygen warning backlog (44 of 45 surviving warnings fixed in this PR):

* nanovdb/GridHandle.h - bogus @param verbose removed; 3x
  @param filename -> @param fileName; @param pool -> @param other.
* nanovdb/NanoVDB.h - 3x @param ijk -> @param xyz on
  Map::apply*Jacobian* methods; fixed @node -> @note typo.
* nanovdb/util/PrefixSum.h - added missing @param op documentation.
* openvdb/TypeList.h - backtick-wrapped 'std::get<I>(mTuple)' so
  Doxygen stops parsing <I> as an unterminated HTML italics tag.
* openvdb/tools/RayTracer.h - added explicit @brief docs to seven
  shader-subclass operator() overrides so the inherited @param nml/dir
  no longer conflict with the override signatures.
* openvdb/version.h.in - @file path now matches the actual filename.
* openvdb_houdini/ParmFactory.h - 2x \link references qualified with
  the houdini_utils:: namespace so they resolve from class scope.
* doc/ax/doc.txt - deleted orphan </div> with no matching <div>.
* doc/nanovdb/SourceTree.md - closed the missing code fence; replaced
  'foo@bar:~$' bash prompt (Doxygen parses @bar as a command) with
  the unlabeled '$' style used elsewhere in doc/nanovdb/.
* doc/nanovdb/FAQ.md - corrected link to nanovdb/cuda/DeviceBuffer.h
  (was util/CudaDeviceBuffer.h, a sibling of #2161).

LaTeX -> MathJax migration:

* DOXYGEN_USE_MATHJAX YES + MATHJAX_VERSION MathJax_3 in
  doc/CMakeLists.txt switches formula rendering from latex/dvips/PNG
  to MathJax. Removes the install_latex step from docs.yml and lets
  contributors build the docs locally without any TeX install (the
  conda-forge texlive-core package on osx-arm64 is incomplete and
  has no working alternative on that channel). AX docs use @f$/@f[
  math; output is now browser-rendered, selectable, and crisper.

Result: 73 raw Doxygen warnings, 73 suppressed by documented patterns,
0 surviving. The strict gate is live and enforces 'no new warnings'
from the next PR onward.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-05-21 18:11:11 +12:00
Jonathan Swartz 3c6f121493 nanovdb python: Phase 4b + 4c — stats, validation, checksum (#2215)
* nanovdb python: Phase 4b + 4c — stats, validation, checksum

Round out Phase 4 by binding the GridStats, GridValidator, and
GridChecksum surfaces that didn't ship with Phase 4a.

Phase 4b — Stats
================

* Per-BuildT `tools.<Suffix>Extrema` and `tools.<Suffix>Stats` classes
  for every scalar and vector BuildT in `BuildTypes.def`. Extrema
  exposes `min` / `max` / `add(value)` and a truthy `bool()` for "has
  at least one sample"; Stats inherits from Extrema and adds `size`,
  `avg` / `mean`, `var` / `variance`, `std` / `stdDev`. Static
  predicates `hasMinMax` / `hasAverage` / `hasStdDeviation` /
  `hasStats` mirror the C++ trait queries.

* `tools.updateGridStats(grid, mode=Default)` — polymorphic dispatch
  via `callNanoGrid` over an `UpdateGridStatsOp`. Scalar and vector
  BuildTs route to `tools::updateGridStats<BuildT>`; the special /
  quantized / index / mask types (`Fp4/8/16/N`, `ValueIndex`,
  `ValueOnIndex`, `ValueMask`, `Point`) raise `ValueError` because
  `Stats<ValueT>` isn't meaningful for them. `bool` falls through to
  the C++ `NoopStats` arm.

* Per-BuildT `tools.getExtrema(grid, bbox)` returning the matching
  `Extrema`. Restricted to scalar + vector BuildTs (the only ones
  with an arithmetic ValueType).

Both `updateGridStats` and `getExtrema` release the GIL during the
traversal.

Phase 4c — Validation & checksum
================================

* `tools.validateGrid(handle, gridID, mode=Default, verbose=False)`
  for `GridHandle<HostBuffer>` — single-grid complement of the
  existing `validateGrids`. Returns `False` (without raising) when
  the gridID is out of range.

* `tools.checkGrid(grid, mode=Full)` — polymorphic via `callNanoGrid`.
  Returns `(ok, error_message)`. The 256-byte char buffer the C++
  helper writes into is hidden inside the binding so Python callers
  see a `(bool, str)` tuple.

* `tools.isValid(grid, mode=Default, verbose=False)` — polymorphic via
  `callNanoGrid<IsValidOp>`, equivalent to `checkGrid` + checksum
  verification rolled into a single bool.

* `tools.evalChecksum(grid, mode=Default)` and
  `tools.validateChecksum(grid, mode=Default)` — accept any bound
  NanoGrid via the existing `GridData` Python upcast (every
  `NanoGrid<T>` Python class is registered with `GridData` as its
  base, so nanobind handles the dispatch transparently). All three
  release the GIL.

Test coverage in new `TestGridStats`, `TestGridValidate`, and
`TestGridChecksum` (11 cases total): Extrema/Stats default-and-add,
polymorphic `updateGridStats` on a float grid, `updateGridStats`
rejection on an OnIndex grid, `getExtrema` over a sub-bbox,
`checkGrid` / `isValid` / `validateGrid` happy paths plus
out-of-range `gridID` and `CheckMode.Disable` short-circuit, and
`evalChecksum` → `updateChecksum` → `validateChecksum` round-trip.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: address Copilot review on #2215

Four items from the Phase 4b/4c review, all valid:

1. UpdateGridStatsOp rejected every BuildTraits<BuildT>::is_special
   type unconditionally (except bool), but tools::updateGridStats
   actually supports StatsMode::BBox on any ValueT via the NoopStats
   path. Drop directly into NoopStats for special BuildTs when mode
   is Disable or BBox; only MinMax / All raise (because those would
   instantiate Stats / Extrema over a non-arithmetic ValueT and the
   semantics are ill-defined). Update the binding docstring to
   describe the actual matrix.

2. The test_update_grid_stats_rejects_index_grid case was renamed to
   test_update_grid_stats_on_index_grid and extended to cover all
   four StatsMode arms on an OnIndexGrid: MinMax and All raise,
   BBox and Disable now succeed.

3. validateGrid's docstring didn't mention the CheckMode.Disable
   short-circuit (which returns True without inspecting the handle
   or gridID). The Python tests already exercised the short-circuit
   via test_validateGrid_disable_mode_always_true, so the behavior
   was correct — just the docstring was missing it.

4. IsValidOp::unknown ignored verbose=True and silently returned
   false, but the C++ callNanoGrid::unknown arm writes an
   "Unsupported GridType" message to std::cerr when verbose is set.
   Mirror that — pull in <iostream> in the binding TU and emit the
   same diagnostic from C++.

5. test_get_extrema_over_active_bbox used a bbox that exactly equals
   the root's active bbox, which triggers C++ getExtrema's
   "bbox contains root.bbox()" short-circuit that unconditionally
   folds the grid background into the extrema — so the min came
   back as 0.0 (background), not 1.0 (the smallest active voxel).
   The test passed but the semantics were muddy: it was really
   asserting "background gets added in this branch" rather than
   anything about getExtrema's bbox restriction. Replace with
   test_get_extrema_strictly_inside_active_region, which uses a
   bbox strictly inside the active region (so the recursive branch
   runs) and asserts min/max are exactly the smallest/largest
   sampled active values.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: bind validateGrid for device handles too

Copilot noted tools.validateGrids was registered for both
GridHandle<HostBuffer> and GridHandle<cuda::DeviceBuffer> (the
latter behind NANOVDB_USE_CUDA), but my Phase 4c addition of the
single-grid tools.validateGrid only covered HostBuffer — leaving
device handles able to validate the whole bundle but not an
individual grid.

Add the matching #ifdef NANOVDB_USE_CUDA overload binding
tools::validateGrid<GridHandle<cuda::DeviceBuffer>>. The C++
helper does host-side dispatch via callNanoGrid on the
host-resident gridData() pointer that DeviceGridHandle exposes, so
the same Python overload pair is appropriate.

New test_validateGrid_on_device_handle exercises the device path
end-to-end (build a CUDA level-set sphere, validate it, confirm
the same out-of-range and Disable-mode short-circuits as the host
overload). The test is gated on cuda module availability so it's
skipped cleanly on CPU-only builds.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: address more Copilot review on #2215

Two more items from the review, both valid:

1. CheckGridOp wrote tools::checkGrid's error message into a 256-byte
   stack buffer. tools::checkGrid (and its util::sprint / util::strcpy
   helpers) trust the caller's buffer is large enough — there is no
   bounds-checked variant. The current error messages are at most ~80
   characters once GridType and GridClass enumerator names are
   stringified, so 256 wasn't actually overflowing today, but the
   margin is uncomfortable and a future error string addition could
   push past it. Bump to a 4096-byte buffer (named via a constexpr
   kErrorBufSize) and zero-init the first byte before the call so the
   ok-detection still works if the helper bails out before writing
   anything.

2. updateGridStats's docstring claimed special (quantized / index /
   mask) BuildTs only accept Disable / BBox, but Boolean grids are
   special yet fall through the if-constexpr filter to the regular
   tools::updateGridStats path (which routes any mode to NoopStats
   for ValueT=bool inside the C++ helper). Reword the docstring to
   call out Boolean as the exception that accepts every StatsMode
   via the NoopStats internal path.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

---------

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-05-21 17:47:28 +12:00
Jonathan Swartz 68b26c4579 nanovdb python: Phase 4a — tools.build.Grid<T> mutable CPU builder (#2214)
* nanovdb python: Phase 4a — tools.build.Grid<T> mutable CPU builder

Bind nanovdb::tools::build::Grid<BuildT>, its ValueAccessor<BuildT>, and
Tree<BuildT>::WriteAccessor under a new nanovdb.tools.build submodule.
One set of classes per writable BuildT in BuildTypes.def — every scalar
(float, double, int16, int32, int64, uint8, uint32) and every vector
(Vec3f, Vec3d, Vec4f, Vec4d, Vec3u8, Vec3u16, Rgba8). Naming mirrors
the C++ namespace: nanovdb.tools.build.FloatGrid is the mutable
counterpart of the read-only nanovdb.FloatGrid.

The binding exposes:

- Grid<T>(background, name='', gridClass=Unknown) — constructor
- getValue / setValue / setValueOn / isActive (the last two convenience-
  wrap a fresh ValueAccessor under the hood, since C++ has no
  setValueOn/isActive on Grid itself)
- nodeCount, gridType, gridClass, getName/setName, setTransform,
  .background property
- getAccessor() / getWriteAccessor() returning the typed Value /
  WriteAccessor proxies
- .to_nanovdb(sMode, cMode, verbose) — bakes the build grid into a
  host NanoGrid<BuildT> handle by calling tools::createNanoGrid

ValueAccessor exposes getValue/setValue/setValueOn/isActive/isValueOn.
WriteAccessor exposes setValue/setValueOn/merge. Both are wired up so
the parent grid is kept alive by Python while the accessor lives.

Implementation notes:

- WriteAccessor's defaulted move constructor leaves its internal
  ValueAccessor::mRoot reference dangling (the reference points into
  the moved-from WriteAccessor's own mRoot field, which is per-object
  state — not the parent Tree's mRoot). The Python binding bypasses
  the move path by heap-allocating via nb::rv_policy::take_ownership
  so the C++ object's address is stable for its entire lifetime.
- ValueAccessor doesn't have this hazard because its mRoot reference
  points at the parent Tree's mRoot (a stable address external to the
  accessor), so move construction is safe.
- Read-only special BuildTs (Boolean, Fp4/8/16/N, ValueIndex,
  ValueOnIndex, ValueMask) and Point are deliberately excluded — they
  have no SetValue<T> specialization and can't be built voxel-by-voxel.

Test coverage in new TestBuildGrid: constructor defaults, setValue
marks active, setValueOn preserves background, accessor parity with
grid, WriteAccessor merge-on-destruction, .to_nanovdb() round-trip
with metadata preserved, .to_nanovdb() doesn't consume the source,
Int32Grid + Vec3fGrid spot-checks, and setTransform propagation to
the baked grid's voxelSize / map.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: address Copilot review on #2214

Four items from the Phase 4a review, all valid:

1. Grid<T>.background property read self.mRoot.mBackground directly,
   touching an internal field even though RootNode exposes a
   background() accessor. Switch to self.mRoot.background() so the
   binding doesn't depend on the underlying field name staying put.

2. .to_nanovdb() can be an expensive bake for large grids but held
   the GIL the whole time. Add nb::call_guard<nb::gil_scoped_release>()
   so other Python threads can run during the conversion (the lambda
   only touches C++ state, no Python object handling).

3. nodeCount()'s docstring said the tuple was "internal node counts",
   but the first element is the leaf (level-0) count — leaves are not
   internal nodes. Reword to just "(leaf_count, lower_count,
   upper_count)" and drop the "internal" mislabel.

4. The WriteAccessor merge test was named "merges_on_destruction" but
   actually called wa.merge() explicitly and never forced the
   destructor to run. Split into two cases:
   - test_write_accessor_explicit_merge — calls .merge() explicitly,
     matching what the original test actually checked
   - test_write_accessor_merges_on_destruction — drops the only
     reference (del wa) and runs gc.collect() so the C++ destructor
     fires, then asserts the change is visible

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: use Vec3f.__eq__ in build::Vec3fGrid test

Copilot noted the component-by-component comparison in
test_vec3f_build_grid was justified by an out-of-date claim that
Vec3f equality isn't bound — it is (PyMath.cc defineVec3 wires
nb::self == nb::self). Use self.assertEqual(got, v) directly.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

---------

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-05-21 17:04:01 +12:00
Jonathan Swartz 3183842375 nanovdb python: VoxelBlockManager (host) — Phase 3 follow-up (#2213)
* nanovdb python: VoxelBlockManager (host) — Phase 3 follow-up

Phase 3 deferred the VoxelBlockManager surface; this picks it up before
moving on to Phase 4. Adds host-side bindings for everything under
nanovdb::tools::VoxelBlockManager*, plus a minimal createOnIndexGrid
scaffold needed to build OnIndex grids from Python (the broader
createNanoGrid<SrcGridT, DstBuildT> binding lands in Phase 5).

New module surface, all under nanovdb.tools:

- VoxelBlockManagerHandle (host) — owns the firstLeafID + jumpMap
  metadata buffers; exposes blockCount(), firstOffset(), lastOffset(),
  reset(), __bool__. Buffers exposed as zero-copy NumPy views:
    firstLeafID() -> (blockCount,) uint32
    jumpMap(jump_map_length=1) -> (blockCount, jump_map_length) uint64
  jumpMap takes a jump_map_length argument because the value depends on
  log2_block_width (= 1 << (log2_block_width - 6)) and isn't stored on
  the handle. handle.decodeBlock(grid, i, log2_block_width=6) is a
  convenience method that slices firstLeafID / jumpMap for block i and
  calls decodeInverseMaps internally.

- buildVoxelBlockManager(grid, log2_block_width=6, first_offset=0,
  last_offset=0, n_blocks=0) — runtime switch over Log2BlockWidth ∈
  {6, 7, 8, 9} (BlockWidth 64/128/256/512) dispatching to the right
  template instantiation. Rejects non-OnIndex grids with TypeError;
  rejects out-of-range log2_block_width with ValueError.

- decodeInverseMaps(grid, first_leaf_id, jump_map, block_first_offset,
  log2_block_width=6) — free function. jump_map is a uint64 NumPy
  array of length JumpMapLength (= 1 << (log2_block_width - 6));
  returns (leaf_index, voxel_offset) freshly-allocated NumPy arrays
  (uint32, uint16) of length BlockWidth.

- createOnIndexGrid(src_grid, channels=0, include_stats=True,
  include_tiles=True, verbose=0) — minimal test-scaffold factory
  that binds tools::createNanoGrid<SrcGridT, ValueOnIndex>. Accepts
  FloatGrid, DoubleGrid, Int32Grid, Vec3fGrid sources. Required for
  end-to-end VBM testing since no other path produces an OnIndex
  grid from Python today. The full createNanoGrid<SrcGridT, DstBuildT>
  surface remains scoped to Phase 5.

Defensive checks:

- decodeBlock validates firstLeafID[block_index] is in [0, leafCount)
  before passing it into the C++ decodeInverseMaps. The underlying
  NanoVDB algorithm doesn't always initialize firstLeafID — blocks
  that no leaf's iteration sweep reaches (e.g. on tile-compressed
  OnIndex grids where some sequential offsets correspond to tile
  values rather than leaf voxels) are left with uninitialized memory.
  Without the guard, decodeInverseMaps would read
  tree.getFirstNode<0>()[garbage] and crash; the guard converts that
  into a Python ValueError with a clear message pointing at the
  workaround (build the source grid voxel-by-voxel via build::Grid
  so it stays uncompressed).

- All entry points reject out-of-range indices, levels, and grid
  build types up front (TypeError / ValueError / IndexError).

Tests under TestVoxelBlockManager exercise:
  - createOnIndexGrid produces an OnIndex / IndexGrid / sequential grid
  - Buffer shapes and dtypes (firstLeafID, jumpMap default + reshape)
  - decodeBlock(0) returns (uint32, uint16) arrays of length BlockWidth
  - decodeInverseMaps free function agrees with handle.decodeBlock
  - Out-of-range block_index raises IndexError
  - Out-of-range log2_block_width (5, 10) raises ValueError
  - Non-OnIndex grid argument raises TypeError
  - createOnIndexGrid(None) raises TypeError

End-to-end decode verification across every block is intentionally
deferred until Phase 4's build::Grid bindings land — that's the only
host-side path to construct a tile-free OnIndex grid where every
block's firstLeafID is reliably initialized by the current upstream
algorithm.

Verified locally:
- CPU build: 84 tests, 75 pass + 8 new VBM tests, 1 pre-existing
  test_read_write_grid BLOSC env-dependent failure unrelated to this PR.
- CUDA sm_120 + BLOSC + ZLIB: 84/84 pass.

No new lines over 100 cols.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: skip ndarray tests cleanly when numpy is missing

Windows CI surfaced three test errors on #2213 from
TestZeroCopyViewLifetimes — the test methods invoked
.values() / .leaf_values() directly without an `import numpy` guard,
so on environments without numpy installed (e.g. the Windows runner)
the bindings raised:

    TypeError: could not export nanobind::ndarray:
        ModuleNotFoundError: No module named 'numpy'

The other ndarray-touching tests (test_leaf_values_zero_copy,
test_bulk_leaf_values, test_node_manager_round_trip, the VBM
test_decode_block_zero, etc.) already guard with `import numpy`
and call self.skipTest. The three lifetime tests just skipped
that guard.

Added the same try/except ImportError + skipTest pattern to:
  - TestZeroCopyViewLifetimes.test_handle_grid_tree_leaf_values_chain
  - TestZeroCopyViewLifetimes.test_grid_leaf_values_temporary
  - TestZeroCopyViewLifetimes.test_node_manager_temporary_grid

While I was here, also dropped an unnecessary numpy guard on
TestVoxelBlockManager.test_create_on_index_grid_rejects_unsupported_source
— that test only checks TypeError on a non-grid input and doesn't
touch numpy, so it can run unconditionally.

Verified locally with both a numpy-enabled venv (84/84 pass minus the
pre-existing BLOSC env failure) and a numpy-less venv (84 ran, 17
skipped, no new errors).

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: install numpy on Windows CI so ndarray tests run

The Windows job for the NanoVDB workflow was missing the install_numpy
step that the main openvdb build.yml and weekly.yml workflows already
run after install_windows.ps1. As a result the vcpkg-supplied Python
on the Windows runner had no numpy, so every test that touches
nb::ndarray<nb::numpy, ...> either errored (before #2213's skip
guards) or now skips silently.

Add the install_numpy step using the existing
ci/install_windows_numpy.ps1 helper, matching the pattern already in
build.yml. The Windows runner will then actually execute the
ndarray-touching VoxelBlockManager / Tree / GridHandle tests instead
of skipping them.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: address Copilot review on #2213

Four issues raised by Copilot on the open VBM follow-up PR, all real:

1. VoxelBlockManagerHandle.jumpMap(jump_map_length) accepted any
   caller-supplied length and used it to shape a zero-copy NumPy view
   over hostJumpMap(). Since the underlying buffer was sized as
   blockCount * JumpMapLength (where JumpMapLength is derived from
   the log2_block_width the handle was BUILT with), a caller passing
   a larger value produced an ndarray whose elements lived past the
   end of the allocated buffer — an OOB read on access.

   Fix: wrap VoxelBlockManagerHandle in a small PyVBMHandle struct
   that also stores the log2_block_width. jumpMap() takes no
   arguments and derives JumpMapLength from the stored width, so the
   returned view always matches the buffer exactly. decodeBlock() no
   longer accepts a log2_block_width either, removing the equivalent
   mismatch hazard there. The new PyVBMHandle exposes
   log2_block_width / block_width / jump_map_length as read-only
   properties for introspection.

2. decodeInverseMaps() did not validate first_leaf_id against
   grid.tree().nodeCount(0). VoxelBlockManager::decodeInverseMaps
   indexes tree.getFirstNode<0>()[first_leaf_id] unconditionally, so
   a stray ID produced an OOB read of the leaf array. Validate up
   front and raise IndexError.

3. buildVoxelBlockManager() did not enforce grid.isSequential() (a
   precondition guarded only by NANOVDB_ASSERT, which is a no-op in
   release) nor that first_offset == 1 (mod BlockWidth). Validate
   both at the Python boundary and raise ValueError; the zero
   first_offset default still flows through unchanged because the
   C++ helper normalizes it to 1 itself.

4. The zero-copy shape/dtype test exercised
   vbm.jumpMap(jump_map_length=2) on a handle built with
   log2_block_width=6 — exactly the OOB case (1) was about. Rewrite
   the test to build a second handle with log2_block_width=7 and
   verify its jumpMap shape is (blockCount, 2), confirming the
   shape now follows the handle. Drop the obsolete
   log2_block_width=6 kwarg from decodeBlock. Add two new tests:
   misaligned first_offset is rejected, and an out-of-range
   first_leaf_id on decodeInverseMaps raises IndexError.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: empty-handle guards + accurate ownership comment

Two follow-up items from Copilot on #2213:

1. firstLeafID() and jumpMap() called hostFirstLeafID() / hostJumpMap()
   on the underlying VoxelBlockManagerHandle and fed the result straight
   into nb::ndarray, but both accessors legally return nullptr on a
   default-constructed or reset() handle (blockCount() == 0). Passing
   nullptr into nb::ndarray is unsafe even with a zero leading shape.
   Mirror the pattern PyTree.h uses for the empty-grid leaf_values()
   case: when the buffer is null, substitute a non-null dummy pointer
   (the handle itself) so nanobind has something to base the empty
   ndarray on; nothing is read because the leading shape is 0. Add
   tests for both default-constructed and reset() handles.

2. The pyDecodeInverseMapsImpl comment described nanobind as allocating
   "fresh memory through numpy", which doesn't match the implementation
   (which uses new[] + a capsule with delete[] as deleter). Rewrite the
   comment to describe the actual ownership model so future maintainers
   aren't misled.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: VBM exception-safety + firstLeafID sentinel prefill

Two more items from Copilot on #2213:

1. pyDecodeInverseMapsImpl allocated leafIndex / voxelOffset with raw
   new[] and only wrapped them in nb::capsule after several intervening
   operations (a second new[], the decodeInverseMaps call itself, and
   the first capsule's own construction). If anything in that window
   threw, the half-built state leaked. Hold the raw arrays in
   std::unique_ptr until each capsule has been constructed, then
   release() so ownership transfers cleanly; any throw during that
   sequence now unwinds without leaking.

2. The C++ allocating overload of buildVoxelBlockManager uses
   HostBuffer::create (uninitialized malloc) for firstLeafID, then
   touches only the slots its iteration sweep reaches. Blocks the
   algorithm doesn't visit retain arbitrary values; the existing
   decodeBlock guard (firstLeafID >= nLeaves) catches values past the
   leaf array but cannot tell garbage that happens to be < nLeaves
   from a real leaf id, so a low garbage byte would silently decode
   against the wrong leaf.

   Switch the Python binding to allocate the metadata buffers itself,
   prefill every firstLeafID slot with the sentinel value `nLeaves`,
   then call the in-place buildVoxelBlockManager(grid, handle)
   overload. The in-place builder zeros the jumpMap and only writes
   firstLeafID slots it visits, so every untouched slot keeps the
   sentinel and deterministically trips the decodeBlock guard.

   Add test_untouched_blocks_trip_sentinel_guard, which sweeps every
   block of the cube VBM and asserts each firstLeafID slot is either
   a real leaf id (< nLeaves) or exactly the sentinel — never any
   other value.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: VBM popcount upper-bound + n_blocks validation

Two more items from Copilot on #2213:

1. The decodeBlock guard only checked firstLeafID < nLeaves, but the
   C++ decodeInverseMaps loops 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). With a corrupt jumpMap or a
   handle paired with a different grid, the loop could read past
   tree.getFirstNode<0>() even with a valid firstLeafID.

   Hoist a popcount-based upper-bound check into
   pyDecodeInverseMapsImpl: compute nExtraLeaves locally and raise
   ValueError if firstLeafID + nExtraLeaves >= grid.tree().nodeCount(0).
   Because both the handle.decodeBlock() and the free-function
   decodeInverseMaps() funnel through this impl, both paths are
   covered without per-caller duplication.

2. buildVoxelBlockManager accepted an explicit n_blocks but didn't
   validate it against the documented precondition
   n_blocks >= ceil((last_offset - first_offset + 1) / BlockWidth).
   A smaller value produced a handle whose blockCount() < the
   coverage implied by lastOffset, silently truncating later
   decodeBlock sweeps. Validate when nonzero and raise ValueError
   with the minimum required value in the message.

   New test_build_voxel_block_manager_rejects_undersized_n_blocks
   covers the n_blocks=1 case on the cube VBM.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: explicit std headers in PyVoxelBlockManager.cc

Copilot noted PyVoxelBlockManager.cc uses std::integral_constant,
std::string/std::to_string, and std::move but relied on transitive
includes from nanobind / NanoVDB headers to bring those in. That works
on the toolchains we currently test but is fragile against stricter
ones.

Add explicit <string>, <type_traits>, and <utility> includes; drop
<cstring> and <stdexcept>, neither of which the translation unit
actually uses now.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

---------

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-05-21 16:20:55 +12:00
Jonathan Swartz 2dcabf97dd nanovdb python: Phase 3 — tree / nodes / NodeManager / leaf_values (#2212)
* nanovdb python: Phase 3 — tree / nodes / NodeManager / leaf_values

Fourth slice of the Python bindings restructure tracked in #2208 and
laid out in nanovdb-python-plan.md. Phase 3 makes the tree itself
walkable from Python: every BuildT now has a bound NanoTree, Root,
Upper / Lower internal node, Leaf, and a host-side NodeManager. Where
the leaf actually carries a contiguous T mValues[512] (regular
scalar BuildTs) we also expose zero-copy NumPy views, including a
high-level grid.leaf_values() bulk extractor.

Per BuildT we now register six new classes:

- <Suffix>Leaf — origin, bbox, dim, voxelCount, memUsage, flags,
  isActive(ijk|n), getValue(offset|ijk), getFirstValue, getLastValue,
  minimum / maximum / average / stdDeviation, valueMask, probeValue,
  and (for arithmetic non-special ValueTs) values() returning a
  zero-copy (512,) NumPy view of the leaf's mValues array.

- <Suffix>Upper, <Suffix>Lower — origin, bbox, dim, memUsage,
  minimum / maximum / average / stdDeviation, valueMask, childMask,
  getValue, getFirstValue, getLastValue, isActive, probeValue. The
  two internal node levels share a single defineInternalNodeBase
  helper since their C++ APIs are identical.

- <Suffix>Root — background, tileCount, getTableSize, isEmpty, bbox,
  minimum / maximum / average / stdDeviation, memUsage, getValue,
  isActive, probeValue.

- <Suffix>Tree — root, background, activeVoxelCount,
  activeTileCount(level), nodeCount(level), totalNodeCount,
  memUsage, getValue, isActive, probeValue, extrema() (returns
  (min, max) tuple), getFirstLeaf / getFirstLower / getFirstUpper.
  Grid.tree() is bound on NanoGrid<T> and returns the typed tree
  as reference_internal.

- <Suffix>NodeManager — isLinear, memUsage, nodeCount(level),
  leafCount, lowerCount, upperCount, leaf(i), lower(i), upper(i)
  returning typed node refs. Constructed via the module-scope
  nanovdb.createNodeManager(grid) which polymorphically picks the
  right BuildT and returns a NodeManagerHandle. Handle exposes
  size(), __bool__(), and mgr() — which itself dispatches by stored
  gridType to return the right typed NodeManager.

- grid.leaf_values() bulk extractor — for arithmetic non-special
  BuildTs (float, double, Int16/32/64, UInt8/UInt32), walks the
  breadth-first leaf array and returns a strided zero-copy
  (N_leaves, 512) NumPy view. Stride between leaves is sizeof(LeafT)
  / sizeof(ValueT), reflecting the leaf header between value blocks.
  Throws ValueError on non-breadth-first grids (createNanoGrid
  produces breadth-first by default).

Mechanical bits:

- All six bindings driven from BuildTypes.def via the existing
  X-macro. New file PyTree.h holds the templated definitions; PyTree.cc
  holds the non-templated NodeManagerHandle + createNodeManager bindings
  (the latter dispatches over every BuildT via the same X-macro).
- Tree / node classes are registered BEFORE defineNanoGrid because
  NanoGrid<T>.tree() returns NanoTree<T>& and nanobind needs the
  return type registered first.

Skipped / deferred:

- LeafT::variance() is NOT bound. NanoVDB.h line 4388 reads
  `Pow2(DataType::getDev())` unqualified, which fails ADL for
  non-float ValueTs (ValueIndex / ValueMask). Same for InternalNode.
  Users can compute variance from stdDeviation() in Python.
- VoxelBlockManager (OnIndexGrid-specific) deferred to a follow-up;
  surface is sizeable enough to merit its own PR.
- Vector leaf values() (Vec3f / Vec3d / Vec4f / Vec4d / Vec3u8 /
  Vec3u16 / Rgba8) deferred — these need a flattened
  (count, dim) component view since nanobind ndarray<Vec3f> isn't
  well-formed. A future PR can add float[N, 512, 3] views.
- Tree iterators (beginValueOn etc.) deferred — the bulk leaf_values
  view + per-leaf valueMask covers the most common use case (mask
  the bulk array and you have your active values).

Verified locally (BLOSC + ZLIB on so the optional-codec tests pass):
- CPU build: 58 tests + 8 new TestPhase3TreeNodes — **all 66 pass**.
- CUDA sm_120 build (RTX PRO 6000 Blackwell, CUDA 13.2):
  **all 66 pass**.
- numpy-backed shape/dtype assertions in the new tests confirm the
  per-leaf values() (512,) float32 and bulk leaf_values()
  (N_leaves, 512) float32 views. NodeManager.leaf(i).values() is
  byte-identical to tree.getFirstLeaf().values() for i==0.

No new lines over 100 cols.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: drop phase numbers + reviewer markers from tests and comments

User-facing strings in tests/comments that reference in-flight project
history (phase numbers, reviewer names) lose their meaning once the
project merges. Drop them and reword the surrounding text so each name
and comment self-describes the feature being tested.

- Renamed TestPhase2BuildTCoverage -> TestBuildTRegistrations.
- Renamed TestPhase3TreeNodes -> TestTreeNodeWalking.
- Reworded docstrings on TestPolymorphicGridAccess, TestGridBase,
  TestGridMetaData, TestBlindDataEmpty, TestSplitMergeCopy,
  TestGridMetaDataGuards to describe the feature instead of the phase.
- Inline comments referencing "Phase 0", "Phase 1a.3", "Copilot review",
  etc. reworded into prose about the actual behavior being asserted or
  the underlying bug being regression-tested.
- Same cleanup on the cuda/PyDeviceGridHandle.cu note about why we don't
  register splitGrids/mergeGrids on DeviceBuffer.

No behavior change. 66/66 tests still pass with BLOSC+ZLIB on.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: fix Clang Tree.nodeCount overload + drop exception-driven createNodeManager dispatch

Two real issues caught in CI / review on #2212:

1) Tree.nodeCount(int) binding doesn't compile on Clang.

   PyTree.h used `nb::overload_cast<int>(&TreeT::nodeCount, nb::const_)`
   to select the non-templated `uint32_t nodeCount(int) const` overload
   on nanovdb::Tree<RootT>. Tree<RootT> also has a templated
   `template<typename NodeT> uint32_t nodeCount() const` overload.
   GCC accepted the overload_cast under SFINAE rules; Clang (used by
   the linux-nanovdb:cxx:clang++-Debug CI leg) rejects it with
   `no matching function for call to object of type
    'const detail::overload_cast_impl<int>'`, repeated once per
   BuildT instantiation.

   Replaced with a direct static_cast to the function pointer type,
   which is unambiguous to both compilers:

       static_cast<uint32_t (TreeT::*)(int) const>(&TreeT::nodeCount)

2) createNodeManager dispatch was exception-driven.

   The previous binding tried nb::cast<NanoGrid<T>&>(py_grid) for every
   BuildT and caught nb::cast_error on each mismatch — so a single call
   to nanovdb.createNodeManager(grid) threw and caught 22 cast_error
   exceptions before landing on the matching BuildT. Replaced with an
   nb::isinstance<GridT>(py_grid) pre-check so the cast is only ever
   attempted on the matching BuildT.

Both linux-nanovdb:cxx:clang++-Debug should now build, and
createNodeManager() no longer pays per-call exception overhead.
Verified locally on CPU and CUDA sm_120 builds: 66/66 tests pass with
BLOSC + ZLIB on.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: bounds-check + lifetime fixes on #2212

Addresses Copilot review notes on PR #2212. Two real classes of bug.

(1) Out-of-range arguments fell through into raw memory access.

Several entry points on Leaf, Tree, and NodeManager rely on
NANOVDB_ASSERT in the underlying C++ to catch invalid indices. That
assertion is a no-op in release builds, so passing an OOB index from
Python would read off the end of mValueMask / mValues / mNodeOffset[]
arrays. Wrapped each with an explicit range check that raises a
Python IndexError or ValueError:

  - Leaf.isActive(n)            n must be < voxelCount()  (512)
  - Leaf.getValue(offset)       offset must be < voxelCount()
  - Tree.activeTileCount(level) level must be 1, 2, or 3
  - Tree.nodeCount(level)       level must be 0, 1, or 2
  - NodeManager.nodeCount(L)    same as Tree
  - NodeManager.leaf(i)         i must be < leafCount()
  - NodeManager.lower(i)        i must be < lowerCount()
  - NodeManager.upper(i)        i must be < upperCount()

(2) Returned pointers / NumPy views did not actually keep their
backing buffers alive.

The pattern `nb::cast(value, nb::rv_policy::reference, parent)` was
used at multiple sites under the assumption that the third argument
established a Python-level keep_alive linkage between the returned
object and `parent`. It doesn't — rv_policy::reference is "no
ownership, no keep_alive" by definition; the parent argument is only
a hint to the cast machinery, not a lifetime guarantee.

As a result, expressions that drop the intermediate handle would
silently free the underlying buffer:

    g = nanovdb.tools.createFogVolumeSphere(name='probe').grid()
    g.gridName()   # SEGFAULT — handle was GC'd

    vals = (nanovdb.tools.createFogVolumeSphere()
            .grid().tree().getFirstLeaf().values())
    vals.sum()     # SEGFAULT

    nm = nanovdb.createNodeManager(
        nanovdb.tools.createFogVolumeSphere().grid()).mgr()
    nm.leaf(0)     # SEGFAULT — NodeManager holds raw ptr to grid

Added explicit `nb::keep_alive<0, 1>()` to the .def for every
affected site so the returned value keeps its parent alive:

  - GridHandle.grid(n)              (PyGridHandle.h)
  - DeviceGridHandle.deviceGrid(n)  (cuda/PyDeviceGridHandle.cu)
  - NodeManagerHandle.mgr()         (PyTree.cc)
  - Grid.getBlindData(n)            (NanoVDBModule.cc)
  - Leaf.values()                   (PyTree.h)
  - Grid.leaf_values()              (PyTree.h)
  - PointAccessor.gridPoints / leafPoints / voxelPoints
                                    (NanoVDBModule.cc)
  - nanovdb.createNodeManager(grid) keep arg 1 (grid) alive as long
    as the returned NodeManagerHandle lives — because the underlying
    NodeManager stores a raw pointer to the grid.

These were pre-existing bugs introduced in Phase 1 (pyHostGrid) and
inherited by every subsequent zero-copy view; Phase 3 surfaced more
of them via the new Tree/Leaf/NodeManager bindings.

Two new test classes lock the fixes in:

  - TestBoundsChecks asserts each guarded entry point raises the
    correct exception type at every boundary and still accepts in-
    range inputs.
  - TestZeroCopyViewLifetimes invokes each fixed binding in the
    chained-temporary form (handle.grid().tree().getFirstLeaf()
    .values(), createNodeManager(temp).mgr().leaf(0).values(), ...),
    runs gc.collect(), then touches the returned value. Pre-fix
    these segfaulted; now they pass cleanly.

CPU + CUDA sm_120 (BLOSC + ZLIB on): 75/75 pass.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: address Copilot follow-up review on #2212

Three further Copilot notes on the bounds-check/lifetime commit
(f6735717), all valid.

(1) PyTree.h relied on transitive includes for std::is_arithmetic_v /
    std::enable_if. Works on GCC + libstdc++ because <type_traits> is
    pulled in by <nanobind/nanobind.h> -> standard headers, but MSVC /
    libc++ may break the chain. Added an explicit
        #include <type_traits>
    near the top.

(2) grid.leaf_values() returned None for empty grids (nLeaves == 0 or
    getFirstLeaf() == nullptr) while its docstring promised an
    (N_leaves, 512) NumPy view. Callers had to special-case the None
    sentinel before iterating. Now returns an empty (0, 512) ndarray
    of the right dtype, so the contract reads cleanly:

        for row in grid.leaf_values(): ...

    works on every grid, empty or not. When nLeaves == 0 we pass a
    dummy non-null aligned pointer (the grid itself) to nb::ndarray so
    nanobind has a valid base for the empty array — no data is read
    since the leading shape is 0. Docstring updated to call out the
    empty-grid behavior explicitly.

(3) The ValueError raised on a non-breadth-first grid said
        "rebuild via tools::createNanoGrid(...)"
    which reads like a C++ symbol. Reworded to the Python-API form
        "rebuild via nanovdb.tools.createNanoGrid(...)"
    so Python users see a Python entry point.

New test method TestTreeNodeWalking.test_bulk_leaf_values_empty_grid_returns_empty_array
constructs a grid with an empty bbox (nLeaves == 0) and asserts
leaf_values() is an (0, 512) float32 NumPy array (not None).

CPU + CUDA sm_120 (BLOSC + ZLIB on): 76/76 pass.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

---------

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-05-21 11:48:54 +12:00
Jonathan Swartz efa513a569 nanovdb python: Phase 2 — broaden BuildT coverage to 24 grid types (#2211)
* nanovdb python: Phase 2 — broaden BuildT coverage to 24 grid types

Third slice of the Python bindings restructure tracked in #2208 and
laid out in nanovdb-python-plan.md. Phase 2 expands the bound BuildT
list from the six surfaced in Phase 0/1 (float, double, int32_t, Vec3f,
Rgba8, Point) to seventeen new types, riding the Phase 0 X-macro so
each addition is a single row in BuildTypes.def plus a polymorphic
dispatch arm.

New BuildTs by category:

- SCALAR (+4): int16_t, int64_t, uint8_t, uint32_t.
  Bound exactly like the existing float/double/int32_t — full
  defineScalarAccessor (with setVoxel) plus defineNodeInfo.
  Class names follow the GridType enum: Int16Grid, Int64Grid,
  UInt8Grid, UInt32Grid.

- VECTOR (+5): Vec3d, Vec4f, Vec4d, Vec3u8, Vec3u16.
  Same shape as the existing Vec3f / Rgba8 — defineVectorAccessor
  with setVoxel, no NodeInfo. Accessor names use the consistent
  "<Suffix>ReadAccessor" form (Vec3dReadAccessor, ...); only the
  legacy Vec3fReadVectorAccessor / RGBA8ReadAccessor names from
  Phase 0 stay as-is.

- READONLY (new category, +8): bool, Fp4, Fp8, Fp16, FpN,
  ValueIndex, ValueOnIndex, ValueMask. These all have
  nanovdb::BuildTraits<T>::is_special == true, which means the C++
  SetVoxel<T> specialization static_asserts and won't compile.
  They get a bare defineAccessor<T> binding — getValue() only,
  no setVoxel, no NodeInfo. Class names: BooleanGrid, Fp4Grid /
  Fp8Grid / Fp16Grid / FpNGrid (quantized — getValue returns
  float), IndexGrid / OnIndexGrid (getValue returns uint64), and
  MaskGrid (getValue returns bool).

The accessor's value type now resolves through
nanovdb::BuildToValueMap<BuildT>::Type rather than
DefaultReadAccessor<BuildT>::ValueType. For ordinary types they're
identical, but for Half / Fp* the accessor decodes to float on read,
for ValueIndex / OnIndex it returns uint64, and for ValueMask / bool
it returns bool — the bound probeValue() out-parameter and the
Python-side return type both want the decoded form. (Without this
change, probeValue<Half>'s instantiation chain mismatches its own
ProbeValue<Half>::ValueT = float.)

nanovdb::Half is intentionally NOT bound. The source declares it as
`class Half{};` (an empty placeholder for IEEE 754 half-precision)
and the C++ ProbeValue<Half> chain is inconsistent — leaf storage
carries Half but ProbeValue expects float, so the template doesn't
instantiate. When the upstream Half implementation lands we can add
it via the same X-macro path.

Polymorphic dispatch in pyHostGrid / pyDeviceGrid gains a fourth
arm (NANOVDB_PY_FOR_EACH_READONLY_BUILDT) covering all eight new
GridType enumerators (Boolean, Fp4/Fp8/Fp16/FpN, Index, OnIndex,
Mask). handle.grid(n) / handle.deviceGrid(n) now return the right
typed subclass for these too.

Tests: a single new TestPhase2BuildTCoverage class with 5 methods
verifies every new BuildT registered, every accessor surface matches
its category (scalars have setVoxel + getNodeInfo; vectors have
setVoxel; read-only have neither), and all the new typed grids
inherit from the polymorphic Grid base. We can't host-construct
Int16Grid / Fp4Grid / IndexGrid / etc. yet because the C++
create*Grid factories for those types land in Phase 5 — but the
registration and the dispatch surface are locked in.

Build + test verified locally:
- CPU (no CUDA, no OpenVDB, no BLOSC): 57 tests, 48 pass, 8 CUDA
  skip, 1 pre-existing test_read_write_grid BLOSC failure unrelated
  to this PR.
- CUDA sm_120 (Blackwell, CUDA 13.2): 57 tests, 55 pass, 2 pre-
  existing BLOSC failures (host + device variants).

No new lines over 100 cols.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: address Copilot review on #2211

Three concrete fixes from Copilot's review of the Phase 2 PR
(https://github.com/AcademySoftwareFoundation/openvdb/pull/2211):

1) Bind GridType.UInt8 in the Python enum.

   Real bug — a Phase 0 oversight that was harmless until Phase 2
   surfaced UInt8Grid. The C++ enumerator nanovdb::GridType::UInt8
   existed (value 26), and the polymorphic dispatch routes it
   correctly internally, but the enum binding was missing the
   `.value("UInt8", GridType::UInt8)` line. Python users therefore
   couldn't write `handle.gridType(n) == nanovdb.GridType.UInt8` to
   discriminate UInt8 grids.

   New regression test TestPhase2BuildTCoverage.
   test_all_grid_type_enums_reachable walks every BuildT we bind
   and confirms its GridType enumerator is reachable from Python,
   so the next oversight gets caught at test time.

2) Update pyHostGrid() docstring in PyGridHandle.h. Was claiming
   "Boolean, Half, Fp16 land in Phase 2" as examples of types that
   weren't yet Python-visible — those examples are now stale (Phase
   2 binds Boolean and Fp16; Half stays unbound but for a different
   reason). Reworded to point at BuildTypes.def as the source of
   truth so the docstring can't go stale again as Phase 5+ adds
   more types.

3) Disambiguate "bool for ValueMask and bool" wording in the
   READONLY macro doc comment in BuildTypes.def. The second "bool"
   referred to the literal BuildT=bool grid; the phrasing read as
   redundant. Now: "bool for ValueMask and for the BuildT=bool
   (Boolean) grid". Same fix also enumerates the quantized types
   explicitly (Fp4/Fp8/Fp16/FpN) instead of writing "Fp*".

Build + test on CUDA sm_120 with BLOSC + ZLIB: 58/58 pass.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

---------

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-05-21 10:07:46 +12:00
Jonathan Swartz dd67710848 nanovdb python: Phase 1 — polymorphic Grid, GridMetaData, blind data, PointAccessor (#2210)
* nanovdb python: Phase 1 — polymorphic Grid, GridMetaData, blind data, PointAccessor

Second slice of the Python bindings restructure tracked in #2208 and
laid out in nanovdb-python-plan.md. Phase 1 bundles the three sub-todos
(1a polymorphic API, 1b type-erased introspection + blind data, 1c
PointAccessor + handle utilities) into a single change on top of the
Phase 0 X-macro foundation.

API changes (pre-1.0 breaks called out in the plan):

- Polymorphic accessors. handle.grid(n=0) and handle.deviceGrid(n=0)
  replace the typed handle.floatGrid()/doubleGrid()/int32Grid()/
  vec3fGrid()/rgba8Grid() and their device equivalents. Dispatch is
  driven by gridType(n) through a switch generated from BuildTypes.def;
  unbound BuildTs route to None rather than throwing.

- Grid base class rename. The Python class previously bound as
  nanovdb.GridData is now nanovdb.Grid (matching the C++ user-facing
  class name Grid<TreeT>). All typed grid classes (FloatGrid, ...)
  inherit from Grid.

- Base-class method lift. version/gridSize/gridIndex/gridCount/voxelSize/
  map/gridType/gridClass/checksum/isLevelSet/isFogVolume/.../hasMinMax/
  hasBBox/.../isBreadthFirst/shortGridName move from per-BuildT
  defineNanoGrid<T> up to defineGrid via lambdas that read GridData data
  members directly. defineNanoGrid<T> now only binds getAccessor,
  activeVoxelCount, and isSequential — the BuildT-dependent slice.

Additive surface:

- GridMetaData. Bound as nanovdb.GridMetaData with constructor from a
  Grid and the full read-only accessor surface (gridType, gridClass,
  shortGridName, gridSize/Index/Count, map, worldBBox, indexBBox,
  voxelSize, blindDataCount, activeVoxelCount, activeTileCount(level),
  nodeCount(level), checksum, version, isValid, isLevelSet/...,
  hasMinMax/..., isBreadthFirst, rootTableSize, isEmpty). Type-erased
  introspector — answer "what's in this buffer?" without knowing BuildT.

- Blind data API on Grid. blindDataCount, blindMetaData(n),
  findBlindData(name), findBlindDataForSemantic(sem), getBlindData(n).
  getBlindData returns a zero-copy NumPy view typed by mDataType (Float
  -> 1D float32, Vec3f -> (N, 3) float32, RGBA8 -> (N, 4) uint8, etc.).
  Out-of-range and unknown-type paths return None / fall back to a flat
  uint8 byte view.

- Enums: GridBlindDataClass (Unknown/IndexArray/AttributeArray/GridName/
  ChannelArray/End) and GridBlindDataSemantic (Unknown/PointPosition/
  PointColor/PointNormal/PointRadius/PointVelocity/PointId/WorldCoords/
  GridCoords/VoxelCoords/LevelSet/FogVolume/Staggered/End). Bound as
  nb::enum_ with .export_values() so the names are also top-level
  attributes of the module.

- GridBlindMetaData struct. Read-only fields valueCount/valueSize/
  semantic/dataClass/dataType, name() accessor, isValid(),
  blindDataSize().

- PointAccessor variants. nanovdb.PointIndexAccessor (uint32 indices,
  used by PointIndex grids) and nanovdb.PointDataAccessor (Vec3f
  positions, used by PointData grids). Methods gridPoints(),
  leafPoints(ijk), voxelPoints(ijk) each return a zero-copy NumPy view
  onto the underlying blind-data buffer, anchored to the accessor
  lifetime via keep_alive.

- GridHandle utilities. handle.copy() does a deep copy into a freshly
  allocated buffer of the same buffer type. Module-scope splitGrids(h)
  -> list[GridHandle] and mergeGrids(handles) -> GridHandle are
  registered for both host and device handles via the existing
  defineGridHandleUtilities<BufferT> template (nanobind merges them as
  an overload set).

Mechanical X-macro changes:

- BuildTypes.def gains a GridTypeEnum column on each row so the
  polymorphic dispatch in pyHostGrid/pyDeviceGrid can `case
  nanovdb::GridType::<GridTypeEnum>:` on it. The Point row maps to
  GridType::PointIndex (there is no GridType::Point).
- HandleMethod/DeviceMethod columns dropped — the typed handle.fooGrid()
  accessors no longer exist.

NB_MODULE bind order:

- defineCheckMode + defineChecksum now bind BEFORE defineGrid because
  Grid.checksum() returns Checksum by value (registration must precede
  use).
- defineGridBlindData binds BEFORE defineGrid for the same reason
  (Grid.findBlindDataForSemantic / blindMetaData reference the new enum
  and class in their signatures).

Tests (TestNanoVDB.py): all typed-accessor call sites rewritten to
handle.grid(i)/handle.deviceGrid(i). New test classes cover the new
surface — TestPolymorphicGridAccess, TestGridBase, TestGridMetaData,
TestBlindDataEmpty, TestSplitMergeCopy — 11 new tests; the full suite
is now 48 tests, 39 pass on a minimal CPU build (8 CUDA skip, 1
pre-existing test_read_write_grid BLOSC failure unrelated to this PR).

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: don't expose splitGrids/mergeGrids for DeviceGridHandle

Caught during a real-CUDA verification pass of #2210: calling
nanovdb.mergeGrids([device_h1, device_h2]) raised std::bad_cast on
sm_120 (Blackwell, CUDA 13.2). Both the host and device overloads of
splitGrids/mergeGrids take nb::list, so nanobind's overload resolution
can't disambiguate by element type — it picks the first match and the
inner nb::cast<HostHandle&&>(device_h) fails.

The host-only variant is what the Phase 1 plan calls for. 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 actually needed. handle.copy() on a DeviceGridHandle continues to
work because copy() is a regular method, no overload resolution
involved.

Full CUDA pytest now reports 46/48 (the 2 failures are the pre-existing
test_read_write_grid BLOSC bug, host + device variants — both call
writeGrid(..., Codec.BLOSC) without try/except, identical to master).

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: address Copilot review on #2210

Three concrete fixes from Copilot's review of the Phase 1 PR
(https://github.com/AcademySoftwareFoundation/openvdb/pull/2210):

1) mergeGrids no longer consumes its input handles.

   The previous implementation built a std::vector<HandleT> via
   nb::cast<HandleT&&>(h), which move-constructs the C++ GridHandle
   out of the Python wrapper — leaving caller's h1/h2 silently
   emptied (gridCount went 1 -> 0, size went non-zero -> 0).
   Reproduced before fix; locked into a regression test
   (TestSplitMergeCopy.test_merge_does_not_consume_inputs).

   Rewrote the binding to read each handle by const reference and
   inline the merge concat directly. The nanovdb::mergeGrids C++
   helper's signature requires a std::vector<GridHandle> (a
   move-only type), so reusing it from Python without moving from
   the inputs would have meant deep-copying each handle twice; the
   inlined version is ~15 lines and does one memcpy per source grid
   with tools::updateGridCount fixing up the per-grid header.

2) getBlindData validates mValueSize against the implied dtype/shape
   before building a typed NumPy view.

   A blind-data channel with mDataType=Float but mValueSize != 4
   (corruption, version mismatch, or an unknown variant of a known
   tag) would previously be exposed as `count` float32 elements —
   i.e. count*4 bytes — even though the underlying region is only
   count*mValueSize bytes. That overruns the channel and returns a
   view onto unrelated bytes.

   Added a `valueSize == sizeof(...)` (or `dim*sizeof(scalar)` for
   vector cases) check on every handled GridType. On mismatch the
   binding falls back to a raw uint8 byte view of mValueCount *
   mValueSize, which is by definition the actual byte extent and
   therefore always safe.

3) GridMetaData ctor + safeCast guard against invalid grids before
   calling into NanoVDB, where NANOVDB_ASSERT(gridData->isValid())
   would abort debug builds and undefined-behave in release.

   nanobind's type system already rejects Python None at the bind-
   site (None can't bind to const GridData*), so the literal
   "GridMetaData(None)" case Copilot called out is a TypeError
   today — but the broader concern (an otherwise-valid Grid object
   wrapping a corrupted buffer) is real.

   __init__ now does an explicit `gd == nullptr || !gd->isValid()`
   check and raises nb::value_error with a descriptive message
   before calling into nanovdb::GridMetaData. safeCast does the
   same and returns False on bad input, matching the spirit of
   "is this safe to cast?".

   New tests: TestGridMetaDataGuards covers the rejection paths and
   the still-works happy path.

Build + test verified locally on both CPU (52 tests, 43 pass, 8 CUDA
skip, 1 pre-existing BLOSC) and full CUDA (52 tests, 50 pass, 2 pre-
existing BLOSC host+device).

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

---------

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-05-21 09:14:41 +12:00
Jonathan Swartz 2523040b08 nanovdb python: Phase 0 foundation for C++ API mirror (#2209)
* nanovdb python: Phase 0 foundation for C++ API mirror

First slice of the Python bindings restructure tracked in #2208 and
laid out in nanovdb-python-plan.md. Phase 0 is the mechanical
groundwork the rest of the plan builds on:

- BuildTypes.def: single X-macro list of currently-bound BuildT types
  (scalar / vector / point / sampleable). NanoVDBModule.cc, PyMath.cc,
  PySampleFromVoxels.cc, PyCreateNanoGrid.cc, PyTools.cc, PyGridHandle.h
  and cuda/PyDeviceGridHandle.cu now drive their per-type instantiations
  from this one file. Adding a BuildT in Phase 2 becomes one line.

- CMakeLists.txt: under SKBUILD, ship the nanovdb/ headers inside the
  Python wheel at nanovdb/include/nanovdb/ so downstream extension
  authors can compile against the same headers the wheel was built with.
  Also wire nanobind_add_stub so a nanovdb.pyi (and py.typed marker) are
  emitted into the wheel for IDE / type-checker support; gated behind
  NANOVDB_BUILD_PYTHON_STUBS and silently skipped on older nanobind.

- __init__.py: add nanovdb.get_include() returning the bundled include
  dir. Also fix the Windows DLL shim, which referenced an undefined
  `directory` variable instead of the local `openvdb_dll_directory`.

- Relocate the batched sampleFromVoxels CUDA kernel binding from the
  phantom nanovdb.math.cuda submodule (which has no C++ counterpart) to
  nanovdb.tools.cuda, alongside the existing signedFloodFill and
  pointsToRGBA8Grid kernels. The math.cuda submodule is no longer
  registered. TestNanoVDB.py updated to match.

- Pre-existing observable bug fixes:
  * GridHandle.__bool__ returned None (lambda was missing return);
    it now returns !handle.empty() as intended.
  * Enable __repr__ on GridType, GridClass and io::Codec via the
    nanovdb::toStr / strlen<> helpers — previously commented out.

Build + pytest verified locally (CPU-only): 28 tests pass; the single
pre-existing test_read_write_grid BLOSC failure is unrelated (test
doesn't wrap the optional codec call in try/except like its sibling
does).

Part of #2208.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: align Phase 0 X-macros with codingstyle.txt

Audit of the new BuildTypes.def + consumers against
nanovdb/nanovdb/docs/codingstyle.txt:

- Rename the X-macro family NVDB_PY_FOR_EACH_*_BUILDT to
  NANOVDB_PY_FOR_EACH_*_BUILDT and the helper sentinels
  NVDB_PY_LOCAL_DEFINED_* to NANOVDB_PY_LOCAL_DEFINED_* to match the
  established NANOVDB_ macro prefix used everywhere else in the codebase
  (NANOVDB_USE_CUDA, NANOVDB_BUILD_PYTHON_MODULE, NANOVDB_HOSTDEV, ...).

- Bring lines under the 100-column limit:
    * Wrap the long VECTOR_BUILDT lines in BuildTypes.def across two
      lines each (cleaner alignment, no behavior change).
    * Rename the consumer macro parameter DeviceHandleMethod to
      DeviceMethod so the #define line itself fits (was 102 cols).
    * Reformat the GridHandle __bool__ lambda onto three lines instead
      of one 119-col line (made worse by the `return` fix in the
      previous commit).

- Add a top-of-file justification block in BuildTypes.def explaining why
  this file is a deliberate exception to the codingstyle "avoid macro
  functions" rule (templates can't emit top-level declarations and
  explicit instantiations across translation units from a single
  canonical list).

No functional change. Rebuild + pytest_nanovdb is identical to the
previous commit: 28 tests pass, 8 CUDA skips, 1 pre-existing BLOSC
failure.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

* nanovdb python: only generate .pyi stubs under SKBUILD by default

Fixes the macOS CI failure on #2209. The previous commit defaulted
NANOVDB_BUILD_PYTHON_STUBS to ON, which made the nanovdb_python_stub
target fire on every in-tree CI build. The macOS GitHub Actions runner
wraps the stubgen invocation with

  cmake -E env DYLD_INSERT_LIBRARIES=.../libclang_rt.tsan_osx_dynamic.dylib:
                                      .../libclang_rt.asan_osx_dynamic.dylib:
                                      .../libclang_rt.ubsan_osx_dynamic.dylib
  ASAN_OPTIONS=detect_leaks=0 python stubgen.py -m nanovdb ...

(this preamble is added by the runner / CMake env wrapper — nothing in
our nanobind_add_stub call sets it, and the .so itself is not built with
-fsanitize). ThreadSanitizer can only install its interceptors at
process start; Python loads first and then dlopen's the compiled .so,
which TSan considers "too late" and aborts:

  ==49907==ERROR: Interceptors are not working. This may be because
  ThreadSanitizer is loaded too late (e.g. via dlopen).

So the macOS build target failed with exit 2 after the .so itself built
fine. The other matrix legs (linux-nanovdb Debug/Release for clang/gcc)
all passed.

Stubs are only useful to wheel consumers — they ship next to the .so in
the installed package layout under SKBUILD. The in-source OpenVDB CI
build never consumes them, so making stub generation default ON only
when SKBUILD is set keeps the wheel build path unchanged and stops the
macOS CI from invoking stubgen under the sanitizer wrapper. The user can
still force generation with -DNANOVDB_BUILD_PYTHON_STUBS=ON for local
dev builds where it's useful.

Verified locally:
  - in-tree config (SKBUILD unset): no nanovdb_python_stub target
    defined; `make nanovdb_python_stub` errors with "no rule". Matches
    desired CI behavior.
  - SKBUILD=ON config: stub target exists, builds, emits nanovdb.pyi
    and py.typed into the install layout (28 kB stub file with all the
    expected GridHandle/GridType/GridClass symbols).

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>

---------

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-05-20 21:16:15 +12:00
Nick Avramoussis fdca38c531 Added an assert to check that function group symbols are unique
Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com>
2026-05-18 20:13:11 +03:00
Nick Avramoussis bd6f6dafdf Fixed a few strict CXX build issues on clang macos
Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com>
2026-05-18 20:12:39 +03:00
Nick Avramoussis 5b8d2a1532 Fixed a mistake in a AX unit test causing bad IR generation
Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com>
2026-05-18 20:11:35 +03:00
Nick Avramoussis 1f46b91f30 Merge pull request #2190 from Idclip/vcl_simd
Introduce VCL, SIMD wrappers, and Vectorized RasterizeSDF(Spheres)
2026-05-16 17:20:20 +12:00
Dan Bailey 0ae36dd0a8 Merge pull request #2202 from danrbailey/mask_iterator
Fix bug in value mask iterator
2026-05-15 09:44:01 -07:00
Nick Avramoussis 54b8bf219a PR feedback - typos, tests and fixes
Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com>
2026-05-15 13:29:57 +03:00
Dan Bailey bbd6924f77 Add pending notes
Signed-off-by: Dan Bailey <danbailey@ilm.com>
2026-05-14 12:25:20 -07:00
Dan Bailey 546347cad4 Add unit test for mask leaf iterator
Signed-off-by: Dan Bailey <danbailey@ilm.com>
2026-05-14 12:25:20 -07:00
Dan Bailey 04cbd98322 Update to Tree Iterator to work with Mask Grids
Signed-off-by: Dan Bailey <danbailey@ilm.com>
2026-05-14 11:19:53 -07:00
Nick Avramoussis da8a5ccaa4 Added pendingchanges
Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com>
2026-05-14 14:54:57 +03:00
Nick Avramoussis a9079f5b2d Converted Rasterize over to new operators away from explicit methods
Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com>
2026-05-14 13:03:32 +03:00
Nick Avramoussis a5c231cd55 Added component wise operators to Tuple, including comparison operators that return Tuples of bools
Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com>
2026-05-14 13:03:32 +03:00
Nick Avramoussis a46e808da1 Re-introduce Vec relational ops as deprecated (but note that these deprecation warning will not fire when used in std containers/system headers) and guarded by a define
Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com>
2026-05-14 13:03:32 +03:00
Nick Avramoussis 33401147f2 Removed Tuple < and > operators - added explicit overloads for cwiseLessThan and cwiseGreaterThan
Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com>
2026-05-14 13:03:32 +03:00
Nick Avramoussis 90729a6445 Minor fixes for AVX512F/AVX512+
Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com>
2026-05-14 12:58:39 +03:00
Nick Avramoussis d43b5583ac Added weekly CI for VCL=ON (OFF by default)
Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com>
2026-05-14 12:58:39 +03:00
Nick Avramoussis 8159467dd5 Initial manual vectorization/matching of SphericalTransfer scheme i.e. RasterizeSDF with spheres
Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com>
2026-05-14 12:58:39 +03:00
Nick Avramoussis 60fa1b861d Added VCL SIMD header wrapper for aliasing between simd, tuple and scalar arithmetic
Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com>
2026-05-14 12:58:39 +03:00
jmlait cac4e20dcd Merge pull request #2187 from sideeffects/send_upstream_fixcannovdbsize
CNanoVDB sync with Houdini
2026-05-13 14:36:08 -04:00
Dan Bailey 84fc1e6ec4 Merge pull request #2193 from danrbailey/io_float_frustum_bbox
Add support for reading file version 221 (FLOAT_FRUSTUM_BBOX)
2026-05-06 15:46:47 -07:00
Dan Bailey 226ac86da2 Add support for reading file version 221 (FLOAT_FRUSTUM_BBOX)
Signed-off-by: Dan Bailey <danbailey@ilm.com>
2026-05-06 14:44:00 -07:00
Dan Bailey 85bd0e52cd Merge pull request #2200 from danrbailey/tbb2023
Update for TBB 2023
2026-05-06 13:48:18 -07:00
Dan Bailey d68d0914fc Switch from tbb/blocked_range.h to tbb/task_arena.h to pull in TBB version macro
Signed-off-by: Dan Bailey <danbailey@ilm.com>
2026-05-06 10:43:33 -07:00
Jeff Lait c0afc0bf56 When syncing accidentally reset the magic number test to the old
version.

Signed-off-by: Jeff Lait <jlait@andorra.sidefx.com>
2026-05-03 20:23:27 -04:00
Jeff Lait c372647a36 Add the static to the cnanovdb_readaccessor_init to match the other
cnanovdb_readaccessor linkage settings.

Signed-off-by: Jeff Lait <jlait@andorra.sidefx.com>
2026-04-30 15:33:58 -04:00
Dan Bailey 32dcb29a25 Merge pull request #2191 from danrbailey/notes_0804
Meeting Notes
2026-04-29 10:57:24 -07:00
Jeff Lait cbfcd732e7 Fix pending change spelling
Fix int/uint mismatch with nanovdb
Fix mFlags mismatch with nanovdb.

Signed-off-by: Jeff Lait <jlait@andorra.sidefx.com>
2026-04-29 13:51:47 -04:00
Jonathan Swartz 716b3574a7 nanovdb/python: fix writeGrids dropping all but the last grid
The Python binding for nanovdb.io.writeGrids iterated the handle list
and called io::writeGrid(fileName, ...) per element. That file-level
overload opens the path with std::ios::trunc, so every iteration wiped
the previously written grid and the resulting .nvdb only ever contained
the last handle in the list (same story for deviceWriteGrids on the
CUDA side).

Mirror io::writeGrids(fileName, handles, codec, verbose) from
nanovdb/io/IO.h directly in the wrapper: open the output stream once
with trunc, then call the ostream overload of writeGrid for each handle
cast from the nb::list. We can't simply forward to io::writeGrids
because GridHandle<BufferT> is move-only (see GridHandle.h) and the
nb::list holds references we must not invalidate.

Tests (nanovdb/python/test/TestNanoVDB.py):
- TestGridHandleExchange.test_list_to_vector now reads the written
  file back via readGridMetaData and readGrids and asserts the full
  handle count is preserved.
- Added TestDeviceReadWriteGrids.test_device_write_grids_multi as a
  CUDA-guarded regression for deviceWriteGrids with multiple handles.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-04-24 21:31:42 +12:00
Nick Avramoussis d6b69a0ecd Removed compiled parts of VCL used for examples and runtime instruction checking - we don't currently need or use this
Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com>
2026-04-15 09:12:29 +12:00
Nick Avramoussis e7d62d92e8 Added copy of VCL (version2) v2.02.03 at dc6cb6cea60bb983314c33386f5f4a7fae5b7efb
Signed-off-by: Nick Avramoussis <4256455+Idclip@users.noreply.github.com>
2026-04-15 09:10:43 +12:00
Dan Bailey 31b11b0a02 Meeting Notes
Signed-off-by: Dan Bailey <danbailey@ilm.com>
2026-04-09 16:05:22 -07:00
Jeff Lait b575a4250d Remove trailing whitespace.
Signed-off-by: Jeff Lait <jlait@andorra.sidefx.com>
2026-03-28 12:59:50 -04:00
Jeff Lait e79508c7c8 NanoVDB:
Improvements:
  - CNanoVDB supports integer (I) grids
  - CNanoVDB has setValue methods to write to voxels
  - CNanoVDB has getLeaf methods to acquire raw leaf pointers

Bug Fixes:
  - static used for local kernel methods in CNanoVDB
  - Remove auto-computed reserved sizes in favour of hard coded sizes
    in CNanoVDB.  While less maintainble, C99 forbids zero sized
    arrays so some compilers crashed when encountering this.
  - Initialize key value when constructing an accessor, some compilers
    entered bad states with this undefined.

Signed-off-by: Jeff Lait <jlait@andorra.sidefx.com>
2026-03-27 13:26:33 -04:00
228 changed files with 78030 additions and 4256 deletions
+14 -9
View File
@@ -19,6 +19,8 @@ on:
- 'pendingchanges/**'
- '**.md'
pull_request:
# Add 'ready_for_review' to run CI when a draft PR is marked ready.
types: [opened, synchronize, reopened, ready_for_review]
branches:
- '**'
paths-ignore:
@@ -53,9 +55,10 @@ jobs:
# VFX platform jobs. These are run on the appropriate CentOS images from
# the provided ASWF docker containers
if: |
github.event_name != 'workflow_dispatch' ||
github.event.inputs.type == 'all' ||
github.event.inputs.type == 'linux'
(github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.type == 'all' || github.event.inputs.type == 'linux')))
runs-on: ${{ (github.repository_owner == 'AcademySoftwareFoundation' && 'ubuntu-22.04-8c-32g-300h') || 'ubuntu-latest' }}
name: >
linux:${{ matrix.config.image }}-abi:${{ matrix.config.abi }}-cxx:${{ matrix.config.cxx }}-type:${{ matrix.config.build }}
@@ -119,9 +122,10 @@ jobs:
windows:
# Windows CI. Tests a dynamic build with MD.
if: |
github.event_name != 'workflow_dispatch' ||
github.event.inputs.type == 'all' ||
github.event.inputs.type == 'win'
(github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.type == 'all' || github.event.inputs.type == 'win')))
runs-on: ${{ (github.repository_owner == 'AcademySoftwareFoundation' && 'windows-2022-8c-32g-300h') || 'windows-latest' }}
name: windows
env:
@@ -163,9 +167,10 @@ jobs:
macos:
if: |
github.event_name != 'workflow_dispatch' ||
github.event.inputs.type == 'all' ||
github.event.inputs.type == 'mac'
(github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.type == 'all' || github.event.inputs.type == 'mac')))
runs-on: ${{ matrix.config.image }}
name: ${{ matrix.config.image }}
env:
+33 -5
View File
@@ -11,7 +11,6 @@ on:
- 'CHANGES'
- 'openvdb_maya/**'
- 'pendingchanges/**'
- '**.md'
pull_request:
branches:
- '**'
@@ -19,7 +18,6 @@ on:
- 'CHANGES'
- 'openvdb_maya/**'
- 'pendingchanges/**'
- '**.md'
workflow_dispatch:
inputs:
deploy:
@@ -33,7 +31,24 @@ concurrency:
cancel-in-progress: true
jobs:
validate_docs:
# Cheap static checks that run before the heavier doxygen build:
# * every #include <...> referenced from a fenced code block in doc/ resolves
# * every github.com/.../blob/<branch>/<path> URL points to a file on disk
# Catches the class of docs-rot from issue #2161 (e.g. nanovdb::util ->
# nanovdb::tools rename) before the doxygen job runs.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: set_up_python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: run_validator
run: python3 ci/validate_docs_references.py
doxygen:
needs: validate_docs
runs-on: ${{ (github.repository_owner == 'AcademySoftwareFoundation' && 'ubuntu-22.04-8c-32g-300h') || 'ubuntu-latest' }}
env:
CXX: g++
@@ -45,9 +60,12 @@ jobs:
run: ./ci/install_doxygen.sh 1_14_0
- name: nanobind
run: ./ci/install_nanobind.sh 2.5.0
- name: install_latex
run: ./ci/install_texlive.sh
- name: build
# Doxygen's native WARN_AS_ERROR=FAIL_ON_WARNINGS gates the docs build:
# any warning fails the job. FAIL_ON_WARNINGS still generates the docs and
# writes the full warning log first, so the artifact step below captures
# it for debugging. Enabled via OPENVDB_DOXYGEN_WARN_AS_ERROR (see
# doc/CMakeLists.txt).
run: >
./ci/build.sh -v
--build-type=Release
@@ -58,7 +76,18 @@ jobs:
-DOPENVDB_PYTHON_WRAP_ALL_GRID_TYPES=ON
-DUSE_EXPLICIT_INSTANTIATION=OFF
-DDISABLE_DEPENDENCY_VERSION_CHECKS=ON
-DOPENVDB_DOXYGEN_WARN_AS_ERROR=ON
\"
- name: upload_doxygen_warning_log
# Always upload so contributors can debug failed PR builds without
# having to reproduce the doxygen environment locally.
if: always()
uses: actions/upload-artifact@v4
with:
name: doxygen-warnings
path: build/doc/doxygen.warnings.log
if-no-files-found: ignore
retention-days: 14
- name: pre_deploy
# Overwrite global SSH configuration
# https://github.com/peaceiris/actions-gh-pages/issues/719
@@ -147,4 +176,3 @@ jobs:
- Deployed from: AcademySoftwareFoundation/openvdb ${{ github.sha }}
Signed-off-by: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>"
+22 -12
View File
@@ -18,6 +18,8 @@ on:
- 'pendingchanges/**'
- '**.md'
pull_request:
# Add 'ready_for_review' to run CI when a draft PR is marked ready.
types: [opened, synchronize, reopened, ready_for_review]
branches:
- '**'
paths-ignore:
@@ -44,10 +46,12 @@ concurrency:
jobs:
linux-nanovdb:
# push OR non-draft PR OR matching manual run
if: |
github.event_name != 'workflow_dispatch' ||
github.event.inputs.type == 'all' ||
github.event.inputs.type == 'linux'
(github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.type == 'all' || github.event.inputs.type == 'linux')))
runs-on: ${{ (github.repository_owner == 'AcademySoftwareFoundation' && 'ubuntu-22.04-8c-32g-300h') || 'ubuntu-latest' }}
name: >
linux-nanovdb:cxx:${{ matrix.config.cxx }}-${{ matrix.config.build }}
@@ -92,9 +96,10 @@ jobs:
windows-nanovdb:
if: |
github.event_name != 'workflow_dispatch' ||
github.event.inputs.type == 'all' ||
github.event.inputs.type == 'win'
(github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.type == 'all' || github.event.inputs.type == 'win')))
runs-on: ${{ (github.repository_owner == 'AcademySoftwareFoundation' && 'windows-2022-8c-32g-300h') || 'windows-latest' }}
env:
VCPKG_DEFAULT_TRIPLET: 'x64-windows'
@@ -115,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
@@ -139,9 +147,10 @@ jobs:
macos-nanovdb:
if: |
github.event_name != 'workflow_dispatch' ||
github.event.inputs.type == 'all' ||
github.event.inputs.type == 'mac'
(github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.type == 'all' || github.event.inputs.type == 'mac')))
runs-on: ${{ matrix.config.runner }}
env:
CXX: ${{ matrix.config.cxx }}
@@ -168,9 +177,10 @@ jobs:
nanovdb-lite:
if: |
github.event_name != 'workflow_dispatch' ||
github.event.inputs.type == 'all' ||
github.event.inputs.type == 'linux'
(github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.type == 'all' || github.event.inputs.type == 'linux')))
runs-on: ${{ (github.repository_owner == 'AcademySoftwareFoundation' && 'ubuntu-22.04-8c-32g-300h') || 'ubuntu-latest' }}
container:
image: aswf/ci-openvdb:2024-clang17.2
+9 -10
View File
@@ -145,16 +145,15 @@ jobs:
strategy:
matrix:
config:
- { name: 'all', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=ON -DUSE_ZLIB=ON -DUSE_EXR=ON -DUSE_PNG=ON' }
- { name: 'lite', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=OFF -DUSE_ZLIB=OFF -DUSE_EXR=OFF -DUSE_PNG=OFF -DOPENVDB_USE_DELAYED_LOADING=OFF' }
- { name: 'half', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=OFF -DUSE_IMATH_HALF=ON' }
- { name: 'sse', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DOPENVDB_SIMD=SSE42' }
- { name: 'avx', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DOPENVDB_SIMD=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: 'conf', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DCMAKE_FIND_PACKAGE_PREFER_CONFIG=ON' }
- { name: 'all', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=ON -DUSE_ZLIB=ON -DUSE_EXR=ON -DUSE_PNG=ON' }
- { name: 'lite', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DUSE_BLOSC=OFF -DUSE_ZLIB=OFF -DUSE_EXR=OFF -DUSE_PNG=OFF -DOPENVDB_USE_DELAYED_LOADING=OFF' }
- { name: '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: 'conf', image: '2026', build: 'Release', components: 'core,python,bin,view,render,test', cmake: '-DCMAKE_FIND_PACKAGE_PREFER_CONFIG=ON' }
fail-fast: false
steps:
- uses: actions/checkout@v3
+50 -29
View File
@@ -80,6 +80,10 @@ option(OPENVDB_BUILD_NANOVDB "Build the NanoVDB library. Turns ON if USE_NANOVDB
# Global options
option(OPENVDB_ENABLE_RPATH "Build with RPATH information" ON)
option(OPENVDB_ENABLE_ASSERTS "Build with asserts in OpenVDB code enabled" OFF)
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
@@ -131,6 +135,30 @@ required.]=] ${USE_EXR})
option(USE_PNG "Use PNG while building openvdb components." OFF)
option(USE_AX "Use OpenVDB AX while building openvdb components." ${OPENVDB_BUILD_AX})
option(USE_NANOVDB "Use NanoVDB while building openvdb components." ${OPENVDB_BUILD_NANOVDB})
option(USE_VCL [=[
Use the internally supplied copy of Agner Fog's vectorclass (VCL) SIMD wrapper
library to instrument OpenVDB tools. This also requires VCL to be installed
along with OpenVDB headers. Only applicable which compiling for x86 targets.]=] OFF)
set(OPENVDB_X86_INSTRSET 0 CACHE STRING [=[
The x86 instruction set to use. When unset, if USE_VCL is ON, this is
automatically configured based on provided build flags. When unset and USE_VCL
is OFF, defaults to 0. It is expected for you to provide this flag. Available
values are:
0: Unknown or unspecified
1: SSE -msse
2: SSE2 -msse2 # Minimum on x86_64
3: SSE3 -msse3
4: SSSE3 -mssse3
5: SSE4.1 -msse4.1
6: SSE4.2 -msse4.2 # Minimum recommended where supported
7: AVX -mavx
8: AVX2 -mavx2
9: AVX512F -mavx512f
10: AVX512BW/DQ/VL -mavx512bw -mavx512dq -mavx512vl
Note that (while not strictly enforced by vendors) each option implies all
preceeding flags. If you need more granular control you can provide your
desired flags via CXX_FLAGS. Only applicable which compiling for x86 targets.
]=])
cmake_dependent_option(OPENVDB_DISABLE_BOOST_IMPLICIT_LINKING
"Disable the implicit linking of Boost libraries on Windows" ON "WIN32" OFF)
@@ -172,19 +200,6 @@ elseif(NOT ${CONCURRENT_MALLOC} IN_LIST _CONCURRENT_MALLOC_OPTIONS)
set(CONCURRENT_MALLOC Auto CACHE STRING FORCE)
endif()
set(_OPENVDB_SIMD_OPTIONS None SSE42 AVX)
if(NOT OPENVDB_SIMD)
set(OPENVDB_SIMD None CACHE STRING
"Choose whether to enable SIMD compiler flags or not, options are: None SSE42 AVX.
Although not required, it is strongly recommended to enable SIMD. AVX implies SSE42.
None is the default." FORCE
)
elseif(NOT ${OPENVDB_SIMD} IN_LIST _OPENVDB_SIMD_OPTIONS)
message(WARNING "Unrecognized or unsupported value for OPENVDB_SIMD, "
"using None instead.")
set(OPENVDB_SIMD None CACHE STRING FORCE)
endif()
if(USE_BLOSC AND NOT USE_ZLIB)
message(WARNING "ZLib can only be disabled if Blosc is also disabled. Enabling ZLib.")
endif()
@@ -385,6 +400,28 @@ endif()
#########################################################################
# Still support -DOPENVDB_SIMD=SSE42 and -DOPENVDB_SIMD=AVX, these alias to
# their respective OPENVDB_X86_INSTRSET value but, as a build option, are
# deprecated. Not that the defines in version.h need to persist in case
# downstream software was using them before including VDB headers
set(_OPENVDB_SIMD_OPTIONS None SSE42 AVX)
if(NOT OPENVDB_SIMD)
set(OPENVDB_SIMD None CACHE STRING "Deprecated, see OPENVDB_X86_INSTRSET." FORCE)
else()
if(NOT ${OPENVDB_SIMD} IN_LIST _OPENVDB_SIMD_OPTIONS)
message(WARNING "Unrecognized or unsupported value for OPENVDB_SIMD, using None instead.")
set(OPENVDB_SIMD None CACHE STRING "Deprecated, see OPENVDB_X86_INSTRSET." FORCE)
endif()
message(DEPRECATION "The OPENVDB_SIMD option is deprecated and will be removed. Use OPENVDB_X86_INSTRSET.")
if(OPENVDB_SIMD STREQUAL "AVX")
set(OPENVDB_X86_INSTRSET 7)
elseif(OPENVDB_SIMD STREQUAL "SSE42")
set(OPENVDB_X86_INSTRSET 6)
endif()
endif()
#########################################################################
# Configure general CMake and CXX settings
include(cmake/config/OpenVDBCXX.cmake)
@@ -404,22 +441,6 @@ if(CONCURRENT_MALLOC STREQUAL "Auto")
endif()
endif()
# Configure SIMD. AVX implies SSE 4.2.
if(OPENVDB_SIMD STREQUAL "AVX")
if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86)|(X86)|(amd64)|(AMD64)")
add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:-mavx>")
add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:-msse4.2>")
endif()
add_compile_definitions("$<$<COMPILE_LANGUAGE:CXX>:OPENVDB_USE_AVX>")
add_compile_definitions("$<$<COMPILE_LANGUAGE:CXX>:OPENVDB_USE_SSE42>")
elseif(OPENVDB_SIMD STREQUAL "SSE42")
if (CMAKE_SYSTEM_PROCESSOR MATCHES "(x86)|(X86)|(amd64)|(AMD64)")
add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:-msse4.2>")
endif()
add_compile_definitions("$<$<COMPILE_LANGUAGE:CXX>:OPENVDB_USE_SSE42>")
endif()
#########################################################################
# Configure our cmake modules to only search for static libraries
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
# Copyright Contributors to the OpenVDB Project
# SPDX-License-Identifier: Apache-2.0
"""Validate cross-references inside the OpenVDB documentation tree.
Run from the repo root (no arguments) or pass paths to validate explicitly.
The script scans Markdown (`.md`) and Doxygen narrative (`.txt`) files under
``doc/`` and checks two classes of cross-reference for staleness:
1. ``#include <path/to/header.h>`` lines that appear inside Markdown
``cpp``/``cuda``/``c``/``c++`` fenced code blocks or inside Doxygen
``@code``/``\\code`` blocks. Each include is resolved to a real file in
``openvdb/``, ``openvdb_ax/``, ``openvdb_houdini/`` or ``nanovdb/``.
2. ``https://github.com/AcademySoftwareFoundation/openvdb/blob/<ref>/<path>``
URLs. The ``<path>`` portion is checked against the local checkout.
The script exits non-zero on any unresolved reference and prints a
``file:line`` style error so the failure is actionable in CI logs. It is
intentionally dependency-free so it can run in a minimal Python container.
This guards against the class of bug tracked in issue #2161, where renames
under ``nanovdb/util/ -> nanovdb/tools/`` and ``nanovdb/util/IO.h ->
nanovdb/io/IO.h`` silently left the docs referencing paths that no longer
exist.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
from typing import Iterable, List, Sequence, Tuple
REPO_ROOT = Path(__file__).resolve().parent.parent
# Map a top-level include namespace to one or more on-disk directories that it
# resolves against. ``#include <nanovdb/io/IO.h>`` resolves to
# ``nanovdb/nanovdb/io/IO.h`` because the repo uses a double-named layout;
# ``#include <houdini_utils/ParmFactory.h>`` resolves to
# ``openvdb_houdini/openvdb_houdini/ParmFactory.h`` because the Houdini plugin
# CMake configuration synthesises a ``houdini_utils/`` subtree at build time
# (see ``openvdb_houdini/openvdb_houdini/CMakeLists.txt``).
INCLUDE_NAMESPACE_ROOTS = {
"openvdb": ("openvdb/openvdb", "openvdb"),
"openvdb_ax": ("openvdb_ax/openvdb_ax", "openvdb_ax"),
"openvdb_houdini": ("openvdb_houdini/openvdb_houdini", "openvdb_houdini"),
"nanovdb": ("nanovdb/nanovdb", "nanovdb"),
"houdini_utils": ("openvdb_houdini/openvdb_houdini",),
}
# Top-level include path components that come from outside this repo. When a
# header's leading component matches one of these, we skip resolution entirely.
# Houdini SDK headers (``UT/``, ``GA/``, ``GEO_*``, etc.) live in the Houdini
# install, not in our checkout.
EXTERNAL_INCLUDE_PREFIXES = (
"boost/",
"tbb/",
"cuda", # cuda_runtime.h, cuda.h, ...
"thrust/",
"Eigen/",
"OpenEXR/",
"Imath/",
"openexr/",
"GL/",
# Houdini SDK prefixes
"UT/",
"GA/",
"GU/",
"GEO/",
"GT/",
"GR/",
"OP/",
"PRM/",
"SOP/",
"SYS/",
"CH/",
"DEP/",
"PI/",
"RE/",
# Standard library headers (no '/' is the usual signal, but list a few
# explicitly to be safe).
"stdio.h",
"iostream",
"vector",
"string",
"memory",
"cstdint",
)
# Match a Markdown fenced code block whose language is one of cpp/cuda/c/c++.
_MARKDOWN_FENCE_RE = re.compile(
r"^```(?:cpp|cuda|c\+\+|c)\s*$(?P<body>.*?)^```\s*$",
re.DOTALL | re.MULTILINE,
)
# Doxygen ``@code``/``\code`` ... ``@endcode``/``\endcode`` blocks. The
# language hint (``@code{.cpp}``) is optional.
_DOXY_CODE_RE = re.compile(
r"[@\\]code(?:\{[^}]*\})?\s*$(?P<body>.*?)^[@\\]endcode\s*$",
re.DOTALL | re.MULTILINE,
)
_INCLUDE_RE = re.compile(r"#\s*include\s*<([^>\n]+)>")
# Match URLs pointing into a blob in this repo. ``<ref>`` may be a branch
# name or a sha; ``<path>`` is the portion that must exist on disk today.
_GITHUB_BLOB_RE = re.compile(
r"https?://github\.com/AcademySoftwareFoundation/openvdb/blob/"
r"(?P<ref>[^/\s]+)/(?P<path>[^\s)\"'#?]+)"
)
def is_external_include(header: str) -> bool:
"""Return True for headers we cannot resolve from this repo."""
head, _, _ = header.partition("/")
if head in INCLUDE_NAMESPACE_ROOTS:
return False
return any(header.startswith(p) for p in EXTERNAL_INCLUDE_PREFIXES) or "/" not in header
def resolve_include(header: str) -> Path | None:
"""Resolve ``#include <header>`` to a path on disk, or None if not found."""
head, _, rest = header.partition("/")
roots = INCLUDE_NAMESPACE_ROOTS.get(head)
if roots is None or not rest:
return None
for root in roots:
candidate = REPO_ROOT / root / rest
if candidate.is_file():
return candidate
return None
def iter_code_block_lines(text: str) -> Iterable[Tuple[int, str]]:
"""Yield (line_number, line_text) for every line inside a code block.
Line numbers are 1-based and refer to the original ``text``. Handles both
Markdown fences (```cpp ...```) and Doxygen ``@code``/``\\code`` blocks.
"""
for match in _MARKDOWN_FENCE_RE.finditer(text):
body_start = match.start("body")
yield from _lines_with_offsets(text, match.group("body"), body_start)
for match in _DOXY_CODE_RE.finditer(text):
body_start = match.start("body")
yield from _lines_with_offsets(text, match.group("body"), body_start)
def _lines_with_offsets(full_text: str, body: str, body_offset: int) -> Iterable[Tuple[int, str]]:
base_line = full_text.count("\n", 0, body_offset) + 1
for i, line in enumerate(body.splitlines()):
yield base_line + i, line
def find_doc_files(extra_paths: Sequence[str]) -> List[Path]:
"""Return the list of doc files to scan.
Without arguments, scan ``doc/**/*.md`` and ``doc/**/*.txt``. With
arguments, scan only those files (resolved relative to the repo root).
"""
if extra_paths:
return [
(Path(p) if Path(p).is_absolute() else REPO_ROOT / p).resolve()
for p in extra_paths
]
doc_dir = REPO_ROOT / "doc"
return sorted(
list(doc_dir.rglob("*.md")) + list(doc_dir.rglob("*.txt"))
)
def check_file(path: Path) -> List[str]:
"""Return a list of human-readable error strings for the given doc file."""
errors: List[str] = []
try:
text = path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
return [f"{path}: failed to read: {exc}"]
rel = path.relative_to(REPO_ROOT) if path.is_relative_to(REPO_ROOT) else path
for line_no, line in iter_code_block_lines(text):
for include_match in _INCLUDE_RE.finditer(line):
header = include_match.group(1).strip()
if is_external_include(header):
continue
if resolve_include(header) is None:
errors.append(
f"{rel}:{line_no}: include <{header}> does not resolve "
"to a file in this checkout"
)
for url_match in _GITHUB_BLOB_RE.finditer(text):
ref = url_match.group("ref")
url_path = url_match.group("path")
on_disk = REPO_ROOT / url_path
if not on_disk.exists():
# Compute the 1-based line number of the URL for the error report.
line_no = text.count("\n", 0, url_match.start()) + 1
errors.append(
f"{rel}:{line_no}: github URL "
f"AcademySoftwareFoundation/openvdb/blob/{ref}/{url_path} "
"does not exist in this checkout"
)
return errors
def main(argv: Sequence[str]) -> int:
files = find_doc_files(argv[1:])
total_errors: List[str] = []
for path in files:
if not path.exists():
total_errors.append(f"{path}: file does not exist")
continue
total_errors.extend(check_file(path))
if total_errors:
print("Documentation reference validation FAILED.", file=sys.stderr)
print("", file=sys.stderr)
for err in total_errors:
print(err, file=sys.stderr)
print("", file=sys.stderr)
print(
f"{len(total_errors)} broken reference(s) across "
f"{len(files)} file(s).",
file=sys.stderr,
)
return 1
print(f"OK: {len(files)} doc file(s) validated, no broken references.")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
+5 -3
View File
@@ -419,9 +419,11 @@ if(NOT TARGET Blosc::blosc)
set(${BLOSC_EXTERNAL_LIB}_LIBRARIES ${${BLOSC_EXTERNAL_LIB}_LIBRARY_DEBUG})
endif()
target_link_libraries(Blosc::blosc INTERFACE
$<$<CONFIG:Release>:${${BLOSC_EXTERNAL_LIB}_LIBRARY_RELEASE}>
$<$<CONFIG:Debug>:${${BLOSC_EXTERNAL_LIB}_LIBRARY_DEBUG}>)
if(${BLOSC_EXTERNAL_LIB}_LIBRARY_RELEASE OR ${BLOSC_EXTERNAL_LIB}_LIBRARY_DEBUG)
target_link_libraries(Blosc::blosc INTERFACE
$<$<CONFIG:Release>:${${BLOSC_EXTERNAL_LIB}_LIBRARY_RELEASE}>
$<$<CONFIG:Debug>:${${BLOSC_EXTERNAL_LIB}_LIBRARY_DEBUG}>)
endif()
endforeach()
endif()
+116 -4
View File
@@ -83,8 +83,8 @@ function(OPENVDB_GET_VERSION_DEFINE HEADER KEY VALUE)
endfunction()
########################################################################
########################################################################
###############################################################################
###############################################################################
function(OPENVDB_VERSION_FROM_HEADER OPENVDB_VERSION_FILE)
@@ -120,8 +120,8 @@ function(OPENVDB_VERSION_FROM_HEADER OPENVDB_VERSION_FILE)
endfunction()
########################################################################
########################################################################
###############################################################################
###############################################################################
function(OPENVDB_ABI_VERSION_FROM_PRINT OPENVDB_PRINT)
@@ -162,3 +162,115 @@ function(OPENVDB_ABI_VERSION_FROM_PRINT OPENVDB_PRINT)
set(${_VDB_ABI} ${_OpenVDB_ABI} PARENT_SCOPE)
endif()
endfunction()
###############################################################################
## @brief Compute the enumerated ISA value for x86/x86_64 architectures that
## VCL will use during compilation of OpenVDB. The value is stored in the
## VCL_INSTRSET variable. Possible values are:
## 0: Unknown or error
## 2: SSE2
## 3: SSE3
## 4: SSSE3
## 5: SSE4.1
## 6: SSE4.2
## 7: AVX
## 8: AVX2
## 9: AVX512F
## 10: AVX512BW/DQ/VL
function(OPENVDB_COMPUTE_X86_INSTRSET_FROM_VCL VCL_INSTRSET)
# Append project specific flags
set(OLD_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS})
if(OPENVDB_SIMD STREQUAL "AVX")
set(CMAKE_REQUIRED_FLAGS "-mavx;-msse4.2")
elseif(OPENVDB_SIMD STREQUAL "SSE42")
set(CMAKE_REQUIRED_FLAGS "-msse4.2")
endif()
# Figure out that the INSTRSET macro would expand to.
# @note would be much faster with try_run but this way doesn't involve
# invoking bad instruction sets on unsupported ISAs on the build machine.
include(CheckCXXSourceCompiles)
check_cxx_source_compiles([[
#include "${CMAKE_SOURCE_DIR}/ext/vcl/openvdb/ext/vcl/instrset.h"
int main() { static_assert(INSTRSET == 10); return 0; } ]] COMPUTE_VCL_INSTRSET__AVX512VL_DQ_BW__)
if(COMPUTE_VCL_INSTRSET__AVX512VL_DQ_BW__)
set(${VCL_INSTRSET} 10 PARENT_SCOPE)
set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
return()
endif()
check_cxx_source_compiles([[
#include "${CMAKE_SOURCE_DIR}/ext/vcl/openvdb/ext/vcl/instrset.h"
int main() { static_assert(INSTRSET == 9); return 0; } ]] COMPUTE_VCL_INSTRSET__AVX512F__)
if(COMPUTE_VCL_INSTRSET__AVX512F__)
set(${VCL_INSTRSET} 9 PARENT_SCOPE)
set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
return()
endif()
check_cxx_source_compiles([[
#include "${CMAKE_SOURCE_DIR}/ext/vcl/openvdb/ext/vcl/instrset.h"
int main() { static_assert(INSTRSET == 8); return 0; } ]] COMPUTE_VCL_INSTRSET__AVX2__)
if(COMPUTE_VCL_INSTRSET__AVX2__)
set(${VCL_INSTRSET} 8 PARENT_SCOPE)
set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
return()
endif()
check_cxx_source_compiles([[
#include "${CMAKE_SOURCE_DIR}/ext/vcl/openvdb/ext/vcl/instrset.h"
int main() { static_assert(INSTRSET == 7); return 0; } ]] COMPUTE_VCL_INSTRSET__AVX__)
if(COMPUTE_VCL_INSTRSET__AVX__)
set(${VCL_INSTRSET} 7 PARENT_SCOPE)
set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
return()
endif()
check_cxx_source_compiles([[
#include "${CMAKE_SOURCE_DIR}/ext/vcl/openvdb/ext/vcl/instrset.h"
int main() { static_assert(INSTRSET == 6); return 0; } ]] COMPUTE_VCL_INSTRSET__SSE4_2__)
if(COMPUTE_VCL_INSTRSET__SSE4_2__)
set(${VCL_INSTRSET} 6 PARENT_SCOPE)
set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
return()
endif()
check_cxx_source_compiles([[
#include "${CMAKE_SOURCE_DIR}/ext/vcl/openvdb/ext/vcl/instrset.h"
int main() { static_assert(INSTRSET == 5); return 0; } ]] COMPUTE_VCL_INSTRSET__SSE4_1__)
if(COMPUTE_VCL_INSTRSET__SSE4_1__)
set(${VCL_INSTRSET} 5 PARENT_SCOPE)
set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
return()
endif()
check_cxx_source_compiles([[
#include "${CMAKE_SOURCE_DIR}/ext/vcl/openvdb/ext/vcl/instrset.h"
int main() { static_assert(INSTRSET == 4); return 0; } ]] COMPUTE_VCL_INSTRSET__SSSE3__)
if(COMPUTE_VCL_INSTRSET__SSSE3__)
set(${VCL_INSTRSET} 4 PARENT_SCOPE)
set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
return()
endif()
check_cxx_source_compiles([[
#include "${CMAKE_SOURCE_DIR}/ext/vcl/openvdb/ext/vcl/instrset.h"
int main() { static_assert(INSTRSET == 3); return 0; } ]] COMPUTE_VCL_INSTRSET__SSE3__)
if(COMPUTE_VCL_INSTRSET__SSE3__)
set(${VCL_INSTRSET} 3 PARENT_SCOPE)
set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
return()
endif()
check_cxx_source_compiles([[
#include "${CMAKE_SOURCE_DIR}/ext/vcl/openvdb/ext/vcl/instrset.h"
int main() { static_assert(INSTRSET == 2); return 0; } ]] COMPUTE_VCL_INSTRSET__SSE2__)
if(COMPUTE_VCL_INSTRSET__SSE2__)
set(${VCL_INSTRSET} 2 PARENT_SCOPE)
set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
return()
endif()
check_cxx_source_compiles([[
#include "${CMAKE_SOURCE_DIR}/ext/vcl/openvdb/ext/vcl/instrset.h"
int main() { static_assert(INSTRSET == 1); return 0; } ]] COMPUTE_VCL_INSTRSET__SSE__)
if(COMPUTE_VCL_INSTRSET__SSE__)
set(${VCL_INSTRSET} 1 PARENT_SCOPE)
set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
return()
endif()
# Unknown or error
set(${VCL_INSTRSET} 0 PARENT_SCOPE)
set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})
endfunction()
+41
View File
@@ -127,6 +127,47 @@ endif()
###############################################################################
# x86 ISA selection
if(CMAKE_SYSTEM_PROCESSOR MATCHES "(x86)|(X86)|(amd64)|(AMD64)")
if(OPENVDB_X86_INSTRSET GREATER_EQUAL 1)
add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:-msse>")
endif()
if(OPENVDB_X86_INSTRSET GREATER_EQUAL 2)
add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:-msse2>")
endif()
if(OPENVDB_X86_INSTRSET GREATER_EQUAL 3)
add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:-msse3>")
endif()
if(OPENVDB_X86_INSTRSET GREATER_EQUAL 4)
add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:-mssse3>")
endif()
if(OPENVDB_X86_INSTRSET GREATER_EQUAL 5)
add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:-msse4.1>")
endif()
if(OPENVDB_X86_INSTRSET GREATER_EQUAL 6)
add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:-msse4.2>")
endif()
if(OPENVDB_X86_INSTRSET GREATER_EQUAL 7)
add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:-mavx>")
endif()
if(OPENVDB_X86_INSTRSET GREATER_EQUAL 8)
add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:-mavx2>")
endif()
if(OPENVDB_X86_INSTRSET GREATER_EQUAL 9)
add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:-mavx512f>")
endif()
if(OPENVDB_X86_INSTRSET GREATER_EQUAL 10)
add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:-mavx512bw>")
add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:-mavx512dq>")
add_compile_options("$<$<COMPILE_LANGUAGE:CXX>:-mavx512vl>")
endif()
elseif(OPENVDB_X86_INSTRSET GREATER 0)
message(WARNING "OPENVDB_X86_INSTRSET has been set but has no effect when building for ${CMAKE_SYSTEM_PROCESSOR}.")
endif()
###############################################################################
# Increase the number of sections that an object file can contain
add_compile_options("$<$<COMPILE_LANG_AND_ID:CXX,MSVC>:/bigobj>")
# Enable MSVC options that make it behave like other compilers
+50 -4
View File
@@ -25,6 +25,14 @@ option(OPENVDB_DOXYGEN_NANOVDB "Build NanoVDB documentation" ON)
option(OPENVDB_DOXYGEN_INTERNAL [=[
Enable documentation of methods and classes which have been explicitly marked as internal
]=] OFF)
# When enabled, Doxygen's WARN_AS_ERROR=FAIL_ON_WARNINGS makes any warning fail
# the docs build (it still generates the docs and writes the full warning log
# first, then exits non-zero). CI turns this ON -- see .github/workflows/docs.yml
# -- so net-new doc warnings fail the PR that introduces them. Defaults OFF so a
# local doc build isn't blocked by a stray warning from a different Doxygen
# version; pass -DOPENVDB_DOXYGEN_WARN_AS_ERROR=ON to gate locally too.
option(OPENVDB_DOXYGEN_WARN_AS_ERROR
"Treat Doxygen warnings as errors" OFF)
find_package(Doxygen 1.0...<1.15.0 REQUIRED doxygen)
if(MINIMUM_DOXYGEN_VERSION)
@@ -64,9 +72,14 @@ if(OPENVDB_DOXYGEN_INTERNAL)
endif()
if(NOT OPENVDB_DOXYGEN_INTERNAL)
# Hide implementation-detail symbols in the lowercase `internal` namespaces
# (the project convention for non-public helpers). Note: do NOT add a
# capital-I "*Internal*" glob here -- it also matches the public
# tree::InternalNode (and nanovdb::InternalNode) class, silently dropping it
# from the docs and breaking every \link to it (e.g. InternalNode::childCount
# in doc/changes.txt) while its RootNode/LeafNode siblings resolve fine.
set(DOXYGEN_EXCLUDE_SYMBOLS
"*internal*"
"*Internal*")
"*internal*")
endif()
set(DOXYGEN_PROJECT_NAME "OpenVDB")
@@ -77,15 +90,41 @@ set(DOXYGEN_FILE_PATTERNS *.h *h.in) # headers only
set(DOXYGEN_EXTENSION_MAPPING .in=C) # parse CMake config headers as C
set(DOXYGEN_IMAGE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/img")
set(DOXYGEN_RECURSIVE NO)
set(DOXYGEN_QUIET YES)
set(DOXYGEN_NUM_PROC_THREADS 0) # detects from system
# Enable \snippet resolution against the on-disk example sources. Paths in
# \snippet directives are resolved relative to EXAMPLE_PATH, so referring to
# `nanovdb/nanovdb/examples/ex_xxx/yyy.cc` from a doc page works as written.
set(DOXYGEN_EXAMPLE_PATH "${CMAKE_SOURCE_DIR}")
set(DOXYGEN_EXAMPLE_PATTERNS *.cc *.cpp *.cu *.cuh *.h *.txt)
set(DOXYGEN_EXAMPLE_RECURSIVE YES)
# Always capture Doxygen warnings to a logfile so CI can publish it as an
# artifact and contributors can see exactly what's broken.
set(DOXYGEN_WARN_LOGFILE "${CMAKE_CURRENT_BINARY_DIR}/doxygen.warnings.log")
if(OPENVDB_DOXYGEN_WARN_AS_ERROR)
# FAIL_ON_WARNINGS keeps generating docs but exits non-zero at the end if
# any warning was emitted. Requires Doxygen >= 1.9.x.
set(DOXYGEN_WARN_AS_ERROR FAIL_ON_WARNINGS)
# Don't suppress messages so contributors can see warnings during the build.
set(DOXYGEN_QUIET NO)
else()
set(DOXYGEN_QUIET YES)
endif()
set(DOXYGEN_GENERATE_HTML YES)
set(DOXYGEN_GENERATE_MAN NO)
set(DOXYGEN_GENERATE_LATEX NO)
set(DOXYGEN_GENERATE_TODOLIST NO)
set(DOXYGEN_HTML_TIMESTAMP NO) # for simpler website diffs
# Render math formulas via MathJax instead of LaTeX -> PNG. This removes the
# entire `latex`/`dvips`/`gs` toolchain dependency from both local doc builds
# and CI (see .github/workflows/docs.yml), eliminates a class of cross-platform
# install pain, and produces crisper, selectable math in the rendered HTML.
set(DOXYGEN_USE_MATHJAX YES)
set(DOXYGEN_MATHJAX_VERSION MathJax_3) # default JS bundle loaded from a CDN
set(DOXYGEN_EXTRACT_ALL YES)
set(DOXYGEN_EXTRACT_STATIC YES)
set(DOXYGEN_EXTRACT_LOCAL_CLASSES NO)
@@ -177,6 +216,11 @@ set(DOXYGEN_PREDEFINED
"OPENVDB_VERSION_NAME=v${OpenVDB_MAJOR_VERSION}_${OpenVDB_MINOR_VERSION}"
"OPENVDB_ABI_VERSION_NUMBER=${OpenVDB_MAJOR_VERSION}"
[[__declspec(x):= __attribute__(x):=]]
# Strip the symbol-visibility/export macro so Doxygen doesn't fold it into a
# declaration's name. Doxygen tolerates `class OPENVDB_API X` but mis-parses
# `enum OPENVDB_API X` (e.g. half::FromBitsTag in ext/imath/.../Half.h),
# reporting the enum as a documented-but-undeclared symbol.
"OPENVDB_API="
# Replace the deprecated macros with doxygen deprecated comments. This ensures
# that every bit of deprecated C++ is listed in the doxygen deprecated list,
# even if it was not properly tagged
@@ -240,7 +284,9 @@ if(OPENVDB_DOXYGEN_HOUDINI)
endif()
if(OPENVDB_DOXYGEN_AX)
set(DOXYGEN_EXTRA_PACKAGES amsmath) # for math formatting
# AX docs use @f$ inline and @f[ block math. Formula font size also applies
# under MathJax (set above). The amsmath package the AX docs assume is
# bundled by default in MathJax v3, so no extra extensions are required.
set(DOXYGEN_FORMULA_FONTSIZE 14)
list(APPEND DOXYGEN_ENABLED_SECTIONS "OPENVDB_AX")
-1
View File
@@ -42,5 +42,4 @@ provided:
- @subpage axfunctionlist "AX Functions" for a list of all available AX functions.
- @ref build "Building OpenVDB" and @ref dependencies "OpenVDB Dependencies" for information on building OpenVDB and <b>OpenVDB AX</b>.
</div>
*/
+37 -39
View File
@@ -54,7 +54,7 @@ OpenVDB:
ellipsoidal rasterizations.
- API Changes:
- Added @vdblink::tree::LeafBuffer::storage() LeafBuffer::storage()@endlink
- Added @c openvdb::tree::LeafBuffer::storage()
for direct const access to the raw data buffer when using the bool LeafNode.
- Removed Grid::evalMinMax(), use
@vdblink::tools::minMax tools::minMax@endlink for superior threaded
@@ -2807,8 +2807,8 @@ Improvements:
<I>[Contributed&nbsp;by&nbsp;Double&nbsp;Negative]</I>
- Registration of OpenVDB&nbsp;Points grid and attribute types is now
handled in @vdblink::initialize() openvdb::initialize@endlink,
and @vdblink::points::initialize() points::initialize@endlink
and @vdblink::points::uninitialize() points::uninitialize@endlink
and @c openvdb::points::initialize()
and @c openvdb::points::uninitialize()
are therefore deprecated.
- Extended multi-pass I/O to handle a variable number of passes per leaf node.
<I>[Contributed&nbsp;by&nbsp;Double&nbsp;Negative]</I>
@@ -3526,11 +3526,10 @@ Houdini:
- @vdblink::tools::volumeToMesh() tools::volumeToMesh@endlink now compiles
for all grid types but throws an exception if the input grid does not
have a scalar value type.
- Added a
@vdblink::io::File::readGrid(const Name&, const BBoxd&) File::readGrid@endlink
overload and @vdblink::GridBase::readBuffers(std::istream&, const CoordBBox&)
readBuffers@endlink overloads to the grid, tree and node classes that allow
one to specify a bounding box against which to clip a grid while reading it.
- Added an <TT>openvdb::io::File::readGrid(const Name&, const BBoxd&)</TT>
overload and <TT>openvdb::GridBase::readBuffers(std::istream&, const CoordBBox&)</TT>
overloads to the grid, tree and node classes that allow one to specify a
bounding box against which to clip a grid while reading it.
For large grids, clipping while reading can result in significantly lower
memory usage than clipping after reading.
- Added @vdblink::GridBase::clipGrid() GridBase::clipGrid@endlink, which
@@ -3589,10 +3588,11 @@ Houdini:
- Added constructors and methods to both
@vdblink::math::RandInt RandInt@endlink and
@vdblink::math::Rand01 Rand01@endlink to set and reset the random seed value.
- Added convenience methods for
@vdblink::math::Transform::indexToWorld(const BBoxd&) const transforming@endlink
@vdblink::math::Transform::worldToIndex(const BBoxd&) const bounding@endlink
@vdblink::math::Transform::worldToIndexCellCentered(const BBoxd&) const boxes@endlink
- Added convenience methods
<TT>openvdb::math::Transform::indexToWorld(const BBoxd&)</TT>,
<TT>openvdb::math::Transform::worldToIndex(const BBoxd&)</TT> and
<TT>openvdb::math::Transform::worldToIndexCellCentered(const BBoxd&)</TT>
for transforming bounding boxes
to @vdblink::math::Transform math::Transform@endlink.
- @c vdb_view is now compatible with both GLFW&nbsp;2 and GLFW&nbsp;3.
- Made many small changes to address type conversion and other warnings
@@ -3612,9 +3612,9 @@ Houdini:
@par
API changes:
- The addition of a
@vdblink::GridBase::readBuffers(std::istream&, const CoordBBox&)
GridBase::readBuffers@endlink virtual function overload and the
- The addition of an
<TT>openvdb::GridBase::readBuffers(std::istream&, const CoordBBox&)</TT>
virtual function overload and the
@vdblink::GridBase::clip() GridBase::clip@endlink
@vdblink::GridBase::readNonresidentBuffers()
GridBase::readNonresidentBuffers@endlink and
@@ -3719,16 +3719,15 @@ Houdini:
extractSparseTreeWithMask@endlink, which copies data from the index-space
intersection of a sparse tree and a dense input grid.
- Added copy constructors to the
@vdblink::Grid::Grid(const Grid<OtherTreeType>&) Grid@endlink,
@vdblink::tree::Tree::Tree(const Tree<OtherRootType>&) Tree@endlink,
@vdblink::tree::RootNode::RootNode(const RootNode<OtherChildType>&)
RootNode@endlink,
@vdblink::tree::InternalNode::InternalNode(const InternalNode<OtherChildNodeType, Log2Dim>&)
InternalNode@endlink and
@vdblink::tree::LeafNode::LeafNode(const LeafNode<OtherValueType, Log2Dim>&) LeafNode@endlink
<TT>openvdb::Grid::Grid(const Grid&lt;OtherTreeType&gt;&)</TT>,
<TT>openvdb::tree::Tree::Tree(const Tree&lt;OtherRootType&gt;&)</TT>,
<TT>openvdb::tree::RootNode::RootNode(const RootNode&lt;OtherChildType&gt;&)</TT>,
<TT>openvdb::tree::InternalNode::InternalNode(const InternalNode&lt;OtherChildNodeType, Log2Dim&gt;&)</TT>
and
<TT>openvdb::tree::LeafNode::LeafNode(const LeafNode&lt;OtherValueType, Log2Dim&gt;&)</TT>
classes, and an assignment operator overload to
@vdblink::tree::RootNode::operator=(const RootNode<OtherChildType>&)
RootNode@endlink, that allow the source and destination to have different
<TT>openvdb::tree::RootNode::operator=(const RootNode&lt;OtherChildType&gt;&)</TT>,
that allow the source and destination to have different
value types.
- Modified @vdblink::tree::Tree::combine2() Tree::combine2@endlink to permit
combination of trees with different value types.
@@ -3780,7 +3779,7 @@ API changes:
LeafManager::LeafRange::Iterator@endlink,
@vdblink::util::BaseMaskIterator::operator=() BaseMaskIterator@endlink,
@vdblink::util::NodeMask::operator=() NodeMask@endlink and
@vdblink::util::RootNodeMask::operator=() RootNodeMask@endlink
@c openvdb::util::RootNodeMask::operator=()
now return references to the respective objects.
- Removed a number of methods that were deprecated in version&nbsp;2.0.0
or earlier.
@@ -4246,7 +4245,7 @@ API changes:
- @vdblink::tree::LeafManager tree::LeafManager@endlink can now be
instantiated with a @const tree, although buffer swapping with @const trees
is disabled.
- Added a @vdblink::Grid::signedFloodFill() Grid::signedFloodFill@endlink
- Added an @c openvdb::Grid::signedFloodFill()
overload that allows one to specify inside and outside values.
- Fixed a bug in Grid::setBackground() so that now only the values of
inactive voxels change.
@@ -4256,9 +4255,9 @@ API changes:
that relied on input and output grids having compatible tree topology.
- @vdblink::math::Transform math::Transform@endlink now includes an
@vdblink::math::Transform::isIdentity() isIdentity@endlink predicate
and methods to @vdblink::math::Transform::preMult(const Mat4d&) pre-@endlink
and @vdblink::math::Transform::postMult(const Mat4d&) postmultiply@endlink
by a matrix.
and methods <TT>openvdb::math::Transform::preMult(const Mat4d&)</TT> and
<TT>openvdb::math::Transform::postMult(const Mat4d&)</TT>
to pre- and postmultiply by a matrix.
- Modified the @link NodeMasks.h node mask@endlink classes to permit
octree-like tree configurations (i.e., with a branching factor of two)
and to use 64-bit operations instead of 32-bit operations.
@@ -4331,7 +4330,7 @@ API changes:
faster for high-resolution meshes where the polygon/voxel size ratio
is small.
- Added @vdblink::createLevelSet() createLevelSet@endlink and
@vdblink::createLevelSetSphere() createLevelSetSphere@endlink
@vdblink::tools::createLevelSetSphere() createLevelSetSphere@endlink
factory functions for level set grids.
- @vdblink::tree::ValueAccessor tree::ValueAccessor@endlink is now faster
for trees of height 2, 3 and 4 (the latter is the default), and it now
@@ -4444,7 +4443,7 @@ API changes:
- Added @vdblink::tools::compReplace() tools::compReplace@endlink,
which copies the active values of one grid into another, and added
a "Replace A With Active B" mode to the Combine SOP.
- @vdblink::Grid::signedFloodFill() Grid::signedFloodFill@endlink
- @c openvdb::Grid::signedFloodFill()
no longer enters an infinite loop when filling an empty grid.
- Fixed a bug in the particle to level set converter that sometimes
produced level sets with holes, and fixed a bug in the SOP that
@@ -4707,8 +4706,8 @@ API changes:
math library and a method to return the
@vdblink::math::Mat3::trace trace@endlink of a @c Mat3.
- Fixed a bug in the
@vdblink::tree::ValueAccessor::ValueAccessor(const ValueAccessor&)
ValueAccessor@endlink copy constructor that caused the copy to reference
<TT>openvdb::tree::ValueAccessor::ValueAccessor(const ValueAccessor&)</TT>
copy constructor that caused the copy to reference
the original.
- Fixed a bug in @vdblink::tree::RootNode::setActiveState()
RootNode::setActiveState@endlink that caused a crash
@@ -4716,7 +4715,7 @@ API changes:
- Added a @c Tree::pruneLevelSet method that is similar to but faster than
Tree::pruneInactive() for level set grids.
- Added fast leaf node voxel access
@vdblink::tree::LeafNode::getValue(Index) const methods@endlink
<TT>openvdb::tree::LeafNode::getValue(Index)</TT> methods
that index by linear offset (as returned by
@vdblink::tree::LeafNode::ValueOnIter::pos() ValueIter::pos@endlink)
instead of by @ijk coordinates.
@@ -4769,7 +4768,7 @@ API changes:
just the active states of voxels from one grid with those of
another grid of a possibly different type.
- Fixed an incorrect scale factor in the Laplacian diffusion
@vdblink::tools::Filter::laplacian() filter@endlink.
@c openvdb::tools::Filter::laplacian() filter.
- Fixed a bug in @vdblink::tree::Tree::merge() Tree::merge@endlink
that could leave a tree with invalid value accessors.
- Added @vdblink::tree::TreeValueIteratorBase::setActiveState()
@@ -4804,7 +4803,7 @@ API changes:
@par
<B>Version 0.92.0</B> - <I>August 23 2012</I>
- Added a Laplacian diffusion
@vdblink::tools::Filter::laplacian() filter@endlink.
@c openvdb::tools::Filter::laplacian() filter.
- Fixed a bug in the initialization of the sparse contour tracer
that caused mesh-to-volume conversion to fail in certain cases.
- Fixed a bug in the curvature stencil that caused mean curvature
@@ -4832,9 +4831,8 @@ API changes:
method that replaces a grid&rsquo;s tree with a new, empty tree of the
correct type.
- Fixed a crash in
@vdblink::tree::Tree::setValueOff(const Coord& xyz, const ValueType& value)
Tree::setValueOff@endlink when the new value was equal to the
background value.
<TT>openvdb::tree::Tree::setValueOff(const Coord& xyz, const ValueType& value)</TT>
when the new value was equal to the background value.
- Fixed bugs in Tree::prune() that could result in output tiles with
incorrect active states.
- Added @c librt to the link dependencies to address build failures
+38 -28
View File
@@ -36,7 +36,7 @@ Reference Platform, but for those that do, their specified versions are
Component | Requirements | Optional
----------------------- | ----------------------------------------------- | --------
OpenVDB Core Library | CMake, C++17 compiler, TBB::tbb | Blosc, ZLib, Log4cplus, Imath::Imath, Boost::iostream
OpenVDB Core Library | CMake, C++17 compiler, TBB::tbb | Blosc, ZLib, Log4cplus, Imath::Imath (included), Boost::iostream, VCL (included)
OpenVDB Print | Core Library dependencies | -
OpenVDB LOD | Core Library dependencies | -
OpenVDB Render | Core Library dependencies | OpenEXR, Imath::Imath, libpng
@@ -45,41 +45,51 @@ OpenVDB Python | Core Library dependencies, Python, nanobind | -
OpenVDB AX | Core Library dependencies, LLVM | Bison, Flex
NanoVDB | - | Core Library, CUDA, TBB, Blosc, ZLib
OpenVDB UnitTests | Core Library dependencies, GoogleTest, numpy* | -
OpenVDB Documentation | Doxygen | -
OpenVDB Documentation | Doxygen, Doxygen Awesome (included) | -
- @b * GLEW is only required for building the vdb_view binary on Windows.
- @b * numpy only required for python binding unit tests
- @b Note - Bison and Flex are only required for re-generation of the AX
grammar which is not typically necessary for the majority of users.
Note that OpenVDB comes bundled with the following dependencies. Please see
their respective license documentation in `ext/THIRD-PARTY.md` for license
information.
- Doxygen Awesome
- Imath::Imath
- VCL
@subsection depDependencyTable Dependency Table
Package | Minimum | Recommended | Description | apt-get | Homebrew | Source
-------------- | ------- | ----------- | ----------------------------------------------------------------- | ------- | -------- | ------
CMake | 3.24 | Latest | Cross-platform family of tools designed to help build software | Y | Y | https://cmake.org
GCC | 11.2.1 | 11.2.1 | C++ 17 Compiler: The GNU Compiler Collection | Y | Y | https://www.gnu.org/software/gcc
Clang | 5.0 | Latest | C++ 17 Compiler: A C language family frontend for LLVM | Y | Y | https://clang.llvm.org
Intel ICC | 19 | Latest | C++ 17 Compiler: Intels C++ Compiler | Y | Y | https://software.intel.com/en-us/c-compilers
MSVC | 19.30 | 19.30 | C++ 17 Compiler: Microsoft Visual C++ Compiler | Y | Y | https://visualstudio.microsoft.com/vs
Imath | 3.2 | >= 3.3 | Half precision floating points | Y | Y | http://www.openexr.com
OpenEXR | 3.2 | >= 3.3 | EXR serialization support | Y | Y | http://www.openexr.com
TBB | 2020.3 | >= 2021 | Threading Building Blocks - template library for task parallelism | Y | Y | https://www.threadingbuildingblocks.org
ZLIB | 1.2.7 | Latest | Compression library for disk serialization compression | Y | Y | https://www.zlib.net
Boost | 1.82 | >= 1.85 | Components: iostreams | Y | Y | https://www.boost.org
LLVM | 15.0.0* | 18.0.0 | Target-independent code generation | Y | Y | https://llvm.org/
Bison | 3.7.0 | 3.7.0 | General-purpose parser generator | Y | Y | https://www.gnu.org/software/gcc
Flex | 2.6.4 | 2.6.4 | Fast lexical analyzer generator | Y | Y | https://github.com/westes/flex
Python | 3.10 | 3.11 | The python interpreter and libraries | Y | Y | https://www.python.org
nanobind** | 2.0.0 | >= 2.5.0 | C++/python bindings | Y | Y | https://nanobind.readthedocs.io
GoogleTest | 1.10 | Latest | A unit testing framework module for C++ | Y | Y | https://github.com/google/googletest
Blosc | 1.17.0* | Latest | Recommended dependency for improved disk compression | Y | Y | https://github.com/Blosc/c-blosc/releases
Log4cplus | 1.1.2 | Latest | An optional dependency for improved OpenVDB Logging | Y | Y | https://github.com/log4cplus/log4cplus
libpng | - | Latest | Library for manipulating PNG images | Y | Y | http://www.libpng.org/pub/png/libpng.html
GLFW | 3.3 | Latest | Simple API for OpenGL development | Y | Y | https://www.glfw.org
OpenGL | 3.2 | Latest | Environment for developing portable graphics applications | Y | Y | https://www.opengl.org
GLEW | 1.0.0 | Latest | A cross-platform OpenGL extension loading library. | Y | Y | http://glew.sourceforge.net
CUDA | - | Latest | Parallel computing platform for graphical processing units. | Y | N | https://developer.nvidia.com/cuda-downloads
Doxygen | 1.8.8 | <= 1.14.0 | Documentation generation from C++ | Y | Y | http://www.doxygen.nl
Package | Minimum | Recommended | Description | apt-get | Homebrew | Source
--------------- | ------- | ----------- | ----------------------------------------------------------------- | ------- | -------- | ------
CMake | 3.24 | Latest | Cross-platform family of tools designed to help build software | Y | Y | https://cmake.org
GCC | 11.2.1 | 11.2.1 | C++ 17 Compiler: The GNU Compiler Collection | Y | Y | https://www.gnu.org/software/gcc
Clang | 5.0 | Latest | C++ 17 Compiler: A C language family frontend for LLVM | Y | Y | https://clang.llvm.org
Intel ICC | 19 | Latest | C++ 17 Compiler: Intels C++ Compiler | Y | Y | https://software.intel.com/en-us/c-compilers
MSVC | 19.30 | 19.30 | C++ 17 Compiler: Microsoft Visual C++ Compiler | Y | Y | https://visualstudio.microsoft.com/vs
Imath | 3.2 | >= 3.3 | Half precision floating points | Y | Y | http://www.openexr.com
OpenEXR | 3.2 | >= 3.3 | EXR serialization support | Y | Y | http://www.openexr.com
TBB | 2020.3 | >= 2021 | Threading Building Blocks - template library for task parallelism | Y | Y | https://www.threadingbuildingblocks.org
ZLIB | 1.2.7 | Latest | Compression library for disk serialization compression | Y | Y | https://www.zlib.net
Boost | 1.82 | >= 1.85 | Components: iostreams | Y | Y | https://www.boost.org
VCL | 2.02.0 | >= 2.02.0 | x86 intrinsics and SIMD class wrappers and | Y | Y | https://github.com/vectorclass/version2
LLVM | 15.0.0* | 18.0.0 | Target-independent code generation | Y | Y | https://llvm.org/
Bison | 3.7.0 | 3.7.0 | General-purpose parser generator | Y | Y | https://www.gnu.org/software/gcc
Flex | 2.6.4 | 2.6.4 | Fast lexical analyzer generator | Y | Y | https://github.com/westes/flex
Python | 3.10 | 3.11 | The python interpreter and libraries | Y | Y | https://www.python.org
nanobind** | 2.0.0 | >= 2.5.0 | C++/python bindings | Y | Y | https://nanobind.readthedocs.io
GoogleTest | 1.10 | Latest | A unit testing framework module for C++ | Y | Y | https://github.com/google/googletest
Blosc | 1.17.0* | Latest | Recommended dependency for improved disk compression | Y | Y | https://github.com/Blosc/c-blosc/releases
Log4cplus | 1.1.2 | Latest | An optional dependency for improved OpenVDB Logging | Y | Y | https://github.com/log4cplus/log4cplus
libpng | - | Latest | Library for manipulating PNG images | Y | Y | http://www.libpng.org/pub/png/libpng.html
GLFW | 3.3 | Latest | Simple API for OpenGL development | Y | Y | https://www.glfw.org
OpenGL | 3.2 | Latest | Environment for developing portable graphics applications | Y | Y | https://www.opengl.org
GLEW | 1.0.0 | Latest | A cross-platform OpenGL extension loading library. | Y | Y | http://glew.sourceforge.net
CUDA | - | Latest | Parallel computing platform for graphical processing units. | Y | N | https://developer.nvidia.com/cuda-downloads
Doxygen | 1.8.8 | <= 1.14.0 | Documentation generation from C++ | Y | Y | http://www.doxygen.nl
Doxygen Awesome | 2.0.0 | >= 2.0.0 | Documentation generation from C++ | Y | Y | https://github.com/jothepro/doxygen-awesome-css
- @b * See the note in [known issues](@ref depKnownIssues) regarding supported blosc/llvm versions.
- @b ** See the note in [known issues](@ref depKnownIssues) regarding supported nanobind build issues.
+1 -1
View File
@@ -26,7 +26,7 @@
* **Does NanoVDB depend on and require CUDA?**
No, NanoVDB is by design agnostic and works with most graphics APIs, e.g. OpenGL, OpenCL, OptiX, CUDA, HLSL, GLSL, DirectX, and even the CPU! The data structure itself (defined in [NanoVDB.h](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/NanoVDB.h) and [PNanoVDB.h](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/PNanoVDB.h)) only depends on C++11 or C99. However, some of the auxiliary math classes are optionally optimized with intrinsic CUDA calls (e.g. fma in Vec3) and some of the [Allocator](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/util/CudaDeviceBuffer.h) uses CUDA for explicit memory management. It should be straightforward to replace (or remove) these CUDA dependencies if you so desire.
No, NanoVDB is by design agnostic and works with most graphics APIs, e.g. OpenGL, OpenCL, OptiX, CUDA, HLSL, GLSL, DirectX, and even the CPU! The data structure itself (defined in [NanoVDB.h](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/NanoVDB.h) and [PNanoVDB.h](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/PNanoVDB.h)) only depends on C++11 or C99. However, some of the auxiliary math classes are optionally optimized with intrinsic CUDA calls (e.g. fma in Vec3) and the [DeviceBuffer](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/cuda/DeviceBuffer.h) allocator uses CUDA for explicit memory management. It should be straightforward to replace (or remove) these CUDA dependencies if you so desire.
* **How does the data structure of NanoVDB compare to that of OpenVDB?**
+25 -141
View File
@@ -1,154 +1,38 @@
# NanoVDB Hello World Examples {#NanoVDB_HelloWorld}
[The first example](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_accessor/openvdb_to_nanovdb_accessor.cc) shows how to convert an OpenVDB level set sphere into a NanoVDB level set, use accessors to print out multiple values from both grids, and save the NanoVDB grid to file. Note that **this example depends on both OpenVDB and NanoVDB**.
The example snippets below are pulled directly from compiled example source
files via Doxygen's `\snippet` mechanism, so the code shown here is always
in sync with what is built and tested in CI. Enable
`-DNANOVDB_BUILD_EXAMPLES=ON` to build them locally.
```cpp
#include <openvdb/tools/LevelSetSphere.h> // replace with your own dependencies for generating the OpenVDB grid
#include <nanovdb/util/CreateNanoGrid.h> // converter from OpenVDB to NanoVDB (includes NanoVDB.h and GridManager.h)
#include <nanovdb/util/IO.h>
## Convert an OpenVDB grid to NanoVDB and use accessors
// Convert an openvdb level set sphere into a nanovdb, use accessors to print out multiple values from both
// grids and save the NanoVDB grid to file.
// Note, main depends on BOTH OpenVDB and NanoVDB.
int main()
{
try {
// Create an OpenVDB grid (here a level set surface but replace this with your own code)
auto srcGrid = openvdb::tools::createLevelSetSphere<openvdb::FloatGrid>(100.0f, openvdb::Vec3f(0.0f), 1.0f);
[The first example](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_accessor/openvdb_to_nanovdb_accessor.cc)
shows how to convert an OpenVDB level set sphere into a NanoVDB level set, use
accessors to print out multiple values from both grids, and save the NanoVDB
grid to file. Note that **this example depends on both OpenVDB and NanoVDB**.
// Convert the OpenVDB grid, srcGrid, into a NanoVDB grid handle.
auto handle = nanovdb::createNanoGrid(*srcGrid);
\snippet nanovdb/nanovdb/examples/ex_openvdb_to_nanovdb_accessor/openvdb_to_nanovdb_accessor.cc openvdb_to_nanovdb_accessor
// Define a (raw) pointer to the NanoVDB grid on the host. Note we match the value type of the srcGrid!
auto* dstGrid = handle.grid<float>();
## Read a NanoVDB grid from a file (host only)
if (!dstGrid)
throw std::runtime_error("GridHandle does not contain a grid with value type float");
[The second example](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor/read_nanovdb_sphere_accessor.cc)
reads a NanoVDB grid from a file (the one saved in the previous example) and
prints out multiple values. Note that **this example does not depend on
OpenVDB (nor CUDA), only NanoVDB**.
// Get accessors for the two grids. Note that accessors only accelerate repeated access!
auto dstAcc = dstGrid->getAccessor();
auto srcAcc = srcGrid->getAccessor();
\snippet nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor/read_nanovdb_sphere_accessor.cc read_nanovdb_sphere_accessor
// Access and print out a cross-section of the narrow-band level set from the two grids
for (int i = 97; i < 104; ++i) {
printf("(%3i,0,0) OpenVDB cpu: % -4.2f, NanoVDB cpu: % -4.2f\n", i, srcAcc.getValue(openvdb::Coord(i, 0, 0)), dstAcc.getValue(nanovdb::Coord(i, 0, 0)));
}
## Read a NanoVDB grid and access it on the CPU and GPU
nanovdb::io::writeGrid("data/sphere.nvdb", handle); // Write the NanoVDB grid to file and throw if writing fails
}
catch (const std::exception& e) {
std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl;
}
return 0;
}
```
[The third example](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda.cu)
reads a NanoVDB grid from a file (the one saved in the first example) and
prints out multiple values on both the CPU and GPU. Note that **this example
does NOT depend on OpenVDB, only NanoVDB and CUDA**.
[The second example](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor/read_nanovdb_sphere_accessor.cc) reads a NanoVDB grid from a file (the one saved in the previous example) and prints out multiple values. Note that **this example does not depend on OpenVDB (nor CUDA), only NanoVDB**.
\snippet nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda.cu read_nanovdb_sphere_accessor_cuda
```cpp
#include <nanovdb/util/IO.h> // this is required to read (and write) NanoVDB files on the host
NVCC requires the CUDA kernel to be defined in
[a separate .cu file](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda_kernel.cu):
/// @brief Read a NanoVDB grid from a file and print out multiple values.
///
/// @note Note This example does NOT depend on OpenVDB (nor CUDA), only NanoVDB.
int main()
{
try {
auto handle = nanovdb::io::readGrid("data/sphere.nvdb"); // reads first grid from file
auto* grid = handle.grid<float>(); // get a (raw) pointer to a NanoVDB grid of value type float
if (!grid)
throw std::runtime_error("File did not contain a grid with value type float");
auto acc = grid->getAccessor(); // create an accessor for fast access to multiple values
for (int i = 97; i < 104; ++i) {
printf("(%3i,0,0) NanoVDB cpu: % -4.2f\n", i, acc.getValue(nanovdb::Coord(i, 0, 0)));
}
}
catch (const std::exception& e) {
std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl;
}
return 0;
}
```
[The third example](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda.cc) reads a NanoVDB grid from a file (the one saved in the first example) and prints out multiple values on both the CPU and GPU. Note that **this example does NOT depend on OpenVDB, only NanoVDB and CUDA**.
```cpp
#include <nanovdb/util/IO.h> // this is required to read (and write) NanoVDB files on the host
#include <nanovdb/util/cuda/CudaDeviceBuffer.h> // required for CUDA memory management
extern "C" void launch_kernels(const nanovdb::NanoGrid<float>*,
const nanovdb::NanoGrid<float>*,
cudaStream_t stream);
/// @brief Read a NanoVDB grid from a file and print out multiple values on both the cpu and gpu.
///
/// @note Note This example does NOT depend on OpenVDB, only NanoVDB and CUDA.
int main()
{
try {
// returns a GridHandle using CUDA for memory management.
auto handle = nanovdb::io::readGrid<nanovdb::CudaDeviceBuffer>("data/sphere.nvdb");
cudaStream_t stream; // Create a CUDA stream to allow for asynchronous copy of pinned CUDA memory.
cudaStreamCreate(&stream);
handle.deviceUpload(stream, false); // Copy the NanoVDB grid to the GPU asynchronously
auto* cpuGrid = handle.grid<float>(); // get a (raw) pointer to a NanoVDB grid of value type float on the CPU
auto* deviceGrid = handle.deviceGrid<float>(); // get a (raw) pointer to a NanoVDB grid of value type float on the GPU
if (!deviceGrid || !cpuGrid)
throw std::runtime_error("GridHandle did not contain a grid with value type float");
launch_kernels(deviceGrid, cpuGrid, stream); // Call a host method to print a grid values on both the CPU and GPU
cudaStreamDestroy(stream); // Destroy the CUDA stream
}
catch (const std::exception& e) {
std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl;
}
return 0;
}
```
NVCC requires the CUDA kernel to be defined in [a separate .cu file](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda.cu):
```cpp
#include <nanovdb/NanoVDB.h> // this defined the core tree data structure of NanoVDB accessible on both the host and device
#include <stdio.h> // for printf
// This is called by the host only
void cpu_kernel(const nanovdb::NanoGrid<float>* cpuGrid)
{
auto cpuAcc = cpuGrid->getAccessor();
for (int i = 97; i < 104; ++i) {
printf("(%3i,0,0) NanoVDB cpu: % -4.2f\n", i, cpuAcc.getValue(nanovdb::Coord(i, 0, 0)));
}
}
// This is called by the device only
__global__ void gpu_kernel(const nanovdb::NanoGrid<float>* deviceGrid)
{
if (threadIdx.x > 6)
return;
int i = 97 + threadIdx.x;
auto gpuAcc = deviceGrid->getAccessor();
printf("(%3i,0,0) NanoVDB gpu: % -4.2f\n", i, gpuAcc.getValue(nanovdb::Coord(i, 0, 0)));
}
// This is called by the client code on the host
extern "C" void launch_kernels(const nanovdb::NanoGrid<float>* deviceGrid,
const nanovdb::NanoGrid<float>* cpuGrid,
cudaStream_t stream)
{
// Launch the device kernel asynchronously
gpu_kernel<<<1, 64, 0, stream>>>(deviceGrid);
// Launch the host "kernel" (synchronously)
cpu_kernel(cpuGrid);
}
```
\snippet nanovdb/nanovdb/examples/ex_read_nanovdb_sphere_accessor_cuda/read_nanovdb_sphere_accessor_cuda_kernel.cu read_nanovdb_sphere_accessor_cuda_kernel
+4 -3
View File
@@ -12,8 +12,8 @@
* [math/HDDA.h](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/math/HDDA.h) HDDA related.
* [math/SampleFromVoxels.h](https://github.com/AcademySoftwareFoundation/openvdb/blob/master/nanovdb/nanovdb/math/SampleFromVoxels.h) interpolation.
```bash
foo@bar:~$ tree
```
$ tree
.
├── CMakeLists.txt
├── cmd
@@ -175,4 +175,5 @@ foo@bar:~$ tree
├── SampleFromVoxels.h
├── Stencils.h
├── Timer.h
└── Util.h
└── Util.h
```
+26
View File
@@ -76,6 +76,32 @@ SOFTWARE.
-------------------------------------------------------------------------
Apache 2.0
* VCL (c) Copyright 2012-2022 Agner Fog.
https://github.com/vectorclass/version2
OpenVDB includes a copy of vectorclass for x86 SIMD intrinsic usage. Usage
of VCL is disabled by default and requires the VCL headers to be shipped with
OpenVDB installations. This behaviour and usage of VCL can be controlled
during OpenVDB configuration. See the full LICENSE terms in:
vcl/openvdb/ext/vcl/LICENSE
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-------------------------------------------------------------------------
Public domain
* The simplex noise implementation included in OpenVDB AX is Copyright (c)
+46 -23
View File
@@ -12,7 +12,7 @@
#ifndef OPENVDB_MATH_HALF_HAS_BEEN_INCLUDED
#define OPENVDB_MATH_HALF_HAS_BEEN_INCLUDED
/// @file half.h
/// @file Half.h
/// The half type is a 16-bit floating number, compatible with the
/// IEEE 754-2008 binary16 type.
///
@@ -21,6 +21,7 @@
/// We assume that a float, f, is an IEEE 754 single-precision
/// floating point number, whose bits are arranged as follows:
///
/// <pre>
/// 31 (msb)
/// |
/// | 30 23
@@ -30,28 +31,36 @@
/// X XXXXXXXX XXXXXXXXXXXXXXXXXXXXXXX
///
/// s e m
/// </pre>
///
/// S is the sign-bit, e is the exponent and m is the significand.
///
/// If e is between 1 and 254, f is a normalized number:
///
/// <pre>
/// s e-127
/// f = (-1) * 2 * 1.m
/// </pre>
///
/// If e is 0, and m is not zero, f is a denormalized number:
///
/// <pre>
/// s -126
/// f = (-1) * 2 * 0.m
/// </pre>
///
/// If e and m are both zero, f is zero:
///
/// <pre>
/// f = 0.0
/// </pre>
///
/// If e is 255, f is an "infinity" or "not a number" (NAN),
/// depending on whether m is zero or not.
///
/// Examples:
///
/// <pre>
/// 0 00000000 00000000000000000000000 = 0.0
/// 0 01111110 00000000000000000000000 = 0.5
/// 0 01111111 00000000000000000000000 = 1.0
@@ -62,11 +71,13 @@
/// 1 11111111 00000000000000000000000 = -infinity
/// 0 11111111 10000000000000000000000 = NAN
/// 1 11111111 11111111111111111111111 = NAN
/// </pre>
///
/// **Representation of a 16-bit half:**
///
/// Here is the bit-layout for a half number, h:
///
/// <pre>
/// 15 (msb)
/// |
/// | 14 10
@@ -76,28 +87,36 @@
/// X XXXXX XXXXXXXXXX
///
/// s e m
/// </pre>
///
/// S is the sign-bit, e is the exponent and m is the significand.
///
/// If e is between 1 and 30, h is a normalized number:
///
/// <pre>
/// s e-15
/// h = (-1) * 2 * 1.m
/// </pre>
///
/// If e is 0, and m is not zero, h is a denormalized number:
///
/// <pre>
/// S -14
/// h = (-1) * 2 * 0.m
/// </pre>
///
/// If e and m are both zero, h is zero:
///
/// <pre>
/// h = 0.0
/// </pre>
///
/// If e is 31, h is an "infinity" or "not a number" (NAN),
/// depending on whether m is zero or not.
///
/// Examples:
///
/// <pre>
/// 0 00000 0000000000 = 0.0
/// 0 01110 0000000000 = 0.5
/// 0 01111 0000000000 = 1.0
@@ -108,13 +127,14 @@
/// 1 11111 0000000000 = -infinity
/// 0 11111 1000000000 = NAN
/// 1 11111 1111111111 = NAN
/// </pre>
///
/// **Conversion via Lookup Table:**
///
/// Converting from half to float is performed by default using a
/// lookup table. There are only 65,536 different half numbers; each
/// of these numbers has been converted and stored in a table pointed
/// to by the ``imath_half_to_float_table`` pointer.
/// to by the `imath_half_to_float_table` pointer.
///
/// Prior to Imath v3.1, conversion from float to half was
/// accomplished with the help of an exponent look table, but this is
@@ -135,30 +155,25 @@
/// 65,536-entry lookup table.
///
/// The lookup table symbol is included in the compilation even if
/// ``IMATH_HALF_USE_LOOKUP_TABLE`` is false, because application code
/// using the exported ``half.h`` may choose to enable the use of the table.
/// `IMATH_HALF_USE_LOOKUP_TABLE` is false, because application code
/// using the exported `half.h` may choose to enable the use of the table.
///
/// An implementation can eliminate the table from compilation by
/// defining the ``IMATH_HALF_NO_LOOKUP_TABLE`` preprocessor symbol.
/// Simply add:
/// defining the `IMATH_HALF_NO_LOOKUP_TABLE` preprocessor symbol.
/// Simply add `#define IMATH_HALF_NO_LOOKUP_TABLE` before including
/// `half.h`, or define the symbol on the compile command line.
///
/// #define IMATH_HALF_NO_LOOKUP_TABLE
///
/// before including ``half.h``, or define the symbol on the compile
/// command line.
///
/// Furthermore, an implementation wishing to receive ``FE_OVERFLOW``
/// and ``FE_UNDERFLOW`` floating point exceptions when converting
/// Furthermore, an implementation wishing to receive `FE_OVERFLOW`
/// and `FE_UNDERFLOW` floating point exceptions when converting
/// float to half by the bit-shift algorithm can define the
/// preprocessor symbol ``IMATH_HALF_ENABLE_FP_EXCEPTIONS`` prior to
/// including ``half.h``:
///
/// #define IMATH_HALF_ENABLE_FP_EXCEPTIONS
/// preprocessor symbol `IMATH_HALF_ENABLE_FP_EXCEPTIONS` (that is,
/// `#define IMATH_HALF_ENABLE_FP_EXCEPTIONS`) prior to including `half.h`.
///
/// **Conversion Performance Comparison:**
///
/// Testing on a Core i9, the timings are approximately:
///
/// <pre>
/// half to float
/// - table: 0.71 ns / call
/// - no table: 1.06 ns / call
@@ -168,6 +183,7 @@
/// - original: 5.2 ns / call
/// - no exp table + opt: 1.27 ns / call
/// - f16c: 0.45 ns / call
/// </pre>
///
/// **Note:** the timing above depends on the distribution of the
/// floats in question.
@@ -206,6 +222,12 @@ namespace internal {
// Thus any attempt to link two versions of OpenVDB with different
// namespaces will clash due to redefinition with a new type.
// The default was not to use a lookup table.
//
/// @cond
/// These internal half-float configuration and limit macros are excluded from
/// the docs: the `half` type itself lives in the (undocumented) `internal`
/// namespace, and documenting the `#define`s makes Doxygen auto-link the
/// literal `#define` token (an unresolvable "link to 'define'" warning).
#undef IMATH_HALF_USE_LOOKUP_TABLE
#define IMATH_HALF_NO_LOOKUP_TABLE
@@ -227,7 +249,7 @@ namespace internal {
#define VDB_HALF_MIN 6.10351562e-05f
/// Largest positive half
#define VDB_HALF_MAX 65504.0f
/// Smallest positive e for which ``half(1.0 + e) != half(1.0)``
/// Smallest positive e for which `half(1.0 + e) != half(1.0)`
#define VDB_HALF_EPSILON 0.00097656f
#else
/// Smallest positive denormalized half
@@ -238,7 +260,7 @@ namespace internal {
#define VDB_HALF_MIN 6.10351562e-05f
/// Largest positive half
#define VDB_HALF_MAX 65504.0
/// Smallest positive e for which ``half(1.0 + e) != half(1.0)``
/// Smallest positive e for which `half(1.0 + e) != half(1.0)`
#define VDB_HALF_EPSILON 0.00097656
#endif
@@ -246,19 +268,19 @@ namespace internal {
#define VDB_HALF_MANT_DIG 11
/// Number of base 10 digits that can be represented without change:
///
/// ``floor( (HALF_MANT_DIG - 1) * log10(2) ) => 3.01... -> 3``
/// `floor( (HALF_MANT_DIG - 1) * log10(2) ) => 3.01... -> 3`
#define VDB_HALF_DIG 3
/// Number of base-10 digits that are necessary to uniquely represent
/// all distinct values:
///
/// ``ceil(HALF_MANT_DIG * log10(2) + 1) => 4.31... -> 5``
/// `ceil(HALF_MANT_DIG * log10(2) + 1) => 4.31... -> 5`
#define VDB_HALF_DECIMAL_DIG 5
/// Base of the exponent
#define VDB_HALF_RADIX 2
/// Minimum negative integer such that ``HALF_RADIX`` raised to the power
/// Minimum negative integer such that `HALF_RADIX` raised to the power
/// of one less than that integer is a normalized half
#define VDBB_HALF_DENORM_MIN_EXP -13
/// Maximum positive integer such that ``HALF_RADIX`` raised to the power
/// Maximum positive integer such that `HALF_RADIX` raised to the power
/// of one less than that integer is a normalized half
#define VDB_HALF_MAX_EXP 16
/// Minimum positive integer such that 10 raised to that power is a
@@ -267,6 +289,7 @@ namespace internal {
/// Maximum positive integer such that 10 raised to that power is a
/// normalized half
#define VDB_HALF_MAX_10_EXP 4
/// @endcond
/// a type for both C-only programs and C++ to use the same utilities
typedef union imath_half_uif
+191
View File
@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2012-2019 Agner Fog.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+14
View File
@@ -0,0 +1,14 @@
# version2
Vector Class Library, latest version
This is a C++ class library for using the Single Instruction Multiple Data (SIMD) instructions to improve performance on modern microprocessors with the x86 or x86/64 instruction set on Windows, Linux, and Mac platforms. There are no plans to support ARM or other instruction sets.
[Latest release](https://github.com/vectorclass/version2/releases)
[Download manual](https://github.com/vectorclass/manual/raw/master/vcl_manual.pdf)
[Add-on packages for particular applications](https://github.com/vectorclass/add-on)
[Getting-started video.](https://www.youtube.com/watch?v=TKjYdLIMTrI) Video blogger Christopher Rose has made this nice video telling how to get started with the Vector Class Library.
**Help:** You may ask for programming help on [StackOverflow](https://stackoverflow.com) using the tag vector-class-library.
+244
View File
@@ -0,0 +1,244 @@
Change log for Vector class library
-----------------------------------
2026-04-14 version 2.02.03
* conversions Vec8h <-> Vec8f and Vec16h <-> Vec16f converted to templates in vectorfp16e.h
2023-07-04 version 2.02.02
* remove various MS compiler warnings
2022-06-03 version 2.02.01
* minor bug fixes and updates
2022-07-20 version 2.02.00
* support half precision floating point vectors
* new trigonometric functions sinpi, cospi, sincospi, tanpi
* floating point modulo and remainder functions
* functions extend_z and concatenate2
* functions compress_saturated with one integer parameter
* support for Intel compiler
* fix bug for NAN inputs in operator > and >=
* fix bug for underflow in pow function
* fix unsafe optimization in store_partial function
* new add-on package with container class and matrix class templates
* major improvements in add-on package 'complex'
* minor improvements in add-on packages 'random' and 'vector3d'
2021-08-18 version 2.01.04
* fix bug in elementtype for Vec8uq
* improved optimization of permute functions in MS compiler by using constexpr
2020-11-04 version 2.01.03
* fix overflow in sin, cos, tan for large x
* fix bug in is_nan for instruction sets prior to AVX
* warning for MS compiler versions with poor support for AVX512
2020-04-11 version 2.01.02
* only minor fixes
2020-02-25 version 2.01.01
* added function store_nt
* New dispatch_example1.cpp, dispatch_example2.cpp
2019-11-23 version 2.01.00
* problem with performance of permute and blend functions fixed by avoiding
unions in constexpr functions
2019-10-31 version 2.00.02
* bug fix in permute function
* is_nan function improved
* templates constant4ui etc. improved
2019-08-02 version 2.00
Derived from version 1.40
* use C++17
* use compact boolean vectors of all sizes if AVX512VL is enabled
* permute and blend functions improved, using C++17 metaprogramming features
* deprecated functions removed
2019-08-02 version 1.40
* hosted on github
* license changed to Apache 2.0.
* added classes Vec64c, Vec64uc, Vec32s, Vec32us
* test bench and scripts for automatic testing of VCL
* new functions: maximum, minimum, to_float, to_double
* conversion of bitfields to boolean vectors with load_bits. This replaces to_vec4ib etc.
* shift_bytes_up/down functions changed to templates (old versions deprecated)
* removed VECTORMATH define. vectormath_lib.h rewritten. svmlpatch.lib added.
* many improvements and bug fixes
* renamed functions: round_to_int and round_to_int64 functions renamed to roundi,
* renamed functions: the type letter is removed from all permute and blend functions,
e.g. permute4f renamed to permute4.
These changes are made to facilitate generic template programming
* renamed functions: to_Vec.. Replaced by load_bits member functions
* deprecated functions: set_bit, get_bit
* deprecated bit vector classes: Vec128b, Vec256b, Vec512b
2017-07-27 version 1.30
* fixed bug in permute8f for a particular combination of indexes
2017-05-10 version 1.29
* Reversed Apple Clang patch in version 1.28 because the problem has reoccurred in
later versions of Clang
2017-05-02 version 1.28
* Fixed problem with Apple Clang version 6.2 in vectorf128.h
* Fixed return type for Vec8sb operator > (Vec8us, Vec8us)
* cpuid function modified in instrset_detect.cpp
2017-02-19 version 1.27
* fixed problem with scatter functions in MS Visual Studio
2016-12-21 version 1.26
* added constant4ui template
* fixed error for complexvec.h with clang
* fixed error in vectormath_exp.h for MAX_VECTOR_SIZE < 512
2016-11-25 version 1.25
* scatter functions
* new functions to_float for unsigned integer vectors
* instrset_detect function can detect AVX512VL, AVX512BW, AVX512DQ
* functions hasF16C and hasAVX512ER for detecting instruction set extensions
* fix bugs in horizontal_and and pow(0,0) for AVX512
* functions improved for AVX512 and AVX512VL: pow, approx_recipr,
approx_rsqrt
* functions improved for AVX512DQ: 64 bit multiplication, to_double,
32 and 64 bit rotate_left, round_to_int64, truncate_to_int64
* functions improved for AVX512ER: approx_recipr, approx_rsqrt,
exponential functions
2016-10-31 version 1.24
* fix bug in Vec8uq constructor in vectori512e.h
2016-09-27 version 1.23
* temporary fix of a problem in Clang version 3.9 inserted in vectorf128.h
2016-05-03 version 1.22
* added optional namespace
* fixed problem with decimal.h
2016-04-24 version 1.21
* fix problems with XOP option in gcc
* improved horizontal_and/or for sse2
* improved Vec2q and Vec4q constructor on Microsoft Visual Studio 2015
* removed warnings by gcc option -Wcast-qual
2015-12-04 version 1.20
* round functions: suppress precision exception under SSE4.1 and higher
* fix compiler problems with AVX512 multiplication in gcc version 5.1
* fix compiler problems with pow function in Microsoft Visual Studio 2015
2015-11-14 version 1.19
* fix various problems with Clang compiler
2015-09-25 version 1.18
* fix compiler error for Vec8s divide_by_i(Vec8s const & x) under Clang compiler
* fix error in Vec4d::size() in vectorf256e.h
2015-07-31 version 1.17
* improved operator > for Vec4uq
* more special cases in blend4q
* nan_code functions made static inline
* template parameter BTYPE renamed to BVTYPE in mathematical functions to avoid clash
with macro named BTYPE in winnt.h
* fixed bug in Vec4db constructor
2014-10-24 version 1.16
* workaround for problem in Clang compiler extended to version 3.09 because not
fixed yet by Clang (vectorf128.h line 134)
* recognize problem with Apple version of Clang reporting wrong version number
* remove various minor problems with Clang
* function pow(vector, int) modified to strengthen type checking and avoid compiler warnings
* manual discusses dynamic allocation of arrays of vectors
* various minor changes
2014-10-17 version 1.15
* added files ranvec1.h and ranvec1.cpp for random number generator
* constructors to make boolean vectors from their elements
* constructors and = operators to broadcast boolean scalar into boolean vectors
* various lookup functions improved
* operators &, |, ^, ~, etc. defined for various boolean vectors to avoid converson
to integer vectors
* nmul_add functions
* mul_add etc. moved to main header files
* explicit fused multiply-and-add used in math functions to improve performance
on compilers that don't automatically insert FMA
2014-07-24 version 1.14
* support for AVX-512f instruction set and 512-bit vectors:
Vec16i, Vec16ui, Vec8q, Vec8uq, Vec16f, Vec8d, and corresponding boolean vectors
* new define MAX_VECTOR_SIZE, valid values are 128, 256 and 512
* added hyperbolic functions sinh, cosh, tanh, asinh, acosh, atanh
* size() member function on all vector classes returns the number of elements
* functions for conversion between boolean vectors and integer bitfields
* extracting an element from a boolean vector now returns a bool, not an int
* improved precision in exp2 and exp10 functions
* various bug fixes
2014-05-11 version 1.13
* pow function improved
* mul_add, mul_sub, mul_sub_x functions
* propagation of error codes through nan_code function
* "denormal" renamed to "subnormal" everywhere, in accordance with IEEE 754-2008 standard
2014-04-20 version 1.12
* inline implementation of mathematical functions added (vectormath_exp.h vectormath_trig.h
vectormath_common.h)
* vectormath.h renamed to vectormath_lib.h because a new alternative is added
* gather functions with constant indexes
* function sign_combine
* function pow_const(vector, const int)
* function pow_ratio(vector, const int, const int)
* functions horizontal_find_first, horizontal_count
* function recipr_sqrt removed
* functions round_to_int64_limited, truncate_to_int64_limited, to_double_limited
* function cubic_root renamed to cbrt
* function atan(vector,vector) renamed to atan2
* function if_mul
* function Vec4i round_to_int(Vec2d)
* operator & (float vector, boolean vector)
* operator &= (int vector, int vector)
* removed constructor Vec128b(int) and Vec256b(int) to avoid implicit conversion
* removed signalling nan function
* minor improvements in various blend and lookup functions
2014-03-01 version 1.11
* fixed missing unsigned operators >>= in vectori256.h
2013-10-04 version 1.10
* clear distinction between boolean vectors and integer vectors for the sake of
compatibility with mask registers in forthcoming AVX512 instruction set
* added function if_add
* tentative support for clang version 3.3 with workaround for bugs
* remove ambiguity for builtin m128i operator == in clang compiler.
* problems in clang compiler, bug reports filed at clang
(http://llvm.org/bugs/show_bug.cgi?id=17164, 17312)
* instrset.h fixes problem with macros named min and max in MS windows.h
* workaround problem in MS Visual Studio 11.0. Bug report 735861 and 804274
* minor bug fixes
2013-03-31 version 1.03 beta
* bug fix for Vec2d cos (Vec2d const & x), VECTORMATH = 1
2012-08-01 version 1.02 beta
* added file vector3d.h for 3-dimensional vectors
* added file complexvec.h for complex numbers and complex vectors
* added file quaternion.h for quaternions
* added function change_sign for floating point vectors
* added operators +, -, *, / between floating point vectors and scalars to remove
overloading ambiguity
2012-07-08 version 1.01 beta
* added file decimal.h with Number <-> string conversion functions:
bin2bcd, bin2ascii, bin2hex_ascii, ascii2bin
* added andnot function for boolean vectors
* added functions shift_bytes_up and shift_bytes_down
* added operators for unsigned integer vector classes: >>=, &, &&, |, ||, ^, ~
* inteldispatchpatch.cpp removed. Use asmlib instead (www.agner.org/optimize/#asmlib)
* prefix ++ and -- operators now return a reference, postfix operators return a value
* various improvements in permute and blend functions
* minor improvement in abs function
* added version number to VECTORCLASS_H
2012-05-30 version 1.00 beta
* first public release at www.agner.org
File diff suppressed because it is too large Load Diff
+828
View File
@@ -0,0 +1,828 @@
/************************** vector_convert.h *******************************
* Author: Agner Fog
* Date created: 2014-07-23
* Last modified: 2022-07-20
* Version: 2.02.00
* Project: vector class library
* Description:
* Header file for conversion between different vector classes with different
* sizes. Also includes verious generic template functions.
*
* (c) Copyright 2012-2022 Agner Fog.
* Apache License version 2.0 or later.
*****************************************************************************/
#ifndef VECTOR_CONVERT_H
#define VECTOR_CONVERT_H
#ifndef VECTORCLASS_H
#include "vectorclass.h"
#endif
#if VECTORCLASS_H < 20200
#error Incompatible versions of vector class library mixed
#endif
#ifdef VCL_NAMESPACE
namespace VCL_NAMESPACE {
#endif
#if MAX_VECTOR_SIZE >= 256
/*****************************************************************************
*
* Extend from 128 to 256 bit vectors
*
*****************************************************************************/
#if INSTRSET >= 8 // AVX2. 256 bit integer vectors
// sign extend
static inline Vec16s extend (Vec16c const a) {
return _mm256_cvtepi8_epi16(a);
}
// zero extend
static inline Vec16us extend (Vec16uc const a) {
return _mm256_cvtepu8_epi16(a);
}
// sign extend
static inline Vec8i extend (Vec8s const a) {
return _mm256_cvtepi16_epi32(a);
}
// zero extend
static inline Vec8ui extend (Vec8us const a) {
return _mm256_cvtepu16_epi32(a);
}
// sign extend
static inline Vec4q extend (Vec4i const a) {
return _mm256_cvtepi32_epi64(a);
}
// zero extend
static inline Vec4uq extend (Vec4ui const a) {
return _mm256_cvtepu32_epi64(a);
}
#else // no AVX2. 256 bit integer vectors are emulated
// sign extend and zero extend functions:
static inline Vec16s extend (Vec16c const a) {
return Vec16s(extend_low(a), extend_high(a));
}
static inline Vec16us extend (Vec16uc const a) {
return Vec16us(extend_low(a), extend_high(a));
}
static inline Vec8i extend (Vec8s const a) {
return Vec8i(extend_low(a), extend_high(a));
}
static inline Vec8ui extend (Vec8us const a) {
return Vec8ui(extend_low(a), extend_high(a));
}
static inline Vec4q extend (Vec4i const a) {
return Vec4q(extend_low(a), extend_high(a));
}
static inline Vec4uq extend (Vec4ui const a) {
return Vec4uq(extend_low(a), extend_high(a));
}
#endif // AVX2
/*****************************************************************************
*
* Conversions between float and double
*
*****************************************************************************/
#if INSTRSET >= 7 // AVX. 256 bit float vectors
// float to double
static inline Vec4d to_double (Vec4f const a) {
return _mm256_cvtps_pd(a);
}
// double to float
static inline Vec4f to_float (Vec4d const a) {
return _mm256_cvtpd_ps(a);
}
#else // no AVX2. 256 bit float vectors are emulated
// float to double
static inline Vec4d to_double (Vec4f const a) {
Vec2d lo = _mm_cvtps_pd(a);
Vec2d hi = _mm_cvtps_pd(_mm_movehl_ps(a, a));
return Vec4d(lo,hi);
}
// double to float
static inline Vec4f to_float (Vec4d const a) {
Vec4f lo = _mm_cvtpd_ps(a.get_low());
Vec4f hi = _mm_cvtpd_ps(a.get_high());
return _mm_movelh_ps(lo, hi);
}
#endif
/*****************************************************************************
*
* Reduce from 256 to 128 bit vectors
*
*****************************************************************************/
#if INSTRSET >= 10 // AVX512VL
// compress functions. overflow wraps around
static inline Vec16c compress (Vec16s const a) {
return _mm256_cvtepi16_epi8(a);
}
static inline Vec16uc compress (Vec16us const a) {
return _mm256_cvtepi16_epi8(a);
}
static inline Vec8s compress (Vec8i const a) {
return _mm256_cvtepi32_epi16(a);
}
static inline Vec8us compress (Vec8ui const a) {
return _mm256_cvtepi32_epi16(a);
}
static inline Vec4i compress (Vec4q const a) {
return _mm256_cvtepi64_epi32(a);
}
static inline Vec4ui compress (Vec4uq const a) {
return _mm256_cvtepi64_epi32(a);
}
// compress_saturated functions. overflow saturates
static inline Vec16c compress_saturated (Vec16s const a) {
return _mm256_cvtsepi16_epi8(a);
}
static inline Vec16uc compress_saturated (Vec16us const a) {
return _mm256_cvtusepi16_epi8(a);
}
static inline Vec8s compress_saturated (Vec8i const a) {
return _mm256_cvtsepi32_epi16(a);
}
static inline Vec8us compress_saturated (Vec8ui const a) {
return _mm256_cvtusepi32_epi16(a);
}
static inline Vec4i compress_saturated (Vec4q const a) {
return _mm256_cvtsepi64_epi32(a);
}
static inline Vec4ui compress_saturated (Vec4uq const a) {
return _mm256_cvtusepi64_epi32(a);
}
#else // no AVX512
// compress functions. overflow wraps around
static inline Vec16c compress (Vec16s const a) {
return compress(a.get_low(), a.get_high());
}
static inline Vec16uc compress (Vec16us const a) {
return compress(a.get_low(), a.get_high());
}
static inline Vec8s compress (Vec8i const a) {
return compress(a.get_low(), a.get_high());
}
static inline Vec8us compress (Vec8ui const a) {
return compress(a.get_low(), a.get_high());
}
static inline Vec4i compress (Vec4q const a) {
return compress(a.get_low(), a.get_high());
}
static inline Vec4ui compress (Vec4uq const a) {
return compress(a.get_low(), a.get_high());
}
// compress_saturated functions. overflow saturates
static inline Vec16c compress_saturated (Vec16s const a) {
return compress_saturated(a.get_low(), a.get_high());
}
static inline Vec16uc compress_saturated (Vec16us const a) {
return compress_saturated(a.get_low(), a.get_high());
}
static inline Vec8s compress_saturated (Vec8i const a) {
return compress_saturated(a.get_low(), a.get_high());
}
static inline Vec8us compress_saturated (Vec8ui const a) {
return compress_saturated(a.get_low(), a.get_high());
}
static inline Vec4i compress_saturated (Vec4q const a) {
return compress_saturated(a.get_low(), a.get_high());
}
static inline Vec4ui compress_saturated (Vec4uq const a) {
return compress_saturated(a.get_low(), a.get_high());
}
#endif // AVX512
#endif // MAX_VECTOR_SIZE >= 256
#if MAX_VECTOR_SIZE >= 512
/*****************************************************************************
*
* Reduce from 512 to 256 bit vectors
*
*****************************************************************************/
#if INSTRSET >= 10 // AVX512VL
// compress_saturated functions. overflow saturates
static inline Vec32c compress_saturated (Vec32s const a) {
return _mm512_cvtsepi16_epi8(a);
}
static inline Vec32uc compress_saturated (Vec32us const a) {
return _mm512_cvtusepi16_epi8(a);
}
static inline Vec16s compress_saturated (Vec16i const a) {
return _mm512_cvtsepi32_epi16(a);
}
static inline Vec16us compress_saturated (Vec16ui const a) {
return _mm512_cvtusepi32_epi16(a);
}
static inline Vec8i compress_saturated (Vec8q const a) {
return _mm512_cvtsepi64_epi32(a);
}
static inline Vec8ui compress_saturated (Vec8uq const a) {
return _mm512_cvtusepi64_epi32(a);
}
#else // no AVX512
// compress_saturated functions. overflow saturates
static inline Vec32c compress_saturated (Vec32s const a) {
return compress_saturated(a.get_low(), a.get_high());
}
static inline Vec32uc compress_saturated (Vec32us const a) {
return compress_saturated(a.get_low(), a.get_high());
}
static inline Vec16s compress_saturated (Vec16i const a) {
return compress_saturated(a.get_low(), a.get_high());
}
static inline Vec16us compress_saturated (Vec16ui const a) {
return compress_saturated(a.get_low(), a.get_high());
}
static inline Vec8i compress_saturated (Vec8q const a) {
return compress_saturated(a.get_low(), a.get_high());
}
static inline Vec8ui compress_saturated (Vec8uq const a) {
return compress_saturated(a.get_low(), a.get_high());
}
#endif // AVX512
/*****************************************************************************
*
* Extend from 256 to 512 bit vectors
*
*****************************************************************************/
#if INSTRSET >= 9 // AVX512. 512 bit integer vectors
// sign extend
static inline Vec32s extend (Vec32c const a) {
#if INSTRSET >= 10
return _mm512_cvtepi8_epi16(a);
#else
return Vec32s(extend_low(a), extend_high(a));
#endif
}
// zero extend
static inline Vec32us extend (Vec32uc const a) {
#if INSTRSET >= 10
return _mm512_cvtepu8_epi16(a);
#else
return Vec32us(extend_low(a), extend_high(a));
#endif
}
// sign extend
static inline Vec16i extend (Vec16s const a) {
return _mm512_cvtepi16_epi32(a);
}
// zero extend
static inline Vec16ui extend (Vec16us const a) {
return _mm512_cvtepu16_epi32(a);
}
// sign extend
static inline Vec8q extend (Vec8i const a) {
return _mm512_cvtepi32_epi64(a);
}
// zero extend
static inline Vec8uq extend (Vec8ui const a) {
return _mm512_cvtepu32_epi64(a);
}
#else // no AVX512. 512 bit vectors are emulated
// sign extend
static inline Vec32s extend (Vec32c const a) {
return Vec32s(extend_low(a), extend_high(a));
}
// zero extend
static inline Vec32us extend (Vec32uc const a) {
return Vec32us(extend_low(a), extend_high(a));
}
// sign extend
static inline Vec16i extend (Vec16s const a) {
return Vec16i(extend_low(a), extend_high(a));
}
// zero extend
static inline Vec16ui extend (Vec16us const a) {
return Vec16ui(extend_low(a), extend_high(a));
}
// sign extend
static inline Vec8q extend (Vec8i const a) {
return Vec8q(extend_low(a), extend_high(a));
}
// zero extend
static inline Vec8uq extend (Vec8ui const a) {
return Vec8uq(extend_low(a), extend_high(a));
}
#endif // AVX512
/*****************************************************************************
*
* Reduce from 512 to 256 bit vectors
*
*****************************************************************************/
#if INSTRSET >= 9 // AVX512F
// compress functions. overflow wraps around
static inline Vec32c compress (Vec32s const a) {
#if INSTRSET >= 10 // AVVX512BW
return _mm512_cvtepi16_epi8(a);
#else
return compress(a.get_low(), a.get_high());
#endif
}
static inline Vec32uc compress (Vec32us const a) {
return Vec32uc(compress(Vec32s(a)));
}
static inline Vec16s compress (Vec16i const a) {
return _mm512_cvtepi32_epi16(a);
}
static inline Vec16us compress (Vec16ui const a) {
return _mm512_cvtepi32_epi16(a);
}
static inline Vec8i compress (Vec8q const a) {
return _mm512_cvtepi64_epi32(a);
}
static inline Vec8ui compress (Vec8uq const a) {
return _mm512_cvtepi64_epi32(a);
}
#else // no AVX512
// compress functions. overflow wraps around
static inline Vec32c compress (Vec32s const a) {
return compress(a.get_low(), a.get_high());
}
static inline Vec32uc compress (Vec32us const a) {
return compress(a.get_low(), a.get_high());
}
static inline Vec16s compress (Vec16i const a) {
return compress(a.get_low(), a.get_high());
}
static inline Vec16us compress (Vec16ui const a) {
return compress(a.get_low(), a.get_high());
}
static inline Vec8i compress (Vec8q const a) {
return compress(a.get_low(), a.get_high());
}
static inline Vec8ui compress (Vec8uq const a) {
return compress(a.get_low(), a.get_high());
}
#endif // AVX512
/*****************************************************************************
*
* Conversions between float and double
*
*****************************************************************************/
#if INSTRSET >= 9 // AVX512. 512 bit float vectors
// float to double
static inline Vec8d to_double (Vec8f const a) {
return _mm512_cvtps_pd(a);
}
// double to float
static inline Vec8f to_float (Vec8d const a) {
return _mm512_cvtpd_ps(a);
}
#else // no AVX512. 512 bit float vectors are emulated
// float to double
static inline Vec8d to_double (Vec8f const a) {
Vec4d lo = to_double(a.get_low());
Vec4d hi = to_double(a.get_high());
return Vec8d(lo,hi);
}
// double to float
static inline Vec8f to_float (Vec8d const a) {
Vec4f lo = to_float(a.get_low());
Vec4f hi = to_float(a.get_high());
return Vec8f(lo, hi);
}
#endif
#endif // MAX_VECTOR_SIZE >= 512
// double to float
static inline Vec4f to_float (Vec2d const a) {
return _mm_cvtpd_ps(a);
}
/*****************************************************************************
*
* Generic template functions
*
* These templates define functions for multiple vector types in one template
*
*****************************************************************************/
// concatenate two vectors into one vector of double size
template <typename T> auto concatenate2(T const a, T const b) {
static_assert(sizeof(T) * 8 < MAX_VECTOR_SIZE, "Maximum vector size exceeded");
return decltype(extend_z(a))(a, b); // call constructor for double size vector type
}
// horizontal min/max of vector elements
// implemented with universal template, works for all vector types:
template <typename T> auto horizontal_min(T const x) {
if constexpr (T::elementtype() >= 15) {
// T is a float or double vector
if (horizontal_or(is_nan(x))) {
// check for NAN because min does not guarantee NAN propagation
return x[horizontal_find_first(is_nan(x))];
}
}
return horizontal_min1(x);
}
template <typename T> auto horizontal_min1(T const x) {
if constexpr (T::elementtype() <= 3) { // boolean vector type
return horizontal_and(x);
}
else if constexpr (sizeof(T) >= 32) {
// split recursively into smaller vectors
return horizontal_min1(min(x.get_low(), x.get_high()));
}
else if constexpr (T::size() == 2) {
T a = permute2 <1, V_DC>(x); // high half
T b = min(a, x);
return b[0];
}
else if constexpr (T::size() == 4) {
T a = permute4<2, 3, V_DC, V_DC>(x); // high half
T b = min(a, x);
a = permute4<1, V_DC, V_DC, V_DC>(b);
b = min(a, b);
return b[0];
}
else if constexpr (T::size() == 8) {
T a = permute8<4, 5, 6, 7, V_DC, V_DC, V_DC, V_DC>(x); // high half
T b = min(a, x);
a = permute8<2, 3, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC>(b);
b = min(a, b);
a = permute8<1, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC>(b);
b = min(a, b);
return b[0];
}
else {
static_assert(T::size() == 16); // no other size is allowed
T a = permute16<8, 9, 10, 11, 12, 13, 14, 15, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC >(x); // high half
T b = min(a, x);
a = permute16<4, 5, 6, 7, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC>(b);
b = min(a, b);
a = permute16<2, 3, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC>(b);
b = min(a, b);
a = permute16<1, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC>(b);
b = min(a, b);
return b[0];
}
}
template <typename T> auto horizontal_max(T const x) {
if constexpr (T::elementtype() >= 15) {
// T is a float or double vector
if (horizontal_or(is_nan(x))) {
// check for NAN because max does not guarantee NAN propagation
return x[horizontal_find_first(is_nan(x))];
}
}
return horizontal_max1(x);
}
template <typename T> auto horizontal_max1(T const x) {
if constexpr (T::elementtype() <= 3) { // boolean vector type
return horizontal_or(x);
}
else if constexpr (sizeof(T) >= 32) {
// split recursively into smaller vectors
return horizontal_max1(max(x.get_low(), x.get_high()));
}
else if constexpr (T::size() == 2) {
T a = permute2 <1, V_DC>(x); // high half
T b = max(a, x);
return b[0];
}
else if constexpr (T::size() == 4) {
T a = permute4<2, 3, V_DC, V_DC>(x); // high half
T b = max(a, x);
a = permute4<1, V_DC, V_DC, V_DC>(b);
b = max(a, b);
return b[0];
}
else if constexpr (T::size() == 8) {
T a = permute8<4, 5, 6, 7, V_DC, V_DC, V_DC, V_DC>(x); // high half
T b = max(a, x);
a = permute8<2, 3, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC>(b);
b = max(a, b);
a = permute8<1, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC>(b);
b = max(a, b);
return b[0];
}
else {
static_assert(T::size() == 16); // no other size is allowed
T a = permute16<8, 9, 10, 11, 12, 13, 14, 15, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC >(x); // high half
T b = max(a, x);
a = permute16<4, 5, 6, 7, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC>(b);
b = max(a, b);
a = permute16<2, 3, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC>(b);
b = max(a, b);
a = permute16<1, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC, V_DC>(b);
b = max(a, b);
return b[0];
}
}
// Find first element that is true in a boolean vector
template <typename V>
static inline int horizontal_find_first(V const x) {
static_assert(V::elementtype() == 2 || V::elementtype() == 3, "Boolean vector expected");
auto bits = to_bits(x); // convert to bits
if (bits == 0) return -1;
if constexpr (V::size() < 32) {
return bit_scan_forward((uint32_t)bits);
}
else {
return bit_scan_forward(bits);
}
}
// Count the number of elements that are true in a boolean vector
template <typename V>
static inline int horizontal_count(V const x) {
static_assert(V::elementtype() == 2 || V::elementtype() == 3, "Boolean vector expected");
auto bits = to_bits(x); // convert to bits
if constexpr (V::size() < 32) {
return vml_popcnt((uint32_t)bits);
}
else {
return (int)vml_popcnt(bits);
}
}
// maximum and minimum functions. This version is sure to propagate NANs,
// conforming to the new IEEE-754 2019 standard
template <typename V>
static inline V maximum(V const a, V const b) {
if constexpr (V::elementtype() < 15) {
return max(a, b); // integer type
}
else { // float or double vector
V y = select(is_nan(a), a, max(a, b));
#ifdef SIGNED_ZERO // pedantic about signed zero
y = select(a == b, a & b, y); // maximum(+0, -0) = +0
#endif
return y;
}
}
template <typename V>
static inline V minimum(V const a, V const b) {
if constexpr (V::elementtype() < 15) {
return min(a, b); // integer type
}
else { // float or double vector
V y = select(is_nan(a), a, min(a, b));
#ifdef SIGNED_ZERO // pedantic about signed zero
y = select(a == b, a | b, y); // minimum(+0, -0) = -0
#endif
return y;
}
}
// floating point remainder
// -denominator/2 <= result < denominator/2
template <typename V>
static inline V fremainder(V const numerator, double const denominator) {
// (Optimization notice: Calculation of 1/denominator and constants for extended precision reduction
// may be optimized by a compiler moving loop-invariant code. This is intended)
static_assert(V::elementtype() == 16 || V::elementtype() == 17, "wrong vector type"); // supports only float and double
if (denominator > 0.) { // denominator must be positive
if constexpr (V::elementtype() == 16) { // float
#ifdef __FMA__
float recipd = float(1.0 / denominator); // reciprocal denominator
float fd = float(denominator); // denominator rounded to single precision
float d2 = float(denominator - fd); // remaining bits for double precision
V q = round(numerator * recipd); // divide and round
V m = nmul_add(q, d2, nmul_add(q, fd, numerator));// double precision reduction
#else // no FMA. Use extended precision reduction
union {
float f;
uint32_t i;
} u;
u.f = float(denominator);
u.i &= 0xFFFFF000; // remove 12 least significant bits for extended precision reduction
float d2 = denominator - u.f; // remaining bits
float recipd = float(1.0 / denominator); // reciprocal
V q = round(numerator * recipd); // divide and round
V m = nmul_add(q, d2, nmul_add(q, u.f, numerator));// extended precision reduction
#endif // FMA
if (true) { // Check that result is within desired interval. This may be omitted if not essential:
// This check may be needed in extreme cases of numerator > 1.E5 * denominator
auto too_high = m >= float( denominator * 0.5);
auto too_low = m < float(-denominator * 0.5);
m = if_sub(too_high, m, float(denominator));
m = if_add(too_low, m, float(denominator));
}
return m;
}
else if constexpr (V::elementtype() == 17) { // double precision
#ifdef __FMA__
double recipd = 1.0 / denominator; // reciprocal
V q = round(numerator * recipd); // divide and round
V m = nmul_add(q, denominator, numerator); // nmul_add has extended precision
#else // no FMA. Use extended precision reduction
union {
double f;
uint64_t i;
} u;
u.f = denominator;
u.i &= 0xFFFFFFFFFF000000; // remove 24 least significant bits for extended precision reduction
double d2 = denominator - u.f; // remaining bits
double recipd = 1.0 / denominator; // reciprocal
V q = round(numerator * recipd); // divide and round
V m = nmul_add(q, d2, nmul_add(q, u.f, numerator));// extended precision reduction
#endif // FMA
if (true) { // Check that result is within desired interval. This may be omitted if not essential:
// This check is rarely needed except in extreme cases of numerator > 1.E14 * denominator
auto too_high = m >= denominator * 0.5;
auto too_low = m < -denominator * 0.5;
m = if_sub(too_high, m, denominator);
m = if_add(too_low, m, denominator);
}
return m;
}
}
else {
return nan_vec<V>(1); // denominator is not positive
}
}
// floating point modulo
// 0 <= result < denominator
template <typename V>
static inline V fmodulo(V const numerator, double const denominator) {
// (Optimization notice: Calculation of 1/denominator and constants for extended precision reduction
// may be optimized by a compiler moving loop-invariant code. This is intended)
static_assert(V::elementtype() == 16 || V::elementtype() == 17, "wrong vector type"); // supports only float and double
if (denominator > 0.) { // denominator must be positive
if constexpr (V::elementtype() == 16) { // float
#ifdef __FMA__
float recipd = float(1.0 / denominator); // reciprocal denominator
float fd = float(denominator); // denominator rounded to single precision
float d2 = float(denominator - fd); // remaining bits for double precision
V q = floor(numerator * recipd); // divide and floor
V m = nmul_add(q, d2, nmul_add(q, fd, numerator));// double precision reduction
#else // no FMA. Use extended precision reduction
union {
float f;
uint32_t i;
} u;
u.f = float(denominator);
u.i &= 0xFFFFF000; // remove 12 least significant bits for extended precision reduction
float d2 = denominator - u.f; // remaining bits
float recipd = float(1.0 / denominator); // reciprocal
V q = floor(numerator * recipd); // divide and floor
V m = nmul_add(q, d2, nmul_add(q, u.f, numerator));// extended precision reduction
#endif // FMA
if (true) { // Check that result is within desired interval. This may be omitted if not essential:
// This check may be needed in extreme cases of numerator > 1.E5 * denominator
auto too_high = m >= float(denominator);
auto too_low = m < 0.f;
m = if_sub(too_high, m, float(denominator));
m = if_add(too_low, m, float(denominator));
}
return m;
}
else if constexpr (V::elementtype() == 17) { // double precision
#ifdef __FMA__
double recipd = 1.0 / denominator; // reciprocal
V q = floor(numerator * recipd); // divide and floor
V m = nmul_add(q, denominator, numerator); // nmul_add has extended precision
#else // no FMA. Use extended precision reduction
union {
double f;
uint64_t i;
} u;
u.f = denominator;
u.i &= 0xFFFFFFFFFF000000; // remove 24 least significant bits for extended precision reduction
double d2 = denominator - u.f; // remaining bits
double recipd = 1.0 / denominator; // reciprocal
V q = floor(numerator * recipd); // divide and floor
V m = nmul_add(q, d2, nmul_add(q, u.f, numerator));// extended precision reduction
#endif // FMA
if (true) { // Check that result is within desired interval. This may be omitted if not essential:
// This check is rarely needed except in extreme cases of numerator > 1.E14 * denominator
auto too_high = m >= denominator;
auto too_low = m < 0.;
m = if_sub(too_high, m, denominator);
m = if_add(too_low, m, denominator);
}
return m;
}
}
else {
return nan_vec<V>(1); // denominator is not positive
}
}
#ifdef VCL_NAMESPACE
}
#endif
#endif // VECTOR_CONVERT_H
+87
View File
@@ -0,0 +1,87 @@
/**************************** vectorclass.h ********************************
* Author: Agner Fog
* Date created: 2012-05-30
* Last modified: 2026-04-14
* Version: 2.02.03
* Project: vector class library
* Home: https://github.com/vectorclass
* Description:
* Header file defining vector classes as interface to intrinsic functions
* in x86 and x86-64 microprocessors with SSE2 and later instruction sets.
*
* Instructions:
* Use Gnu, Clang, Microsoft, or Intel C++ compiler. Compile for the desired
* instruction set, which must be at least SSE2. Specify the supported
* instruction set by a command line define, e.g. __SSE4_1__ if the
* compiler does not automatically do so.
* For detailed instructions, see vcl_manual.pdf
*
* Each vector object is represented internally in the CPU as a vector
* register with 128, 256 or 512 bits.
*
* This header file includes the appropriate header files depending on the
* selected instruction set.
*
* (c) Copyright 2012-2026 Agner Fog.
* Apache License version 2.0 or later.
******************************************************************************/
#ifndef VECTORCLASS_H
#define VECTORCLASS_H 20203
// Maximum vector size, bits. Allowed values are 128, 256, 512
#ifndef MAX_VECTOR_SIZE
#define MAX_VECTOR_SIZE 512
#endif
// Determine instruction set, and define platform-dependent functions
#include "instrset.h" // Select supported instruction set
#if INSTRSET < 2 // instruction set SSE2 is the minimum
#error Please compile for the SSE2 instruction set or higher
#else
// Select appropriate .h files depending on instruction set
#include "vectori128.h" // 128-bit integer vectors
#include "vectorf128.h" // 128-bit floating point vectors
#if MAX_VECTOR_SIZE >= 256
#if INSTRSET >= 8
#include "vectori256.h" // 256-bit integer vectors, requires AVX2 instruction set
#else
#include "vectori256e.h" // 256-bit integer vectors, emulated
#endif // INSTRSET >= 8
#if INSTRSET >= 7
#include "vectorf256.h" // 256-bit floating point vectors, requires AVX instruction set
#else
#include "vectorf256e.h" // 256-bit floating point vectors, emulated
#endif // INSTRSET >= 7
#endif // MAX_VECTOR_SIZE >= 256
#if MAX_VECTOR_SIZE >= 512
#if INSTRSET >= 9
#include "vectori512.h" // 512-bit vectors of 32 and 64 bit integers, requires AVX512F instruction set
#include "vectorf512.h" // 512-bit floating point vectors, requires AVX512F instruction set
#else
#include "vectori512e.h" // 512-bit integer vectors, emulated
#include "vectorf512e.h" // 512-bit floating point vectors, emulated
#endif // INSTRSET >= 9
#if INSTRSET >= 10
#include "vectori512s.h" // 512-bit vectors of 8 and 16 bit integers, requires AVX512BW instruction set
#else
#include "vectori512se.h" // 512-bit vectors of 8 and 16 bit integers, emulated
#endif
#endif // MAX_VECTOR_SIZE >= 512
#include "vector_convert.h" // conversion between different vector sizes, and common templates
#endif // INSTRSET >= 2
#else // VECTORCLASS_H
#if VECTORCLASS_H < 20000
#error Mixed versions of vector class library
#endif
#endif // VECTORCLASS_H
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+327
View File
@@ -0,0 +1,327 @@
/*************************** vectormath_common.h ****************************
* Author: Agner Fog
* Date created: 2014-04-18
* Last modified: 2022-07-20
* Version: 2.02.00
* Project: vector classes
* Description:
* Header file containing common code for inline version of mathematical functions.
*
* For detailed instructions, see VectorClass.pdf
*
* (c) Copyright 2014-2022 Agner Fog.
* Apache License version 2.0 or later.
******************************************************************************/
#ifndef VECTORMATH_COMMON_H
#define VECTORMATH_COMMON_H 2
#ifdef VECTORMATH_LIB_H
#error conflicting header files. More than one implementation of mathematical functions included
#endif
#include <cmath>
#ifndef VECTORCLASS_H
#include "vectorclass.h"
#endif
#if VECTORCLASS_H < 20200
#error Incompatible versions of vector class library mixed
#endif
/******************************************************************************
Define NAN payload values
******************************************************************************/
#define NAN_LOG 0x101 // logarithm for x<0
#define NAN_POW 0x102 // negative number raised to non-integer power
#define NAN_HYP 0x104 // acosh for x<1 and atanh for abs(x)>1
/******************************************************************************
Define mathematical constants
******************************************************************************/
#define VM_PI 3.14159265358979323846 // pi
#define VM_PI_2 1.57079632679489661923 // pi / 2
#define VM_PI_4 0.785398163397448309616 // pi / 4
#define VM_SQRT2 1.41421356237309504880 // sqrt(2)
#define VM_LOG2E 1.44269504088896340736 // 1/log(2)
#define VM_LOG10E 0.434294481903251827651 // 1/log(10)
#define VM_LOG210 3.321928094887362347808 // log2(10)
#define VM_LN2 0.693147180559945309417 // log(2)
#define VM_LN10 2.30258509299404568402 // log(10)
#define VM_SMALLEST_NORMAL 2.2250738585072014E-308 // smallest normal number, double
#define VM_SMALLEST_NORMALF 1.17549435E-38f // smallest normal number, float
#ifdef VCL_NAMESPACE
namespace VCL_NAMESPACE {
#endif
/******************************************************************************
templates for producing infinite and nan in desired vector type
******************************************************************************/
template <typename VTYPE>
static inline VTYPE infinite_vec();
template <>
inline Vec2d infinite_vec<Vec2d>() {
return infinite2d();
}
template <>
inline Vec4f infinite_vec<Vec4f>() {
return infinite4f();
}
#if MAX_VECTOR_SIZE >= 256
template <>
inline Vec4d infinite_vec<Vec4d>() {
return infinite4d();
}
template <>
inline Vec8f infinite_vec<Vec8f>() {
return infinite8f();
}
#endif // MAX_VECTOR_SIZE >= 256
#if MAX_VECTOR_SIZE >= 512
template <>
inline Vec8d infinite_vec<Vec8d>() {
return infinite8d();
}
template <>
inline Vec16f infinite_vec<Vec16f>() {
return infinite16f();
}
#endif // MAX_VECTOR_SIZE >= 512
/******************************************************************************
* Detect NAN codes
*
* These functions return the code hidden in a NAN. The sign bit is ignored
******************************************************************************/
static inline Vec4ui nan_code(Vec4f const x) {
Vec4ui a = Vec4ui(reinterpret_i(x));
Vec4ui const n = 0x007FFFFF;
return select(Vec4ib(is_nan(x)), a & n, 0);
}
// This function returns the code hidden in a NAN. The sign bit is ignored
static inline Vec2uq nan_code(Vec2d const x) {
Vec2uq a = Vec2uq(reinterpret_i(x));
return select(Vec2qb(is_nan(x)), a << 12 >> (12+29), 0);
}
#if MAX_VECTOR_SIZE >= 256
// This function returns the code hidden in a NAN. The sign bit is ignored
static inline Vec8ui nan_code(Vec8f const x) {
Vec8ui a = Vec8ui(reinterpret_i(x));
Vec8ui const n = 0x007FFFFF;
return select(Vec8ib(is_nan(x)), a & n, 0);
}
// This function returns the code hidden in a NAN. The sign bit is ignored
static inline Vec4uq nan_code(Vec4d const x) {
Vec4uq a = Vec4uq(reinterpret_i(x));
return select(Vec4qb(is_nan(x)), a << 12 >> (12+29), 0);
}
#endif // MAX_VECTOR_SIZE >= 256
#if MAX_VECTOR_SIZE >= 512
// This function returns the code hidden in a NAN. The sign bit is ignored
static inline Vec16ui nan_code(Vec16f const x) {
Vec16ui a = Vec16ui(reinterpret_i(x));
Vec16ui const n = 0x007FFFFF;
return select(Vec16ib(is_nan(x)), a & n, 0);
}
// This function returns the code hidden in a NAN. The sign bit is ignored
static inline Vec8uq nan_code(Vec8d const x) {
Vec8uq a = Vec8uq(reinterpret_i(x));
return select(Vec8qb(is_nan(x)), a << 12 >> (12+29), 0);
}
#endif // MAX_VECTOR_SIZE >= 512
/******************************************************************************
templates for polynomials
Using Estrin's scheme to make shorter dependency chains and use FMA, starting
longest dependency chains first.
******************************************************************************/
// template <typedef VECTYPE, typedef CTYPE>
template <class VTYPE, class CTYPE>
static inline VTYPE polynomial_2(VTYPE const x, CTYPE c0, CTYPE c1, CTYPE c2) {
// calculates polynomial c2*x^2 + c1*x + c0
// VTYPE may be a vector type, CTYPE is a scalar type
VTYPE x2 = x * x;
//return = x2 * c2 + (x * c1 + c0);
return mul_add(x2, c2, mul_add(x, c1, c0));
}
template<class VTYPE, class CTYPE>
static inline VTYPE polynomial_3(VTYPE const x, CTYPE c0, CTYPE c1, CTYPE c2, CTYPE c3) {
// calculates polynomial c3*x^3 + c2*x^2 + c1*x + c0
// VTYPE may be a vector type, CTYPE is a scalar type
VTYPE x2 = x * x;
//return (c2 + c3*x)*x2 + (c1*x + c0);
return mul_add(mul_add(c3, x, c2), x2, mul_add(c1, x, c0));
}
template<class VTYPE, class CTYPE>
static inline VTYPE polynomial_4(VTYPE const x, CTYPE c0, CTYPE c1, CTYPE c2, CTYPE c3, CTYPE c4) {
// calculates polynomial c4*x^4 + c3*x^3 + c2*x^2 + c1*x + c0
// VTYPE may be a vector type, CTYPE is a scalar type
VTYPE x2 = x * x;
VTYPE x4 = x2 * x2;
//return (c2+c3*x)*x2 + ((c0+c1*x) + c4*x4);
return mul_add(mul_add(c3, x, c2), x2, mul_add(c1, x, c0) + c4*x4);
}
template<class VTYPE, class CTYPE>
static inline VTYPE polynomial_4n(VTYPE const x, CTYPE c0, CTYPE c1, CTYPE c2, CTYPE c3) {
// calculates polynomial 1*x^4 + c3*x^3 + c2*x^2 + c1*x + c0
// VTYPE may be a vector type, CTYPE is a scalar type
VTYPE x2 = x * x;
VTYPE x4 = x2 * x2;
//return (c2+c3*x)*x2 + ((c0+c1*x) + x4);
return mul_add(mul_add(c3, x, c2), x2, mul_add(c1, x, c0) + x4);
}
template<class VTYPE, class CTYPE>
static inline VTYPE polynomial_5(VTYPE const x, CTYPE c0, CTYPE c1, CTYPE c2, CTYPE c3, CTYPE c4, CTYPE c5) {
// calculates polynomial c5*x^5 + c4*x^4 + c3*x^3 + c2*x^2 + c1*x + c0
// VTYPE may be a vector type, CTYPE is a scalar type
VTYPE x2 = x * x;
VTYPE x4 = x2 * x2;
//return (c2+c3*x)*x2 + ((c4+c5*x)*x4 + (c0+c1*x));
return mul_add(mul_add(c3, x, c2), x2, mul_add(mul_add(c5, x, c4), x4, mul_add(c1, x, c0)));
}
template<class VTYPE, class CTYPE>
static inline VTYPE polynomial_5n(VTYPE const x, CTYPE c0, CTYPE c1, CTYPE c2, CTYPE c3, CTYPE c4) {
// calculates polynomial 1*x^5 + c4*x^4 + c3*x^3 + c2*x^2 + c1*x + c0
// VTYPE may be a vector type, CTYPE is a scalar type
VTYPE x2 = x * x;
VTYPE x4 = x2 * x2;
//return (c2+c3*x)*x2 + ((c4+x)*x4 + (c0+c1*x));
return mul_add(mul_add(c3, x, c2), x2, mul_add(c4 + x, x4, mul_add(c1, x, c0)));
}
template<class VTYPE, class CTYPE>
static inline VTYPE polynomial_6(VTYPE const x, CTYPE c0, CTYPE c1, CTYPE c2, CTYPE c3, CTYPE c4, CTYPE c5, CTYPE c6) {
// calculates polynomial c6*x^6 + c5*x^5 + c4*x^4 + c3*x^3 + c2*x^2 + c1*x + c0
// VTYPE may be a vector type, CTYPE is a scalar type
VTYPE x2 = x * x;
VTYPE x4 = x2 * x2;
//return (c4+c5*x+c6*x2)*x4 + ((c2+c3*x)*x2 + (c0+c1*x));
return mul_add(mul_add(c6, x2, mul_add(c5, x, c4)), x4, mul_add(mul_add(c3, x, c2), x2, mul_add(c1, x, c0)));
}
template<class VTYPE, class CTYPE>
static inline VTYPE polynomial_6n(VTYPE const x, CTYPE c0, CTYPE c1, CTYPE c2, CTYPE c3, CTYPE c4, CTYPE c5) {
// calculates polynomial 1*x^6 + c5*x^5 + c4*x^4 + c3*x^3 + c2*x^2 + c1*x + c0
// VTYPE may be a vector type, CTYPE is a scalar type
VTYPE x2 = x * x;
VTYPE x4 = x2 * x2;
//return (c4+c5*x+x2)*x4 + ((c2+c3*x)*x2 + (c0+c1*x));
return mul_add(mul_add(c5, x, c4 + x2), x4, mul_add(mul_add(c3, x, c2), x2, mul_add(c1, x, c0)));
}
template<class VTYPE, class CTYPE>
static inline VTYPE polynomial_7(VTYPE const x, CTYPE c0, CTYPE c1, CTYPE c2, CTYPE c3, CTYPE c4, CTYPE c5, CTYPE c6, CTYPE c7) {
// calculates polynomial c7*x^7 + c6*x^6 + c5*x^5 + c4*x^4 + c3*x^3 + c2*x^2 + c1*x + c0
// VTYPE may be a vector type, CTYPE is a scalar type
VTYPE x2 = x * x;
VTYPE x4 = x2 * x2;
//return ((c6+c7*x)*x2 + (c4+c5*x))*x4 + ((c2+c3*x)*x2 + (c0+c1*x));
return mul_add(mul_add(mul_add(c7, x, c6), x2, mul_add(c5, x, c4)), x4, mul_add(mul_add(c3, x, c2), x2, mul_add(c1, x, c0)));
}
template<class VTYPE, class CTYPE>
static inline VTYPE polynomial_8(VTYPE const x, CTYPE c0, CTYPE c1, CTYPE c2, CTYPE c3, CTYPE c4, CTYPE c5, CTYPE c6, CTYPE c7, CTYPE c8) {
// calculates polynomial c8*x^8 + c7*x^7 + c6*x^6 + c5*x^5 + c4*x^4 + c3*x^3 + c2*x^2 + c1*x + c0
// VTYPE may be a vector type, CTYPE is a scalar type
VTYPE x2 = x * x;
VTYPE x4 = x2 * x2;
VTYPE x8 = x4 * x4;
//return ((c6+c7*x)*x2 + (c4+c5*x))*x4 + (c8*x8 + (c2+c3*x)*x2 + (c0+c1*x));
return mul_add(mul_add(mul_add(c7, x, c6), x2, mul_add(c5, x, c4)), x4,
mul_add(mul_add(c3, x, c2), x2, mul_add(c1, x, c0) + c8*x8));
}
template<class VTYPE, class CTYPE>
static inline VTYPE polynomial_9(VTYPE const x, CTYPE c0, CTYPE c1, CTYPE c2, CTYPE c3, CTYPE c4, CTYPE c5, CTYPE c6, CTYPE c7, CTYPE c8, CTYPE c9) {
// calculates polynomial c9*x^9 + c8*x^8 + c7*x^7 + c6*x^6 + c5*x^5 + c4*x^4 + c3*x^3 + c2*x^2 + c1*x + c0
// VTYPE may be a vector type, CTYPE is a scalar type
VTYPE x2 = x * x;
VTYPE x4 = x2 * x2;
VTYPE x8 = x4 * x4;
//return (((c6+c7*x)*x2 + (c4+c5*x))*x4 + (c8+c9*x)*x8) + ((c2+c3*x)*x2 + (c0+c1*x));
return mul_add(mul_add(c9, x, c8), x8, mul_add(
mul_add(mul_add(c7, x, c6), x2, mul_add(c5, x, c4)), x4,
mul_add(mul_add(c3, x, c2), x2, mul_add(c1, x, c0))));
}
template<class VTYPE, class CTYPE>
static inline VTYPE polynomial_10(VTYPE const x, CTYPE c0, CTYPE c1, CTYPE c2, CTYPE c3, CTYPE c4, CTYPE c5, CTYPE c6, CTYPE c7, CTYPE c8, CTYPE c9, CTYPE c10) {
// calculates polynomial c10*x^10 + c9*x^9 + c8*x^8 + c7*x^7 + c6*x^6 + c5*x^5 + c4*x^4 + c3*x^3 + c2*x^2 + c1*x + c0
// VTYPE may be a vector type, CTYPE is a scalar type
VTYPE x2 = x * x;
VTYPE x4 = x2 * x2;
VTYPE x8 = x4 * x4;
//return (((c6+c7*x)*x2 + (c4+c5*x))*x4 + (c8+c9*x+c10*x2)*x8) + ((c2+c3*x)*x2 + (c0+c1*x));
return mul_add(mul_add(x2, c10, mul_add(c9, x, c8)), x8,
mul_add(mul_add(mul_add(c7, x, c6), x2, mul_add(c5, x, c4)), x4,
mul_add(mul_add(c3, x, c2), x2, mul_add(c1, x, c0))));
}
template<class VTYPE, class CTYPE>
static inline VTYPE polynomial_13(VTYPE const x, CTYPE c0, CTYPE c1, CTYPE c2, CTYPE c3, CTYPE c4, CTYPE c5, CTYPE c6, CTYPE c7, CTYPE c8, CTYPE c9, CTYPE c10, CTYPE c11, CTYPE c12, CTYPE c13) {
// calculates polynomial c13*x^13 + c12*x^12 + ... + c1*x + c0
// VTYPE may be a vector type, CTYPE is a scalar type
VTYPE x2 = x * x;
VTYPE x4 = x2 * x2;
VTYPE x8 = x4 * x4;
return mul_add(
mul_add(
mul_add(c13, x, c12), x4,
mul_add(mul_add(c11, x, c10), x2, mul_add(c9, x, c8))), x8,
mul_add(
mul_add(mul_add(c7, x, c6), x2, mul_add(c5, x, c4)), x4,
mul_add(mul_add(c3, x, c2), x2, mul_add(c1, x, c0))));
}
template<class VTYPE, class CTYPE>
static inline VTYPE polynomial_13m(VTYPE const x, CTYPE c2, CTYPE c3, CTYPE c4, CTYPE c5, CTYPE c6, CTYPE c7, CTYPE c8, CTYPE c9, CTYPE c10, CTYPE c11, CTYPE c12, CTYPE c13) {
// calculates polynomial c13*x^13 + c12*x^12 + ... + x + 0
// VTYPE may be a vector type, CTYPE is a scalar type
VTYPE x2 = x * x;
VTYPE x4 = x2 * x2;
VTYPE x8 = x4 * x4;
// return ((c8+c9*x) + (c10+c11*x)*x2 + (c12+c13*x)*x4)*x8 + (((c6+c7*x)*x2 + (c4+c5*x))*x4 + ((c2+c3*x)*x2 + x));
return mul_add(
mul_add(mul_add(c13, x, c12), x4, mul_add(mul_add(c11, x, c10), x2, mul_add(c9, x, c8))), x8,
mul_add(mul_add(mul_add(c7, x, c6), x2, mul_add(c5, x, c4)), x4, mul_add(mul_add(c3, x, c2), x2, x)));
}
#ifdef VCL_NAMESPACE
}
#endif
#endif
File diff suppressed because it is too large Load Diff
+717
View File
@@ -0,0 +1,717 @@
/**************************** vectormath_hyp.h ******************************
* Author: Agner Fog
* Date created: 2014-07-09
* Last modified: 2022-07-20
* Version: 2.02.00
* Project: vector class library
* Description:
* Header file containing inline vector functions of hyperbolic and inverse
* hyperbolic functions:
* sinh hyperbolic sine
* cosh hyperbolic cosine
* tanh hyperbolic tangent
* asinh inverse hyperbolic sine
* acosh inverse hyperbolic cosine
* atanh inverse hyperbolic tangent
*
* Theory, methods and inspiration based partially on these sources:
* > Moshier, Stephen Lloyd Baluk: Methods and programs for mathematical functions.
* Ellis Horwood, 1989.
* > VDT library developed on CERN by Danilo Piparo, Thomas Hauth and
* Vincenzo Innocente, 2012, https://svnweb.cern.ch/trac/vdt
* > Cephes math library by Stephen L. Moshier 1992,
* http://www.netlib.org/cephes/
*
* For detailed instructions, see vectormath_common.h and vcl_manual.pdf
*
* (c) Copyright 2014-2022 Agner Fog.
* Apache License version 2.0 or later.
******************************************************************************/
#ifndef VECTORMATH_HYP_H
#define VECTORMATH_HYP_H 202
#include "vectormath_exp.h"
#ifdef VCL_NAMESPACE
namespace VCL_NAMESPACE {
#endif
/******************************************************************************
* Hyperbolic functions
******************************************************************************/
// Template for sinh function, double precision
// This function does not produce denormals
// Template parameters:
// VTYPE: double vector type
template<typename VTYPE>
static inline VTYPE sinh_d(VTYPE const x0) {
// The limit of abs(x) is 709.7, as defined by max_x in vectormath_exp.h for 0.5*exp(x).
// Coefficients
const double p0 = -3.51754964808151394800E5;
const double p1 = -1.15614435765005216044E4;
const double p2 = -1.63725857525983828727E2;
const double p3 = -7.89474443963537015605E-1;
const double q0 = -2.11052978884890840399E6;
const double q1 = 3.61578279834431989373E4;
const double q2 = -2.77711081420602794433E2;
const double q3 = 1.0;
// data vectors
VTYPE x, x2, y1, y2;
x = abs(x0);
auto x_small = x <= 1.0; // use Pade approximation if abs(x) <= 1
if (horizontal_or(x_small)) {
// At least one element needs small method
x2 = x*x;
y1 = polynomial_3(x2, p0, p1, p2, p3) / polynomial_3(x2, q0, q1, q2, q3);
y1 = mul_add(y1, x*x2, x); // y1 = x + x2*(x*y1);
}
if (!horizontal_and(x_small)) {
// At least one element needs big method
y2 = exp_d<VTYPE, 0, 1>(x); // 0.5 * exp(x)
y2 -= 0.25 / y2; // - 0.5 * exp(-x)
}
y1 = select(x_small, y1, y2); // choose method
y1 = sign_combine(y1, x0); // get original sign
// you can avoid the sign_combine by replacing x by x0 above, but at a loss of precision
return y1;
}
// instances of sinh_d template
static inline Vec2d sinh(Vec2d const x) {
return sinh_d(x);
}
#if MAX_VECTOR_SIZE >= 256
static inline Vec4d sinh(Vec4d const x) {
return sinh_d(x);
}
#endif // MAX_VECTOR_SIZE >= 256
#if MAX_VECTOR_SIZE >= 512
static inline Vec8d sinh(Vec8d const x) {
return sinh_d(x);
}
#endif // MAX_VECTOR_SIZE >= 512
// Template for sinh function, single precision
// This function does not produce denormals
// Template parameters:
// VTYPE: double vector type
template<typename VTYPE>
static inline VTYPE sinh_f(VTYPE const x0) {
// The limit of abs(x) is 89.0, as defined by max_x in vectormath_exp.h for 0.5*exp(x).
// Coefficients
const float r0 = 1.66667160211E-1f;
const float r1 = 8.33028376239E-3f;
const float r2 = 2.03721912945E-4f;
// data vectors
VTYPE x, x2, y1, y2;
x = abs(x0);
auto x_small = x <= 1.0f; // use polynomial approximation if abs(x) <= 1
if (horizontal_or(x_small)) {
// At least one element needs small method
x2 = x*x;
y1 = polynomial_2(x2, r0, r1, r2);
y1 = mul_add(y1, x2*x, x); // y1 = x + x2*(x*y1);
}
if (!horizontal_and(x_small)) {
// At least one element needs big method
y2 = exp_f<VTYPE, 0, 1>(x); // 0.5 * exp(x)
y2 -= 0.25f / y2; // - 0.5 * exp(-x)
}
y1 = select(x_small, y1, y2); // choose method
y1 = sign_combine(y1, x0); // get original sign
// you can avoid the sign_combine by replacing x by x0 above, but at a loss of precision
return y1;
}
// instances of sinh_f template
static inline Vec4f sinh(Vec4f const x) {
return sinh_f(x);
}
#if MAX_VECTOR_SIZE >= 256
static inline Vec8f sinh(Vec8f const x) {
return sinh_f(x);
}
#endif // MAX_VECTOR_SIZE >= 256
#if MAX_VECTOR_SIZE >= 512
static inline Vec16f sinh(Vec16f const x) {
return sinh_f(x);
}
#endif // MAX_VECTOR_SIZE >= 512
// Template for cosh function, double precision
// This function does not produce denormals
// Template parameters:
// VTYPE: double vector type
template<typename VTYPE>
static inline VTYPE cosh_d(VTYPE const x0) {
// The limit of abs(x) is 709.7, as defined by max_x in vectormath_exp.h for 0.5*exp(x).
// data vectors
VTYPE x, y;
x = abs(x0);
y = exp_d<VTYPE, 0, 1>(x); // 0.5 * exp(x)
y += 0.25 / y; // + 0.5 * exp(-x)
return y;
}
// instances of sinh_d template
static inline Vec2d cosh(Vec2d const x) {
return cosh_d(x);
}
#if MAX_VECTOR_SIZE >= 256
static inline Vec4d cosh(Vec4d const x) {
return cosh_d(x);
}
#endif // MAX_VECTOR_SIZE >= 256
#if MAX_VECTOR_SIZE >= 512
static inline Vec8d cosh(Vec8d const x) {
return cosh_d(x);
}
#endif // MAX_VECTOR_SIZE >= 512
// Template for cosh function, single precision
// This function does not produce denormals
// Template parameters:
// VTYPE: double vector type
template<typename VTYPE>
static inline VTYPE cosh_f(VTYPE const x0) {
// The limit of abs(x) is 89.0, as defined by max_x in vectormath_exp.h for 0.5*exp(x).
// data vectors
VTYPE x, y;
x = abs(x0);
y = exp_f<VTYPE, 0, 1>(x); // 0.5 * exp(x)
y += 0.25f / y; // + 0.5 * exp(-x)
return y;
}
// instances of sinh_d template
static inline Vec4f cosh(Vec4f const x) {
return cosh_f(x);
}
#if MAX_VECTOR_SIZE >= 256
static inline Vec8f cosh(Vec8f const x) {
return cosh_f(x);
}
#endif // MAX_VECTOR_SIZE >= 256
#if MAX_VECTOR_SIZE >= 512
static inline Vec16f cosh(Vec16f const x) {
return cosh_f(x);
}
#endif // MAX_VECTOR_SIZE >= 512
// Template for tanh function, double precision
// This function does not produce denormals
// Template parameters:
// VTYPE: double vector type
template<typename VTYPE>
static inline VTYPE tanh_d(VTYPE const x0) {
// Coefficients
const double p0 = -1.61468768441708447952E3;
const double p1 = -9.92877231001918586564E1;
const double p2 = -9.64399179425052238628E-1;
const double q0 = 4.84406305325125486048E3;
const double q1 = 2.23548839060100448583E3;
const double q2 = 1.12811678491632931402E2;
const double q3 = 1.0;
// data vectors
VTYPE x, x2, y1, y2;
x = abs(x0);
auto x_small = x <= 0.625; // use Pade approximation if abs(x) <= 5/8
if (horizontal_or(x_small)) {
// At least one element needs small method
x2 = x*x;
y1 = polynomial_2(x2, p0, p1, p2) / polynomial_3(x2, q0, q1, q2, q3);
y1 = mul_add(y1, x2*x, x); // y1 = x + x2*(x*y1);
}
if (!horizontal_and(x_small)) {
// At least one element needs big method
y2 = exp(x+x); // exp(2*x)
y2 = 1.0 - 2.0 / (y2 + 1.0); // tanh(x)
}
auto x_big = x > 350.;
y1 = select(x_small, y1, y2); // choose method
y1 = select(x_big, 1.0, y1); // avoid overflow
y1 = sign_combine(y1, x0); // get original sign
return y1;
}
// instances of tanh_d template
static inline Vec2d tanh(Vec2d const x) {
return tanh_d(x);
}
#if MAX_VECTOR_SIZE >= 256
static inline Vec4d tanh(Vec4d const x) {
return tanh_d(x);
}
#endif // MAX_VECTOR_SIZE >= 256
#if MAX_VECTOR_SIZE >= 512
static inline Vec8d tanh(Vec8d const x) {
return tanh_d(x);
}
#endif // MAX_VECTOR_SIZE >= 512
// Template for tanh function, single precision
// This function does not produce denormals
// Template parameters:
// VTYPE: double vector type
template<typename VTYPE>
static inline VTYPE tanh_f(VTYPE const x0) {
// The limit of abs(x) is 89.0, as defined by max_x in vectormath_exp.h for 0.5*exp(x).
// Coefficients
const float r0 = -3.33332819422E-1f;
const float r1 = 1.33314422036E-1f;
const float r2 = -5.37397155531E-2f;
const float r3 = 2.06390887954E-2f;
const float r4 = -5.70498872745E-3f;
// data vectors
VTYPE x, x2, y1, y2;
x = abs(x0);
auto x_small = x <= 0.625f; // use polynomial approximation if abs(x) <= 5/8
if (horizontal_or(x_small)) {
// At least one element needs small method
x2 = x*x;
y1 = polynomial_4(x2, r0, r1, r2, r3, r4);
y1 = mul_add(y1, x2*x, x); // y1 = x + (x2*x)*y1;
}
if (!horizontal_and(x_small)) {
// At least one element needs big method
y2 = exp(x+x); // exp(2*x)
y2 = 1.0f - 2.0f / (y2 + 1.0f); // tanh(x)
}
auto x_big = x > 44.4f;
y1 = select(x_small, y1, y2); // choose method
y1 = select(x_big, 1.0f, y1); // avoid overflow
y1 = sign_combine(y1, x0); // get original sign
return y1;
}
// instances of tanh_f template
static inline Vec4f tanh(Vec4f const x) {
return tanh_f(x);
}
#if MAX_VECTOR_SIZE >= 256
static inline Vec8f tanh(Vec8f const x) {
return tanh_f(x);
}
#endif // MAX_VECTOR_SIZE >= 256
#if MAX_VECTOR_SIZE >= 512
static inline Vec16f tanh(Vec16f const x) {
return tanh_f(x);
}
#endif // MAX_VECTOR_SIZE >= 512
/******************************************************************************
* Inverse hyperbolic functions
******************************************************************************/
// Template for asinh function, double precision
// This function does not produce denormals
// Template parameters:
// VTYPE: double vector type
template<typename VTYPE>
static inline VTYPE asinh_d(VTYPE const x0) {
// Coefficients
const double p0 = -5.56682227230859640450E0;
const double p1 = -9.09030533308377316566E0;
const double p2 = -4.37390226194356683570E0;
const double p3 = -5.91750212056387121207E-1;
const double p4 = -4.33231683752342103572E-3;
const double q0 = 3.34009336338516356383E1;
const double q1 = 6.95722521337257608734E1;
const double q2 = 4.86042483805291788324E1;
const double q3 = 1.28757002067426453537E1;
const double q4 = 1.0;
// data vectors
VTYPE x, x2, y1, y2;
x2 = x0 * x0;
x = abs(x0);
auto x_small = x <= 0.533; // use Pade approximation if abs(x) <= 0.5
// Both methods give the highest error close to 0.5.
// This limit is adjusted for minimum error
auto x_huge = x > 1.E20; // simple approximation, avoid overflow
if (horizontal_or(x_small)) {
// At least one element needs small method
y1 = polynomial_4(x2, p0, p1, p2, p3, p4) / polynomial_4(x2, q0, q1, q2, q3, q4);
y1 = mul_add(y1, x2*x, x); // y1 = x + (x2*x)*y1;
}
if (!horizontal_and(x_small)) {
// At least one element needs big method
y2 = log(x + sqrt(x2 + 1.0));
if (horizontal_or(x_huge)) {
// At least one element needs huge method to avoid overflow
y2 = select(x_huge, log(x) + VM_LN2, y2);
}
}
y1 = select(x_small, y1, y2); // choose method
y1 = sign_combine(y1, x0); // get original sign
return y1;
}
// instances of asinh_d template
static inline Vec2d asinh(Vec2d const x) {
return asinh_d(x);
}
#if MAX_VECTOR_SIZE >= 256
static inline Vec4d asinh(Vec4d const x) {
return asinh_d(x);
}
#endif // MAX_VECTOR_SIZE >= 256
#if MAX_VECTOR_SIZE >= 512
static inline Vec8d asinh(Vec8d const x) {
return asinh_d(x);
}
#endif // MAX_VECTOR_SIZE >= 512
// Template for asinh function, single precision
// This function does not produce denormals
// Template parameters:
// VTYPE: double vector type
template<typename VTYPE>
static inline VTYPE asinh_f(VTYPE const x0) {
// Coefficients
const float r0 = -1.6666288134E-1f;
const float r1 = 7.4847586088E-2f;
const float r2 = -4.2699340972E-2f;
const float r3 = 2.0122003309E-2f;
// data vectors
VTYPE x, x2, y1, y2;
x2 = x0 * x0;
x = abs(x0);
auto x_small = x <= 0.51f; // use polynomial approximation if abs(x) <= 0.5
auto x_huge = x > 1.E10f; // simple approximation, avoid overflow
if (horizontal_or(x_small)) {
// At least one element needs small method
y1 = polynomial_3(x2, r0, r1, r2, r3);
y1 = mul_add(y1, x2*x, x); // y1 = x + (x2*x)*y1;
}
if (!horizontal_and(x_small)) {
// At least one element needs big method
y2 = log(x + sqrt(x2 + 1.0f));
if (horizontal_or(x_huge)) {
// At least one element needs huge method to avoid overflow
y2 = select(x_huge, log(x) + (float)VM_LN2, y2);
}
}
y1 = select(x_small, y1, y2); // choose method
y1 = sign_combine(y1, x0); // get original sign
return y1;
}
// instances of asinh_f template
static inline Vec4f asinh(Vec4f const x) {
return asinh_f(x);
}
#if MAX_VECTOR_SIZE >= 256
static inline Vec8f asinh(Vec8f const x) {
return asinh_f(x);
}
#endif // MAX_VECTOR_SIZE >= 256
#if MAX_VECTOR_SIZE >= 512
static inline Vec16f asinh(Vec16f const x) {
return asinh_f(x);
}
#endif // MAX_VECTOR_SIZE >= 512
// Template for acosh function, double precision
// This function does not produce denormals
// Template parameters:
// VTYPE: double vector type
template<typename VTYPE>
static inline VTYPE acosh_d(VTYPE const x0) {
// Coefficients
const double p0 = 1.10855947270161294369E5;
const double p1 = 1.08102874834699867335E5;
const double p2 = 3.43989375926195455866E4;
const double p3 = 3.94726656571334401102E3;
const double p4 = 1.18801130533544501356E2;
const double q0 = 7.83869920495893927727E4;
const double q1 = 8.29725251988426222434E4;
const double q2 = 2.97683430363289370382E4;
const double q3 = 4.15352677227719831579E3;
const double q4 = 1.86145380837903397292E2;
const double q5 = 1.0;
// data vectors
VTYPE x1, y1, y2;
x1 = x0 - 1.0;
auto undef = x0 < 1.0; // result is NAN
auto x_small = x1 < 0.49; // use Pade approximation if abs(x-1) < 0.5
auto x_huge = x1 > 1.E20; // simple approximation, avoid overflow
if (horizontal_or(x_small)) {
// At least one element needs small method
y1 = sqrt(x1) * (polynomial_4(x1, p0, p1, p2, p3, p4) / polynomial_5(x1, q0, q1, q2, q3, q4, q5));
// x < 1 generates NAN
y1 = select(undef, nan_vec<VTYPE>(NAN_HYP), y1);
}
if (!horizontal_and(x_small)) {
// At least one element needs big method
y2 = log(x0 + sqrt(mul_sub(x0,x0,1.0)));
if (horizontal_or(x_huge)) {
// At least one element needs huge method to avoid overflow
y2 = select(x_huge, log(x0) + VM_LN2, y2);
}
}
y1 = select(x_small, y1, y2); // choose method
return y1;
}
// instances of acosh_d template
static inline Vec2d acosh(Vec2d const x) {
return acosh_d(x);
}
#if MAX_VECTOR_SIZE >= 256
static inline Vec4d acosh(Vec4d const x) {
return acosh_d(x);
}
#endif // MAX_VECTOR_SIZE >= 256
#if MAX_VECTOR_SIZE >= 512
static inline Vec8d acosh(Vec8d const x) {
return acosh_d(x);
}
#endif // MAX_VECTOR_SIZE >= 512
// Template for acosh function, single precision
// This function does not produce denormals
// Template parameters:
// VTYPE: double vector type
template<typename VTYPE>
static inline VTYPE acosh_f(VTYPE const x0) {
// Coefficients
const float r0 = 1.4142135263E0f;
const float r1 = -1.1784741703E-1f;
const float r2 = 2.6454905019E-2f;
const float r3 = -7.5272886713E-3f;
const float r4 = 1.7596881071E-3f;
// data vectors
VTYPE x1, y1, y2;
x1 = x0 - 1.0f;
auto undef = x0 < 1.0f; // result is NAN
auto x_small = x1 < 0.49f; // use Pade approximation if abs(x-1) < 0.5
auto x_huge = x1 > 1.E10f; // simple approximation, avoid overflow
if (horizontal_or(x_small)) {
// At least one element needs small method
y1 = sqrt(x1) * polynomial_4(x1, r0, r1, r2, r3, r4);
// x < 1 generates NAN
y1 = select(undef, nan_vec<VTYPE>(NAN_HYP), y1);
}
if (!horizontal_and(x_small)) {
// At least one element needs big method
y2 = log(x0 + sqrt(mul_sub(x0,x0,1.0)));
if (horizontal_or(x_huge)) {
// At least one element needs huge method to avoid overflow
y2 = select(x_huge, log(x0) + (float)VM_LN2, y2);
}
}
y1 = select(x_small, y1, y2); // choose method
return y1;
}
// instances of acosh_f template
static inline Vec4f acosh(Vec4f const x) {
return acosh_f(x);
}
#if MAX_VECTOR_SIZE >= 256
static inline Vec8f acosh(Vec8f const x) {
return acosh_f(x);
}
#endif // MAX_VECTOR_SIZE >= 256
#if MAX_VECTOR_SIZE >= 512
static inline Vec16f acosh(Vec16f const x) {
return acosh_f(x);
}
#endif // MAX_VECTOR_SIZE >= 512
// Template for atanh function, double precision
// This function does not produce denormals
// Template parameters:
// VTYPE: double vector type
template<typename VTYPE>
static inline VTYPE atanh_d(VTYPE const x0) {
// Coefficients
const double p0 = -3.09092539379866942570E1;
const double p1 = 6.54566728676544377376E1;
const double p2 = -4.61252884198732692637E1;
const double p3 = 1.20426861384072379242E1;
const double p4 = -8.54074331929669305196E-1;
const double q0 = -9.27277618139601130017E1;
const double q1 = 2.52006675691344555838E2;
const double q2 = -2.49839401325893582852E2;
const double q3 = 1.08938092147140262656E2;
const double q4 = -1.95638849376911654834E1;
const double q5 = 1.0;
// data vectors
VTYPE x, x2, y1, y2, y3;
x = abs(x0);
auto x_small = x < 0.5; // use Pade approximation if abs(x) < 0.5
if (horizontal_or(x_small)) {
// At least one element needs small method
x2 = x * x;
y1 = polynomial_4(x2, p0, p1, p2, p3, p4) / polynomial_5(x2, q0, q1, q2, q3, q4, q5);
y1 = mul_add(y1, x2*x, x);
}
if (!horizontal_and(x_small)) {
// At least one element needs big method
y2 = log((1.0+x)/(1.0-x)) * 0.5;
// check if out of range
y3 = select(x == 1.0, infinite_vec<VTYPE>(), nan_vec<VTYPE>(NAN_HYP));
y2 = select(x >= 1.0, y3, y2);
}
y1 = select(x_small, y1, y2); // choose method
y1 = sign_combine(y1, x0); // get original sign
return y1;
}
// instances of atanh_d template
static inline Vec2d atanh(Vec2d const x) {
return atanh_d(x);
}
#if MAX_VECTOR_SIZE >= 256
static inline Vec4d atanh(Vec4d const x) {
return atanh_d(x);
}
#endif // MAX_VECTOR_SIZE >= 256
#if MAX_VECTOR_SIZE >= 512
static inline Vec8d atanh(Vec8d const x) {
return atanh_d(x);
}
#endif // MAX_VECTOR_SIZE >= 512
// Template for atanh function, single precision
// This function does not produce denormals
// Template parameters:
// VTYPE: double vector type
template<typename VTYPE>
static inline VTYPE atanh_f(VTYPE const x0) {
// Coefficients
const float r0 = 3.33337300303E-1f;
const float r1 = 1.99782164500E-1f;
const float r2 = 1.46691431730E-1f;
const float r3 = 8.24370301058E-2f;
const float r4 = 1.81740078349E-1f;
// data vectors
VTYPE x, x2, y1, y2, y3;
x = abs(x0);
auto x_small = x < 0.5f; // use polynomial approximation if abs(x) < 0.5
if (horizontal_or(x_small)) {
// At least one element needs small method
x2 = x * x;
y1 = polynomial_4(x2, r0, r1, r2, r3, r4);
y1 = mul_add(y1, x2*x, x);
}
if (!horizontal_and(x_small)) {
// At least one element needs big method
y2 = log((1.0f+x)/(1.0f-x)) * 0.5f;
// check if out of range
y3 = select(x == 1.0f, infinite_vec<VTYPE>(), nan_vec<VTYPE>(NAN_HYP));
y2 = select(x >= 1.0f, y3, y2);
}
y1 = select(x_small, y1, y2); // choose method
y1 = sign_combine(y1, x0); // get original sign
return y1;
}
// instances of atanh_f template
static inline Vec4f atanh(Vec4f const x) {
return atanh_f(x);
}
#if MAX_VECTOR_SIZE >= 256
static inline Vec8f atanh(Vec8f const x) {
return atanh_f(x);
}
#endif // MAX_VECTOR_SIZE >= 256
#if MAX_VECTOR_SIZE >= 512
static inline Vec16f atanh(Vec16f const x) {
return atanh_f(x);
}
#endif // MAX_VECTOR_SIZE >= 512
#ifdef VCL_NAMESPACE
}
#endif
#endif
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+5
View File
@@ -31,6 +31,7 @@ option(NANOVDB_BUILD_PYTHON_MODULE "Build the nanovdb Python module" OFF)
option(NANOVDB_USE_INTRINSICS "Build with hardware intrinsics support" OFF)
option(NANOVDB_USE_CUDA "Build with CUDA support" OFF)
option(NANOVDB_CUDA_KEEP_PTX "Keep CUDA PTX" OFF)
option(NANOVDB_USE_OLD_ACCESSOR "Use the old (buggy) ReadAccessor instead of the new fixed one" OFF)
option(NANOVDB_USE_OPENVDB "Build with OpenVDB support" OFF)
option(NANOVDB_USE_BLOSC "Build with BLOSC support" ${USE_BLOSC})
@@ -304,6 +305,10 @@ if(NANOVDB_USE_INTRINSICS)
target_compile_definitions(nanovdb INTERFACE -DNANOVDB_USE_INTRINSICS)
endif()
if(NANOVDB_USE_OLD_ACCESSOR)
target_compile_definitions(nanovdb INTERFACE -DNANOVDB_USE_OLD_ACCESSOR)
endif()
if(NANOVDB_USE_OPENVDB)
if(NOT OPENVDB_BUILD_CORE)
target_link_libraries(nanovdb INTERFACE OpenVDB::openvdb)
+269 -29
View File
@@ -5,14 +5,15 @@
// Simple C-wrapper for the nanovdb structure
// Meant for systems where you lack a C++ compiler.
//
/// @warning This header file will soon be deprecated in favor of PNanoVDB.h
#ifndef __CNANOVDB__
#define __CNANOVDB__
#define CNANOVDB_DATA_ALIGNMENT 32
#define CNANOVDB_ALIGNMENT_PADDING(x, n) (-(x) & ((n)-1))
// We compute alignment, but if it evaluates to 0
// it will be invalid C99, allowing pedantic compilers to
// crash and claim they remain standards conforming.
//#define CNANOVDB_DATA_ALIGNMENT 32
//#define CNANOVDB_ALIGNMENT_PADDING(x, n) (-(x) & ((n)-1))
#define USE_SINGLE_ROOT_KEY
@@ -53,6 +54,21 @@ enum cnanovdb_GridType
cnanovdb_GridType_End = 10
};
// #define versions of the grid type to allow pre-processor
// tests.
#define CNANOVDB_GRIDTYPE_UNKNOWN 0
#define CNANOVDB_GRIDTYPE_FLOAT 1
#define CNANOVDB_GRIDTYPE_DOUBLE 2
#define CNANOVDB_GRIDTYPE_INT16 3
#define CNANOVDB_GRIDTYPE_INT32 4
#define CNANOVDB_GRIDTYPE_INT64 5
#define CNANOVDB_GRIDTYPE_VEC3F 6
#define CNANOVDB_GRIDTYPE_VEC3D 7
#define CNANOVDB_GRIDTYPE_MASK 8
#define CNANOVDB_GRIDTYPE_FP16 9
#define CNANOVDB_GRIDTYPE_END 10
#define ROOT_LEVEL 3
#define DEFINEMASK_int(LOG2DIM, SIZE) \
@@ -88,7 +104,7 @@ typedef struct
double mInvMatD[9]; // r,c = 3*r + c
double mVecD[3];
double mTaperD;
} cnanovdb_map;
} cnanovdb_map; // Size 264
typedef struct
{
@@ -204,12 +220,13 @@ typedef struct
{
int64_t mByteOffset; // byte offset to the blind data, relative to the GridData.
uint64_t mElementCount; // number of elements, e.g. point count
uint32_t mFlags; // flags
uint32_t mValueSize; // byte size of each value, etc.
uint32_t mSemantic; // semantic meaning of the data.
uint32_t mDataClass; // 4 bytes
uint32_t mDataType; // 4 bytes
char mName[256];
uint8_t _reserved[CNANOVDB_ALIGNMENT_PADDING(sizeof(int64_t)+sizeof(uint64_t)+2*sizeof(uint32_t)+2*sizeof(uint32_t)+256*sizeof(char), CNANOVDB_DATA_ALIGNMENT)];
// 2*8+4*4+256 : aligned, so don't include.
// uint8_t _reserved[CNANOVDB_ALIGNMENT_PADDING(sizeof(int64_t)+sizeof(uint64_t)+2*sizeof(uint32_t)+2*sizeof(uint32_t)+256*sizeof(char), CNANOVDB_DATA_ALIGNMENT)];
} cnanovdb_gridblindmetadata;
typedef struct
@@ -227,9 +244,12 @@ typedef struct
double mVoxelSize[3]; // 24B. size of a voxel in world units
uint32_t mGridClass; // 4B.
uint32_t mGridType; // 4B.
uint64_t mBlindMetadataOffset; // 8B. offset of GridBlindMetaData structures.
int32_t mBlindMetadataCount; // 4B. count of GridBlindMetaData structures.
uint32_t _reserved[CNANOVDB_ALIGNMENT_PADDING(8 + 8 + 4 + 4 + 4 + 4 + 8 + 256 + 24 + 24 + sizeof(cnanovdb_map) + 24 + 4 + 4 + 8 + 4, CNANOVDB_DATA_ALIGNMENT) / 4];
int64_t mBlindMetadataOffset; // 8B. offset of GridBlindMetaData structures.
uint32_t mBlindMetadataCount; // 4B. count of GridBlindMetaData structures.
// Total 652: misaligned by 20.
// uint32_t _reserved[CNANOVDB_ALIGNMENT_PADDING(8 + 8 + 4 + 4 + 4 + 4 + 8 + 256 + 24 + 24 + sizeof(cnanovdb_map) + 24 + 4 + 4 + 8 + 4, CNANOVDB_DATA_ALIGNMENT) / 4];
uint32_t _reserved[5];
} cnanovdb_griddata;
static void
@@ -268,7 +288,8 @@ typedef struct
uint32_t mNodeCount[ROOT_LEVEL];
uint32_t mTileCount[ROOT_LEVEL];
uint64_t mVoxelCount;
uint8_t _reserved[CNANOVDB_ALIGNMENT_PADDING(4*sizeof(uint64_t)+(3+3)*sizeof(uint32_t)+sizeof(uint64_t), CNANOVDB_DATA_ALIGNMENT)];
// Total 64, so aligned, so do not include:
// uint8_t _reserved[CNANOVDB_ALIGNMENT_PADDING(4*sizeof(uint64_t)+(3+3)*sizeof(uint32_t)+sizeof(uint64_t), CNANOVDB_DATA_ALIGNMENT)];
} cnanovdb_treedata;
static const CNANOVDB_GLOBAL cnanovdb_treedata *
@@ -301,6 +322,17 @@ cnanovdb_readaccessor_insert(cnanovdb_readaccessor *RESTRICT acc, int childlevel
acc->mKey.mVec[2] = ijk->mVec[2];
}
// The padding of nodes varies on the signature, as the value type may
// result in aligned or misaligned...
#define LEAFDATA_PAD_F
#define LEAFDATA_PAD_F3 uint32_t _reserved[4];
#define LEAFDATA_PAD_I
#define NODEDATA_PAD_F uint8_t _reserved[16];
#define NODEDATA_PAD_F3
#define NODEDATA_PAD_I uint8_t _reserved[16];
#define CREATE_LEAF_NODE_int(LEVEL, LOG2DIM, CHILDTOTAL, TOTAL, MASK, VALUETYPE, STATSTYPE, SUFFIX) \
typedef struct \
{ \
@@ -312,7 +344,8 @@ typedef struct \
VALUETYPE mMaximum; \
STATSTYPE mAverage; \
STATSTYPE mStdDevi; \
uint32_t _reserved[ CNANOVDB_ALIGNMENT_PADDING(sizeof(cnanovdb_mask##LOG2DIM)+2*sizeof(VALUETYPE)+2*sizeof(STATSTYPE)+sizeof(cnanovdb_coord)+sizeof(uint8_t[3])+sizeof(uint8_t), CNANOVDB_DATA_ALIGNMENT)/4]; \
LEAFDATA_PAD_##SUFFIX \
/* uint32_t _reserved[ CNANOVDB_ALIGNMENT_PADDING(sizeof(cnanovdb_mask##LOG2DIM)+2*sizeof(VALUETYPE)+2*sizeof(STATSTYPE)+sizeof(cnanovdb_coord)+sizeof(uint8_t[3])+sizeof(uint8_t), CNANOVDB_DATA_ALIGNMENT)/4]; */ \
VALUETYPE mVoxels[1u << (3*LOG2DIM)]; \
} cnanovdb_node##LEVEL##SUFFIX; \
\
@@ -340,6 +373,23 @@ cnanovdb_node##LEVEL##SUFFIX##_getValueAndCache(const CNANOVDB_GLOBAL cnanovdb_n
} \
\
static bool \
cnanovdb_node##LEVEL##SUFFIX##_setValue(CNANOVDB_GLOBAL cnanovdb_node##LEVEL##SUFFIX *RESTRICT node, const cnanovdb_coord *RESTRICT ijk, VALUETYPE value) \
{ \
uint32_t n = cnanovdb_node##LEVEL##SUFFIX##_CoordToOffset(ijk); \
node->mVoxels[n] = value; \
return true; \
} \
\
static bool \
cnanovdb_node##LEVEL##SUFFIX##_setValueAndCache(CNANOVDB_GLOBAL cnanovdb_node##LEVEL##SUFFIX *RESTRICT node, const cnanovdb_coord *RESTRICT ijk, VALUETYPE value,cnanovdb_readaccessor *RESTRICT /* DO NOT REMOVE: Required for C99 compliance */ acc) \
{ \
(void)(acc); \
uint32_t n = cnanovdb_node##LEVEL##SUFFIX##_CoordToOffset(ijk); \
node->mVoxels[n] = value; \
return true; \
} \
\
static bool \
cnanovdb_node##LEVEL##SUFFIX##_isActive(const CNANOVDB_GLOBAL cnanovdb_node##LEVEL##SUFFIX *RESTRICT node, const cnanovdb_coord *RESTRICT ijk) \
{ \
uint32_t n = cnanovdb_node##LEVEL##SUFFIX##_CoordToOffset(ijk); \
@@ -379,7 +429,7 @@ typedef struct \
cnanovdb_mask##LOG2DIM mValueMask, mChildMask; \
VALUETYPE mMinimum, mMaximum; \
STATSTYPE mAverage, mStdDevi; \
uint8_t _reserved[CNANOVDB_ALIGNMENT_PADDING(sizeof(cnanovdb_mask##LOG2DIM)+sizeof(VALUETYPE)*2+sizeof(STATSTYPE)*2+sizeof(cnanovdb_coord)*2+sizeof(int32_t)+sizeof(uint32_t), CNANOVDB_DATA_ALIGNMENT)]; \
NODEDATA_PAD_##SUFFIX \
cnanovdb_tileentry##SUFFIX mTable[1u << (3*LOG2DIM)]; \
} cnanovdb_node##LEVEL##SUFFIX; \
\
@@ -398,6 +448,13 @@ cnanovdb_node##LEVEL##SUFFIX##_getChild(const CNANOVDB_GLOBAL cnanovdb_node##LEV
return childnode; \
} \
\
static CNANOVDB_GLOBAL cnanovdb_node##CHILDLEVEL##SUFFIX * \
cnanovdb_node##LEVEL##SUFFIX##_getChildNC(CNANOVDB_GLOBAL cnanovdb_node##LEVEL##SUFFIX *RESTRICT node, uint32_t n) \
{ \
CNANOVDB_GLOBAL cnanovdb_node##CHILDLEVEL##SUFFIX *childnode = (CNANOVDB_GLOBAL cnanovdb_node##CHILDLEVEL##SUFFIX *)( ((CNANOVDB_GLOBAL uint8_t *)node) + node->mTable[n].child); \
return childnode; \
} \
\
static VALUETYPE \
cnanovdb_node##LEVEL##SUFFIX##_getValue(const CNANOVDB_GLOBAL cnanovdb_node##LEVEL##SUFFIX *RESTRICT node, const cnanovdb_coord *RESTRICT ijk) \
{ \
@@ -424,6 +481,31 @@ cnanovdb_node##LEVEL##SUFFIX##_getValueAndCache(const CNANOVDB_GLOBAL cnanovdb_n
} \
\
static bool \
cnanovdb_node##LEVEL##SUFFIX##_setValue(CNANOVDB_GLOBAL cnanovdb_node##LEVEL##SUFFIX *RESTRICT node, const cnanovdb_coord *RESTRICT ijk, VALUETYPE value) \
{ \
uint32_t n = cnanovdb_node##LEVEL##SUFFIX##_CoordToOffset(ijk); \
if (cnanovdb_mask##LOG2DIM##_isOn(&node->mChildMask, n)) \
{ \
CNANOVDB_GLOBAL cnanovdb_node##CHILDLEVEL##SUFFIX *child = cnanovdb_node##LEVEL##SUFFIX##_getChildNC(node, n); \
return cnanovdb_node##CHILDLEVEL##SUFFIX##_setValue(child, ijk, value); \
} \
return false; \
} \
\
static bool \
cnanovdb_node##LEVEL##SUFFIX##_setValueAndCache(CNANOVDB_GLOBAL cnanovdb_node##LEVEL##SUFFIX *RESTRICT node, const cnanovdb_coord *RESTRICT ijk, VALUETYPE value, cnanovdb_readaccessor *RESTRICT acc) \
{ \
uint32_t n = cnanovdb_node##LEVEL##SUFFIX##_CoordToOffset(ijk); \
if (cnanovdb_mask##LOG2DIM##_isOn(&node->mChildMask, n)) \
{ \
CNANOVDB_GLOBAL cnanovdb_node##CHILDLEVEL##SUFFIX *child = cnanovdb_node##LEVEL##SUFFIX##_getChildNC(node, n); \
cnanovdb_readaccessor_insert(acc, CHILDLEVEL, child, ijk); \
return cnanovdb_node##CHILDLEVEL##SUFFIX##_setValueAndCache(child, ijk, value, acc); \
} \
return false; \
} \
\
static bool \
cnanovdb_node##LEVEL##SUFFIX##_isActive(const CNANOVDB_GLOBAL cnanovdb_node##LEVEL##SUFFIX *RESTRICT node, const cnanovdb_coord *RESTRICT ijk) \
{ \
uint32_t n = cnanovdb_node##LEVEL##SUFFIX##_CoordToOffset(ijk); \
@@ -462,10 +544,33 @@ CREATE_INTERNAL_NODE_int(CHILDLEVEL, LEVEL, LOG2DIM, (TOTAL-LOG2DIM), TOTAL, ((1
#ifdef USE_SINGLE_ROOT_KEY
// Whether this is aligned varies with the signature...
#define ROOTDATATILE_PAD_F uint8_t _reserved[8];
#define ROOTDATATILE_PAD_I uint8_t _reserved[8];
#define ROOTDATATILE_PAD_F3
#define DEFINE_KEY(KEY) \
uint64_t KEY;
#define KEYSIZE sizeof(uint64_t)
#ifdef CNANOVDB_FULL_ROOT_SEARCH
#define KEYSEARCH(SUFFIX) \
uint64_t key; \
const CNANOVDB_GLOBAL cnanovdb_rootdata_tile##SUFFIX *result = 0; \
key = cnanovdb_coord_to_key(ijk); \
\
(void)high; \
/* Re-read tablesize for compiler bug on intel windows driver */ \
for (int idx = low; idx < rootdata->mTableSize; idx++) \
{ \
const CNANOVDB_GLOBAL cnanovdb_rootdata_tile##SUFFIX *tile = tiles + idx; \
if (tile->key == key) \
result = tile; \
} \
return result; \
/**/
#else
#define KEYSEARCH(SUFFIX) \
uint64_t key; \
key = cnanovdb_coord_to_key(ijk); \
@@ -477,7 +582,14 @@ CREATE_INTERNAL_NODE_int(CHILDLEVEL, LEVEL, LOG2DIM, (TOTAL-LOG2DIM), TOTAL, ((1
return tile; \
} \
/**/
#endif
#else
// Whether this is aligned varies with the signature...
#define ROOTDATATILE_PAD_F
#define ROOTDATATILE_PAD_I
#define ROOTDATATILE_PAD_F3 uint8_t _reserved[24];
#define DEFINE_KEY(KEY) \
cnanovdb_coord KEY;
#define KEYSIZE sizeof(cnanovdb_coord)
@@ -504,6 +616,9 @@ CREATE_INTERNAL_NODE_int(CHILDLEVEL, LEVEL, LOG2DIM, (TOTAL-LOG2DIM), TOTAL, ((1
/**/
#endif
#define ROOTDATA_PAD_F uint32_t reserved[4];
#define ROOTDATA_PAD_F3 uint32_t reserved[6];
#define ROOTDATA_PAD_I uint32_t reserved[4];
#define CREATE_ROOTDATA(VALUETYPE, STATSTYPE, SUFFIX) \
typedef struct \
@@ -512,7 +627,11 @@ typedef struct \
int64_t child; \
uint32_t state; \
VALUETYPE value; \
uint8_t _reserved[CNANOVDB_ALIGNMENT_PADDING(sizeof(KEYSIZE)+sizeof(VALUETYPE)+sizeof(int64_t)+sizeof(uint32_t), CNANOVDB_DATA_ALIGNMENT)]; \
/* Key is either 8 or 3*4, coord will round to 4*4 due to alignment */ \
/* of child, so 8+8 or 4*4+8. state+value is either 4+4 or 4+3*4 */ \
/* 8+8+4+4:pad, 8+8+4*4:align, 4*4+8+2*4:align, 4*4+8+4*4:pad */ \
ROOTDATATILE_PAD_##SUFFIX \
/* uint8_t _reserved[CNANOVDB_ALIGNMENT_PADDING(sizeof(KEYSIZE)+sizeof(VALUETYPE)+sizeof(int64_t)+sizeof(uint32_t), CNANOVDB_DATA_ALIGNMENT)]; */ \
} cnanovdb_rootdata_tile##SUFFIX; \
\
typedef struct \
@@ -522,7 +641,10 @@ typedef struct \
VALUETYPE mBackground; \
VALUETYPE mMinimum, mMaximum; \
STATSTYPE mAverage, mStdDevi; \
uint32_t _reserved[CNANOVDB_ALIGNMENT_PADDING(sizeof(cnanovdb_coord)*2+sizeof(uint32_t)+sizeof(VALUETYPE)*3+sizeof(STATSTYPE)*2, CNANOVDB_DATA_ALIGNMENT)/4]; \
/* 2*3*4 + 4 + 3*4 + 2*4: 12*4, unaligned */ \
/* 2*3*4 + 4 + 3*3*4 + 2*4: 18*4, unaligned */ \
ROOTDATA_PAD_##SUFFIX \
/* uint32_t _reserved[CNANOVDB_ALIGNMENT_PADDING(sizeof(cnanovdb_coord)*2+sizeof(uint32_t)+sizeof(VALUETYPE)*3+sizeof(STATSTYPE)*2, CNANOVDB_DATA_ALIGNMENT)/4]; */ \
} cnanovdb_rootdata##SUFFIX; \
\
static const CNANOVDB_GLOBAL cnanovdb_rootdata##SUFFIX * \
@@ -540,6 +662,13 @@ cnanovdb_rootdata##SUFFIX##_getTile(const CNANOVDB_GLOBAL cnanovdb_rootdata##SUF
\
static const CNANOVDB_GLOBAL cnanovdb_node2##SUFFIX * \
cnanovdb_rootdata##SUFFIX##_getChild(const CNANOVDB_GLOBAL cnanovdb_rootdata##SUFFIX *RESTRICT rootdata, const CNANOVDB_GLOBAL cnanovdb_rootdata_tile##SUFFIX *RESTRICT tile) \
{ \
CNANOVDB_GLOBAL cnanovdb_node2##SUFFIX *basenode = (CNANOVDB_GLOBAL cnanovdb_node2##SUFFIX *) (((CNANOVDB_GLOBAL uint8_t *) rootdata) + tile->child); \
return basenode; \
} \
\
static CNANOVDB_GLOBAL cnanovdb_node2##SUFFIX * \
cnanovdb_rootdata##SUFFIX##_getChildNC(CNANOVDB_GLOBAL cnanovdb_rootdata##SUFFIX *RESTRICT rootdata, CNANOVDB_GLOBAL cnanovdb_rootdata_tile##SUFFIX *RESTRICT tile) \
{ \
CNANOVDB_GLOBAL cnanovdb_node2##SUFFIX *basenode = (CNANOVDB_GLOBAL cnanovdb_node2##SUFFIX *) (((CNANOVDB_GLOBAL uint8_t *) rootdata) + tile->child); \
return basenode; \
@@ -555,6 +684,13 @@ cnanovdb_rootdata##SUFFIX##_findTile(const CNANOVDB_GLOBAL cnanovdb_rootdata##SU
return 0; \
} \
\
static CNANOVDB_GLOBAL cnanovdb_rootdata_tile##SUFFIX * \
cnanovdb_rootdata##SUFFIX##_findTileNC(CNANOVDB_GLOBAL cnanovdb_rootdata##SUFFIX *RESTRICT rootdata, const cnanovdb_coord *RESTRICT ijk) \
{ \
return (CNANOVDB_GLOBAL cnanovdb_rootdata_tile##SUFFIX *) \
cnanovdb_rootdata##SUFFIX##_findTile(rootdata, ijk); \
} \
\
static VALUETYPE \
cnanovdb_rootdata##SUFFIX##_getValue(const CNANOVDB_GLOBAL cnanovdb_rootdata##SUFFIX *RESTRICT rootdata, const cnanovdb_coord *RESTRICT ijk) \
{ \
@@ -577,6 +713,30 @@ cnanovdb_rootdata##SUFFIX##_getValueAndCache(const CNANOVDB_GLOBAL cnanovdb_root
const CNANOVDB_GLOBAL cnanovdb_node2##SUFFIX *child = cnanovdb_rootdata##SUFFIX##_getChild(rootdata, tile); \
cnanovdb_readaccessor_insert(acc, 2, child, ijk); \
return cnanovdb_node2##SUFFIX##_getValueAndCache( child, ijk, acc ); \
} \
\
static bool \
cnanovdb_rootdata##SUFFIX##_setValue(CNANOVDB_GLOBAL cnanovdb_rootdata##SUFFIX *RESTRICT rootdata, const cnanovdb_coord *RESTRICT ijk, VALUETYPE value) \
{ \
CNANOVDB_GLOBAL cnanovdb_rootdata_tile##SUFFIX *tile = cnanovdb_rootdata##SUFFIX##_findTileNC(rootdata, ijk); \
if (!tile) \
return false; \
if (tile->child == 0) \
return false; \
return cnanovdb_node2##SUFFIX##_setValue( cnanovdb_rootdata##SUFFIX##_getChildNC(rootdata, tile), ijk, value ); \
} \
\
static bool \
cnanovdb_rootdata##SUFFIX##_setValueAndCache(CNANOVDB_GLOBAL cnanovdb_rootdata##SUFFIX *RESTRICT rootdata, const cnanovdb_coord *RESTRICT ijk, VALUETYPE value, cnanovdb_readaccessor *RESTRICT acc) \
{ \
CNANOVDB_GLOBAL cnanovdb_rootdata_tile##SUFFIX *tile = cnanovdb_rootdata##SUFFIX##_findTileNC(rootdata, ijk); \
if (!tile) \
return false; \
if (tile->child == 0) \
return false; \
CNANOVDB_GLOBAL cnanovdb_node2##SUFFIX *child = cnanovdb_rootdata##SUFFIX##_getChildNC(rootdata, tile); \
cnanovdb_readaccessor_insert(acc, 2, child, ijk); \
return cnanovdb_node2##SUFFIX##_setValueAndCache( child, ijk, value, acc );\
} \
\
static bool \
@@ -605,16 +765,19 @@ cnanovdb_rootdata##SUFFIX##_isActiveAndCache(const CNANOVDB_GLOBAL cnanovdb_root
/**/
inline void
static inline void
cnanovdb_readaccessor_init(cnanovdb_readaccessor *RESTRICT acc,
const CNANOVDB_GLOBAL void /*cnanovdb_rootdata* */ *RESTRICT rootdata)
{
acc->mKey.mVec[0] = 0;
acc->mKey.mVec[1] = 0;
acc->mKey.mVec[2] = 0;
acc->mNode[0] = acc->mNode[1] = acc->mNode[2] = 0;
acc->mNode[3] = rootdata;
}
#define DEFINE_ISCACHED(LEVEL, MASK) \
inline bool \
static inline bool \
cnanovdb_readaccessor_isCached##LEVEL(cnanovdb_readaccessor *RESTRICT acc, int32_t dirty) \
{ \
if (!acc->mNode[LEVEL]) \
@@ -632,7 +795,7 @@ DEFINE_ISCACHED(0, ((1u << 3) - 1u) )
DEFINE_ISCACHED(1, ((1u << 7) - 1u) )
DEFINE_ISCACHED(2, ((1u << 12) - 1u) )
inline int32_t
static inline int32_t
cnanovdb_readaccessor_computeDirty(const cnanovdb_readaccessor *RESTRICT acc, const cnanovdb_coord *RESTRICT ijk)
{
return (ijk->mVec[0] ^ acc->mKey.mVec[0]) |
@@ -641,35 +804,102 @@ cnanovdb_readaccessor_computeDirty(const cnanovdb_readaccessor *RESTRICT acc, co
}
#define CREATE_ACCESSOR(VALUETYPE, SUFFIX) \
inline VALUETYPE \
static inline VALUETYPE \
cnanovdb_readaccessor_getValue##SUFFIX(cnanovdb_readaccessor *RESTRICT acc, const cnanovdb_coord *RESTRICT ijk) \
{ \
int32_t dirty = cnanovdb_readaccessor_computeDirty(acc, ijk); \
\
if (cnanovdb_readaccessor_isCached0(acc, dirty)) \
return cnanovdb_node0##SUFFIX##_getValue( ((CNANOVDB_GLOBAL cnanovdb_node0##SUFFIX *) acc->mNode[0]), ijk); \
return cnanovdb_node0##SUFFIX##_getValue( ((const CNANOVDB_GLOBAL cnanovdb_node0##SUFFIX *) acc->mNode[0]), ijk); \
if (cnanovdb_readaccessor_isCached1(acc, dirty)) \
return cnanovdb_node1##SUFFIX##_getValueAndCache( ((CNANOVDB_GLOBAL cnanovdb_node1##SUFFIX *) acc->mNode[1]), ijk, acc); \
return cnanovdb_node1##SUFFIX##_getValueAndCache( ((const CNANOVDB_GLOBAL cnanovdb_node1##SUFFIX *) acc->mNode[1]), ijk, acc); \
if (cnanovdb_readaccessor_isCached2(acc, dirty)) \
return cnanovdb_node2##SUFFIX##_getValueAndCache( ((CNANOVDB_GLOBAL cnanovdb_node2##SUFFIX *) acc->mNode[2]), ijk, acc); \
return cnanovdb_node2##SUFFIX##_getValueAndCache( ((const CNANOVDB_GLOBAL cnanovdb_node2##SUFFIX *) acc->mNode[2]), ijk, acc); \
\
return cnanovdb_rootdata##SUFFIX##_getValueAndCache( ((CNANOVDB_GLOBAL cnanovdb_rootdata##SUFFIX *)acc->mNode[3]), ijk, acc); \
return cnanovdb_rootdata##SUFFIX##_getValueAndCache( ((const CNANOVDB_GLOBAL cnanovdb_rootdata##SUFFIX *)acc->mNode[3]), ijk, acc); \
} \
\
inline bool \
static inline VALUETYPE \
cnanovdb_readaccessor_getIJKValue##SUFFIX(cnanovdb_readaccessor *RESTRICT acc, int i, int j, int k) \
{ \
cnanovdb_coord coord; \
coord.mVec[0] = i; coord.mVec[1] = j; coord.mVec[2] = k; \
return cnanovdb_readaccessor_getValue##SUFFIX(acc, &coord); \
} \
\
static inline CNANOVDB_GLOBAL VALUETYPE * \
cnanovdb_readaccessor_getLeaf##SUFFIX(cnanovdb_readaccessor *RESTRICT acc, const cnanovdb_coord *RESTRICT ijk) \
{ \
int32_t dirty = cnanovdb_readaccessor_computeDirty(acc, ijk); \
\
if (cnanovdb_readaccessor_isCached0(acc, dirty)) \
return ((CNANOVDB_GLOBAL cnanovdb_node0##SUFFIX *) acc->mNode[0])->mVoxels; \
else if (cnanovdb_readaccessor_isCached1(acc, dirty)) \
cnanovdb_node1##SUFFIX##_getValueAndCache( ((const CNANOVDB_GLOBAL cnanovdb_node1##SUFFIX *) acc->mNode[1]), ijk, acc); \
else if (cnanovdb_readaccessor_isCached2(acc, dirty)) \
cnanovdb_node2##SUFFIX##_getValueAndCache( ((const CNANOVDB_GLOBAL cnanovdb_node2##SUFFIX *) acc->mNode[2]), ijk, acc); \
else \
cnanovdb_rootdata##SUFFIX##_getValueAndCache( ((const CNANOVDB_GLOBAL cnanovdb_rootdata##SUFFIX *)acc->mNode[3]), ijk, acc); \
if (acc->mNode[0]) \
return ((CNANOVDB_GLOBAL cnanovdb_node0##SUFFIX *) acc->mNode[0])->mVoxels; \
return 0; \
} \
\
static inline CNANOVDB_GLOBAL VALUETYPE * \
cnanovdb_readaccessor_getIJKLeaf##SUFFIX(cnanovdb_readaccessor *RESTRICT acc, int i, int j, int k) \
{ \
cnanovdb_coord coord; \
coord.mVec[0] = i; coord.mVec[1] = j; coord.mVec[2] = k; \
return cnanovdb_readaccessor_getLeaf##SUFFIX(acc, &coord); \
} \
\
static inline bool \
cnanovdb_readaccessor_isActive##SUFFIX(cnanovdb_readaccessor *RESTRICT acc, const cnanovdb_coord *RESTRICT ijk) \
{ \
int32_t dirty = cnanovdb_readaccessor_computeDirty(acc, ijk); \
\
if (cnanovdb_readaccessor_isCached0(acc, dirty)) \
return cnanovdb_node0##SUFFIX##_isActive( ((CNANOVDB_GLOBAL cnanovdb_node0##SUFFIX *) acc->mNode[0]), ijk); \
return cnanovdb_node0##SUFFIX##_isActive( ((const CNANOVDB_GLOBAL cnanovdb_node0##SUFFIX *) acc->mNode[0]), ijk); \
if (cnanovdb_readaccessor_isCached1(acc, dirty)) \
return cnanovdb_node1##SUFFIX##_isActiveAndCache( ((CNANOVDB_GLOBAL cnanovdb_node1##SUFFIX *) acc->mNode[1]), ijk, acc); \
return cnanovdb_node1##SUFFIX##_isActiveAndCache( ((const CNANOVDB_GLOBAL cnanovdb_node1##SUFFIX *) acc->mNode[1]), ijk, acc); \
if (cnanovdb_readaccessor_isCached2(acc, dirty)) \
return cnanovdb_node2##SUFFIX##_isActiveAndCache( ((CNANOVDB_GLOBAL cnanovdb_node2##SUFFIX *) acc->mNode[2]), ijk, acc); \
return cnanovdb_node2##SUFFIX##_isActiveAndCache( ((const CNANOVDB_GLOBAL cnanovdb_node2##SUFFIX *) acc->mNode[2]), ijk, acc); \
\
return cnanovdb_rootdata##SUFFIX##_isActiveAndCache( ((CNANOVDB_GLOBAL cnanovdb_rootdata##SUFFIX *)acc->mNode[3]), ijk, acc); \
return cnanovdb_rootdata##SUFFIX##_isActiveAndCache( ((const CNANOVDB_GLOBAL cnanovdb_rootdata##SUFFIX *)acc->mNode[3]), ijk, acc); \
} \
\
static inline bool \
cnanovdb_readaccessor_isIJKActive##SUFFIX(cnanovdb_readaccessor *RESTRICT acc, int i, int j, int k) \
{ \
cnanovdb_coord coord; \
coord.mVec[0] = i; coord.mVec[1] = j; coord.mVec[2] = k; \
return cnanovdb_readaccessor_isActive##SUFFIX(acc, &coord); \
} \
\
static inline bool \
cnanovdb_readaccessor_setValue##SUFFIX(cnanovdb_readaccessor *RESTRICT acc, const cnanovdb_coord *RESTRICT ijk, VALUETYPE value) \
{ \
int32_t dirty = cnanovdb_readaccessor_computeDirty(acc, ijk); \
\
if (cnanovdb_readaccessor_isCached0(acc, dirty)) \
return cnanovdb_node0##SUFFIX##_setValue( ((CNANOVDB_GLOBAL cnanovdb_node0##SUFFIX *) acc->mNode[0]), ijk, value); \
if (cnanovdb_readaccessor_isCached1(acc, dirty)) \
return cnanovdb_node1##SUFFIX##_setValueAndCache( ((CNANOVDB_GLOBAL cnanovdb_node1##SUFFIX *) acc->mNode[1]), ijk, value, acc); \
if (cnanovdb_readaccessor_isCached2(acc, dirty)) \
return cnanovdb_node2##SUFFIX##_setValueAndCache( ((CNANOVDB_GLOBAL cnanovdb_node2##SUFFIX *) acc->mNode[2]), ijk, value, acc); \
\
return cnanovdb_rootdata##SUFFIX##_setValueAndCache( ((CNANOVDB_GLOBAL cnanovdb_rootdata##SUFFIX *)acc->mNode[3]), ijk, value, acc); \
} \
\
static inline bool \
cnanovdb_readaccessor_setIJKValue##SUFFIX(cnanovdb_readaccessor *RESTRICT acc, int i, int j, int k, VALUETYPE value) \
{ \
cnanovdb_coord coord; \
coord.mVec[0] = i; coord.mVec[1] = j; coord.mVec[2] = k; \
return cnanovdb_readaccessor_setValue##SUFFIX(acc, &coord, value); \
} \
\
\
/**/
@@ -684,6 +914,7 @@ CREATE_ACCESSOR(VALUETYPE, SUFFIX) \
CREATE_GRIDTYPE(float, float, F)
CREATE_GRIDTYPE(cnanovdb_Vec3F, float, F3)
CREATE_GRIDTYPE(int, int, I)
static int
cnanovdb_griddata_valid(const CNANOVDB_GLOBAL cnanovdb_griddata *RESTRICT grid)
@@ -715,4 +946,13 @@ cnanovdb_griddata_validF3(const CNANOVDB_GLOBAL cnanovdb_griddata *RESTRICT grid
return 1;
}
static int
cnanovdb_griddata_validI(const CNANOVDB_GLOBAL cnanovdb_griddata *RESTRICT grid)
{
if (!cnanovdb_griddata_valid(grid))
return 0;
if (grid->mGridType != cnanovdb_GridType_Int32)
return 0;
return 1;
}
#endif
+6 -6
View File
@@ -163,7 +163,6 @@ public:
/// @brief Return a const pointer to the @a n'th grid encoded in this GridHandle on the device, e.g. GPU
/// @tparam ValueT Value type of the grid point to be returned
/// @param n Index of the grid pointer to be returned
/// @param verbose if non-zero error messages will be printed in case something failed
/// @warning Note that the return pointer can be NULL if the GridHandle was not initialized, @a n is invalid,
/// or if the template parameter does not match the specified grid.
template<typename ValueT, typename U = BufferT>
@@ -293,7 +292,7 @@ public:
void read(std::istream& is, const std::string &gridName, const BufferT& pool = BufferT());
/// @brief Read a raw grid buffer from a file
/// @param filename string name of the input file containing a raw grid buffer
/// @param fileName string name of the input file containing a raw grid buffer
/// @param pool optional pool from which to allocate the new grid buffer
void read(const std::string &fileName, const BufferT& pool = BufferT()) {
std::ifstream is(fileName, std::ios::in | std::ios::binary);
@@ -302,7 +301,7 @@ public:
}
/// @brief Read a specific grid from a file containing a raw grid buffer
/// @param filename string name of the input file containing a raw grid buffer
/// @param fileName string name of the input file containing a raw grid buffer
/// @param n zero-based index of the grid to be read
/// @param pool optional pool from which to allocate the new grid buffer
/// @throw Will throw a std::ios_base::failure if the file does not exist and a
@@ -314,7 +313,7 @@ public:
}
/// @brief Read a specific grid from a file containing a raw grid buffer
/// @param filename string name of the input file containing a raw grid buffer
/// @param fileName string name of the input file containing a raw grid buffer
/// @param gridName string name of the grid to be read
/// @param pool optional pool from which to allocate the new grid buffer
/// @throw Will throw a std::ios_base::failure if the file does not exist and a
@@ -400,6 +399,7 @@ GridHandle<BufferT>::deviceGrid(uint32_t n) const
template<typename BufferT>
void GridHandle<BufferT>::read(std::istream& is, const BufferT& pool)
{
const std::streampos start = is.tellg();// remember where the raw buffer begins
GridData data;
is.read((char*)&data, sizeof(GridData));
if (data.isValid()) {
@@ -410,7 +410,7 @@ void GridHandle<BufferT>::read(std::istream& is, const BufferT& pool)
sum += data.mGridSize;
}
auto buffer = BufferT::create(size + sum, &pool);
is.seekg(-int64_t(sum + sizeof(GridData)), std::ios::cur);// rewind to start
is.seekg(start);// rewind to the start of the raw buffer
is.read((char*)(buffer.data()), buffer.size());
*this = GridHandle(std::move(buffer));
} else {
@@ -470,7 +470,7 @@ void GridHandle<BufferT>::read(std::istream& is, const std::string &gridName, co
/// @brief Split all grids in a single GridHandle into a vector of multiple GridHandles each with a single grid
/// @tparam BufferT Type of the input and output grid buffers
/// @param handle GridHandle with grids that will be slip into individual GridHandles
/// @param pool optional pool used for allocation of output GridHandle
/// @param other optional pool used for allocation of output GridHandle
/// @return Vector of GridHandles each containing a single grid
template<typename BufferT, template <class, class...> class VectorT = std::vector>
inline VectorT<GridHandle<BufferT>>
+82 -15
View File
@@ -145,7 +145,7 @@
#define NANOVDB_MAJOR_VERSION_NUMBER 32 // reflects changes to the ABI and hence also the file format
#define NANOVDB_MINOR_VERSION_NUMBER 9 // reflects changes to the API but not ABI
#define NANOVDB_PATCH_VERSION_NUMBER 1 // reflects changes that does not affect the ABI or API
#define NANOVDB_PATCH_VERSION_NUMBER 2 // reflects changes that do not affect the ABI or API
#define TBB_SUPPRESS_DEPRECATED_MESSAGES 1
@@ -158,6 +158,11 @@
// Use this to switch between std::ofstream or FILE implementations
//#define NANOVDB_USE_IOSTREAMS
// Define NANOVDB_USE_OLD_ACCESSOR before including this header to temporarily
// restore legacy ReadAccessor behavior where value lookups fall back to root
// after a leaf-cache miss.
// Comment out to use (slower) branched version of LeafData<FpN,...>::getValue
#define NANOVDB_FPN_BRANCHLESS
#if !defined(NANOVDB_ALIGN)
@@ -290,7 +295,8 @@ enum class GridClass : uint32_t { Unknown = 0,
VoxelVolume = 7, // volume of geometric cubes, e.g. colors cubes in Minecraft
IndexGrid = 8, // grid whose values are offsets, e.g. into an external array
TensorGrid = 9, // Index grid for indexing learnable tensor features
End = 10,// total number of types in this enum (excluding StrLen since it's not a type)
VoxelBVH = 10, // grid where each voxel points to list of primitive ids
End = 11,// total number of types in this enum (excluding StrLen since it's not a type)
StrLen = End + 7};// this entry is used to determine the minimum size of c-string
@@ -310,6 +316,7 @@ __hostdev__ inline char* toStr(char *dst, GridClass gridClass)
case GridClass::VoxelVolume: return util::strcpy(dst, "VOX");
case GridClass::IndexGrid: return util::strcpy(dst, "INDEX");
case GridClass::TensorGrid: return util::strcpy(dst, "TENSOR");
case GridClass::VoxelBVH: return util::strcpy(dst, "VOXBVH");
default: return util::strcpy(dst, "END");
}
}
@@ -421,7 +428,17 @@ enum class GridBlindDataSemantic : uint32_t { Unknown = 0,
LevelSet = 10, // narrow band level set, e.g. SDF
FogVolume = 11, // fog volume, e.g. density
Staggered = 12, // staggered MAC grid, e.g. velocity
End = 13 };
PointOpacity = 13, // opacity associated with point
PointQuat = 14, // quaternion rotation, wxyz convention
PointScale = 15, // vec3 scale
PointSH0 = 16, // spherical harmonics, DC component
PointSHN = 17, // spherical haromnics, AC components
LineId = 18, // integer ID of line
TriangleId = 19, // integer ID of triangle
GaussianId = 20, // integer ID of Gaussian
Range = 21, // begin/end pair of indices
VoxelBVH = 22, // voxelbvh 64-bit voxel value
End = 23 };
/// @brief Maps from GridBlindDataSemantic to GridClass
/// @note Useful when converting an IndexGrid with blind data of type T into a Grid<T>
@@ -450,6 +467,8 @@ __hostdev__ inline GridClass toGridClass(GridBlindDataSemantic semantics,
return GridClass::FogVolume;
case GridBlindDataSemantic::Staggered:
return GridClass::Staggered;
case GridBlindDataSemantic::VoxelBVH:
return GridClass::VoxelBVH;
default:
return defaultClass;
}
@@ -472,6 +491,8 @@ __hostdev__ inline GridBlindDataSemantic toSemantic(GridClass gridClass,
return GridBlindDataSemantic::FogVolume;
case GridClass::Staggered:
return GridBlindDataSemantic::Staggered;
case GridClass::VoxelBVH:
return GridBlindDataSemantic::VoxelBVH;
default:
return defaultSemantic;
}
@@ -1503,7 +1524,7 @@ struct Map
/// e.g. inverse scale and inverse rotation WITHOUT translation.
/// @note Typically this operation is used for scale and rotation from world -> index mapping
/// @tparam Vec3T Template type of the 3D vector to be mapped
/// @param ijk 3D vector to be mapped - typically floating point index coordinates
/// @param xyz 3D vector to be mapped - typically floating point index coordinates
/// @return linear inverse 3x3 mapping of the input vector i.e. xyz x mat^-1
template<typename Vec3T>
__hostdev__ Vec3T applyInverseJacobian(const Vec3T& xyz) const { return math::matMult(mInvMatD, xyz); }
@@ -1512,7 +1533,7 @@ struct Map
/// e.g. inverse scale and inverse rotation WITHOUT translation.
/// @note Typically this operation is used for scale and rotation from world -> index mapping
/// @tparam Vec3T Template type of the 3D vector to be mapped
/// @param ijk 3D vector to be mapped - typically floating point index coordinates
/// @param xyz 3D vector to be mapped - typically floating point index coordinates
/// @return linear inverse 3x3 mapping of the input vector i.e. xyz x mat^-1
template<typename Vec3T>
__hostdev__ Vec3T applyInverseJacobianF(const Vec3T& xyz) const { return math::matMult(mInvMatF, xyz); }
@@ -1521,7 +1542,7 @@ struct Map
/// e.g. inverse scale and inverse rotation WITHOUT translation.
/// @note Typically this operation is used for scale and rotation from world -> index mapping
/// @tparam Vec3T Template type of the 3D vector to be mapped
/// @param ijk 3D vector to be mapped - typically floating point index coordinates
/// @param xyz 3D vector to be mapped - typically floating point index coordinates
/// @return linear inverse 3x3 mapping of the input vector i.e. xyz x mat^-1
template<typename Vec3T>
__hostdev__ Vec3T applyIJT(const Vec3T& xyz) const { return math::matMultT(mInvMatD, xyz); }
@@ -2262,6 +2283,7 @@ public:
__hostdev__ bool isPointIndex() const { return DataType::mGridClass == GridClass::PointIndex; }
__hostdev__ bool isGridIndex() const { return DataType::mGridClass == GridClass::IndexGrid; }
__hostdev__ bool isPointData() const { return DataType::mGridClass == GridClass::PointData; }
__hostdev__ bool isVoxelBVH() const { return DataType::mGridClass == GridClass::VoxelBVH; }
__hostdev__ bool isMask() const { return DataType::mGridClass == GridClass::Topology; }
__hostdev__ bool isUnknown() const { return DataType::mGridClass == GridClass::Unknown; }
__hostdev__ bool hasMinMax() const { return DataType::mFlags.isMaskOn(GridFlags::HasMinMax); }
@@ -4719,7 +4741,7 @@ using OnIndexGrid = Grid<OnIndexTree>;
* @endcode
*/
/// @brief Use this function, which depends a pointer to GridData, to call
/// @brief Use this function, which depends on a pointer to GridData, to call
/// other functions that depend on a NanoGrid of a known ValueType.
/// @details This function allows for generic programming by converting GridData
/// to a NanoGrid of the type encoded in GridData::mGridType.
@@ -4831,7 +4853,7 @@ public:
}
/// @brief Reset this access to its initial state, i.e. with an empty cache
/// @node Noop since this template specialization has no cache
/// @note Noop since this template specialization has no cache
__hostdev__ void clear() {}
__hostdev__ const RootT& root() const { return *mRoot; }
@@ -5163,7 +5185,11 @@ public:
#endif
if constexpr(OpT::LEVEL <= LEVEL0) {
if (this->isCached1(dirty)) return mNode1->template getAndCache<OpT>(ijk, *this, args...);
} else if constexpr(OpT::LEVEL <= LEVEL1) {
}
#ifdef NANOVDB_USE_OLD_ACCESSOR
else
#endif
if constexpr(OpT::LEVEL <= LEVEL1) {
if (this->isCached2(dirty)) return mNode2->template getAndCache<OpT>(ijk, *this, args...);
}
return mRoot->template getAndCache<OpT>(ijk, *this, args...);
@@ -5179,7 +5205,11 @@ public:
#endif
if constexpr(OpT::LEVEL <= LEVEL0) {
if (this->isCached1(dirty)) return const_cast<Node1T*>(mNode1)->template setAndCache<OpT>(ijk, *this, args...);
} else if constexpr(OpT::LEVEL <= LEVEL1) {
}
#ifdef NANOVDB_USE_OLD_ACCESSOR
else
#endif
if constexpr(OpT::LEVEL <= LEVEL1) {
if (this->isCached2(dirty)) return const_cast<Node2T*>(mNode2)->template setAndCache<OpT>(ijk, *this, args...);
}
return const_cast<RootT*>(mRoot)->template setAndCache<OpT>(ijk, *this, args...);
@@ -5337,7 +5367,7 @@ public:
}
#endif
__hostdev__ ValueType getValue(const CoordType& ijk) const {return this->template get<GetValue<BuildT>>(ijk);}
__hostdev__ ValueType getValue(const CoordType& ijk) const {return this->template get<GetValue<BuildT>>(ijk);}
__hostdev__ ValueType getValue(int i, int j, int k) const { return this->template get<GetValue<BuildT>>(CoordType(i, j, k)); }
__hostdev__ ValueType operator()(const CoordType& ijk) const { return this->template get<GetValue<BuildT>>(ijk); }
__hostdev__ ValueType operator()(int i, int j, int k) const { return this->template get<GetValue<BuildT>>(CoordType(i, j, k)); }
@@ -5356,9 +5386,17 @@ public:
#endif
if constexpr(OpT::LEVEL <=0) {
if (this->isCached<LeafT>(dirty)) return ((const LeafT*)mNode[0])->template getAndCache<OpT>(ijk, *this, args...);
} else if constexpr(OpT::LEVEL <= 1) {
}
#ifdef NANOVDB_USE_OLD_ACCESSOR
else
#endif
if constexpr(OpT::LEVEL <= 1) {
if (this->isCached<NodeT1>(dirty)) return ((const NodeT1*)mNode[1])->template getAndCache<OpT>(ijk, *this, args...);
} else if constexpr(OpT::LEVEL <= 2) {
}
#ifdef NANOVDB_USE_OLD_ACCESSOR
else
#endif
if constexpr(OpT::LEVEL <= 2) {
if (this->isCached<NodeT2>(dirty)) return ((const NodeT2*)mNode[2])->template getAndCache<OpT>(ijk, *this, args...);
}
return mRoot->template getAndCache<OpT>(ijk, *this, args...);
@@ -5374,9 +5412,17 @@ public:
#endif
if constexpr(OpT::LEVEL <= 0) {
if (this->isCached<LeafT>(dirty)) return ((LeafT*)mNode[0])->template setAndCache<OpT>(ijk, *this, args...);
} else if constexpr(OpT::LEVEL <= 1) {
}
#ifdef NANOVDB_USE_OLD_ACCESSOR
else
#endif
if constexpr(OpT::LEVEL <= 1) {
if (this->isCached<NodeT1>(dirty)) return ((NodeT1*)mNode[1])->template setAndCache<OpT>(ijk, *this, args...);
} else if constexpr(OpT::LEVEL <= 2) {
}
#ifdef NANOVDB_USE_OLD_ACCESSOR
else
#endif
if constexpr(OpT::LEVEL <= 2) {
if (this->isCached<NodeT2>(dirty)) return ((NodeT2*)mNode[2])->template setAndCache<OpT>(ijk, *this, args...);
}
return ((RootT*)mRoot)->template setAndCache<OpT>(ijk, *this, args...);
@@ -5513,6 +5559,7 @@ public:
__hostdev__ bool isPointIndex() const { return mGridData.mGridClass == GridClass::PointIndex; }
__hostdev__ bool isGridIndex() const { return mGridData.mGridClass == GridClass::IndexGrid; }
__hostdev__ bool isPointData() const { return mGridData.mGridClass == GridClass::PointData; }
__hostdev__ bool isVoxelBVH() const { return mGridData.mGridClass == GridClass::VoxelBVH; }
__hostdev__ bool isMask() const { return mGridData.mGridClass == GridClass::Topology; }
__hostdev__ bool isUnknown() const { return mGridData.mGridClass == GridClass::Unknown; }
__hostdev__ bool hasMinMax() const { return mGridData.mFlags.isMaskOn(GridFlags::HasMinMax); }
@@ -5762,6 +5809,26 @@ public:
}; // ChannelAccessor
/// @brief Generic Accessor type that maps to either a ReadAccessor or ChannelAccessor
/// @tparam BuildT Build type, e.g. float or ValueOnIndex
/// @tparam ValueT Value type, e.g. float or Vec3f
template <typename BuildT, typename ValueT>
using AccType = typename util::conditional<BuildTraits<BuildT>::is_index,
ChannelAccessor<ValueT, BuildT>, DefaultReadAccessor<BuildT>>::type;
/// @brief Generic template functions that return an Accessor to either an index grid or a regular grid
template <typename GridT, typename ValueT>
inline __hostdev__ auto getAccessor(const GridT &grid, ValueT *sideCar = nullptr)
{
using BuildT = typename GridT::BuildType;
if constexpr(BuildTraits<BuildT>::is_index) {
return sideCar ? ChannelAccessor<ValueT, BuildT>(grid, sideCar) : ChannelAccessor<ValueT, BuildT>(grid);
} else {
static_assert(util::is_same<ValueT, typename GridT::ValueType>::value, "wrong ValueT for regular GridT");
return DefaultReadAccessor<BuildT>(grid);
}
}
#if 0
// This MiniGridHandle class is only included as a stand-alone example. Note that aligned_alloc is a C++17 feature!
// Normally we recommend using GridHandle defined in util/GridHandle.h but this minimal implementation could be an
+31 -17
View File
@@ -1088,7 +1088,7 @@ PNANOVDB_FORCE_INLINE void pnanovdb_write_vec3(pnanovdb_buf_t buf, pnanovdb_addr
#define PNANOVDB_MAJOR_VERSION_NUMBER 32// reflects changes to the ABI
#define PNANOVDB_MINOR_VERSION_NUMBER 9// reflects changes to the API but not ABI
#define PNANOVDB_PATCH_VERSION_NUMBER 1// reflects bug-fixes with no ABI or API changes
#define PNANOVDB_PATCH_VERSION_NUMBER 2// reflects bug-fixes with no ABI or API changes
#define PNANOVDB_GRID_TYPE_UNKNOWN 0
#define PNANOVDB_GRID_TYPE_FLOAT 1
@@ -1130,8 +1130,9 @@ PNANOVDB_FORCE_INLINE void pnanovdb_write_vec3(pnanovdb_buf_t buf, pnanovdb_addr
#define PNANOVDB_GRID_CLASS_TOPOLOGY 6 // grid with active states only (no values)
#define PNANOVDB_GRID_CLASS_VOXEL_VOLUME 7 // volume of geometric cubes, e.g. minecraft
#define PNANOVDB_GRID_CLASS_INDEX_GRID 8 // grid whose values are offsets, e.g. into an external array
#define PNANOVDB_GRID_CLASS_TENSOR_GRID 9 // grid which can have extra metadata and features
#define PNANOVDB_GRID_CLASS_END 10
#define PNANOVDB_GRID_CLASS_TENSOR_GRID 9 // grid which can have extra metadata and features
#define PNANOVDB_GRID_CLASS_VOXEL_BVH 10 // grid where each voxel points to list of primitive ids
#define PNANOVDB_GRID_CLASS_END 11
#define PNANOVDB_GRID_FLAGS_HAS_LONG_GRID_NAME (1 << 0)
#define PNANOVDB_GRID_FLAGS_HAS_BBOX (1 << 1)
@@ -1412,23 +1413,36 @@ PNANOVDB_FORCE_INLINE pnanovdb_uint32_t pnanovdb_version_get_patch(pnanovdb_uint
}
#define PNANOVDB_GRIDBLINDMETADATA_CLASS_UNKNOWN 0
#define PNANOVDB_GRIDBLINDMETADATA_CLASS_INDEX_ARRAY 1
#define PNANOVDB_GRIDBLINDMETADATA_CLASS_ATTRIBUTE_ARRAY 2
#define PNANOVDB_GRIDBLINDMETADATA_CLASS_GRID_NAME 3
#define PNANOVDB_GRIDBLINDMETADATA_CLASS_CHANNEL_ARRAY 4
#define PNANOVDB_GRIDBLINDMETADATA_CLASS_INDEX_ARRAY 1 // indices typically used for mapping into other arrays
#define PNANOVDB_GRIDBLINDMETADATA_CLASS_ATTRIBUTE_ARRAY 2 // attributes typically associated with points
#define PNANOVDB_GRIDBLINDMETADATA_CLASS_GRID_NAME 3 // grid names of length longer than 256 characters
#define PNANOVDB_GRIDBLINDMETADATA_CLASS_CHANNEL_ARRAY 4 // channel of values typically used by index grids
#define PNANOVDB_GRIDBLINDMETADATA_CLASS_END 5
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_UNKNOWN 0
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_POSITION 1
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_COLOR 2
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_NORMAL 3
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_RADIUS 4
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_VELOCITY 5
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_ID 6
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_WORLD_COORDS 7
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_GRID_COORDS 8
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_VOXEL_COORDS 9
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_END 10
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_POSITION 1 // 3D coordinates in an unknown space
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_COLOR 2 // color associated with point
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_NORMAL 3 // normal associated with point
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_RADIUS 4 // radius of point
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_VELOCITY 5 // velocity associated with point
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_ID 6 // integer ID of point
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_WORLD_COORDS 7 // 3D coordinates in world space, e.g. (0.056, 0.8, 1,8)
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_GRID_COORDS 8 // 3D coordinates in grid space, e.g. (1.2, 4.0, 5.7), aka index-space
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_VOXEL_COORDS 9 // 3D coordinates in voxel space, e.g. (0.2, 0.0, 0.7)
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_LEVEL_SET 10 // narrow band level set, e.g. SDF
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_FOG_VOLUME 11 // fog volume, e.g. density
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_STAGGERED 12 // staggered MAC grid, e.g. velocity
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_OPACITY 13 // opacity associated with point
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_QUAT 14 // quaternion rotation, wxyz convention
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_SCALE 15 // vec3 scale
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_SH0 16 // spherical harmonics, DC component
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_POINT_SHN 17 // spherical haromnics, AC components
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_LINE_ID 18 // integer ID of line
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_TRIANGLE_ID 19 // integer ID of triangle
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_GAUSSIAN_ID 20 // integer ID of Gaussian
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_RANGE 21 // begin/end pair of indices
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_VOXEL_BVH 22 // voxelbvh 64-bit voxel value
#define PNANOVDB_GRIDBLINDMETADATA_SEMANTIC_END 23
struct pnanovdb_gridblindmetadata_t
{
+1
View File
@@ -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
+68
View File
@@ -7,10 +7,23 @@
#include <cuda_runtime_api.h>
#include <nanovdb/util/cuda/Util.h>
#include <cstddef>
#include <type_traits>
#include <utility>
namespace nanovdb {
namespace cuda {
/// @brief Default stream-ordered device memory resource. Allocations are made
/// with cudaMallocAsync and freed with cudaFreeAsync via the
/// util::cuda wrappers.
/// @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:
@@ -26,8 +39,63 @@ public:
static void deallocateAsync(void *p, size_t, size_t, cudaStream_t stream) {
cudaCheck(util::cuda::freeAsync(p, stream));
}
/// @brief Instance allocation entry point modelling the AsyncResource concept.
/// @param bytes number of bytes to allocate
/// @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 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);
}
};
/// @brief Returns a program-lifetime, address-stable reference to a default
/// instance of resource @c R.
/// @details The instance is a function-local static, so it outlives every
/// caller and is safe to bind through a default function/constructor
/// argument. @c R must be default-constructible.
template <class R>
inline R& default_resource()
{
static R sResource;
return sResource;
}
/// @brief Detection trait: @c is_async_resource<R>::value is true iff @c R
/// models the stream-ordered AsyncResource concept, i.e. exposes
/// allocate_async(size_t, size_t, cudaStream_t) and
/// deallocate_async(void*, size_t, size_t, cudaStream_t).
/// @details Use it to dispatch between a stream-ordered resource and a
/// synchronous one (which exposes allocate/deallocate without a
/// stream argument):
/// @code
/// template<typename R>
/// void* allocate(R& resource, size_t bytes, size_t alignment, cudaStream_t stream)
/// {
/// if constexpr (nanovdb::cuda::is_async_resource<R>::value)
/// return resource.allocate_async(bytes, alignment, stream); // stream-ordered
/// else
/// return resource.allocate(bytes, alignment); // synchronous
/// }
/// @endcode
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}))>>
: std::true_type {};
}
} // namespace nanovdb::cuda
+58
View File
@@ -0,0 +1,58 @@
// Copyright Contributors to the OpenVDB Project
// SPDX-License-Identifier: Apache-2.0
//
#ifndef NANOVDB_CUDA_PINNEDRESOURCE_H_HAS_BEEN_INCLUDED
#define NANOVDB_CUDA_PINNEDRESOURCE_H_HAS_BEEN_INCLUDED
#include <cuda_runtime_api.h>
#include <nanovdb/util/cuda/Util.h>
#include <cstddef>
namespace nanovdb {
namespace cuda {
/// @brief Default host-pinned memory resource. Allocations are page-locked
/// host memory (cudaMallocHost) that is both host-accessible and
/// device-accessible, so it can serve as the host side of an
/// asynchronous host<->device copy.
/// @note This resource is *synchronous*: cudaMallocHost / cudaFreeHost
/// synchronize the context, so it models the synchronous Resource
/// concept (allocate / deallocate, no stream) rather than the
/// stream-ordered AsyncResource concept. It is a seam so callers can
/// substitute a pooled / genuinely stream-ordered pinned-host resource
/// and avoid that synchronization.
class PinnedResource
{
public:
// cudaMallocHost over-aligns (to at least the page size), so the requested
// alignment is always satisfied; 256 is the nominal default advertised here.
static constexpr size_t DEFAULT_ALIGNMENT = 256;
/// @brief Synchronous allocation of page-locked host memory.
/// @param bytes number of bytes to allocate
/// @param alignment requested alignment (ignored; cudaMallocHost is page-aligned)
void* allocate(size_t bytes, size_t alignment) {
(void)alignment;
void* p = nullptr;
cudaCheck(cudaMallocHost(&p, bytes));
return p;
}
/// @brief Synchronous deallocation.
/// @param p pointer previously returned by allocate
/// @param bytes size passed to the matching allocate (unused)
/// @param alignment alignment passed to the matching allocate (unused)
void deallocate(void* p, size_t bytes, size_t alignment) {
(void)bytes;
(void)alignment;
cudaCheck(cudaFreeHost(p));
}
};
}
} // namespace nanovdb::cuda
#endif // end of NANOVDB_CUDA_PINNEDRESOURCE_H_HAS_BEEN_INCLUDED
+28 -10
View File
@@ -5,13 +5,16 @@
///
/// @details See nanovdb/tools/cuda/PointToGrid.cuh and nanovdb/tools/cuda/DistributedPointToGrid.cuh
/// for examples. Also note that this explains the somewhat unusual API with direct access to
/// protected member data.
/// private member data.
//
#ifndef NANOVDB_CUDA_TEMPPOOL_H_HAS_BEEN_INCLUDED
#define NANOVDB_CUDA_TEMPPOOL_H_HAS_BEEN_INCLUDED
#include <nanovdb/cuda/DeviceResource.h>
#include <cstddef>
#include <cuda_runtime_api.h>
namespace nanovdb {
namespace cuda {
@@ -20,13 +23,21 @@ template <class Resource>
class TempPool {
public:
/// @brief Default c-tor of an empty memory pool.
TempPool() : mData(nullptr), mSize(0), mRequestedSize(0) {}
/// @brief Default c-tor of an empty memory pool that uses the default
/// instance of @c Resource for all allocations.
TempPool() : mResource(&default_resource<Resource>()), mData(nullptr), mSize(0), mRequestedSize(0), mStream(nullptr) {}
/// @brief Destructor
/// @brief C-tor of an empty memory pool that routes all allocations through
/// the supplied @c Resource instance.
/// @param resource resource instance to allocate from; must outlive this pool.
explicit TempPool(Resource& resource) : mResource(&resource), mData(nullptr), mSize(0), mRequestedSize(0), mStream(nullptr) {}
/// @brief Destructor. Frees the managed memory on the stream of the most
/// recent reallocate(), so the stream-ordered free is ordered after
/// the work that used the memory (rather than on the null stream).
~TempPool() {
mRequestedSize = 0;
Resource::deallocateAsync(mData, mSize, Resource::DEFAULT_ALIGNMENT, nullptr);
mResource->deallocate_async(mData, mSize, Resource::DEFAULT_ALIGNMENT, mStream);
mData = nullptr;
mSize = 0;
}
@@ -41,20 +52,27 @@ public:
/// @note This requested size should always be less than or smaller than the actual size().
size_t& requestedSize() {return mRequestedSize;}
/// @brief Returns the stream that the managed memory was last (re)allocated on,
/// i.e. the stream this pool will free on at destruction.
cudaStream_t stream() const {return mStream;}
/// @brief Re-allocation of the data managed by this instance. Only has affect if the pool in empty or
/// the requested memory is larger than the existing size.
/// @param stream cuda stream used for asynchronous de-allocation and allocation.
void reallocate(cudaStream_t stream) {
if (!mData || mRequestedSize > mSize) {
Resource::deallocateAsync(mData, mSize, Resource::DEFAULT_ALIGNMENT, stream);
mData = Resource::allocateAsync(mRequestedSize, Resource::DEFAULT_ALIGNMENT, stream);
mResource->deallocate_async(mData, mSize, Resource::DEFAULT_ALIGNMENT, stream);
mData = mResource->allocate_async(mRequestedSize, Resource::DEFAULT_ALIGNMENT, stream);
mSize = mRequestedSize;
}
mStream = stream;// retained so the destructor frees on the most-recently-used stream
}
private:
void *mData;
size_t mSize;
size_t mRequestedSize;
Resource *mResource;
void *mData;
size_t mSize;
size_t mRequestedSize;
cudaStream_t mStream;
};// TempPool<Resource> class
using TempDevicePool = TempPool<DeviceResource>;
+2
View File
@@ -109,10 +109,12 @@ nanovdb_example(NAME "ex_bump_pool_buffer")
nanovdb_example(NAME "ex_collide_level_set")
nanovdb_example(NAME "ex_raytrace_fog_volume")
nanovdb_example(NAME "ex_raytrace_level_set")
nanovdb_example(NAME "ex_raytrace_iso_surface")
nanovdb_example(NAME "ex_dilate_nanovdb_cuda" OPENVDB)
nanovdb_example(NAME "ex_merge_nanovdb_cuda" OPENVDB)
nanovdb_example(NAME "ex_refine_nanovdb_cuda" OPENVDB)
nanovdb_example(NAME "ex_coarsen_nanovdb_cuda" OPENVDB)
nanovdb_example(NAME "ex_mesh_to_grid_cuda" OPENVDB)
if(CUDAToolkit_FOUND)
nanovdb_example(NAME "ex_make_mgpu_nanovdb") # requires cuRAND
@@ -0,0 +1,179 @@
// Copyright Contributors to the OpenVDB Project
// SPDX-License-Identifier: Apache-2.0
// the following files are from OpenVDB
#include <openvdb/util/CpuTimer.h>
#include <openvdb/tools/MeshToVolume.h>
// the following files are from NanoVDB
#include <nanovdb/NanoVDB.h>
#include <thrust/universal_vector.h>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
template<typename BuildT>
void mainMeshToGrid(
const nanovdb::Vec3f *devicePoints,
const int pointCount,
const nanovdb::Vec3i *deviceTriangles,
const int triangleCount,
const nanovdb::Map map,
const openvdb::FloatGrid::Ptr refGrid);
void readOBJ(const std::string& filename,
std::vector<openvdb::Vec3s>& points,
std::vector<openvdb::Vec3I>& triangles,
std::vector<openvdb::Vec4I>& quads)
{
std::ifstream file(filename);
if (!file.is_open()) {
OPENVDB_THROW(openvdb::IoError, "Failed to open OBJ file: " + filename);
}
std::string line;
int lineNumber = 0;
while (std::getline(file, line)) {
lineNumber++;
std::istringstream iss(line);
std::string type;
iss >> type;
if (type == "v") {
float x, y, z;
iss >> x >> y >> z;
points.push_back(openvdb::Vec3s(x, y, z));
} else if (type == "f") {
std::vector<int> faceIndices;
std::string vertexData;
while (iss >> vertexData) {
// Isolate the vertex index (everything before the first slash)
size_t slashPos = vertexData.find('/');
std::string indexStr = vertexData.substr(0, slashPos);
if (indexStr.empty()) continue;
int raw_idx = std::stoi(indexStr);
int actual_idx = 0;
// Handle negative indices: relative to the number of points parsed so far
if (raw_idx < 0) {
actual_idx = points.size() + raw_idx;
} else {
// Standard positive indices: OBJ is 1-based, convert to 0-based for C++
actual_idx = raw_idx - 1;
}
// Strict bounds checking to prevent segfaults
if (actual_idx < 0 || actual_idx >= points.size()) {
OPENVDB_THROW(openvdb::ValueError,
"OBJ parse error on line " + std::to_string(lineNumber) +
": Face index out of bounds (Raw: " + std::to_string(raw_idx) +
", Computed: " + std::to_string(actual_idx) + ", Total Points: " +
std::to_string(points.size()) + ")");
}
faceIndices.push_back(actual_idx);
}
// Add to the appropriate OpenVDB list
if (faceIndices.size() == 3) {
triangles.push_back(openvdb::Vec3I(faceIndices[0], faceIndices[1], faceIndices[2]));
} else if (faceIndices.size() == 4) {
quads.push_back(openvdb::Vec4I(faceIndices[0], faceIndices[1], faceIndices[2], faceIndices[3]));
} else if (faceIndices.size() > 4) {
std::cerr << "Warning on line " << lineNumber << ": Skipping face with "
<< faceIndices.size() << " vertices. Triangulate your mesh!" << std::endl;
}
}
}
}
/// @brief This example depends on OpenVDB, NanoVDB, and CUDA
int main(int argc, char *argv[])
{
using BuildT = nanovdb::ValueOnIndex;
openvdb::util::CpuTimer cpuTimer;
try {
if (argc<2) OPENVDB_THROW(openvdb::ValueError, "usage: "+std::string(argv[0])+" input.obj [output.vdb]\n");
std::string inputFile = argv[1];
std::string outputFile = "output.vdb";
if (argc > 2)
outputFile = argv[2];
float voxelSize = 0.001f;
if (argc > 3)
voxelSize = std::stof(argv[3]);
std::vector<openvdb::Vec3s> openvdb_points;
std::vector<openvdb::Vec3I> openvdb_triangles;
std::vector<openvdb::Vec4I> quads;
// Read the OBJ file
std::cout << "Reading " << inputFile << "..." << std::endl;
readOBJ(inputFile, openvdb_points, openvdb_triangles, quads);
std::cout << "Loaded " << openvdb_points.size() << " vertices, "
<< openvdb_triangles.size() << " triangles, and "
<< quads.size() << " quads." << std::endl;
// Initialize OpenVDB
openvdb::initialize();
// Setup Grid Transform (Voxel Size)
openvdb::math::Transform::Ptr transform =
openvdb::math::Transform::createLinearTransform(voxelSize);
// Convert Mesh to Unsigned Distance Field (UDF)
// halfband specifies the half-width of the narrow band in voxel units
float halfband = 3.0f;
cpuTimer.start("Converting mesh to OpenVDB unsigned distance field");
openvdb::FloatGrid::Ptr grid = openvdb::tools::meshToUnsignedDistanceField<openvdb::FloatGrid>(
*transform, openvdb_points, openvdb_triangles, quads, halfband);
cpuTimer.stop();
// Write the Grid to a VDB File
grid->setName("UnsignedDistanceField");
grid->print(std::cout, 2);
std::cout << "Writing to " << outputFile << "..." << std::endl;
openvdb::GridPtrVec grids;
grids.push_back(grid);
openvdb::io::File file(outputFile);
file.write(grids);
file.close();
// Cast the raw pointers from the std::vector data
const auto* nano_pts_data = reinterpret_cast<const nanovdb::Vec3f*>(openvdb_points.data());
const auto* nano_tris_data = reinterpret_cast<const nanovdb::Vec3i*>(openvdb_triangles.data());
// Initialize the thrust vectors using the casted pointer ranges
thrust::universal_vector<nanovdb::Vec3f> nanovdb_points(nano_pts_data, nano_pts_data + openvdb_points.size());
thrust::universal_vector<nanovdb::Vec3i> nanovdb_triangles(nano_tris_data, nano_tris_data + openvdb_triangles.size());
// Convert OpenVDB transform to nanovdb::Map
const auto openvdb_mat4 = transform->baseMap()->getAffineMap()->getMat4();
nanovdb::Map map;
map.set(openvdb_mat4, openvdb_mat4.inverse());
mainMeshToGrid<BuildT>(
nanovdb_points.data().get(),
nanovdb_points.size(),
nanovdb_triangles.data().get(),
nanovdb_triangles.size(),
map,
grid);
return 0;
}
catch (const std::exception& e) {
std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl;
}
return 0;
}
@@ -0,0 +1,123 @@
// Copyright Contributors to the OpenVDB Project
// SPDX-License-Identifier: Apache-2.0
#include <nanovdb/NanoVDB.h>
#include <nanovdb/tools/cuda/MeshToGrid.cuh>
#include <openvdb/openvdb.h>
#include <algorithm>
#include <limits>
#include <vector>
template<typename BuildT>
void mainMeshToGrid(
const nanovdb::Vec3f *devicePoints,
const int pointCount,
const nanovdb::Vec3i *deviceTriangles,
const int triangleCount,
const nanovdb::Map map,
const openvdb::FloatGrid::Ptr refGrid)
{
// Test topology-only path (getHandle)
{
nanovdb::tools::cuda::MeshToGrid<BuildT> conv(devicePoints, pointCount, deviceTriangles, triangleCount, map);
conv.setVerbose(1);
auto handle = conv.getHandle();
std::cout << "[getHandle] completed, buffer size: " << handle.bufferSize() << " bytes\n";
}
// Initialize mesh-to-grid converter
nanovdb::tools::cuda::MeshToGrid<BuildT> converter( devicePoints, pointCount, deviceTriangles, triangleCount, map );
converter.setVerbose(1);
auto [handle, sidecar] = converter.getHandleAndUDF();
// --- Comparison against OpenVDB reference ---
// Download NanoVDB grid to host for CPU-side analysis.
std::vector<char> hostBuf(handle.bufferSize());
cudaCheck(cudaMemcpy(hostBuf.data(), handle.deviceData(), handle.bufferSize(), cudaMemcpyDeviceToHost));
const auto *nanoGrid = reinterpret_cast<const nanovdb::NanoGrid<BuildT>*>(hostBuf.data());
// Download sidecar.
const uint64_t sidecarFloats = sidecar.size() / sizeof(float);
std::vector<float> hostSidecar(sidecarFloats);
cudaCheck(cudaMemcpy(hostSidecar.data(), sidecar.deviceData(),
sidecar.size(), cudaMemcpyDeviceToHost));
// Encode a voxel coord as a sortable int64_t key.
auto encodeCoord = [](int x, int y, int z) -> int64_t {
return (int64_t(x) + (1<<20))
| ((int64_t(y) + (1<<20)) << 21)
| ((int64_t(z) + (1<<20)) << 42);
};
auto ovdbAcc = refGrid->getConstAccessor();
const float voxelSize = (float)refGrid->voxelSize()[0];
uint64_t falsePositives = 0, falseNegatives = 0;
float minMissedUDF = std::numeric_limits<float>::max();
uint64_t badUDF = 0;
float maxErrVoxels = 0.f;
std::vector<int64_t> ourCoords;
ourCoords.reserve(12000000);
const uint32_t nanoLeafCount = nanoGrid->tree().nodeCount(0);
const auto *leaves = nanoGrid->tree().getFirstLeaf();
for (uint32_t li = 0; li < nanoLeafCount; ++li) {
const auto &leaf = leaves[li];
const auto origin = leaf.origin();
for (uint32_t vi = 0; vi < 512; ++vi) {
if (!leaf.isActive(vi)) continue;
const auto local = nanovdb::NanoLeaf<BuildT>::OffsetToLocalCoord(vi);
const int x = origin[0]+local[0], y = origin[1]+local[1], z = origin[2]+local[2];
ourCoords.push_back(encodeCoord(x, y, z));
const openvdb::Coord coord(x, y, z);
if (!ovdbAcc.isValueOn(coord)) {
++falsePositives;
} else {
const float err = std::abs(hostSidecar[leaf.getValue(vi)] - ovdbAcc.getValue(coord)) / voxelSize;
maxErrVoxels = std::max(maxErrVoxels, err);
if (err > 0.1f) ++badUDF;
}
}
}
std::sort(ourCoords.begin(), ourCoords.end());
for (auto it = refGrid->tree().cbeginValueOn(); it; ++it) {
const auto c = it.getCoord();
if (!std::binary_search(ourCoords.begin(), ourCoords.end(), encodeCoord(c[0], c[1], c[2]))) {
++falseNegatives;
minMissedUDF = std::min(minMissedUDF, *it);
}
}
const uint64_t ourActiveCount = ourCoords.size();
const uint64_t nTP = ourActiveCount - falsePositives;
std::cout << "\n--- Comparison against OpenVDB reference ---\n";
std::cout << "Active voxels: ours=" << ourActiveCount
<< " OpenVDB=" << refGrid->tree().activeVoxelCount() << "\n";
std::cout << "False negatives: " << falseNegatives;
if (falseNegatives > 0)
std::cout << " (smallest missed UDF: " << minMissedUDF << ")";
std::cout << "\n";
std::cout << "False positives: " << falsePositives
<< " (" << (100.0 * falsePositives / ourActiveCount) << "%)\n";
std::cout << "UDF vs OpenVDB (true positives=" << nTP << "): "
<< "max error=" << maxErrVoxels << " voxels, "
<< "exceeding 0.1 voxels: " << badUDF << "\n";
}
template
void mainMeshToGrid<nanovdb::ValueOnIndex>(
const nanovdb::Vec3f *devicePoints,
const int pointCount,
const nanovdb::Vec3i *deviceTriangles,
const int triangleCount,
const nanovdb::Map map,
const openvdb::FloatGrid::Ptr refGrid);
@@ -1,6 +1,7 @@
// Copyright Contributors to the OpenVDB Project
// SPDX-License-Identifier: Apache-2.0
//! [openvdb_to_nanovdb_accessor]
#include <openvdb/tools/LevelSetSphere.h> // replace with your own dependencies for generating the OpenVDB grid
#include <nanovdb/tools/CreateNanoGrid.h> // converter from OpenVDB to NanoVDB (includes NanoVDB.h and GridManager.h)
#include <nanovdb/io/IO.h>
@@ -38,4 +39,5 @@ int main()
std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl;
}
return 0;
}
}
//! [openvdb_to_nanovdb_accessor]
@@ -0,0 +1,187 @@
// Copyright Contributors to the OpenVDB Project
// SPDX-License-Identifier: Apache-2.0
#pragma once
#define _USE_MATH_DEFINES
#include <cmath>
#include <chrono>
#include <fstream>
#include <nanovdb/NanoVDB.h>
#include "ComputePrimitives.h"
struct RenderOp;
template<typename GridT>
__global__ void renderIsoSurfacePersistentKernel(RenderOp renderOp, float* image, const GridT* grid, int numPixels, int* nextPixel);
struct RenderOp
{
using Vec3T = nanovdb::math::Vec3<float>;
using RayT = nanovdb::math::Ray<float>;
int mWidth, mHeight;
float mDx, mIso, mWBBoxDimZ;
Vec3T mWBBoxCenter;
template<typename BufferT>
RenderOp(nanovdb::GridHandle<BufferT>& handle, int width, int height)
{
mWidth = width;
mHeight = height;
const auto *metaData = handle.gridMetaData();
mDx = float(metaData->voxelSize()[0]);
mIso = mDx;
mWBBoxDimZ = (float)metaData->worldBBox().dim()[2] * 2;
mWBBoxCenter = Vec3T(metaData->worldBBox().min() + metaData->worldBBox().dim() * 0.5f);
}
template<typename GridT>
inline float renderImage(bool useCuda, float* image, const GridT* grid)
{
using ClockT = std::chrono::high_resolution_clock;
auto t0 = ClockT::now();
computeForEach(
useCuda, mWidth * mHeight, 512, __FILE__, __LINE__, [this, image, grid] __hostdev__(int start, int end) {
(*this)(start, end, image, grid);
});
computeSync(useCuda, __FILE__, __LINE__);
auto t1 = ClockT::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count() / 1000.f;
return duration;
}
template <typename GridT>
inline __hostdev__ void operator()(int start, int end, float* image, const GridT* grid) const
{
static_assert(nanovdb::util::is_same<typename GridT::BuildType, float, nanovdb::ValueOnIndex, nanovdb::ValueIndex>::value, "only works for float and OnIndex grids");
auto acc = nanovdb::getAccessor<GridT, float>(*grid);
for (int i = start; i < end; ++i) {
this->renderPixel(i, image, grid, acc);
}
}
template <typename GridT, typename AccT>
inline __hostdev__ void renderPixel(int i, float* image, const GridT* grid, AccT& acc) const
{
float t0, v;
nanovdb::Coord ijk;
RayT iRay = this->getIndexRay(i, grid);
if (nanovdb::math::isoCrossing(iRay, acc, ijk, v, t0, mIso)) {// intersect...
this->composite(image, i, (t0 * mDx) / (mWBBoxDimZ * 2), 1.0f);
} else {
this->composite(image, i, 0.0f, 0.0f);// write background value.
}
}
template<typename GridT>
inline float renderImagePersistent(float* image, const GridT* grid, int* nextPixel) const
{
int device = 0;
NANOVDB_CUDA_CHECK_ERROR(cudaGetDevice(&device), __FILE__, __LINE__);
cudaDeviceProp properties;
NANOVDB_CUDA_CHECK_ERROR(cudaGetDeviceProperties(&properties, device), __FILE__, __LINE__);
constexpr int blockSize = 256;
// Launch a small, fixed pool of blocks that persists on the GPU and
// pulls pixel work from a global counter instead of launching one
// logical thread per pixel up front.
int blockCount = properties.multiProcessorCount * 4;
if (blockCount < 1) blockCount = 1;
// Reset the work queue before each timed render. The kernel advances
// this counter by one warp of pixels at a time.
NANOVDB_CUDA_CHECK_ERROR(cudaMemset(nextPixel, 0, sizeof(int)), __FILE__, __LINE__);
using ClockT = std::chrono::high_resolution_clock;
auto t0 = ClockT::now();
const int numPixels = mWidth * mHeight;
renderIsoSurfacePersistentKernel<GridT><<<blockCount, blockSize>>>(*this, image, grid, numPixels, nextPixel);
NANOVDB_CUDA_CHECK_ERROR(cudaGetLastError(), __FILE__, __LINE__);
NANOVDB_CUDA_CHECK_ERROR(cudaDeviceSynchronize(), __FILE__, __LINE__);
auto t1 = ClockT::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count() / 1000.f;
return duration;
}
template <typename GridT>
inline __hostdev__ RayT getIndexRay(int i, const GridT *grid) const
{
// perspective camera along Z-axis...
const uint32_t x = i % mWidth, y = i / mWidth;
const float fov = 45.f;
const float u = (float(x) + 0.5f) / mWidth;
const float v = (float(y) + 0.5f) / mHeight;
const float aspect = mWidth / float(mHeight);
const float Px = (2.f * u - 1.f) * tanf(fov / 2 * 3.14159265358979323846f / 180.f) * aspect;
const float Py = (2.f * v - 1.f) * tanf(fov / 2 * 3.14159265358979323846f / 180.f);
const Vec3T origin = mWBBoxCenter + Vec3T(0, 0, mWBBoxDimZ);
Vec3T dir(Px, Py, -1.f);
dir.normalize();
RayT wRay(origin, dir);
return wRay.worldToIndexF(*grid);// transform the ray to the grid's index-space.
}
inline __hostdev__ void composite(float* outImage, int offset, float value, float alpha) const
{
const uint32_t x = offset % mWidth, y = offset / mWidth;
// checkerboard background...
const int mask = 1 << 7;
const float bg = ((x & mask) ^ (y & mask)) ? 1.0f : 0.5f;
outImage[offset] = alpha * value + (1.0f - alpha) * bg;
}
inline void saveImage(const std::string& filename, const float* image) const
{
const auto isLittleEndian = []() -> bool {
static int x = 1;
static bool result = reinterpret_cast<uint8_t*>(&x)[0] == 1;
return result;
};
float scale = 1.0f;
if (isLittleEndian()) scale = -scale;
std::fstream fs(filename, std::ios::out | std::ios::binary);
if (!fs.is_open()) throw std::runtime_error("Unable to open file: " + filename);
fs << "Pf\n"
<< mWidth << "\n"
<< mHeight << "\n"
<< scale << "\n";
for (int i = 0; i < mWidth * mHeight; ++i) {
float r = image[i];
fs.write((char*)&r, sizeof(float));
}
}
};
template<typename GridT>
__global__ void renderIsoSurfacePersistentKernel(RenderOp renderOp, float* image, const GridT* grid, int numPixels, int* nextPixel)
{
static_assert(nanovdb::util::is_same<typename GridT::BuildType, float, nanovdb::ValueOnIndex, nanovdb::ValueIndex>::value, "only works for float and OnIndex grids");
auto acc = nanovdb::getAccessor<GridT, float>(*grid);
const unsigned int lane = threadIdx.x & 31u;
// Keep the fixed set of launched threads busy until all pixels have been assigned.
while (true) {
int base = 0;
// Each warp asks the shared counter for the next batch of 32 pixels.
// Only lane 0 updates the counter; __shfl_sync copies lane 0's result
// to the other lanes in the warp.
if (lane == 0) base = atomicAdd(nextPixel, 32);
base = __shfl_sync(0xFFFFFFFFu, base, 0);
// Each lane renders one pixel from the batch: lane 0 renders base,
// lane 1 renders base + 1, and so on.
const int i = base + int(lane);
if (i >= numPixels) break;
renderOp.renderPixel(i, image, grid, acc);
}
}
@@ -0,0 +1,52 @@
// Copyright Contributors to the OpenVDB Project
// SPDX-License-Identifier: Apache-2.0
#include <algorithm>
#include <cstring>
#include <iostream>
#include <nanovdb/io/IO.h>
#include <nanovdb/tools/CreatePrimitives.h>
#include <nanovdb/cuda/DeviceBuffer.h>
#if defined(NANOVDB_USE_CUDA)
using BufferT = nanovdb::cuda::DeviceBuffer;
#else
using BufferT = nanovdb::HostBuffer;
#endif
extern void runNanoVDB(nanovdb::GridHandle<BufferT>& handle, int numIterations, int width, int height, BufferT& imageBuffer, bool usePersistentThreads);
int main(int ac, char** av)
{
try {
bool usePersistentThreads = false;
const char* gridName = nullptr;
for (int i = 1; i < ac; ++i) {
if (std::strcmp(av[i], "--persistent") == 0) {
usePersistentThreads = true;
} else if (!gridName) {
gridName = av[i];
} else {
throw std::runtime_error("Usage: ex_raytrace_iso_surface [--persistent] [grid.nvdb]");
}
}
nanovdb::GridHandle<BufferT> handle;
if (gridName) {
handle = nanovdb::io::readGrid<BufferT>(gridName);
std::cout << "Loaded NanoVDB grid[" << handle.gridMetaData()->shortGridName() << "]...\n";
} else {
handle = nanovdb::tools::createLevelSetSphere<float, BufferT>(100.0f, nanovdb::Vec3d(-20, 0, 0), 1.0, 3.0, nanovdb::Vec3d(0), "sphere");
}
const int numIterations = 50;
const int width = 4096;
const int height = 4096;
BufferT imageBuffer(width * height * sizeof(float));
runNanoVDB(handle, numIterations, width, height, imageBuffer, usePersistentThreads);
}
catch (const std::exception& e) {
std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl;
}
return 0;
}
@@ -0,0 +1,66 @@
// Copyright Contributors to the OpenVDB Project
// SPDX-License-Identifier: Apache-2.0
#define _USE_MATH_DEFINES
#include <cmath>
#include <chrono>
#if defined(NANOVDB_USE_CUDA)
#include <nanovdb/cuda/DeviceBuffer.h>
using BufferT = nanovdb::cuda::DeviceBuffer;
#else
using BufferT = nanovdb::HostBuffer;
#endif
#include <nanovdb/GridHandle.h>
#include <nanovdb/io/IO.h>
#include <nanovdb/math/Ray.h>
#include <nanovdb/math/HDDA.h>
#include "common.h"
void runNanoVDB(nanovdb::GridHandle<BufferT>& handle, int numIterations, int width, int height, BufferT& imageBuffer, bool usePersistentThreads)
{
float *h_outImage = reinterpret_cast<float*>(imageBuffer.data());
RenderOp renderOp(handle, width, height);
auto kernel = [&](auto *h_grid){
float sum = 0;
for (int i = 0; i < numIterations; ++i, sum += renderOp.renderImage(false/*useCuda*/, h_outImage, h_grid));
std::cout << "Average of " << numIterations << " renderings (NanoVDB-Host) = " << (sum/numIterations) << " ms" << std::endl;
renderOp.saveImage("raytrace_iso_surface-nanovdb-host.pfm", (float*)imageBuffer.data());
#if defined(NANOVDB_USE_CUDA)
handle.deviceUpload();
using BuildT = typename nanovdb::util::remove_pointer_t<decltype(h_grid)>::BuildType;
auto* d_grid = handle.deviceGrid<BuildT>();
if (!d_grid) throw std::runtime_error("GridHandle does not contain a valid device grid");
imageBuffer.deviceUpload();
float* d_outImage = reinterpret_cast<float*>(imageBuffer.deviceData());
sum = 0;
if (usePersistentThreads) {
int* d_nextPixel = nullptr;
NANOVDB_CUDA_CHECK_ERROR(cudaMalloc(&d_nextPixel, sizeof(int)), __FILE__, __LINE__);
for (int i = 0; i < numIterations; ++i, sum += renderOp.renderImagePersistent(d_outImage, d_grid, d_nextPixel));
NANOVDB_CUDA_CHECK_ERROR(cudaFree(d_nextPixel), __FILE__, __LINE__);
std::cout << "Average of " << numIterations << " renderings (NanoVDB-Cuda-Persistent) = " << (sum/numIterations) << " ms " << std::endl;
imageBuffer.deviceDownload();
renderOp.saveImage("raytrace_iso_surface-nanovdb-cuda-persistent.pfm", (float*)imageBuffer.data());
} else {
for (int i = 0; i < numIterations; ++i, sum += renderOp.renderImage(true/*useCuda*/, d_outImage, d_grid));
std::cout << "Average of " << numIterations << " renderings (NanoVDB-Cuda) = " << (sum/numIterations) << " ms " << std::endl;
imageBuffer.deviceDownload();
renderOp.saveImage("raytrace_iso_surface-nanovdb-cuda.pfm", (float*)imageBuffer.data());
}
#endif
};// kernel
if (auto *h_grid = handle.grid<float>()) {
kernel(h_grid);
} else if (auto *h_grid = handle.grid<nanovdb::ValueIndex>()) {
kernel(h_grid);
} else if (auto *h_grid = handle.grid<nanovdb::ValueOnIndex>()) {
kernel(h_grid);
} else {
throw std::runtime_error("GridHandle does not contain a valid device grid");
}
}// runNanoVDB
@@ -20,15 +20,14 @@ using BufferT = nanovdb::HostBuffer;
void runNanoVDB(nanovdb::GridHandle<BufferT>& handle, int numIterations, int width, int height, BufferT& imageBuffer)
{
using GridT = nanovdb::FloatGrid;
using GridT = nanovdb::FloatGrid;
using CoordT = nanovdb::Coord;
using RealT = float;
using Vec3T = nanovdb::math::Vec3<RealT>;
using RayT = nanovdb::math::Ray<RealT>;
using RealT = float;
using Vec3T = nanovdb::math::Vec3<RealT>;
using RayT = nanovdb::math::Ray<RealT>;
auto *h_grid = handle.grid<float>();
if (!h_grid)
throw std::runtime_error("GridHandle does not contain a valid host grid");
if (!h_grid) throw std::runtime_error("GridHandle does not contain a valid host grid");
float* h_outImage = reinterpret_cast<float*>(imageBuffer.data());
@@ -58,7 +57,7 @@ void runNanoVDB(nanovdb::GridHandle<BufferT>& handle, int numIterations, int wid
float t0;
CoordT ijk;
float v;
if (nanovdb::math::ZeroCrossing(iRay, acc, ijk, v, t0)) {
if (nanovdb::math::zeroCrossing(iRay, acc, ijk, v, t0)) {
// write distance to surface. (we assume it is a uniform voxel)
float wT0 = t0 * float(grid->voxelSize()[0]);
compositeOp(image, i, width, height, wT0 / (wBBoxDimZ * 2), 1.0f);
@@ -1,5 +1,7 @@
// Copyright Contributors to the OpenVDB Project
// SPDX-License-Identifier: Apache-2.0
//! [read_nanovdb_sphere_accessor]
#include <nanovdb/io/IO.h> // this is required to read (and write) NanoVDB files on the host
/// @brief Read a NanoVDB grid from a file and print out multiple values.
@@ -24,4 +26,5 @@ int main()
std::cerr << "An exception occurred: \"" << e.what() << "\"" << std::endl;
}
return 0;
}
}
//! [read_nanovdb_sphere_accessor]
@@ -1,6 +1,7 @@
// Copyright Contributors to the OpenVDB Project
// SPDX-License-Identifier: Apache-2.0
//! [read_nanovdb_sphere_accessor_cuda]
#include <nanovdb/io/IO.h> // this is required to read (and write) NanoVDB files on the host
#include <nanovdb/cuda/DeviceBuffer.h> // required for CUDA memory management
#include <nanovdb/GridHandle.h>
@@ -38,4 +39,5 @@ int main(int, char**)
}
return 0;
}
}
//! [read_nanovdb_sphere_accessor_cuda]
@@ -1,6 +1,7 @@
// Copyright Contributors to the OpenVDB Project
// SPDX-License-Identifier: Apache-2.0
//! [read_nanovdb_sphere_accessor_cuda_kernel]
#include <nanovdb/NanoVDB.h> // this defined the core tree data structure of NanoVDB accessable on both the host and device
#include <stdio.h> // for printf
@@ -33,4 +34,5 @@ extern "C" void launch_kernels(const nanovdb::NanoGrid<float>* deviceGrid,
// Launch the host "kernel" (synchronously)
cpu_kernel(cpuGrid);
}
}
//! [read_nanovdb_sphere_accessor_cuda_kernel]
@@ -19,24 +19,6 @@
#include <tuple>
// forward compatibility for C++14 Standard Library
namespace cxx14 {
template<std::size_t...>
struct index_sequence
{
};
template<std::size_t N, std::size_t... Is>
struct make_index_sequence : make_index_sequence<N - 1, N - 1, Is...>
{
};
template<std::size_t... Is>
struct make_index_sequence<0, Is...> : index_sequence<Is...>
{
};
} // namespace cxx14
#if defined(__CUDACC__)
static inline bool checkCUDA(cudaError_t result, const char* file, const int line)
@@ -75,7 +57,7 @@ public:
}
template<std::size_t... Is>
void call(int start, int end, cxx14::index_sequence<Is...>) const
void call(int start, int end, std::index_sequence<Is...>) const
{
mFunc(start, end, std::get<Is>(mArgs)...);
}
@@ -86,7 +68,7 @@ public:
int end = i * mBlockSize + mBlockSize;
if (end > mCount)
end = mCount;
call(start, end, cxx14::make_index_sequence<sizeof...(Args)>());
call(start, end, std::make_index_sequence<sizeof...(Args)>());
}
#if defined(NANOVDB_USE_TBB)
@@ -96,7 +78,7 @@ public:
int end = r.end();
if (end > mCount)
end = mCount;
call(start, end, cxx14::make_index_sequence<sizeof...(Args)>());
call(start, end, std::make_index_sequence<sizeof...(Args)>());
}
#endif
+47 -5
View File
@@ -179,7 +179,7 @@ private:
Vec3T mDelta, mNext; // delta time and next time
}; // class HDDA
/////////////////////////////////////////// ZeroCrossing ////////////////////////////////////////////
/////////////////////////////////////////// zeroCrossing ////////////////////////////////////////////
/// @brief returns true if the ray intersects a zero-crossing at the voxel level of the grid in the accessor
/// The empty-space ray-marching is performed at all levels of the tree using an
@@ -187,8 +187,9 @@ private:
/// voxel after the intersection point, v contains the grid values at ijk, and t is set to the time of
/// the intersection along the ray.
template<typename RayT, typename AccT>
inline __hostdev__ bool ZeroCrossing(RayT& ray, AccT& acc, Coord& ijk, typename AccT::ValueType& v, float& t)
inline __hostdev__ bool zeroCrossing(RayT& ray, AccT& acc, Coord& ijk, typename AccT::ValueType& v, float& t)
{
static_assert(util::is_floating_point<typename AccT::ValueType>::value, "zeroCrossing assumed a grid with floating point values");
if (!ray.clip(acc.root().bbox()) || ray.t1() > 1e20)
return false; // clip ray to bbox
static const float Delta = 1.0001f;
@@ -210,8 +211,42 @@ inline __hostdev__ bool ZeroCrossing(RayT& ray, AccT& acc, Coord& ijk, typename
}
}
return false;
}// zeroCrossing
template<typename RayT, typename AccT>
[[deprecated("Use zeroCrossing(ray, acc, ijk, v, t)")]]
inline __hostdev__ bool ZeroCrossing(RayT& ray, AccT& acc, Coord& ijk, typename AccT::ValueType& v, float& t)
{
return zeroCrossing(ray, acc, ijk, v,t);
}
/////////////////////////////////////////// isoCrossing ////////////////////////////////////////////
template<typename RayT, typename AccT>
inline __hostdev__ bool isoCrossing(RayT& ray, AccT& acc, Coord& ijk, typename AccT::ValueType& v, float& t, const typename AccT::ValueType& iso = 0.0f)
{
static_assert(util::is_floating_point<typename AccT::ValueType>::value, "isoCrossing assumed a grid with floating point values");
if (!ray.clip(acc.root().bbox()) || ray.t1() > 1e20) return false; // clip ray to bbox
static const float Delta = 1.0001f;
ijk = RoundDown<Coord>(ray.start()); // first hit of bbox
HDDA<RayT, Coord> hdda(ray, acc.getDim(ijk, ray));
const auto v0 = acc.getValue(ijk) - iso;
while (hdda.step()) {
ijk = RoundDown<Coord>(ray(hdda.time() + Delta));
hdda.update(ray, acc.getDim(ijk, ray));
if (hdda.dim() > 1 || !acc.isActive(ijk)) continue; // either a tile value or an inactive voxel
while (hdda.step() && acc.isActive(hdda.voxel())) { // in the narrow band
v = acc.getValue(hdda.voxel()) - iso;
if (v * v0 < 0) { // zero crossing
ijk = hdda.voxel();
t = hdda.time();
return true;
}
}
}
return false;
}// isoCrossing
/////////////////////////////////////////// DDA ////////////////////////////////////////////
/// @brief A Digital Differential Analyzer. Unlike HDDA (defined above) this DDA
@@ -343,10 +378,10 @@ private:
Vec3T mDelta, mNext; // delta time and next time
}; // class DDA
/////////////////////////////////////////// ZeroCrossingNode ////////////////////////////////////////////
/////////////////////////////////////////// zeroCrossingNode ////////////////////////////////////////////
template<typename RayT, typename NodeT>
inline __hostdev__ bool ZeroCrossingNode(RayT& ray, const NodeT& node, float v0, nanovdb::math::Coord& ijk, float& v, float& t)
inline __hostdev__ bool zeroCrossingNode(RayT& ray, const NodeT& node, float v0, nanovdb::math::Coord& ijk, float& v, float& t)
{
math::BBox<Coord> bbox(node.origin(), node.origin() + Coord(node.dim() - 1));
@@ -376,6 +411,13 @@ inline __hostdev__ bool ZeroCrossingNode(RayT& ray, const NodeT& node, float v0,
}
}
return false;
}// zeroCrossingNode
template<typename RayT, typename NodeT>
[[deprecated("Use zeroCrossingNode(ray, node, v0, ijk, v, t)")]]
inline __hostdev__ bool ZeroCrossingNode(RayT& ray, const NodeT& node, float v0, nanovdb::math::Coord& ijk, float& v, float& t)
{
return zeroCrossingNode(ray, node, v0, ijk, v, t);
}
/////////////////////////////////////////// TreeMarcher ////////////////////////////////////////////
@@ -399,7 +441,7 @@ inline __hostdev__ bool firstActive(RayT& ray, AccT& acc, Coord &ijk, float& t)
ijk = RoundDown<Coord>( ray(t) );// update ijk
}
return true;
}
}// firstActive
/////////////////////////////////////////// TreeMarcher ////////////////////////////////////////////
+150
View File
@@ -0,0 +1,150 @@
// Copyright Contributors to the OpenVDB Project
// SPDX-License-Identifier: Apache-2.0
/*!
\file Proximity.h
\author Efty Sifakis
\brief Closest-point queries on geometric primitives,
suitable for use in both host and device code.
*/
#ifndef NANOVDB_MATH_PROXIMITY_H_HAS_BEEN_INCLUDED
#define NANOVDB_MATH_PROXIMITY_H_HAS_BEEN_INCLUDED
#include <nanovdb/util/Util.h> // for __hostdev__
#include <nanovdb/math/Math.h> // for nanovdb::math::Sqrt
namespace nanovdb {
namespace math {
/// @brief Returns the closest point on the line segment [v0,v1] to @a p.
///
/// The result lies on the closed segment (t clamped to [0,1]).
///
/// @tparam Vec3T Any Vec3 type supporting arithmetic and dot().
template<typename Vec3T>
__hostdev__ inline Vec3T
closestPointOnSegmentToPoint(const Vec3T &v0, const Vec3T &v1, const Vec3T &p)
{
const Vec3T seg = v1 - v0;
const Vec3T w = p - v0;
const auto c1 = seg.dot(w);
if (c1 <= 0) return v0;
const auto c2 = seg.dot(seg);
if (c2 <= c1) return v1;
return v0 + (c1 / c2) * seg;
}
/// @brief Returns the closest point on triangle [v0,v1,v2] to @a p,
/// and (via @a t0, @a t1) the barycentric coordinates of that
/// point: closest = v0 + t0*(v1-v0) + t1*(v2-v0).
///
/// Uses Voronoi-region decomposition (Ericson, "Real-Time Collision
/// Detection", Sec. 5.1.5). Degenerate triangles (zero area, collinear or
/// coincident vertices) are handled implicitly: the face-interior test
/// naturally fails and the code falls through to the nearest edge or
/// vertex result.
///
/// @tparam Vec3T Any Vec3 type supporting arithmetic and dot().
template<typename Vec3T>
__hostdev__ inline Vec3T
closestPointOnTriangleToPoint(
const Vec3T &v0,
const Vec3T &v1,
const Vec3T &v2,
const Vec3T &p,
typename Vec3T::ValueType &t0,
typename Vec3T::ValueType &t1)
{
using RealT = typename Vec3T::ValueType;
const Vec3T ab = v1 - v0;
const Vec3T ac = v2 - v0;
const Vec3T ap = p - v0;
// --- Region A (vertex v0) ---
const RealT d1 = ab.dot(ap);
const RealT d2 = ac.dot(ap);
if (d1 <= RealT(0) && d2 <= RealT(0)) {
t0 = RealT(0);
t1 = RealT(0);
return v0;
}
// --- Region B (vertex v1) ---
const Vec3T bp = p - v1;
const RealT d3 = ab.dot(bp);
const RealT d4 = ac.dot(bp);
if (d3 >= RealT(0) && d4 <= d3) {
t0 = RealT(1);
t1 = RealT(0);
return v1;
}
// --- Region AB (edge v0-v1) ---
const RealT vc = d1 * d4 - d3 * d2;
if (vc <= RealT(0) && d1 >= RealT(0) && d3 <= RealT(0)) {
t0 = d1 / (d1 - d3);
t1 = RealT(0);
return v0 + t0 * ab;
}
// --- Region C (vertex v2) ---
const Vec3T cp = p - v2;
const RealT d5 = ab.dot(cp);
const RealT d6 = ac.dot(cp);
if (d6 >= RealT(0) && d5 <= d6) {
t0 = RealT(0);
t1 = RealT(1);
return v2;
}
// --- Region AC (edge v0-v2) ---
const RealT vb = d5 * d2 - d1 * d6;
if (vb <= RealT(0) && d2 >= RealT(0) && d6 <= RealT(0)) {
t1 = d2 / (d2 - d6);
t0 = RealT(0);
return v0 + t1 * ac;
}
// --- Region BC (edge v1-v2) ---
const RealT va = d3 * d6 - d5 * d4;
if (va <= RealT(0) && (d4 - d3) >= RealT(0) && (d5 - d6) >= RealT(0)) {
t1 = (d4 - d3) / ((d4 - d3) + (d5 - d6));
t0 = RealT(1) - t1;
return v1 + t1 * (v2 - v1);
}
// --- Interior of triangle ---
const RealT denom = RealT(1) / (va + vb + vc);
t0 = vb * denom;
t1 = vc * denom;
return v0 + t0 * ab + t1 * ac;
}
/// @brief Returns the squared distance from @a p to the closest point
/// on triangle [v0,v1,v2].
template<typename Vec3T>
__hostdev__ inline typename Vec3T::ValueType
pointToTriangleDistSqr(
const Vec3T &v0,
const Vec3T &v1,
const Vec3T &v2,
const Vec3T &p)
{
typename Vec3T::ValueType t0, t1;
const Vec3T closest = closestPointOnTriangleToPoint(v0, v1, v2, p, t0, t1);
return (p - closest).lengthSqr();
}
} // namespace math
} // namespace nanovdb
#endif // NANOVDB_MATH_PROXIMITY_H_HAS_BEEN_INCLUDED
+141
View File
@@ -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
+59 -2
View File
@@ -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()
File diff suppressed because it is too large Load Diff
+239
View File
@@ -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
+20
View File
@@ -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
+543 -6
View File
@@ -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
View File
@@ -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
+58 -3
View File
@@ -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
+5
View File
@@ -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
+1
View File
@@ -22,6 +22,7 @@ void defineHostGridHandle(nb::module_& m)
new (&handle) GridHandle<BufferT>(std::move(buffer));
},
"t"_a.noconvert());
defineGridHandleUtilities<BufferT>(m);
}
} // namespace pynanovdb
+200 -36
View File
@@ -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);
+208 -1
View File
@@ -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
+6
View File
@@ -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
+133 -2
View File
@@ -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
+6
View File
@@ -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
+4 -1
View File
@@ -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
+88 -41
View File
@@ -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)
@@ -69,8 +96,15 @@ template<typename BufferT> nb::list readGrids(const std::string& fileName, int v
template<typename BufferT> void writeGrids(const std::string& fileName, nb::list handles, io::Codec codec, int verbose)
{
std::ofstream os(fileName, std::ios::out | std::ios::binary | std::ios::trunc);
if (!os.is_open()) {
throw std::ios_base::failure("Unable to open file named \"" + fileName + "\" for output");
}
for (size_t i = 0; i < handles.size(); ++i) {
nanovdb::io::writeGrid(fileName, nb::cast<const GridHandle<BufferT>&>(handles[i]), codec, verbose);
nanovdb::io::writeGrid(os, nb::cast<const GridHandle<BufferT>&>(handles[i]), codec);
}
if (verbose) {
std::cout << "Wrote " << handles.size() << " nanovdb::Grid(s) to file named \"" << fileName << "\"" << std::endl;
}
}
@@ -84,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
@@ -112,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
@@ -134,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
View File
@@ -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
+356 -23
View File
@@ -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_&);
+45 -25
View File
@@ -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
+13 -4
View File
@@ -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
}
+121
View File
@@ -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
+486
View File
@@ -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
+22 -5
View File
@@ -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*);
+48
View File
@@ -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()

Some files were not shown because too many files have changed in this diff Show More