diff --git a/nanovdb/nanovdb/python/CMakeLists.txt b/nanovdb/nanovdb/python/CMakeLists.txt index e7b0bab3ae..aaaeb6128c 100644 --- a/nanovdb/nanovdb/python/CMakeLists.txt +++ b/nanovdb/nanovdb/python/CMakeLists.txt @@ -22,7 +22,6 @@ nanobind_add_module(nanovdb_python NB_STATIC PyGridHandle.cc PyGridStats.cc PyGridValidator.cc - PyHostBuffer.cc PyIO.cc PyMath.cc PyNanoToOpenVDB.cc @@ -31,7 +30,6 @@ nanobind_add_module(nanovdb_python NB_STATIC PyTools.cc PyTree.cc PyVoxelBlockManager.cc - cuda/PyDeviceBuffer.cc cuda/PyDeviceGridHandle.cu cuda/PyPointsToGrid.cu cuda/PySampleFromVoxels.cu diff --git a/nanovdb/nanovdb/python/NanoVDBModule.cc b/nanovdb/nanovdb/python/NanoVDBModule.cc index 38b69f2db8..5f21410a34 100644 --- a/nanovdb/nanovdb/python/NanoVDBModule.cc +++ b/nanovdb/nanovdb/python/NanoVDBModule.cc @@ -14,10 +14,8 @@ #include -#include "cuda/PyDeviceBuffer.h" #include "PyBuildGrid.h" #include "PyGridHandle.h" -#include "PyHostBuffer.h" #include "PyIO.h" #include "PyMath.h" #include "PyTools.h" @@ -1189,8 +1187,7 @@ NB_MODULE(nanovdb, m) defineAccessor(m, #Suffix "ReadAccessor"); #include "BuildTypes.def" - // Host-side NodeManagerHandle + module-scope createNodeManager. - defineNodeManagerHandle(m); + // Module-scope createNodeManager (returns the typed NodeManager). defineCreateNodeManager(m); // PointAccessor variants — PointIndex grids carry uint32 indices, @@ -1214,11 +1211,9 @@ NB_MODULE(nanovdb, m) "channel of an IndexGrid or OnIndexGrid, dispatching on the " "channel's recorded dataType. The accessor keeps the grid alive."); - defineHostBuffer(m); defineHostGridHandle(m); #ifdef NANOVDB_USE_CUDA - defineDeviceBuffer(m); defineDeviceGridHandle(m); #endif diff --git a/nanovdb/nanovdb/python/PyGridHandle.h b/nanovdb/nanovdb/python/PyGridHandle.h index fe377fb842..2370c48e9c 100644 --- a/nanovdb/nanovdb/python/PyGridHandle.h +++ b/nanovdb/nanovdb/python/PyGridHandle.h @@ -95,7 +95,7 @@ template nb::class_> defineGridHa return nb::class_>(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).") + "access individual grids via handle.grid(n), handle[i], or iteration.") .def(nb::init<>(), "Construct an empty handle. Use the nanovdb.tools.create* " "factories or nanovdb.io.readGrid(s) instead in normal use.") @@ -124,6 +124,26 @@ template nb::class_> defineGridHa "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("__len__", &nanovdb::GridHandle::gridCount, + "Number of grids stored in this handle (same as gridCount()).") + .def( + "__getitem__", + [](nb::handle py_handle, Py_ssize_t i) { + auto& handle = nb::cast&>(py_handle); + const Py_ssize_t count = static_cast(handle.gridCount()); + if (i < 0) i += count; + if (i < 0 || i >= count) + throw nb::index_error("GridHandle index out of range [-gridCount(), gridCount())."); + return pyHostGrid(py_handle, static_cast(i)); + }, + nb::arg("i"), + nb::keep_alive<0, 1>(), + "handle[i] -> the i-th grid as a typed Grid subclass (same " + "dispatch as grid(n)); negative indices count from the end. " + "Raises IndexError when out of range, which also makes handles " + "iterable via the sequence protocol (`for grid in handle`). " + "Grids whose BuildT is not bound in Python are returned as " + "None, matching grid(n).") .def("isPadded", &nanovdb::GridHandle::isPadded, "True iff this handle's buffer is aligned past the natural " "GridData alignment (used by the I/O code path).") diff --git a/nanovdb/nanovdb/python/PyHostBuffer.cc b/nanovdb/nanovdb/python/PyHostBuffer.cc deleted file mode 100644 index 7a929aacf4..0000000000 --- a/nanovdb/nanovdb/python/PyHostBuffer.cc +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: Apache-2.0 -#include "PyHostBuffer.h" - -#include - -namespace nb = nanobind; -using namespace nanovdb; - -namespace pynanovdb { - -void defineHostBuffer(nb::module_& m) -{ - nb::class_(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 diff --git a/nanovdb/nanovdb/python/PyHostBuffer.h b/nanovdb/nanovdb/python/PyHostBuffer.h deleted file mode 100644 index 29b5a917ce..0000000000 --- a/nanovdb/nanovdb/python/PyHostBuffer.h +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: Apache-2.0 -#ifndef NANOVDB_PYHOSTBUFFER_HAS_BEEN_INCLUDED -#define NANOVDB_PYHOSTBUFFER_HAS_BEEN_INCLUDED - -#include - -namespace nb = nanobind; - -namespace pynanovdb { - -void defineHostBuffer(nb::module_& m); - -} - -#endif diff --git a/nanovdb/nanovdb/python/PyIO.cc b/nanovdb/nanovdb/python/PyIO.cc index 4573b93d02..74c63cbf57 100644 --- a/nanovdb/nanovdb/python/PyIO.cc +++ b/nanovdb/nanovdb/python/PyIO.cc @@ -84,9 +84,9 @@ template void defineReadWriteGrid(nb::module_& m) "Return a FileGridMetaDataVector describing every grid stored in the .nvdb file."); } -template nb::list readGrids(const std::string& fileName, int verbose, const BufferT& buffer) +template nb::list readGrids(const std::string& fileName, int verbose) { - auto handles = nanovdb::io::readGrids(fileName, verbose, buffer); + auto handles = nanovdb::io::readGrids(fileName, verbose); nb::list handleList; for (size_t i = 0; i < handles.size(); ++i) { handleList.append(std::move(handles[i])); @@ -123,20 +123,22 @@ void defineHostReadWriteGrid(nb::module_& m) m.def("writeGrids", &writeGrids, "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(&io::template readGrid), + [](const std::string& fileName, int n, int verbose) { + return io::template readGrid(fileName, n, verbose); + }, "fileName"_a, "n"_a = 0, "verbose"_a = 0, - "buffer"_a = BufferT(), "Read the n-th grid from the .nvdb file at fileName into a fresh GridHandle."); m.def("readGrid", - nb::overload_cast(&io::template readGrid), + [](const std::string& fileName, const std::string& gridName, int verbose) { + return io::template readGrid(fileName, gridName, verbose); + }, "fileName"_a, "gridName"_a, "verbose"_a = 0, - "buffer"_a = BufferT(), "Read the grid named gridName from the .nvdb file at fileName into a fresh GridHandle."); - m.def("readGrids", &readGrids, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT(), + m.def("readGrids", &readGrids, "fileName"_a, "verbose"_a = 0, "Read every grid from the .nvdb file at fileName, returning a list of GridHandles."); } @@ -156,20 +158,22 @@ void defineDeviceReadWriteGrid(nb::module_& m) m.def("deviceWriteGrids", &writeGrids, "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(&io::template readGrid), + [](const std::string& fileName, int n, int verbose) { + return io::template readGrid(fileName, n, verbose); + }, "fileName"_a, "n"_a = 0, "verbose"_a = 0, - "buffer"_a = BufferT(), "Read the n-th grid from the .nvdb file at fileName into a fresh DeviceGridHandle."); m.def("deviceReadGrid", - nb::overload_cast(&io::template readGrid), + [](const std::string& fileName, const std::string& gridName, int verbose) { + return io::template readGrid(fileName, gridName, verbose); + }, "fileName"_a, "gridName"_a, "verbose"_a = 0, - "buffer"_a = BufferT(), "Read the grid named gridName from the .nvdb file at fileName into a fresh DeviceGridHandle."); - m.def("deviceReadGrids", &readGrids, "fileName"_a, "verbose"_a = 0, "buffer"_a = BufferT(), + m.def("deviceReadGrids", &readGrids, "fileName"_a, "verbose"_a = 0, "Read every grid from the .nvdb file at fileName into device-backed handles."); } #endif diff --git a/nanovdb/nanovdb/python/PyPrimitives.cc b/nanovdb/nanovdb/python/PyPrimitives.cc index e061435af9..692f380369 100644 --- a/nanovdb/nanovdb/python/PyPrimitives.cc +++ b/nanovdb/nanovdb/python/PyPrimitives.cc @@ -26,12 +26,11 @@ GridHandle createLevelSetSphere(GridType gridType, const Vec3d& origin, const std::string& name, tools::StatsMode sMode, - CheckMode cMode, - const BufferT& buffer) + CheckMode cMode) { switch (gridType) { - case GridType::Float: return createLevelSetSphere(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); - case GridType::Double: return createLevelSetSphere(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); + case GridType::Float: return createLevelSetSphere(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode); + case GridType::Double: return createLevelSetSphere(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode); default: throw std::runtime_error( "createLevelSetSphere: only float and double grid types are supported"); @@ -48,14 +47,13 @@ GridHandle createLevelSetTorus(GridType gridType, const Vec3d& origin, const std::string& name, tools::StatsMode sMode, - CheckMode cMode, - const BufferT& buffer) + CheckMode cMode) { switch (gridType) { case GridType::Float: - return createLevelSetTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); + return createLevelSetTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode); case GridType::Double: - return createLevelSetTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); + return createLevelSetTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode); default: throw std::runtime_error( "createLevelSetTorus: only float and double grid types are supported"); @@ -71,12 +69,11 @@ GridHandle createFogVolumeSphere(GridType gridType, const Vec3d& origin, const std::string& name, tools::StatsMode sMode, - CheckMode cMode, - const BufferT& buffer) + CheckMode cMode) { switch (gridType) { - case GridType::Float: return createFogVolumeSphere(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); - case GridType::Double: return createFogVolumeSphere(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); + case GridType::Float: return createFogVolumeSphere(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode); + case GridType::Double: return createFogVolumeSphere(radius, center, voxelSize, halfWidth, origin, name, sMode, cMode); default: throw std::runtime_error( "createFogVolumeSphere: only float and double grid types are supported"); @@ -93,14 +90,13 @@ GridHandle createFogVolumeTorus(GridType gridType, const Vec3d& origin, const std::string& name, tools::StatsMode sMode, - CheckMode cMode, - const BufferT& buffer) + CheckMode cMode) { switch (gridType) { case GridType::Float: - return createFogVolumeTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); + return createFogVolumeTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode); case GridType::Double: - return createFogVolumeTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); + return createFogVolumeTorus(majorRadius, minorRadius, center, voxelSize, halfWidth, origin, name, sMode, cMode); default: throw std::runtime_error( "createFogVolumeTorus: only float and double grid types are supported"); @@ -124,16 +120,15 @@ GridHandle createLevelSetBox(GridType gridType, const Vec3d& origin, const std::string& name, tools::StatsMode sMode, - CheckMode cMode, - const BufferT& buffer) + CheckMode cMode) { switch (gridType) { case GridType::Float: return tools::createLevelSetBox( - width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); + width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode); case GridType::Double: return tools::createLevelSetBox( - width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); + width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode); default: throw std::runtime_error( "createLevelSetBox: only float and double grid types are supported"); @@ -152,16 +147,15 @@ GridHandle createLevelSetBBox(GridType gridType, const Vec3d& origin, const std::string& name, tools::StatsMode sMode, - CheckMode cMode, - const BufferT& buffer) + CheckMode cMode) { switch (gridType) { case GridType::Float: return tools::createLevelSetBBox( - width, height, depth, thickness, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); + width, height, depth, thickness, center, voxelSize, halfWidth, origin, name, sMode, cMode); case GridType::Double: return tools::createLevelSetBBox( - width, height, depth, thickness, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); + width, height, depth, thickness, center, voxelSize, halfWidth, origin, name, sMode, cMode); default: throw std::runtime_error( "createLevelSetBBox: only float and double grid types are supported"); @@ -177,16 +171,15 @@ GridHandle createLevelSetOctahedron(GridType gridType, const Vec3d& origin, const std::string& name, tools::StatsMode sMode, - CheckMode cMode, - const BufferT& buffer) + CheckMode cMode) { switch (gridType) { case GridType::Float: return tools::createLevelSetOctahedron( - scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); + scale, center, voxelSize, halfWidth, origin, name, sMode, cMode); case GridType::Double: return tools::createLevelSetOctahedron( - scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); + scale, center, voxelSize, halfWidth, origin, name, sMode, cMode); default: throw std::runtime_error( "createLevelSetOctahedron: only float and double grid types are supported"); @@ -204,16 +197,15 @@ GridHandle createFogVolumeBox(GridType gridType, const Vec3d& origin, const std::string& name, tools::StatsMode sMode, - CheckMode cMode, - const BufferT& buffer) + CheckMode cMode) { switch (gridType) { case GridType::Float: return tools::createFogVolumeBox( - width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); + width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode); case GridType::Double: return tools::createFogVolumeBox( - width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); + width, height, depth, center, voxelSize, halfWidth, origin, name, sMode, cMode); default: throw std::runtime_error( "createFogVolumeBox: only float and double grid types are supported"); @@ -229,16 +221,15 @@ GridHandle createFogVolumeOctahedron(GridType gridType, const Vec3d& origin, const std::string& name, tools::StatsMode sMode, - CheckMode cMode, - const BufferT& buffer) + CheckMode cMode) { switch (gridType) { case GridType::Float: return tools::createFogVolumeOctahedron( - scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); + scale, center, voxelSize, halfWidth, origin, name, sMode, cMode); case GridType::Double: return tools::createFogVolumeOctahedron( - scale, center, voxelSize, halfWidth, origin, name, sMode, cMode, buffer); + scale, center, voxelSize, halfWidth, origin, name, sMode, cMode); default: throw std::runtime_error( "createFogVolumeOctahedron: only float and double grid types are supported"); @@ -259,11 +250,10 @@ GridHandle createPointSphere(int pointsPerVoxel, double voxelSize, const Vec3d& origin, const std::string& name, - CheckMode mode, - const BufferT& buffer) + CheckMode mode) { return tools::createPointSphere( - pointsPerVoxel, radius, center, voxelSize, origin, name, mode, buffer); + pointsPerVoxel, radius, center, voxelSize, origin, name, mode); } template @@ -274,11 +264,10 @@ GridHandle createPointTorus(int pointsPerVoxel, double voxelSize, const Vec3d& origin, const std::string& name, - CheckMode cMode, - const BufferT& buffer) + CheckMode cMode) { return tools::createPointTorus( - pointsPerVoxel, majorRadius, minorRadius, center, voxelSize, origin, name, cMode, buffer); + pointsPerVoxel, majorRadius, minorRadius, center, voxelSize, origin, name, cMode); } template @@ -290,11 +279,10 @@ GridHandle createPointBox(int pointsPerVoxel, double voxelSize, const Vec3d& origin, const std::string& name, - CheckMode mode, - const BufferT& buffer) + CheckMode mode) { return tools::createPointBox( - pointsPerVoxel, width, height, depth, center, voxelSize, origin, name, mode, buffer); + pointsPerVoxel, width, height, depth, center, voxelSize, origin, name, mode); } // createPointScatter takes an existing level set as its source. We bind @@ -306,11 +294,10 @@ template GridHandle createPointScatter(const NanoGrid& srcGrid, int pointsPerVoxel, const std::string& name, - CheckMode mode, - const BufferT& buffer) + CheckMode mode) { return tools::createPointScatter( - srcGrid, pointsPerVoxel, name, mode, buffer); + srcGrid, pointsPerVoxel, name, mode); } } // namespace @@ -318,7 +305,7 @@ GridHandle createPointScatter(const NanoGrid& srcGrid, template void definePrimitives(nb::module_& m) { m.def("createLevelSetSphere", - nb::overload_cast( + nb::overload_cast( &createLevelSetSphere), "gridType"_a = GridType::Float, "radius"_a = 100.0, @@ -329,7 +316,6 @@ template void definePrimitives(nb::module_& m) "name"_a = "sphere_ls", "sMode"_a = tools::StatsMode::Default, "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), "Narrow-band level set of a sphere of the given radius and center."); m.def("createLevelSetTorus", @@ -342,8 +328,7 @@ template void definePrimitives(nb::module_& m) const Vec3d&, const std::string&, tools::StatsMode, - CheckMode, - const BufferT&>(&createLevelSetTorus), + CheckMode>(&createLevelSetTorus), "gridType"_a = GridType::Float, "majorRadius"_a = 100.0, "minorRadius"_a = 50.0, @@ -354,11 +339,10 @@ template void definePrimitives(nb::module_& m) "name"_a = "torus_ls", "sMode"_a = tools::StatsMode::Default, "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), "Narrow-band level set of a torus with the given major and minor radii."); m.def("createFogVolumeSphere", - nb::overload_cast( + nb::overload_cast( &createFogVolumeSphere), "gridType"_a = GridType::Float, "radius"_a = 100.0, @@ -369,7 +353,6 @@ template void definePrimitives(nb::module_& m) "name"_a = "sphere_fog", "sMode"_a = tools::StatsMode::Default, "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), "Sparse fog volume of a sphere of the given radius and center."); m.def("createFogVolumeTorus", @@ -382,8 +365,7 @@ template void definePrimitives(nb::module_& m) const Vec3d&, const std::string&, tools::StatsMode, - CheckMode, - const BufferT&>(&createFogVolumeTorus), + CheckMode>(&createFogVolumeTorus), "gridType"_a = GridType::Float, "majorRadius"_a = 100.0, "minorRadius"_a = 50.0, @@ -394,7 +376,6 @@ template void definePrimitives(nb::module_& m) "name"_a = "torus_fog", "sMode"_a = tools::StatsMode::Default, "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), "Sparse fog volume of a torus with the given major and minor radii."); // ---------- Level-set / fog-volume box / bbox / octahedron primitives ---- @@ -410,7 +391,6 @@ template void definePrimitives(nb::module_& m) "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, @@ -426,7 +406,6 @@ template void definePrimitives(nb::module_& m) "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)."); @@ -443,7 +422,6 @@ template void definePrimitives(nb::module_& m) "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, @@ -458,7 +436,6 @@ template void definePrimitives(nb::module_& m) "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)."); @@ -472,7 +449,6 @@ template void definePrimitives(nb::module_& m) "name"_a = "octahedron_fog", "sMode"_a = tools::StatsMode::Default, "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), "Sparse fog volume of an octahedron."); // ---------- Point primitives ---------- @@ -484,7 +460,6 @@ template void definePrimitives(nb::module_& m) "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."); @@ -498,7 +473,6 @@ template void definePrimitives(nb::module_& m) "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."); @@ -512,7 +486,6 @@ template void definePrimitives(nb::module_& m) "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."); @@ -521,7 +494,6 @@ template void definePrimitives(nb::module_& m) "pointsPerVoxel"_a = 1, "name"_a = "point_scatter", "cMode"_a = CheckMode::Default, - "buffer"_a = BufferT(), "Scatter a PointDataGrid into the active voxels of a " "NanoGrid level set. The source grid must satisfy " "srcGrid.isLevelSet() and have an active bounding box; " diff --git a/nanovdb/nanovdb/python/PyTree.cc b/nanovdb/nanovdb/python/PyTree.cc index 64eb7533b7..6c3d00924d 100644 --- a/nanovdb/nanovdb/python/PyTree.cc +++ b/nanovdb/nanovdb/python/PyTree.cc @@ -10,75 +10,38 @@ 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() ptr that would be reinterpreted. -template -static nb::object pyNodeMgr(nb::handle py_self) -{ - using HandleT = NodeManagerHandle; - auto& handle = nb::cast(py_self); - if (!handle.data()) return nb::none(); - // We need to read the stored gridType, but it's private. The public - // mgr() 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()) { \ - 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()) { \ - 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()) { \ - 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()) { \ - return nb::cast(m, nb::rv_policy::reference, py_self); \ - } -#include "BuildTypes.def" - return nb::none(); -} - -void defineNodeManagerHandle(nb::module_& m) -{ - using HandleT = NodeManagerHandle; - nb::class_(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, - 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). -// 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. +// based on the runtime type of `grid` (any nb::class_-bound NanoGrid) +// and returns the typed NodeManager directly. 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. +// +// Lifetime: the C++ NodeManagerHandle that owns the node-index buffer is +// moved to the heap and owned by an nb::capsule; reference_internal +// parents the returned NodeManager to that capsule, so the buffer lives +// exactly as long as the manager (and, transitively, as long as any +// leaf(i)/lower(i)/upper(i) node view, which are reference_internal to +// the manager). The def-level keep_alive<0,1> on createNodeManager below +// additionally ties the manager to the source grid, whose memory the +// nodes point into. template static nb::object tryCreateNodeManager(nb::handle py_grid) { using GridT = NanoGrid; + using HandleT = NodeManagerHandle; if (!nb::isinstance(py_grid)) { return nb::object(); // sentinel: "not this BuildT, try next" } auto& grid = nb::cast(py_grid); - return nb::cast(createNodeManager(grid)); + auto* handle = new HandleT(createNodeManager(grid)); + nb::capsule owner(handle, [](void* p) noexcept { + delete static_cast(p); + }); + // Non-null by construction: the handle was just built for this BuildT. + NodeManager* mgr = handle->template mgr(); + return nb::cast(mgr, nb::rv_policy::reference_internal, owner); } void defineCreateNodeManager(nb::module_& m) @@ -108,14 +71,14 @@ void defineCreateNodeManager(nb::module_& m) "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. + // The constructed NodeManager stores raw pointers back into the + // grid; the returned manager 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."); + "Build and return the typed NodeManager (e.g. FloatNodeManager) " + "for the given grid. The manager owns its node-index buffer " + "internally and keeps the source grid (and transitively its " + "GridHandle) alive for as long as it lives."); } } // namespace pynanovdb diff --git a/nanovdb/nanovdb/python/PyTree.h b/nanovdb/nanovdb/python/PyTree.h index f6029317e2..4cf433e351 100644 --- a/nanovdb/nanovdb/python/PyTree.h +++ b/nanovdb/nanovdb/python/PyTree.h @@ -340,12 +340,12 @@ template void defineNanoTree(nb::module_& m, const char* name) // -------------------- NodeManager -------------------- // -// 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. +// NodeManager is heap-managed by a C++ NodeManagerHandle (move-only, owns +// the underlying memory), which is deliberately NOT bound in Python. We +// bind one NodeManager class per BuildT; users get the typed NodeManager +// directly from nanovdb.createNodeManager(grid) — its lifetime is anchored +// to an internal capsule owning the node-index buffer and, via keep_alive, +// to the source grid. template void defineNodeManager(nb::module_& m, const char* name) { using NMT = nanovdb::NodeManager; @@ -411,7 +411,6 @@ template void defineNodeManager(nb::module_& m, const char* nam "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 -------------------- diff --git a/nanovdb/nanovdb/python/cuda/PyDeviceBuffer.cc b/nanovdb/nanovdb/python/cuda/PyDeviceBuffer.cc deleted file mode 100644 index e024d63939..0000000000 --- a/nanovdb/nanovdb/python/cuda/PyDeviceBuffer.cc +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: Apache-2.0 -#ifdef NANOVDB_USE_CUDA - -#include "PyDeviceBuffer.h" - -#include - -namespace nb = nanobind; -using namespace nanovdb; - -namespace pynanovdb { - -void defineDeviceBuffer(nb::module_& m) -{ - nb::class_(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 - -#endif diff --git a/nanovdb/nanovdb/python/cuda/PyDeviceBuffer.h b/nanovdb/nanovdb/python/cuda/PyDeviceBuffer.h deleted file mode 100644 index 87a081f638..0000000000 --- a/nanovdb/nanovdb/python/cuda/PyDeviceBuffer.h +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright Contributors to the OpenVDB Project -// SPDX-License-Identifier: Apache-2.0 -#ifndef NANOVDB_CUDA_PYDEVICEBUFFER_HAS_BEEN_INCLUDED -#define NANOVDB_CUDA_PYDEVICEBUFFER_HAS_BEEN_INCLUDED - -#include - -namespace nb = nanobind; - -namespace pynanovdb { - -#ifdef NANOVDB_USE_CUDA -void defineDeviceBuffer(nb::module_& m); -#endif - -} // namespace pynanovdb - -#endif diff --git a/nanovdb/nanovdb/python/examples/README.md b/nanovdb/nanovdb/python/examples/README.md index 6cf82117ec..31a066b505 100644 --- a/nanovdb/nanovdb/python/examples/README.md +++ b/nanovdb/nanovdb/python/examples/README.md @@ -34,7 +34,7 @@ PYTHONPATH=. python /path/to/.py | [`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`. | +| [`node_manager.py`](node_manager.py) | Linearized node iteration with `createNodeManager` (returns the typed NodeManager directly): 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 diff --git a/nanovdb/nanovdb/python/examples/load_inspect.py b/nanovdb/nanovdb/python/examples/load_inspect.py index 632f78ec33..38c3582543 100644 --- a/nanovdb/nanovdb/python/examples/load_inspect.py +++ b/nanovdb/nanovdb/python/examples/load_inspect.py @@ -16,17 +16,19 @@ def describe_handle(handle): - print(f"Handle contains {handle.gridCount()} grid(s).") - for i in range(handle.gridCount()): + # Handles are sequences: len(handle) == handle.gridCount(), and + # handle[i] / iteration return the same typed grids as handle.grid(i). + print(f"Handle contains {len(handle)} grid(s).") + for i in range(len(handle)): # GridType / gridSize are cheap to query on the handle itself. gtype = handle.gridType(i) gsize = handle.gridSize(i) print(f" [{i}] type={gtype}, size={gsize} bytes") - # handle.grid(i) returns the matching Grid subclass + # handle[i] returns the matching Grid subclass # at runtime — no isinstance dispatch needed at the call site. # The grid name lives on the grid itself, not the handle. - grid = handle.grid(i) + grid = handle[i] print(f" name={grid.gridName()!r}, " f"gridClass={grid.gridClass()}") print(f" activeVoxelCount={grid.activeVoxelCount()}") @@ -53,8 +55,7 @@ def main(): print() print("Polymorphic dispatch from a runtime GridType:") - for i in range(handle.gridCount()): - grid = handle.grid(i) + for i, grid in enumerate(handle): # Each typed grid carries a getAccessor() that returns the # appropriate ReadAccessor — float for FloatGrid, # double for DoubleGrid, etc. diff --git a/nanovdb/nanovdb/python/examples/node_manager.py b/nanovdb/nanovdb/python/examples/node_manager.py index 3ca3feffbe..15953cdcaf 100644 --- a/nanovdb/nanovdb/python/examples/node_manager.py +++ b/nanovdb/nanovdb/python/examples/node_manager.py @@ -19,8 +19,9 @@ def main(): grid = handle.grid() tree = grid.tree() - nmh = nanovdb.createNodeManager(grid) - nm = nmh.mgr() + # createNodeManager returns the typed NodeManager directly; it owns + # its node-index buffer internally and keeps `grid` alive. + nm = nanovdb.createNodeManager(grid) print(f"NodeManager over {grid.gridName()!r} (linear={nm.isLinear()}):") print(f" leaves={nm.leafCount()}, lower={nm.lowerCount()}, " f"upper={nm.upperCount()}") diff --git a/nanovdb/nanovdb/python/test/TestNanoVDB.py b/nanovdb/nanovdb/python/test/TestNanoVDB.py index f8dbaf92db..939737c4b4 100644 --- a/nanovdb/nanovdb/python/test/TestNanoVDB.py +++ b/nanovdb/nanovdb/python/test/TestNanoVDB.py @@ -493,6 +493,66 @@ def test_copy_is_deep(self): self.assertEqual(cp.grid().gridName(), "orig") +class TestGridHandleSequenceProtocol(unittest.TestCase): + """GridHandle supports len(), indexing (incl. negative), and iteration. + handle[i] uses the same polymorphic dispatch as handle.grid(i) but + raises IndexError out of range, where grid(i) returns None — that + contract (pinned by TestPolymorphicGridAccess) is unchanged.""" + + @classmethod + def setUpClass(cls): + h1 = nanovdb.tools.createFogVolumeSphere(name="a") + h2 = nanovdb.tools.createLevelSetTorus(nanovdb.GridType.Double, name="b") + cls.merged = nanovdb.mergeGrids([h1, h2]) + + def test_len_matches_grid_count(self): + self.assertEqual(len(self.merged), self.merged.gridCount()) + self.assertEqual(len(self.merged), 2) + self.assertEqual(len(nanovdb.GridHandle()), 0) + + def test_getitem_typed_dispatch(self): + self.assertIsInstance(self.merged[0], nanovdb.FloatGrid) + self.assertIsInstance(self.merged[1], nanovdb.DoubleGrid) + self.assertEqual(self.merged[0].gridName(), "a") + self.assertEqual(self.merged[1].gridName(), "b") + + def test_negative_indices(self): + self.assertIsInstance(self.merged[-1], nanovdb.DoubleGrid) + self.assertEqual(self.merged[-2].gridName(), "a") + + def test_out_of_range_raises_index_error(self): + for i in (2, 99, -3): + with self.assertRaises(IndexError): + self.merged[i] + with self.assertRaises(IndexError): + nanovdb.GridHandle()[0] + + def test_iteration(self): + grids = list(self.merged) + self.assertEqual(len(grids), 2) + self.assertIsInstance(grids[0], nanovdb.FloatGrid) + self.assertIsInstance(grids[1], nanovdb.DoubleGrid) + self.assertEqual([g.gridName() for g in self.merged], ["a", "b"]) + + def test_iteration_keeps_temporary_handle_alive(self): + # Grids materialized by handle[i] carry a keep_alive on the handle, + # so views harvested from a temporary handle must survive its gc. + import gc + + grids = list( + nanovdb.mergeGrids( + [ + nanovdb.tools.createFogVolumeSphere(name="tmp"), + nanovdb.tools.createLevelSetTorus(nanovdb.GridType.Float), + ] + ) + ) + for _ in range(3): + gc.collect() + self.assertEqual(grids[0].gridName(), "tmp") + self.assertGreater(grids[1].activeVoxelCount(), 0) + + class TestBuildTRegistrations(unittest.TestCase): """Every BuildT we bind exposes the right shape — a Grid class, a ReadAccessor, and (for arithmetic-valued scalars) a NodeInfo. Accessor @@ -658,10 +718,7 @@ def test_node_manager_round_trip(self): import numpy as np except ImportError: self.skipTest("numpy not installed") - handle = nanovdb.createNodeManager(self.g) - self.assertGreater(handle.size(), 0) - self.assertTrue(bool(handle)) - nm = handle.mgr() + nm = nanovdb.createNodeManager(self.g) self.assertIsInstance(nm, nanovdb.FloatNodeManager) self.assertTrue(nm.isLinear()) # createNanoGrid produces breadth-first self.assertEqual(nm.leafCount(), self.tree.nodeCount(0)) @@ -688,8 +745,7 @@ def setUpClass(cls): cls.g = cls.h.grid() cls.tree = cls.g.tree() cls.leaf = cls.tree.getFirstLeaf() - cls.nm_handle = nanovdb.createNodeManager(cls.g) - cls.nm = cls.nm_handle.mgr() + cls.nm = nanovdb.createNodeManager(cls.g) def test_leaf_offset_bounds(self): n = nanovdb.FloatLeaf.voxelCount() @@ -782,7 +838,7 @@ def test_node_manager_temporary_grid(self): except ImportError: self.skipTest("numpy not installed") nm = nanovdb.createNodeManager( - nanovdb.tools.createFogVolumeSphere().grid()).mgr() + nanovdb.tools.createFogVolumeSphere().grid()) self._force_gc() self.assertGreater(nm.leafCount(), 0) leaf0_vals = nm.leaf(0).values() @@ -1119,6 +1175,41 @@ def test_read_write_grids(self): print("ZIP compression codec not supported. Skipping...") +@unittest.skipIf( + not nanovdb.isCudaAvailable(), "nanovdb module was compiled without CUDA support" +) +@unittest.skipIf( + not nanovdb.isGpuAvailable(), "No CUDA-capable GPU available" +) +class TestDeviceGridHandleSequenceProtocol(unittest.TestCase): + """DeviceGridHandle shares the sequence protocol with GridHandle: + len(), handle[i] (negative indices, IndexError out of range), and + iteration. handle[i] returns the host-side typed grid view, same as + handle.grid(i); deviceGrid(i) is unaffected.""" + + def setUp(self): + self.handle = nanovdb.tools.cuda.createLevelSetSphere(nanovdb.GridType.Float) + + def test_len(self): + self.assertEqual(len(self.handle), 1) + self.assertEqual(len(self.handle), self.handle.gridCount()) + + def test_getitem(self): + self.assertIsInstance(self.handle[0], nanovdb.FloatGrid) + self.assertIsInstance(self.handle[-1], nanovdb.FloatGrid) + + def test_out_of_range_raises_index_error(self): + with self.assertRaises(IndexError): + self.handle[1] + with self.assertRaises(IndexError): + self.handle[-2] + + def test_iteration(self): + grids = list(self.handle) + self.assertEqual(len(grids), 1) + self.assertIsInstance(grids[0], nanovdb.FloatGrid) + + @unittest.skipIf( not nanovdb.isCudaAvailable(), "nanovdb module was compiled without CUDA support" ) diff --git a/pendingchanges/nanovdbpythonhandles.txt b/pendingchanges/nanovdbpythonhandles.txt new file mode 100644 index 0000000000..2313560b0e --- /dev/null +++ b/pendingchanges/nanovdbpythonhandles.txt @@ -0,0 +1,4 @@ +NanoVDB: + + Improvements: + - The NanoVDB Python bindings shed their memory-management ceremony: GridHandle and DeviceGridHandle now support the sequence protocol (len(handle), handle[i] with negative indices and IndexError out of range, and iteration — same typed-grid dispatch as handle.grid(n)); nanovdb.createNodeManager(grid) returns the typed NodeManager directly (the NodeManagerHandle class and its mgr() hop are gone; the manager keeps its node-index buffer and the source grid alive internally); and the vestigial buffer= keyword was removed from nanovdb.io.readGrid(s)/deviceReadGrid(s) and the 13 tools.create* primitive factories along with the opaque zero-method HostBuffer and DeviceBuffer Python classes, aligning the Python vocabulary with the planned C++ HostBuffer/DeviceBuffer retirement (issue #2232). Factories still return GridHandles, keeping host/device and single/multi-grid usage uniform.