Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-11-13 11:00:02

0001 /// @file IndexedBVH.h
0002 /// @brief Compact host-built, host/device-queryable BVH with explicit child indices.
0003 ///
0004 /// Started June 2026 by Sandro Wenzel (sandro.wenzel@cern.ch) and Tristan Wenzel
0005 /// (tristan.wenzel@cern.ch).
0006 
0007 #ifndef VECGEOM_BASE_INDEXEDBVH_H_
0008 #define VECGEOM_BASE_INDEXEDBVH_H_
0009 
0010 #include "VecGeom/base/AABB.h"
0011 #include "VecGeom/base/Assert.h"
0012 #include "VecGeom/base/Config.h"
0013 #include "VecGeom/base/Cuda.h"
0014 
0015 #include <algorithm>
0016 #include <cstddef>
0017 #include <cmath>
0018 #include <cstring>
0019 #include <limits>
0020 #include <new>
0021 #include <numeric>
0022 #include <type_traits>
0023 #include <utility>
0024 #include <vector>
0025 
0026 namespace vecgeom {
0027 namespace cuda {
0028 template <typename Real_t>
0029 class IndexedBVH;
0030 template <typename Real_t>
0031 struct IndexedBVHNode;
0032 } // namespace cuda
0033 VECGEOM_DEVICE_DECLARE_CONV_TEMPLATE(class, IndexedBVH, typename);
0034 VECGEOM_DEVICE_DECLARE_CONV_TEMPLATE(struct, IndexedBVHNode, typename);
0035 inline namespace VECGEOM_IMPL_NAMESPACE {
0036 
0037 /**
0038  * @brief One node in the flat explicit-index hierarchy.
0039  *
0040  * This type remains top-level so host code can name
0041  * `cuda::IndexedBVHNode<Real_t>` while `cuda::IndexedBVH` is forward-declared.
0042  *
0043  * A leaf has `count > 0`; `first` indexes the primitive permutation. An
0044  * internal node has `count == 0`; its children are `first` and `first + 1`.
0045  */
0046 template <typename Real_t>
0047 struct IndexedBVHNode {
0048   Real_t bounds[6]; ///< `[min_x,max_x,min_y,max_y,min_z,max_z]`, optimized for slab endpoint loads.
0049   int first{0};     ///< Primitive-permutation offset for a leaf, left-child node index otherwise.
0050   int count{0};     ///< Number of leaf primitives; zero identifies an internal node.
0051 
0052   /** Return whether this node is a leaf. */
0053   VECCORE_ATT_HOST_DEVICE
0054   bool IsLeaf() const { return count > 0; }
0055 
0056   /** Reconstruct this node's AABB from its interleaved bounds. */
0057   VECCORE_ATT_HOST_DEVICE
0058   AABB<Real_t> Bounds() const
0059   {
0060     return AABB<Real_t>(Vector3D<Real_t>(bounds[0], bounds[2], bounds[4]),
0061                         Vector3D<Real_t>(bounds[1], bounds[3], bounds[5]));
0062   }
0063 
0064   /** Store @p box with a one-ULP outward bias in the hierarchy precision. */
0065   VECCORE_ATT_HOST
0066   void SetBounds(const AABB<Real_t> &b)
0067   {
0068     const auto lo  = b.Min();
0069     const auto hi  = b.Max();
0070     const auto inf = std::numeric_limits<Real_t>::infinity();
0071     // One stored ULP covers query-point narrowing without adding work in squared-Safety traversal.
0072     bounds[0] = std::nextafter(lo[0], -inf);
0073     bounds[1] = std::nextafter(hi[0], inf);
0074     bounds[2] = std::nextafter(lo[1], -inf);
0075     bounds[3] = std::nextafter(hi[1], inf);
0076     bounds[4] = std::nextafter(lo[2], -inf);
0077     bounds[5] = std::nextafter(hi[2], inf);
0078   }
0079 };
0080 
0081 /**
0082  * @brief Compact explicit-index BVH for primitive or mapped-item acceleration.
0083  *
0084  * @details A binary tree is stored in one compact node array. For example:
0085  *
0086  * @verbatim
0087  *                         node 0: inner, first=1
0088  *                         /                    \
0089  *        node 1: leaf, first=0,count=2    node 2: inner, first=3
0090  *                                         /                    \
0091  *                        node 3: leaf, first=2,count=1    node 4: leaf, first=3,count=2
0092  *
0093  * node index:       0       1       2       3       4
0094  * fNodes:         inner    leaf    inner    leaf    leaf
0095  * first:            1       0       3       2       3
0096  * count:            0       2       0       1       2
0097  *
0098  * fPrimId:         [4, 1, 0, 3, 2]
0099  *                    ^-- node 1 uses slots [0,2): box ids 4 and 1
0100  *
0101  * box id 4 -> fAABBs[4] -> optional fBoxToDaughter[4]
0102  * @endverbatim
0103  *
0104  * An inner node's children are `first` and `first + 1`. A leaf owns `count`
0105  * consecutive slots in `fPrimId`, starting at `first`. Each slot gives the box
0106  * id used to access `fAABBs`. `fBoxToDaughter` is optional and allows several
0107  * boxes to map to one item.
0108  * Explicit child indices represent unbalanced trees without empty implicit-heap
0109  * slots; allocating siblings together preserves contiguous child loads.
0110  *
0111  * The host builder tests sweep-SAH splits on all axes. It keeps a leaf when the
0112  * split cost is not lower, unless the leaf cap forces a longest-axis median
0113  * split. Query owners traverse the explicit node indices with stacks bounded by
0114  * `BVH_MAX_DEPTH` and use the inlined node/primitive AABB predicates below.
0115  *
0116  * CPU and CUDA use the same arrays. The CPU slab test uses fixed endpoint loads
0117  * and min/max; CUDA selects endpoints with direction-octant bits.
0118  *
0119  * @tparam Real_t Scalar type stored in nodes and AABBs. Bounds are rounded
0120  *                    outward when converted to this type.
0121  */
0122 template <typename Real_t>
0123 class IndexedBVH {
0124 public:
0125   /** @name Runtime hierarchy access
0126    *  These host/device declarations expose the compact arrays, traversal limits, and optional box-to-item mapping.
0127    *  @{ */
0128 
0129   /** Maximum construction depth; also bounds every fixed traversal stack. */
0130   static constexpr int BVH_MAX_DEPTH = 32;
0131   /// A depth-N binary DFS may hold N deferred siblings plus the next node.
0132   static constexpr int kTraversalStackSize = BVH_MAX_DEPTH + 1;
0133 
0134   /// Node representation stored in the flat hierarchy array.
0135   using Node = IndexedBVHNode<Real_t>;
0136 
0137   IndexedBVH() = default;
0138 
0139   /// Opaque owner id supplied at construction.
0140   VECCORE_ATT_HOST_DEVICE int GetRootId() const { return fRootId; }
0141   /// Number of primitive boxes in the primary hierarchy, including split boxes.
0142   VECCORE_ATT_HOST_DEVICE int GetRootNChild() const { return fNPrim; }
0143   /// Number of mapped items, or zero for an identity-mapped hierarchy.
0144   VECCORE_ATT_HOST_DEVICE int GetRootNDaughter() const { return fNDaughter; }
0145   /// Number of live nodes in the primary hierarchy.
0146   VECCORE_ATT_HOST_DEVICE int GetNNodes() const { return fNNodes; }
0147   /// Primary flat node array.
0148   VECCORE_ATT_HOST_DEVICE const Node *GetNodes() const { return fNodes; }
0149   /// Primary leaf-slot to primitive-box permutation.
0150   VECCORE_ATT_HOST_DEVICE const int *GetPrimId() const { return fPrimId; }
0151   /// Primary primitive AABBs indexed by primitive-box id.
0152   VECCORE_ATT_HOST_DEVICE const AABB<Real_t> *GetAABBs() const { return fAABBs; }
0153   /// Optional primitive-box to mapped-item table.
0154   VECCORE_ATT_HOST_DEVICE const int *GetBoxToDaughter() const { return fBoxToDaughter; }
0155   /// Whether primitive boxes are mapped to a smaller item set.
0156   VECCORE_ATT_HOST_DEVICE bool HasDaughterMap() const { return fBoxToDaughter != nullptr; }
0157   /// Resolve a primitive-box id to its mapped item id, or return it unchanged.
0158   VECCORE_ATT_HOST_DEVICE int GetDaughterId(int boxId) const { return fBoxToDaughter ? fBoxToDaughter[boxId] : boxId; }
0159 
0160   /** @} */
0161 
0162   /** @name Runtime AABB predicates
0163    *  Query owners compose these inlined host/device tests into their own traversal and exact primitive reduction.
0164    *  @{ */
0165 
0166   /**
0167    * @brief Test one hierarchy node against a bounded ray.
0168    * @param nodeId Node-array index.
0169    * @param point Ray origin in hierarchy precision.
0170    * @param inverseDirection Component-wise reciprocal direction.
0171    * @param octant Direction-sign bits used for CUDA endpoint selection.
0172    * @param step Maximum accepted ray parameter.
0173    * @param[out] approach Non-negative entry parameter.
0174    * @return Whether the ray intersects the node within @p step.
0175    */
0176   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE bool IntersectNode(unsigned int nodeId, Vector3D<Real_t> const &point,
0177                                                                   Vector3D<Real_t> const &inverseDirection,
0178                                                                   unsigned int octant, Real_t step,
0179                                                                   Real_t &approach) const
0180   {
0181     return IntersectOctant(fNodes[nodeId].bounds, point, inverseDirection, octant, step, approach);
0182   }
0183 
0184   /**
0185    * @brief Test one primitive AABB against a bounded ray.
0186    * @param boxId Primitive-box index.
0187    * @param point Ray origin in hierarchy precision.
0188    * @param inverseDirection Component-wise reciprocal direction.
0189    * @param step Maximum accepted ray parameter.
0190    * @param[out] approach Non-negative entry parameter.
0191    * @return Whether the ray intersects the box within @p step.
0192    */
0193   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE bool IntersectBox(int boxId, Vector3D<Real_t> const &point,
0194                                                                  Vector3D<Real_t> const &inverseDirection, Real_t step,
0195                                                                  Real_t &approach) const
0196   {
0197     return fAABBs[boxId].IntersectInvDirApproach(point, inverseDirection, step, approach);
0198   }
0199 
0200   /**
0201    * @brief Return conservative squared Safety to one hierarchy node.
0202    * @param nodeId Node-array index.
0203    * @param point Query point in hierarchy precision.
0204    * @return Downward-biased squared distance to the node AABB.
0205    */
0206   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE Real_t NodeSafetySqr(unsigned int nodeId,
0207                                                                     Vector3D<Real_t> const &point) const
0208   {
0209     return ConservativeSafetySqr(fNodes[nodeId], point);
0210   }
0211 
0212   /**
0213    * @brief Apply the final conservative bias to a reduced squared Safety.
0214    * @param safetySqr Squared Safety reduced in hierarchy precision.
0215    * @return Downward-biased value converted to geometry precision.
0216    */
0217   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static Precision FinalizeSafetySqr(Real_t safetySqr)
0218   {
0219     return static_cast<Precision>(kSafetyShrink * safetySqr);
0220   }
0221 
0222   /**
0223    * @brief Return Safety from a point to one primitive AABB.
0224    * @param boxId Primitive-box index.
0225    * @param point Query point in geometry precision.
0226    * @return Signed AABB Safety margin; negative when @p point is inside.
0227    */
0228   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE Precision BoxSafety(int boxId, Vector3D<Precision> const &point) const
0229   {
0230     return fAABBs[boxId].Safety(point);
0231   }
0232 
0233   /**
0234    * @brief Test whether one hierarchy node contains a point.
0235    * @param nodeId Node-array index.
0236    * @param point Query point in hierarchy precision.
0237    * @return Whether the node AABB contains @p point.
0238    */
0239   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE bool NodeContains(unsigned int nodeId,
0240                                                                  Vector3D<Real_t> const &point) const
0241   {
0242     return fNodes[nodeId].Bounds().Contains(point);
0243   }
0244 
0245   /**
0246    * @brief Test whether one primitive AABB contains a point.
0247    * @param boxId Primitive-box index.
0248    * @param point Query point in hierarchy precision.
0249    * @return Whether the primitive AABB contains @p point.
0250    */
0251   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE bool BoxContains(int boxId, Vector3D<Real_t> const &point) const
0252   {
0253     return fAABBs[boxId].Contains(point);
0254   }
0255 
0256   /** @} */
0257 
0258   /** @name Packed device representation
0259    *  These declarations describe and materialize non-owning views over arrays copied into a device arena.
0260    *  @{ */
0261 
0262 #ifdef VECGEOM_ENABLE_CUDA
0263   /** Opaque byte-offset layout for one non-owning device view and its arrays. */
0264   struct DeviceImageLayout {
0265     int rootId{0};
0266     int nPrim{0};
0267     int nDaughter{0};
0268     int nNodes{0};
0269     std::size_t nodesOffset{0};
0270     std::size_t primIdOffset{0};
0271     std::size_t aabbsOffset{0};
0272     std::size_t boxToDaughterOffset{0};
0273   };
0274   static_assert(std::is_trivially_copyable<DeviceImageLayout>::value,
0275                 "IndexedBVH device-image layout must remain byte-copyable");
0276 
0277   /**
0278    * @brief Construct an identity-mapped non-owning device view.
0279    * @param rootId Opaque owner id available to query clients.
0280    * @param nPrim Number of primitive boxes.
0281    * @param nNodes Number of hierarchy nodes.
0282    * @param nodes Node array.
0283    * @param primId Primitive-id permutation.
0284    * @param aabbs Primitive AABB array.
0285    */
0286   VECCORE_ATT_HOST_DEVICE
0287   IndexedBVH(int rootId, int nPrim, int nNodes, Node *nodes, int *primId, AABB<Real_t> *aabbs)
0288       : IndexedBVH(rootId, nPrim, 0, nNodes, nodes, primId, aabbs, nullptr)
0289   {
0290   }
0291 
0292   /**
0293    * @brief Construct a non-owning device view over already resident arrays.
0294    *
0295    * @details This constructor is called by `CopyToGpu()` through placement
0296    * construction. Device code must not release any supplied buffer.
0297    *
0298    * @param rootId Opaque owner id available to query clients.
0299    * @param nPrim Number of primitive boxes in the primary hierarchy.
0300    * @param nDaughter Number of mapped items, or zero for identity mapping.
0301    * @param nNodes Number of nodes in the primary hierarchy.
0302    * @param nodes Primary node array.
0303    * @param primId Primary primitive-id permutation.
0304    * @param aabbs Primary primitive AABBs.
0305    * @param boxToDaughter Optional primitive-box to item map.
0306    */
0307   VECCORE_ATT_HOST_DEVICE
0308   IndexedBVH(int rootId, int nPrim, int nDaughter, int nNodes, Node *nodes, int *primId, AABB<Real_t> *aabbs,
0309              int *boxToDaughter)
0310       : fRootId(rootId), fNPrim(nPrim), fNDaughter(nDaughter), fNNodes(nNodes), fNodeCapacity(nNodes), fNodes(nodes),
0311         fPrimId(primId), fAABBs(aabbs), fBoxToDaughter(boxToDaughter)
0312   {
0313   }
0314 
0315   /**
0316    * @brief Replace one packed descriptor with a non-owning view over its arena.
0317    * @param arena Base address containing the descriptor and copied arrays.
0318    * @param objectOffset Byte offset of the descriptor/object slot.
0319    * @param target Optional separate address for the resulting view; the descriptor slot is used when null.
0320    */
0321   VECCORE_ATT_HOST_DEVICE
0322   static void MaterializeDeviceImage(char *arena, std::size_t objectOffset, IndexedBVH<Real_t> *target = nullptr)
0323   {
0324     DeviceImageLayout layout;
0325     memcpy(&layout, arena + objectOffset, sizeof(layout));
0326 
0327     auto *bvh = target ? target : reinterpret_cast<IndexedBVH<Real_t> *>(arena + objectOffset);
0328     if (layout.nPrim == 0) {
0329       new (bvh) IndexedBVH<Real_t>();
0330       return;
0331     }
0332 
0333     auto *nodes  = reinterpret_cast<Node *>(arena + layout.nodesOffset);
0334     auto *primId = reinterpret_cast<int *>(arena + layout.primIdOffset);
0335     auto *aabbs  = reinterpret_cast<AABB<Real_t> *>(arena + layout.aabbsOffset);
0336     auto *boxToDaughter =
0337         layout.boxToDaughterOffset ? reinterpret_cast<int *>(arena + layout.boxToDaughterOffset) : nullptr;
0338 
0339     new (bvh) IndexedBVH<Real_t>(layout.rootId, layout.nPrim, layout.nDaughter, layout.nNodes, nodes, primId, aabbs,
0340                                  boxToDaughter);
0341   }
0342 #endif
0343 
0344 #ifdef VECGEOM_CUDA_INTERFACE
0345   /**
0346    * @brief Reserve this BVH's arrays in a packed arena.
0347    * @param[in,out] nextOffset First free byte; advanced past the reserved arrays.
0348    * @return Opaque layout consumed by `PackDeviceImage()` and `MaterializeDeviceImage()`.
0349    */
0350   DeviceImageLayout MakeDeviceImageLayout(std::size_t &nextOffset) const;
0351 
0352   /**
0353    * @brief Write this BVH's descriptor and arrays into a host staging arena.
0354    * @param arena Base address of the staging arena.
0355    * @param arenaSize Available bytes in @p arena.
0356    * @param objectOffset Byte offset reserved for this BVH object.
0357    * @param layout Layout returned by `MakeDeviceImageLayout()` for this BVH.
0358    */
0359   void PackDeviceImage(char *arena, std::size_t arenaSize, std::size_t objectOffset,
0360                        DeviceImageLayout const &layout) const;
0361 
0362   /**
0363    * @brief Copy all active arrays and placement-construct a device view.
0364    *
0365    * @param addr Device address reserved for the `cuda::IndexedBVH` object.
0366    * @return Device pointer referring to the object constructed at @p addr.
0367    *
0368    * All active arrays share one packed allocation. The mapped-item array is
0369    * present only when needed. No `LogicalVolume` lookup is required.
0370    */
0371   DevicePtr<cuda::IndexedBVH<Real_t>> CopyToGpu(void *addr) const;
0372 #endif
0373 
0374   /** @} */
0375 
0376   /** @name Host construction and ownership
0377    *  This section owns input validation, sweep-SAH build policy, allocation, move, and cleanup on the host.
0378    *  @{ */
0379 
0380   /** Default policy for relatively expensive work after accepting an AABB candidate. */
0381   static constexpr Real_t kCostTraversal = Real_t(1.0); ///< Relative internal-node traversal cost.
0382   static constexpr Real_t kCostIntersect = Real_t(3.0); ///< Relative exact primitive-candidate cost.
0383   static constexpr int kMaxLeafSize      = 4;           ///< Default maximum non-improving leaf size.
0384 
0385   /** @brief Host-side sweep-SAH cost model; it is not stored in the hierarchy. */
0386   struct BuildConfig {
0387     /// Relative cost of descending through an internal node and testing its children.
0388     double traversalCost{double(kCostTraversal)};
0389     /// Relative cost of accepting and processing one primitive in a leaf.
0390     double primitiveCost{double(kCostIntersect)};
0391     /// Largest leaf retained when SAH finds no cost-improving split.
0392     int maxLeafSize{kMaxLeafSize};
0393   };
0394 
0395   /// Binary levels of default leaves retained by the fine-grained volume policy.
0396   static constexpr int kVolumeFineTreeLevels = 1;
0397   /// Largest primitive count using the fine-grained volume policy.
0398   static constexpr int kVolumeWideLeafThreshold = kMaxLeafSize * (1 << kVolumeFineTreeLevels);
0399   /// Scale factor converting the default leaf cap to the wide-leaf cap.
0400   static constexpr int kVolumeWideLeafFactor = 3;
0401 
0402   /**
0403    * @brief Return a wide-leaf policy for hierarchies with cheap primitive tests.
0404    *
0405    * Up to eight boxes use `{1,2,4}`; larger hierarchies use `{2,1,12}`.
0406    * @p primitiveCount includes mapped or split boxes.
0407    * The large-tree traversal cost models two child AABB tests and permits wider
0408    * leaves; small trees retain finer leaves for point location.
0409    *
0410    * @param primitiveCount Number of primitive boxes in the hierarchy.
0411    * @return Sweep-SAH costs and leaf cap selected for this hierarchy size.
0412    */
0413   static constexpr BuildConfig VolumeNavigationBuildConfig(int primitiveCount)
0414   {
0415     return primitiveCount <= kVolumeWideLeafThreshold ? BuildConfig{1.0, 2.0, kMaxLeafSize}
0416                                                       : BuildConfig{2.0, 1.0, kVolumeWideLeafFactor * kMaxLeafSize};
0417   }
0418 
0419   /**
0420    * @brief Construct an identity-mapped hierarchy with the default policy.
0421    *
0422    * @param rootId Id of the volume/solid this BVH was built for (opaque to this class).
0423    * @param ptrAABB Array of @p nChild (min,max) corner pairs, one per primitive.
0424    * @param nChild Number of primitives.
0425    */
0426   template <typename InputReal_t>
0427   VECCORE_ATT_HOST IndexedBVH(int rootId, Vector3D<InputReal_t> const *ptrAABB, int nChild)
0428       : IndexedBVH(rootId, ptrAABB, nChild, BuildConfig{})
0429   {
0430   }
0431 
0432   /**
0433    * @brief Construct an identity-mapped hierarchy with an explicit host policy.
0434    *
0435    * @param rootId Id of the volume/solid this BVH was built for.
0436    * @param ptrAABB Array of @p nChild (min,max) corner pairs.
0437    * @param nChild Number of primitive boxes.
0438    * @param config Host-side construction policy.
0439    */
0440   template <typename InputReal_t>
0441   VECCORE_ATT_HOST IndexedBVH(int rootId, Vector3D<InputReal_t> const *ptrAABB, int nChild, BuildConfig const &config)
0442       : IndexedBVH(rootId, ptrAABB, nChild, nullptr, 0, config)
0443   {
0444   }
0445 
0446   /**
0447    * @brief Construct a hierarchy whose boxes may map to fewer logical items.
0448    *
0449    * @details The optional map must be dense in `[0,nDaughter)` and every item
0450    * must own at least one box. Repeated item ids are permitted and represent
0451    * conservative split boxes. Passing zero for @p nDaughter derives the count
0452    * from the largest mapped id.
0453    *
0454    * @param rootId Id of the volume/solid this BVH was built for.
0455    * @param ptrAABB Array of @p nChild (min,max) corner pairs.
0456    * @param nChild Number of primitive boxes.
0457    * @param boxToDaughter Optional primitive-box to item map. Null means identity.
0458    * @param nDaughter Number of mapped items, or zero to derive it from the map.
0459    * @param config Host-side construction policy.
0460    */
0461   template <typename InputReal_t>
0462   VECCORE_ATT_HOST IndexedBVH(int rootId, Vector3D<InputReal_t> const *ptrAABB, int nChild, int const *boxToDaughter,
0463                               int nDaughter = 0, BuildConfig const &config = BuildConfig{})
0464       : fRootId(rootId), fNPrim(nChild)
0465   {
0466     VECGEOM_VALIDATE(nChild > 0, << "Cannot construct IndexedBVH for a volume with no primitives!");
0467     VECGEOM_VALIDATE(config.traversalCost > 0. && config.primitiveCost > 0. && config.maxLeafSize > 0,
0468                      << "IndexedBVH build costs and maximum leaf size must be positive!");
0469 
0470     if (boxToDaughter) {
0471       VECGEOM_VALIDATE(nDaughter <= 0 || nDaughter <= nChild,
0472                        << "Cannot construct IndexedBVH with more mapped items than primitive boxes!");
0473       int mappedDaughters = 0;
0474       for (int boxId = 0; boxId < nChild; ++boxId) {
0475         int const daughterId = boxToDaughter[boxId];
0476         VECGEOM_VALIDATE(daughterId >= 0, << "Cannot construct IndexedBVH with a negative mapped item id!");
0477         VECGEOM_VALIDATE(daughterId < nChild, << "Cannot construct IndexedBVH with a sparse mapped-item id!");
0478         VECGEOM_VALIDATE(nDaughter <= 0 || daughterId < nDaughter,
0479                          << "Cannot construct IndexedBVH with an item id outside the configured range!");
0480         mappedDaughters = std::max(mappedDaughters, daughterId + 1);
0481       }
0482       if (nDaughter <= 0) nDaughter = mappedDaughters;
0483 
0484       std::vector<bool> seen(nDaughter, false);
0485       for (int boxId = 0; boxId < nChild; ++boxId)
0486         seen[boxToDaughter[boxId]] = true;
0487       VECGEOM_VALIDATE(std::all_of(seen.begin(), seen.end(), [](bool value) { return value; }),
0488                        << "Cannot construct IndexedBVH when a mapped item has no primitive box!");
0489 
0490       fNDaughter     = nDaughter;
0491       fBoxToDaughter = new int[nChild];
0492       std::copy(boxToDaughter, boxToDaughter + nChild, fBoxToDaughter);
0493     }
0494 
0495     fPrimId = new int[nChild];
0496     std::iota(fPrimId, fPrimId + nChild, 0);
0497 
0498     // Per-primitive AABBs indexed by primitive id. Corners are rounded outward when narrowing the input,
0499     // then expanded by one stored ULP to cover nearby query-point narrowing,
0500     // including when the source bounds are exactly representable.
0501     fAABBs = new AABB<Real_t>[nChild];
0502     for (int i = 0; i < nChild; ++i) {
0503       Vector3D<Real_t> lo(RoundOutward<true>(ptrAABB[2 * i].x()), RoundOutward<true>(ptrAABB[2 * i].y()),
0504                           RoundOutward<true>(ptrAABB[2 * i].z()));
0505       Vector3D<Real_t> hi(RoundOutward<false>(ptrAABB[2 * i + 1].x()), RoundOutward<false>(ptrAABB[2 * i + 1].y()),
0506                           RoundOutward<false>(ptrAABB[2 * i + 1].z()));
0507       const auto inf = std::numeric_limits<Real_t>::infinity();
0508       for (int axis = 0; axis < 3; ++axis) {
0509         lo[axis] = std::nextafter(lo[axis], -inf);
0510         hi[axis] = std::nextafter(hi[axis], inf);
0511       }
0512       fAABBs[i] = AABB<Real_t>(lo, hi);
0513     }
0514 
0515     // A binary tree with at most nChild leaves has at most 2 * nChild - 1 nodes.
0516     fNodeCapacity = std::max(1, 2 * nChild - 1);
0517     fNodes        = new Node[fNodeCapacity];
0518     fNNodes       = 1;
0519     BuildNode(0, 0, fAABBs, fPrimId, fPrimId + nChild, config);
0520 
0521     // The recursive builder needs worst-case 2*N-1 scratch capacity. SAH normally stops at wider
0522     // leaves, so discard the unused tail once construction is complete. Traversal indices remain
0523     // unchanged because the live nodes already occupy the contiguous prefix [0, fNNodes).
0524     if (fNNodes < fNodeCapacity) {
0525       auto *compactNodes = new Node[fNNodes];
0526       std::copy(fNodes, fNodes + fNNodes, compactNodes);
0527       delete[] fNodes;
0528       fNodes        = compactNodes;
0529       fNodeCapacity = fNNodes;
0530     }
0531   }
0532 
0533   IndexedBVH(IndexedBVH const &)            = delete;
0534   IndexedBVH &operator=(IndexedBVH const &) = delete;
0535 
0536   /** Transfer ownership of all host arrays. */
0537   VECCORE_ATT_HOST
0538   IndexedBVH(IndexedBVH &&other) noexcept { MoveFrom(other); }
0539 
0540   /** Release current host arrays and take ownership from @p other. */
0541   VECCORE_ATT_HOST
0542   IndexedBVH &operator=(IndexedBVH &&other) noexcept
0543   {
0544     if (this != &other) {
0545       Clear();
0546       MoveFrom(other);
0547     }
0548     return *this;
0549   }
0550 
0551   /** @return Bytes owned by the active host arrays, excluding the object itself. */
0552   std::size_t GetAllocatedSize() const
0553   {
0554     std::size_t size = static_cast<std::size_t>(fNodeCapacity) * sizeof(Node) +
0555                        static_cast<std::size_t>(fNPrim) * (sizeof(int) + sizeof(AABB<Real_t>));
0556     if (fBoxToDaughter) size += static_cast<std::size_t>(fNPrim) * sizeof(int);
0557     return size;
0558   }
0559 
0560   /** @return Active device-array payload bytes, excluding layout, alignment, and object storage. */
0561   std::size_t GetDeviceAllocatedSize() const
0562   {
0563     std::size_t size = static_cast<std::size_t>(fNNodes) * sizeof(Node) +
0564                        static_cast<std::size_t>(fNPrim) * (sizeof(int) + sizeof(AABB<Real_t>));
0565     if (fBoxToDaughter) size += static_cast<std::size_t>(fNPrim) * sizeof(int);
0566     return size;
0567   }
0568 
0569   ~IndexedBVH() { Clear(); }
0570 
0571   /** Release every host-owned node, primitive, AABB, and mapping array. */
0572   VECCORE_ATT_HOST
0573   void Clear()
0574   {
0575 #ifndef VECCORE_CUDA_DEVICE_COMPILATION
0576     delete[] fNodes;
0577     fNodes = nullptr;
0578     delete[] fPrimId;
0579     fPrimId = nullptr;
0580     delete[] fAABBs;
0581     fAABBs = nullptr;
0582     delete[] fBoxToDaughter;
0583     fBoxToDaughter = nullptr;
0584     fRootId        = 0;
0585     fNPrim         = 0;
0586     fNDaughter     = 0;
0587     fNNodes        = 0;
0588     fNodeCapacity  = 0;
0589 #endif
0590   }
0591 
0592   /** @} */
0593 
0594   /** @name Host build diagnostics
0595    *  These host-only estimates characterize the completed hierarchy without participating in runtime queries.
0596    *  @{ */
0597 
0598   /**
0599    * @brief Estimate isotropic ray work using surface-area probabilities.
0600    * @param[out] nodeVisits Estimated internal and leaf node visits.
0601    * @param[out] primitiveBoxTests Estimated primitive AABB tests.
0602    * @param[out] candidateCalls Estimated exact primitive operations.
0603    */
0604   VECCORE_ATT_HOST void EstimateTraversalWork(double &nodeVisits, double &primitiveBoxTests,
0605                                               double &candidateCalls) const
0606   {
0607     nodeVisits = primitiveBoxTests = candidateCalls = 0.;
0608     if (!fNodes || !fPrimId || !fAABBs) return;
0609 
0610     const double rootArea = static_cast<double>(fNodes[0].Bounds().SurfaceArea());
0611     if (!(rootArea > 0.)) return;
0612 
0613     unsigned int stack[kTraversalStackSize];
0614     int sp   = 1;
0615     stack[0] = 0;
0616     while (sp > 0) {
0617       const unsigned int id    = stack[--sp];
0618       const double probability = id == 0 ? 1. : static_cast<double>(fNodes[id].Bounds().SurfaceArea()) / rootArea;
0619       nodeVisits += probability;
0620       if (fNodes[id].IsLeaf()) {
0621         primitiveBoxTests += probability * fNodes[id].count;
0622         for (int child = 0; child < fNodes[id].count; ++child) {
0623           const int boxId = fPrimId[fNodes[id].first + child];
0624           candidateCalls += static_cast<double>(fAABBs[boxId].SurfaceArea()) / rootArea;
0625         }
0626       } else {
0627         stack[sp++] = fNodes[id].first;
0628         stack[sp++] = fNodes[id].first + 1;
0629       }
0630     }
0631   }
0632 
0633   /**
0634    * @brief Estimate point-Safety work for the primary hierarchy.
0635    * @details Node visits use boundary-biased surface-area ratios; leaf and
0636    * candidate work use occupied-volume ratios.
0637    * @param[out] nodeVisits Estimated node visits.
0638    * @param[out] primitiveBoxTests Estimated primitive AABB tests.
0639    * @param[out] candidateCalls Estimated exact item operations.
0640    */
0641   VECCORE_ATT_HOST void EstimateSafetyWork(double &nodeVisits, double &primitiveBoxTests, double &candidateCalls) const
0642   {
0643     nodeVisits = primitiveBoxTests = candidateCalls = 0.;
0644     if (!fNodes || !fPrimId || !fAABBs) return;
0645 
0646     const auto volume = [](AABB<Real_t> const &box) {
0647       const auto extent = box.Size();
0648       return static_cast<double>(extent[0]) * static_cast<double>(extent[1]) * static_cast<double>(extent[2]);
0649     };
0650     const double rootArea   = static_cast<double>(fNodes[0].Bounds().SurfaceArea());
0651     const double rootVolume = volume(fNodes[0].Bounds());
0652     if (!(rootArea > 0.) || !(rootVolume > 0.)) return;
0653 
0654     unsigned int stack[kTraversalStackSize];
0655     int sp   = 1;
0656     stack[0] = 0;
0657     while (sp > 0) {
0658       const unsigned int id = stack[--sp];
0659       nodeVisits += id == 0 ? 1. : static_cast<double>(fNodes[id].Bounds().SurfaceArea()) / rootArea;
0660       if (fNodes[id].IsLeaf()) {
0661         const double occupancy = id == 0 ? 1. : volume(fNodes[id].Bounds()) / rootVolume;
0662         primitiveBoxTests += occupancy * fNodes[id].count;
0663         for (int child = 0; child < fNodes[id].count; ++child) {
0664           const int boxId = fPrimId[fNodes[id].first + child];
0665           candidateCalls += volume(fAABBs[boxId]) / rootVolume;
0666         }
0667       } else {
0668         stack[sp++] = fNodes[id].first;
0669         stack[sp++] = fNodes[id].first + 1;
0670       }
0671     }
0672   }
0673 
0674   /** @} */
0675 
0676 private:
0677   // Runtime storage
0678   // The same pointer-only representation is used by owning host objects and non-owning device views.
0679   int fRootId{0};                ///< Opaque owner id available to query clients.
0680   int fNPrim{0};                 ///< Number of primitive boxes in the primary hierarchy.
0681   int fNDaughter{0};             ///< Number of mapped items; zero means identity mapping.
0682   int fNNodes{0};                ///< Live nodes in the primary hierarchy.
0683   int fNodeCapacity{0};          ///< Host allocation capacity; compacted to `fNNodes` after build.
0684   Node *fNodes{nullptr};         ///< Flat primary hierarchy.
0685   int *fPrimId{nullptr};         ///< Leaf-slot to primitive-box id permutation.
0686   AABB<Real_t> *fAABBs{nullptr}; ///< Primary AABBs indexed by primitive-box id.
0687   int *fBoxToDaughter{nullptr};  ///< Optional primitive-box id to mapped-item id.
0688 
0689   // Runtime predicate implementation
0690   // These helpers implement the public host/device box predicates without navigation semantics.
0691   /// Downward relative bias applied to squared AABB Safety in hierarchy precision.
0692   static constexpr Real_t kSafetyShrink = Real_t(1.) - Real_t(8.) * vecgeom::kEpsilonT<Real_t>;
0693   /**
0694    * @brief Return a conservative squared distance to one node.
0695    * @details Bounds are already widened. Multiplying the squared result by
0696    * `kSafetyShrink` also covers accumulated `Real_t` subtraction, max, square,
0697    * and addition rounding without a square root in the hot traversal.
0698    */
0699   VECGEOM_FORCE_INLINE VECCORE_ATT_HOST_DEVICE static Real_t ConservativeSafetySqr(const Node &node,
0700                                                                                    const Vector3D<Real_t> &point)
0701   {
0702     const Real_t dx = vecgeom::Max(vecgeom::Max(node.bounds[0] - point[0], Real_t(0.)), point[0] - node.bounds[1]);
0703     const Real_t dy = vecgeom::Max(vecgeom::Max(node.bounds[2] - point[1], Real_t(0.)), point[1] - node.bounds[3]);
0704     const Real_t dz = vecgeom::Max(vecgeom::Max(node.bounds[4] - point[2], Real_t(0.)), point[2] - node.bounds[5]);
0705     return kSafetyShrink * (dx * dx + dy * dy + dz * dz);
0706   }
0707 
0708   /**
0709    * @brief Test a ray against one node AABB.
0710    *
0711    * CUDA uses @p octant to select slab endpoints. CPU loads both endpoints and
0712    * applies min/max. Both compute the same entry/exit interval, preserving hit
0713    * decisions and near-child ordering within `[0,step]`.
0714    *
0715    * @param bounds Interleaved minimum/maximum endpoints of the node AABB.
0716    * @param point Ray origin in the hierarchy coordinate system.
0717    * @param invdir Component-wise reciprocal ray direction.
0718    * @param octant Direction-sign bits used by the CUDA endpoint loads; ignored on the host.
0719    * @param step Maximum accepted ray parameter.
0720    * @param[out] tmin Entry distance, clamped to >= 0; used by the caller for near/far child ordering.
0721    * @return Whether the ray crosses @p bounds within `[0,step]`.
0722    */
0723   VECCORE_ATT_HOST_DEVICE
0724   static bool IntersectOctant(const Real_t (&bounds)[6], const Vector3D<Real_t> &point, const Vector3D<Real_t> &invdir,
0725                               unsigned int octant, Real_t step, Real_t &tmin)
0726   {
0727 #ifdef VECCORE_CUDA_DEVICE_COMPILATION
0728     // bit_i is 0/1 so 2*axis+bit_i / 2*axis+(1-bit_i) are plain indexed loads, no select per axis.
0729     const unsigned int bit0 = octant & 1u;
0730     const unsigned int bit1 = (octant >> 1) & 1u;
0731     const unsigned int bit2 = (octant >> 2) & 1u;
0732 
0733     const Real_t t0x = (bounds[bit0] - point[0]) * invdir[0];
0734     const Real_t t1x = (bounds[1 - bit0] - point[0]) * invdir[0];
0735     const Real_t t0y = (bounds[2 + bit1] - point[1]) * invdir[1];
0736     const Real_t t1y = (bounds[3 - bit1] - point[1]) * invdir[1];
0737     const Real_t t0z = (bounds[4 + bit2] - point[2]) * invdir[2];
0738     const Real_t t1z = (bounds[5 - bit2] - point[2]) * invdir[2];
0739 
0740     using vecCore::math::Max;
0741     using vecCore::math::Min;
0742     tmin = Max(Max(Max(t0x, t0y), t0z), Real_t(0.));
0743     // Same outward bias on the exit t as AABB::ComputeIntersectionInvDir, for float-narrowing safety.
0744     const Real_t tmax = Min(Min(Min(t1x, t1y), t1z) * (Real_t(1.) + vecgeom::kToleranceDist<Real_t>), step);
0745     return tmin <= tmax;
0746 #else
0747     // Load both endpoints from fixed addresses; min/max assigns near and far after multiplication.
0748     // This is algebraically identical to selecting the bounds first from the direction octant.
0749     (void)octant;
0750     const Real_t t0x = (bounds[0] - point[0]) * invdir[0];
0751     const Real_t t1x = (bounds[1] - point[0]) * invdir[0];
0752     const Real_t t0y = (bounds[2] - point[1]) * invdir[1];
0753     const Real_t t1y = (bounds[3] - point[1]) * invdir[1];
0754     const Real_t t0z = (bounds[4] - point[2]) * invdir[2];
0755     const Real_t t1z = (bounds[5] - point[2]) * invdir[2];
0756 
0757     using vecCore::math::Max;
0758     using vecCore::math::Min;
0759     tmin              = Max(Max(Max(Min(t0x, t1x), Min(t0y, t1y)), Min(t0z, t1z)), Real_t(0.));
0760     const Real_t tmax = Min(
0761         Min(Min(Max(t0x, t1x), Max(t0y, t1y)), Max(t0z, t1z)) * (Real_t(1.) + vecgeom::kToleranceDist<Real_t>), step);
0762     return tmin <= tmax;
0763 #endif
0764   }
0765 
0766   // Host construction implementation
0767   // Ownership transfer, conservative input conversion, SAH partitioning, and recursive building remain host-only.
0768   VECCORE_ATT_HOST
0769   void MoveFrom(IndexedBVH &other) noexcept
0770   {
0771     fRootId        = std::exchange(other.fRootId, 0);
0772     fNPrim         = std::exchange(other.fNPrim, 0);
0773     fNDaughter     = std::exchange(other.fNDaughter, 0);
0774     fNNodes        = std::exchange(other.fNNodes, 0);
0775     fNodeCapacity  = std::exchange(other.fNodeCapacity, 0);
0776     fNodes         = std::exchange(other.fNodes, nullptr);
0777     fPrimId        = std::exchange(other.fPrimId, nullptr);
0778     fAABBs         = std::exchange(other.fAABBs, nullptr);
0779     fBoxToDaughter = std::exchange(other.fBoxToDaughter, nullptr);
0780   }
0781 
0782   /**
0783    * @brief Narrow a bound to `Real_t` without shrinking its AABB.
0784    *
0785    * @tparam isMin Round down when true and up otherwise.
0786    * @tparam From Source scalar type.
0787    * @param v Bound to convert.
0788    * @return The converted bound, advanced by one representable value only when
0789    *         the ordinary conversion rounded inward.
0790    */
0791   template <bool isMin, typename From>
0792   VECCORE_ATT_HOST static Real_t RoundOutward(From v)
0793   {
0794     Real_t r = static_cast<Real_t>(v);
0795     if (isMin ? (static_cast<From>(r) > v) : (static_cast<From>(r) < v)) {
0796       r = isMin ? std::nextafter(r, -std::numeric_limits<Real_t>::infinity())
0797                 : std::nextafter(r, std::numeric_limits<Real_t>::infinity());
0798     }
0799     return r;
0800   }
0801 
0802   /**
0803    * @brief Lexicographically compare 3D keys starting with one axis.
0804    *
0805    * @tparam T Three-component key type.
0806    * @param left First key.
0807    * @param right Second key.
0808    * @param sortAxis Primary axis; the other two axes break ties cyclically.
0809    * @return Whether @p left precedes @p right. Identical keys remain equivalent.
0810    */
0811   template <typename T>
0812   VECCORE_ATT_HOST static bool less3D(const T &left, const T &right, int sortAxis)
0813   {
0814     return left[sortAxis] < right[sortAxis] ||
0815            (left[sortAxis] == right[sortAxis] && (left[(sortAxis + 1) % 3] < right[(sortAxis + 1) % 3] ||
0816                                                   (left[(sortAxis + 1) % 3] == right[(sortAxis + 1) % 3] &&
0817                                                    left[(sortAxis + 2) % 3] < right[(sortAxis + 2) % 3])));
0818   }
0819 
0820   /**
0821    * @brief Compute prefix/suffix surface areas for one sorted primitive range.
0822    *
0823    * @details The returned `[k].first` covers `[begin,begin+k)`, while
0824    * `[k].second` covers `[begin+k,end)`, allowing every partition cost on an
0825    * axis to be evaluated in linear time after sorting.
0826    *
0827    * @param boxes Primitive AABBs indexed by the values in the range.
0828    * @param begin First primitive-id slot.
0829    * @param end One-past-last primitive-id slot.
0830    * @return Prefix/suffix enclosing-box surface areas for all split positions.
0831    */
0832   VECCORE_ATT_HOST
0833   static std::vector<std::pair<double, double>> SweepSurfaceArea(AABB<Real_t> const *boxes, int const *begin,
0834                                                                  int const *end)
0835   {
0836     if (begin >= end) return {};
0837 
0838     std::vector<std::pair<double, double>> areas(std::distance(begin, end), {0., 0.});
0839 
0840     AABB<Real_t> box{boxes[*begin]};
0841     for (auto it = begin + 1; it < end; ++it) {
0842       areas[it - begin].first = box.SurfaceArea();
0843       box                     = AABB<Real_t>::Union(box, boxes[*it]);
0844     }
0845 
0846     AABB<Real_t> box2{boxes[*(end - 1)]};
0847     for (auto it = end; it != begin;) {
0848       --it;
0849       box2                     = AABB<Real_t>::Union(box2, boxes[*it]);
0850       areas[it - begin].second = box2.SurfaceArea();
0851     }
0852 
0853     return areas;
0854   }
0855 
0856   /**
0857    * @brief Find a cost-improving sweep-SAH partition over all three axes.
0858    *
0859    * Minimizes `c_trav + c_prim * sum(P(child) * N(child))` against the leaf
0860    * cost. A small extent term orders boxes with equal centroids.
0861    *
0862    * @param boxes Primitive AABBs indexed by the mutable range.
0863    * @param first First primitive-id slot.
0864    * @param last One-past-last primitive-id slot.
0865    * @param nodeArea Surface area of the range's enclosing AABB.
0866    * @param config Relative traversal/primitive costs and leaf cap.
0867    * @param[out] improves Whether a strictly cost-improving split was found.
0868    * @return First element of the second group when @p improves, otherwise @p last.
0869    */
0870   VECCORE_ATT_HOST
0871   static int *SweepSAHSplit(AABB<Real_t> const *boxes, int *first, int *last, Real_t nodeArea,
0872                             BuildConfig const &config, bool &improves)
0873   {
0874     const auto n         = std::distance(first, last);
0875     double bestCost      = double(n) * config.primitiveCost; // baseline: a single leaf
0876     int bestSplitAxis    = -1;
0877     int bestSplitOffset  = -1;
0878     const double invArea = 1.0 / double(nodeArea);
0879 
0880     int currentSortAxis = 0;
0881     auto sorter         = [boxes, &currentSortAxis](int a, int b) {
0882       const auto centroidA   = boxes[a].Center();
0883       const auto centroidB   = boxes[b].Center();
0884       constexpr double shift = 0.01;
0885       return less3D(centroidA + shift * (centroidA - boxes[a].Min()), centroidB + shift * (centroidB - boxes[b].Min()),
0886                     currentSortAxis);
0887     };
0888 
0889     for (int axis = 0; axis <= 2; ++axis) {
0890       currentSortAxis = axis;
0891       std::sort(first, last, sorter);
0892 
0893       const std::vector<std::pair<double, double>> sweep = SweepSurfaceArea(boxes, first, last);
0894 
0895       // k = number of elements in the left group; both groups must be non-empty.
0896       for (int *splitObject = first + 1; splitObject < last; ++splitObject) {
0897         const auto k      = std::distance(first, splitObject);
0898         const double cost = config.traversalCost +
0899                             config.primitiveCost * (sweep[k].first * invArea * k + sweep[k].second * invArea * (n - k));
0900 
0901         if (cost < bestCost) {
0902           bestCost        = cost;
0903           bestSplitAxis   = axis;
0904           bestSplitOffset = static_cast<int>(k);
0905         }
0906       }
0907     }
0908 
0909     improves = (bestSplitAxis != -1);
0910     if (!improves) return last;
0911 
0912     currentSortAxis = bestSplitAxis;
0913     std::sort(first, last, sorter);
0914     return first + bestSplitOffset;
0915   }
0916 
0917   /**
0918    * @brief Partition at the median centroid along the longest node axis.
0919    *
0920    * Used when the leaf cap requires a split that SAH rejects.
0921    *
0922    * @param boxes Primitive AABBs indexed by the mutable range.
0923    * @param first First primitive-id slot.
0924    * @param last One-past-last primitive-id slot.
0925    * @param bounds Enclosing AABB used to select the longest axis.
0926    * @return First primitive-id slot in the right partition.
0927    */
0928   VECCORE_ATT_HOST
0929   static int *MedianSplitLongestAxis(AABB<Real_t> const *boxes, int *first, int *last, AABB<Real_t> const &bounds)
0930   {
0931     const auto size = bounds.Size();
0932     const int axis  = (size[0] > size[2]) ? (size[0] > size[1] ? 0 : 1) : (size[1] > size[2] ? 1 : 2);
0933     int *mid        = first + std::distance(first, last) / 2;
0934     std::nth_element(first, mid, last,
0935                      [boxes, axis](int a, int b) { return less3D(boxes[a].Center(), boxes[b].Center(), axis); });
0936     return mid;
0937   }
0938 
0939   /**
0940    * @brief Recursively build one node for the primitive range `[first,last)`.
0941    * @details Stores the range union, accepts a leaf at one primitive or maximum
0942    * depth, otherwise applies sweep SAH. A non-improving range remains a leaf up
0943    * to `BuildConfig::maxLeafSize`; larger ranges use the longest-axis median.
0944    * Child nodes are allocated as one contiguous pair before recursion.
0945    *
0946    * @param id Node-array slot to populate.
0947    * @param depth Current root-relative depth.
0948    * @param boxes Primitive AABBs indexed by values in `[first,last)`.
0949    * @param first First primitive-id slot owned by this node.
0950    * @param last One-past-last primitive-id slot owned by this node.
0951    * @param config Construction cost model and maximum leaf size.
0952    */
0953   VECCORE_ATT_HOST
0954   void BuildNode(int id, int depth, AABB<Real_t> const *boxes, int *first, int *last, BuildConfig const &config)
0955   {
0956     Node &node  = fNodes[id];
0957     const int n = static_cast<int>(std::distance(first, last));
0958 
0959     AABB<Real_t> bbox = boxes[*first];
0960     for (int *it = first + 1; it != last; ++it)
0961       bbox = AABB<Real_t>::Union(bbox, boxes[*it]);
0962     node.SetBounds(bbox);
0963 
0964     auto makeLeaf = [&] {
0965       node.first = static_cast<int>(first - fPrimId);
0966       node.count = n;
0967     };
0968 
0969     if (n == 1 || depth >= BVH_MAX_DEPTH) return makeLeaf();
0970 
0971     const Real_t nodeArea = bbox.SurfaceArea();
0972     bool improves         = false;
0973     int *pivot = (nodeArea > Real_t(0.)) ? SweepSAHSplit(boxes, first, last, nodeArea, config, improves) : last;
0974 
0975     if (!improves) {
0976       if (n <= config.maxLeafSize) return makeLeaf();
0977       // The leaf-size cap still forces a split: fall back to a median split.
0978       pivot = MedianSplitLongestAxis(boxes, first, last, bbox);
0979       // Truly degenerate (all primitives coincide): accept an oversized leaf rather than loop forever.
0980       if (pivot == first || pivot == last) return makeLeaf();
0981     }
0982 
0983     const int childL = fNNodes++;
0984     const int childR = fNNodes++;
0985     node.first       = childL;
0986     node.count       = 0;
0987 
0988     BuildNode(childL, depth + 1, boxes, first, pivot, config);
0989     BuildNode(childR, depth + 1, boxes, pivot, last, config);
0990   }
0991 };
0992 
0993 } // namespace VECGEOM_IMPL_NAMESPACE
0994 
0995 #ifdef VECGEOM_ENABLE_CUDA
0996 /**
0997  * @brief Copy a contiguous host array of IndexedBVHs into one device arena.
0998  *
0999  * The returned pointer is both the device object-array base and the allocation
1000  * to release. Each object materializes its own arrays from the packed arena.
1001  */
1002 template <typename Real_t>
1003 cuda::IndexedBVH<Real_t> *CopyManyIndexedBVHToGpu(void const *hostBVHs, std::size_t count);
1004 #endif
1005 
1006 } // namespace vecgeom
1007 
1008 #endif // VECGEOM_BASE_INDEXEDBVH_H_