|
| 1 | +// Copyright Contributors to the OpenVDB Project |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +/// @file connected_components_cuda_kernels.cu |
| 5 | +/// |
| 6 | +/// @brief CUDA / NanoVDB side of the connected-components example. Rasterizes a triangle mesh into a |
| 7 | +/// ValueOnIndex narrow-band grid (nanovdb::tools::cuda::MeshToGrid), discards the |
| 8 | +/// surface/barrier shell (unsigned distance within sqrt(3)/2 voxels of the surface) with |
| 9 | +/// nanovdb::tools::cuda::PruneGrid, and runs connected-components labeling with |
| 10 | +/// nanovdb::tools::cuda::ConnectedComponents. A CPU union-find oracle independently verifies |
| 11 | +/// the GPU labeling (component count + per-voxel partition). |
| 12 | + |
| 13 | +#include <nanovdb/NanoVDB.h> |
| 14 | +#include <nanovdb/GridHandle.h> |
| 15 | +#include <nanovdb/cuda/DeviceBuffer.h> |
| 16 | + |
| 17 | +#include <nanovdb/tools/cuda/MeshToGrid.cuh> // rasterize mesh -> ValueOnIndex + UDF |
| 18 | +#include <nanovdb/tools/cuda/PruneGrid.cuh> // topological prune (barrier removal) |
| 19 | +#include <nanovdb/tools/cuda/ConnectedComponents.cuh> // the connected-components labeling |
| 20 | +#include <nanovdb/util/cuda/Util.h> // operatorKernel, cudaCheck |
| 21 | +#include <nanovdb/util/cuda/DeviceGridTraits.cuh> // DeviceGridTraits |
| 22 | + |
| 23 | +#include <thrust/universal_vector.h> |
| 24 | + |
| 25 | +#include <cstdint> |
| 26 | +#include <functional> |
| 27 | +#include <iostream> |
| 28 | +#include <numeric> |
| 29 | +#include <unordered_map> |
| 30 | +#include <vector> |
| 31 | + |
| 32 | +namespace { |
| 33 | + |
| 34 | +using BuildT = nanovdb::ValueOnIndex; |
| 35 | +using GridHandleT = nanovdb::GridHandle<nanovdb::cuda::DeviceBuffer>; |
| 36 | +using Traits = nanovdb::util::cuda::DeviceGridTraits<BuildT>; |
| 37 | + |
| 38 | +constexpr int LEAF_SIZE = 512; // 8^3 |
| 39 | + |
| 40 | +// Per-leaf retain-mask functor: a voxel is kept iff its unsigned distance to the surface exceeds the |
| 41 | +// barrier threshold sqrt(3)/2 voxels (i.e. UDF^2 >= 0.75 * voxelSize^2 in world units). Removing the |
| 42 | +// barrier shell splits each closed surface's narrow band into disjoint inner/outer shells, which is |
| 43 | +// what connected components then labels. One CUDA block per leaf, one thread per voxel offset. |
| 44 | +struct UDFBarrierPruneMaskFunctor |
| 45 | +{ |
| 46 | + static constexpr int MaxThreadsPerBlock = LEAF_SIZE; |
| 47 | + static constexpr int MinBlocksPerMultiprocessor = 1; |
| 48 | + |
| 49 | + __device__ void operator()(const nanovdb::NanoGrid<BuildT>* d_grid, |
| 50 | + const float* d_udf, // UDF sidecar, WORLD units |
| 51 | + float barrierSqWorld, // (sqrt(3)/2 * voxelSize)^2 |
| 52 | + nanovdb::Mask<3>* d_dstLeafMasks) |
| 53 | + { |
| 54 | + const int leafID = blockIdx.x; |
| 55 | + const int threadID = threadIdx.x; |
| 56 | + |
| 57 | + const auto& leaf = d_grid->tree().getFirstNode<0>()[leafID]; |
| 58 | + auto& resultMask = d_dstLeafMasks[leafID]; |
| 59 | + |
| 60 | + // Clear the leaf's mask words in parallel, then set the retain bits. |
| 61 | + if (threadID < nanovdb::Mask<3>::WORD_COUNT) |
| 62 | + resultMask.words()[threadID] = 0UL; |
| 63 | + __syncthreads(); |
| 64 | + |
| 65 | + if (auto n = leaf.data()->getValue(threadID)) { // n != 0 => active voxel |
| 66 | + const float udf = d_udf[n]; |
| 67 | + if (udf * udf >= barrierSqWorld) // retain non-barrier voxels |
| 68 | + resultMask.setOnAtomic(threadID); |
| 69 | + } |
| 70 | + } |
| 71 | +}; |
| 72 | + |
| 73 | +// Pack a voxel coordinate into a sortable/ hashable int64 key (offset so negatives stay positive). |
| 74 | +inline int64_t encodeCoord(const nanovdb::Coord& c) |
| 75 | +{ |
| 76 | + return (int64_t(c[0]) + (1 << 20)) |
| 77 | + | ((int64_t(c[1]) + (1 << 20)) << 21) |
| 78 | + | ((int64_t(c[2]) + (1 << 20)) << 42); |
| 79 | +} |
| 80 | + |
| 81 | +// CPU union-find oracle: independently label the derived grid's active voxels by 6-connectivity and |
| 82 | +// verify the GPU result (a) has the same component count and (b) induces the same partition (two |
| 83 | +// voxels share a GPU label iff the oracle puts them in the same component). Returns true on PASS. |
| 84 | +bool validateAgainstOracle(const GridHandleT& derivedHandle, uint32_t leafCount, uint64_t active, |
| 85 | + const uint32_t* d_labels, uint64_t gpuCount) |
| 86 | +{ |
| 87 | + // Download the derived grid blob + the per-voxel labels to the host. |
| 88 | + std::vector<char> blob(derivedHandle.bufferSize()); |
| 89 | + cudaCheck(cudaMemcpy(blob.data(), derivedHandle.deviceData(), blob.size(), cudaMemcpyDeviceToHost)); |
| 90 | + const auto* h_grid = reinterpret_cast<const nanovdb::NanoGrid<BuildT>*>(blob.data()); |
| 91 | + |
| 92 | + if (active == 0) { |
| 93 | + std::cout << "CPU-oracle self-check: PASS (empty grid, 0 components)\n"; |
| 94 | + return gpuCount == 0; |
| 95 | + } |
| 96 | + |
| 97 | + std::vector<uint32_t> labels(active + 1); |
| 98 | + cudaCheck(cudaMemcpy(labels.data(), d_labels, (active + 1) * sizeof(uint32_t), cudaMemcpyDeviceToHost)); |
| 99 | + |
| 100 | + // Gather active voxels: dense id -> {coord, gpu label}, plus a coord->id lookup for neighbours. |
| 101 | + std::unordered_map<int64_t, uint32_t> coordToId; |
| 102 | + std::vector<nanovdb::Coord> idToCoord; |
| 103 | + std::vector<uint32_t> idToGpuLabel; |
| 104 | + coordToId.reserve(active * 2); |
| 105 | + idToCoord.reserve(active); |
| 106 | + idToGpuLabel.reserve(active); |
| 107 | + |
| 108 | + const auto* leaves = h_grid->tree().getFirstLeaf(); |
| 109 | + for (uint32_t li = 0; li < leafCount; ++li) { |
| 110 | + const auto& leaf = leaves[li]; |
| 111 | + for (uint32_t n = 0; n < uint32_t(LEAF_SIZE); ++n) { |
| 112 | + if (!leaf.isActive(n)) continue; |
| 113 | + const nanovdb::Coord c = leaf.origin() + nanovdb::NanoLeaf<BuildT>::OffsetToLocalCoord(n); |
| 114 | + const uint64_t slot = leaf.getValue(n); |
| 115 | + const uint32_t id = uint32_t(idToCoord.size()); |
| 116 | + coordToId.emplace(encodeCoord(c), id); |
| 117 | + idToCoord.push_back(c); |
| 118 | + idToGpuLabel.push_back(labels[slot]); |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + // Union-find over 6-connectivity. Visiting only +X/+Y/+Z reaches every undirected edge once. |
| 123 | + std::vector<uint32_t> parent(idToCoord.size()); |
| 124 | + std::iota(parent.begin(), parent.end(), 0u); |
| 125 | + std::function<uint32_t(uint32_t)> find = [&](uint32_t x) { |
| 126 | + while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } |
| 127 | + return x; |
| 128 | + }; |
| 129 | + auto unite = [&](uint32_t a, uint32_t b) { |
| 130 | + a = find(a); b = find(b); |
| 131 | + if (a != b) parent[a > b ? a : b] = (a < b ? a : b); |
| 132 | + }; |
| 133 | + const nanovdb::Coord dirs[3] = { {1,0,0}, {0,1,0}, {0,0,1} }; |
| 134 | + for (uint32_t id = 0; id < idToCoord.size(); ++id) |
| 135 | + for (const auto& d : dirs) { |
| 136 | + auto it = coordToId.find(encodeCoord(idToCoord[id] + d)); |
| 137 | + if (it != coordToId.end()) unite(id, it->second); |
| 138 | + } |
| 139 | + |
| 140 | + // Count oracle components and check the GPU labels induce the same partition. |
| 141 | + std::unordered_map<uint32_t, uint32_t> rootToComp; // oracle root -> dense component id |
| 142 | + for (uint32_t id = 0; id < parent.size(); ++id) { |
| 143 | + const uint32_t r = find(id); |
| 144 | + if (!rootToComp.count(r)) rootToComp.emplace(r, uint32_t(rootToComp.size())); |
| 145 | + } |
| 146 | + const uint64_t oracleCount = rootToComp.size(); |
| 147 | + |
| 148 | + std::unordered_map<uint32_t, uint32_t> gpuToOracle; // gpu label -> oracle component |
| 149 | + uint64_t partitionViolations = 0; |
| 150 | + for (uint32_t id = 0; id < parent.size(); ++id) { |
| 151 | + const uint32_t gl = idToGpuLabel[id]; |
| 152 | + const uint32_t oc = rootToComp[find(id)]; |
| 153 | + auto it = gpuToOracle.find(gl); |
| 154 | + if (it == gpuToOracle.end()) gpuToOracle.emplace(gl, oc); |
| 155 | + else if (it->second != oc) ++partitionViolations; |
| 156 | + } |
| 157 | + const uint64_t gpuDistinctLabels = gpuToOracle.size(); |
| 158 | + |
| 159 | + const bool pass = (gpuCount == oracleCount) && |
| 160 | + (gpuDistinctLabels == oracleCount) && |
| 161 | + (partitionViolations == 0); |
| 162 | + |
| 163 | + std::cout << "CPU-oracle self-check: " << (pass ? "PASS" : "FAIL") |
| 164 | + << " (gpu=" << gpuCount << ", oracle=" << oracleCount |
| 165 | + << ", distinct gpu labels=" << gpuDistinctLabels |
| 166 | + << ", partition violations=" << partitionViolations << ")\n"; |
| 167 | + return pass; |
| 168 | +} |
| 169 | + |
| 170 | +} // anonymous namespace |
| 171 | + |
| 172 | +uint64_t connectedComponentsFromMesh(const std::vector<nanovdb::Vec3f>& points, |
| 173 | + const std::vector<nanovdb::Vec3i>& triangles, |
| 174 | + const nanovdb::Map& map, |
| 175 | + float bandWidth) |
| 176 | +{ |
| 177 | + const cudaStream_t stream = 0; |
| 178 | + |
| 179 | + // ---- Step 1: rasterize the mesh -> ValueOnIndex narrow-band grid + UDF sidecar. ---- |
| 180 | + thrust::universal_vector<nanovdb::Vec3f> dPoints(points.begin(), points.end()); |
| 181 | + thrust::universal_vector<nanovdb::Vec3i> dTriangles(triangles.begin(), triangles.end()); |
| 182 | + |
| 183 | + nanovdb::tools::cuda::MeshToGrid<BuildT> converter( |
| 184 | + dPoints.data().get(), uint32_t(dPoints.size()), |
| 185 | + dTriangles.data().get(), uint32_t(dTriangles.size()), map); |
| 186 | + converter.setVerbose(1); |
| 187 | + converter.setNarrowBandWidth(bandWidth); |
| 188 | + auto [origHandle, udfSidecar] = converter.getHandleAndUDF(); |
| 189 | + const auto* d_orig = origHandle.template deviceGrid<BuildT>(); |
| 190 | + |
| 191 | + // World-space voxel size from the map (uniform scale here, but read it generically). |
| 192 | + const nanovdb::Vec3d w0 = map.applyMap(nanovdb::Vec3d(0.0, 0.0, 0.0)); |
| 193 | + const nanovdb::Vec3d wx = map.applyMap(nanovdb::Vec3d(1.0, 0.0, 0.0)); |
| 194 | + const float voxelSize = float(wx[0] - w0[0]); |
| 195 | + |
| 196 | + // ---- Step 2: discard the surface/barrier shell -> derived topology. ---- |
| 197 | + const float barrierSqWorld = 0.75f * voxelSize * voxelSize; // (sqrt(3)/2 * voxelSize)^2 |
| 198 | + const uint32_t srcLeafCount = Traits::getTreeData(d_orig).mNodeCount[0]; |
| 199 | + |
| 200 | + auto retainMask = nanovdb::cuda::DeviceBuffer::create( |
| 201 | + std::size_t(srcLeafCount) * sizeof(nanovdb::Mask<3>), nullptr, false); |
| 202 | + auto* d_retainMask = static_cast<nanovdb::Mask<3>*>(retainMask.deviceData()); |
| 203 | + |
| 204 | + nanovdb::util::cuda::operatorKernel<UDFBarrierPruneMaskFunctor> |
| 205 | + <<<srcLeafCount, UDFBarrierPruneMaskFunctor::MaxThreadsPerBlock, 0, stream>>>( |
| 206 | + d_orig, static_cast<const float*>(udfSidecar.deviceData()), barrierSqWorld, d_retainMask); |
| 207 | + cudaCheckError(); |
| 208 | + |
| 209 | + nanovdb::tools::cuda::PruneGrid<BuildT> pruner(d_orig, d_retainMask, stream); |
| 210 | + auto derivedHandle = pruner.getHandle(); |
| 211 | + const auto* d_derived = derivedHandle.template deviceGrid<BuildT>(); |
| 212 | + |
| 213 | + // ---- Step 3: connected-components labeling on the derived grid. ---- |
| 214 | + nanovdb::tools::cuda::ConnectedComponents<BuildT> cc(d_derived, stream); |
| 215 | + auto [d_labels, numComponents] = cc.getVoxelLabelsAndCount(); |
| 216 | + cudaCheck(cudaStreamSynchronize(stream)); |
| 217 | + |
| 218 | + // Diagnostics. |
| 219 | + const uint64_t derivedActive = Traits::getActiveVoxelCount(d_derived); |
| 220 | + const uint32_t derivedLeaves = Traits::getTreeData(d_derived).mNodeCount[0]; |
| 221 | + std::cout << "Derived (barrier-removed) grid: " << derivedActive << " active voxels, " |
| 222 | + << derivedLeaves << " leaves.\n"; |
| 223 | + |
| 224 | + // CPU-oracle self-check. |
| 225 | + validateAgainstOracle(derivedHandle, derivedLeaves, derivedActive, d_labels, numComponents); |
| 226 | + |
| 227 | + return numComponents; |
| 228 | +} |
0 commit comments