@@ -18,6 +18,7 @@ namespace xs = xsimd;
1818#endif
1919
2020#include < array>
21+ #include < atomic>
2122
2223using namespace std ::placeholders;
2324
@@ -95,6 +96,11 @@ void LBVH::build(
9596}
9697
9798namespace {
99+ // / Returns the number of common leading bits (CLZ of XOR) between sorted
100+ // / Morton codes at positions i and j. code_i is the Morton code at position
101+ // / i, passed explicitly to avoid a redundant lookup. Returns -1 when j is
102+ // / out of bounds. Duplicate codes fall back to CLZ of the index XOR
103+ // / (offset by 32 so it sorts after any code-level difference).
98104 int delta (
99105 const LBVH ::MortonCodeElements& sorted_morton_codes,
100106 int i,
@@ -107,8 +113,8 @@ namespace {
107113 uint64_t code_j = sorted_morton_codes[j].morton_code ;
108114 if (code_i == code_j) {
109115 // handle duplicate morton codes
110- int element_idx_i = i; // sorted_morton_codes[i].elementIdx;
111- int element_idx_j = j; // sorted_morton_codes[j].elementIdx;
116+ int element_idx_i = i;
117+ int element_idx_j = j;
112118
113119 // add 32 for common prefix of code_i ^ code_j
114120#if defined(__GNUC__) || defined(__clang__)
@@ -123,72 +129,6 @@ namespace {
123129 return __lzcnt64 (code_i ^ code_j);
124130#endif
125131 }
126-
127- void determine_range (
128- const LBVH ::MortonCodeElements& sorted_morton_codes,
129- int idx,
130- int & lower,
131- int & upper)
132- {
133- // determine direction of the range (+1 or -1)
134- const uint64_t code = sorted_morton_codes[idx].morton_code ;
135- const int delta_l = delta (sorted_morton_codes, idx, code, idx - 1 );
136- const int delta_r = delta (sorted_morton_codes, idx, code, idx + 1 );
137- const int d = (delta_r >= delta_l) ? 1 : -1 ;
138-
139- // compute upper bound for the length of the range
140- const int delta_min = std::min (delta_l, delta_r);
141- int l_max = 2 ;
142- while (delta (sorted_morton_codes, idx, code, idx + l_max * d)
143- > delta_min) {
144- l_max = l_max << 1 ;
145- }
146-
147- // find the other end using binary search
148- int l = 0 ;
149- for (int t = l_max >> 1 ; t > 0 ; t >>= 1 ) {
150- if (delta (sorted_morton_codes, idx, code, idx + (l + t) * d)
151- > delta_min) {
152- l += t;
153- }
154- }
155- int jdx = idx + l * d;
156-
157- // ensure idx < jdx
158- lower = std::min (idx, jdx);
159- upper = std::max (idx, jdx);
160- }
161-
162- int find_split (
163- const LBVH ::MortonCodeElements& sorted_morton_codes,
164- int first,
165- int last)
166- {
167- uint64_t first_code = sorted_morton_codes[first].morton_code ;
168-
169- // Calculate the number of highest bits that are the same
170- // for all objects, using the count-leading-zeros intrinsic.
171- int common_prefix = delta (sorted_morton_codes, first, first_code, last);
172-
173- // Use binary search to find where the next bit differs.
174- // Specifically, we are looking for the highest object that
175- // shares more than common_prefix bits with the first one.
176- int split = first; // initial guess
177- int stride = last - first;
178- do {
179- stride = (stride + 1 ) >> 1 ; // exponential decrease
180- int new_split = split + stride; // proposed new position
181- if (new_split < last) {
182- int split_prefix =
183- delta (sorted_morton_codes, first, first_code, new_split);
184- if (split_prefix > common_prefix) {
185- split = new_split; // accept proposal
186- }
187- }
188- } while (stride > 1 );
189-
190- return split;
191- }
192132} // namespace
193133
194134void LBVH::init_bvh (
@@ -238,17 +178,35 @@ void LBVH::init_bvh(
238178 }
239179
240180 assert (boxes.size () <= std::numeric_limits<int >::max ());
241- const int LEAF_OFFSET = int (boxes.size ()) - 1 ;
181+ const int N_LEAVES = int (boxes.size ());
182+ const int LEAF_OFFSET = N_LEAVES - 1 ;
242183
243184 if (rightmost_leaves.size () != lbvh.size ()) {
244185 rightmost_leaves.resize (lbvh.size ());
245186 }
246187
247188 LBVH ::ConstructionInfos construction_infos (lbvh.size ());
248189 {
249- IPC_TOOLKIT_PROFILE_BLOCK (" build_hierarchy" );
250- tbb::parallel_for (size_t (0 ), boxes.size (), [&](size_t i) {
251- assert (i < boxes.size ());
190+ IPC_TOOLKIT_PROFILE_BLOCK (" init_visitation_counts" );
191+ tbb::parallel_for (size_t (0 ), lbvh.size (), [&](size_t i) {
192+ construction_infos[i].visitation_count .store (
193+ 0 , std::memory_order_relaxed);
194+ });
195+ }
196+
197+ // Apetrei 2014: single bottom-up pass that simultaneously builds the
198+ // hierarchy and computes bounding boxes. Each leaf thread walks toward the
199+ // root, choosing its parent in O(1) by comparing the CLZ-delta values at
200+ // the two ends of its current key range.
201+ //
202+ // In this layout internal node j always splits between sorted keys j and
203+ // j+1. The root is NOT necessarily at index 0, so after construction we
204+ // swap the root into position 0 to match the traversal code's expectation.
205+ std::atomic<int > root_idx (-1 );
206+ {
207+ IPC_TOOLKIT_PROFILE_BLOCK (" build_hierarchy_and_boxes" );
208+ tbb::parallel_for (0 , N_LEAVES , [&](int i) {
209+ // --- Initialize leaf node ---
252210 {
253211 const auto & box = boxes[morton_codes[i].box_id ];
254212
@@ -261,59 +219,51 @@ void LBVH::init_bvh(
261219 rightmost_leaves[LEAF_OFFSET + i] = i;
262220 }
263221
264- if (i < LEAF_OFFSET ) {
265- // Find out which range of objects the node corresponds
266- // to. (This is where the magic happens!)
267-
268- int first, last ;
269- determine_range (morton_codes, int (i), first, last) ;
222+ // --- Bottom-up walk (Apetrei 2014, Fig. 2) ---
223+ // Invariant: the current subtree covers the sorted-key range
224+ // [left_key, right_key].
225+ int left_key = i;
226+ int right_key = i ;
227+ int current_node = LEAF_OFFSET + i ;
270228
271- // Determine where to split the range
272- int split = find_split (morton_codes, first, last);
273-
274- // Select child_a
275- int child_a = -1 ;
276- if (split == first) {
277- // pointer to leaf node
278- child_a = LEAF_OFFSET + split;
279- } else {
280- child_a = split; // pointer to internal node
281- }
282-
283- // Select child_b
284- int child_b = -1 ;
285- if (split + 1 == last) {
286- child_b = LEAF_OFFSET + split + 1 ; // pointer to leaf node
229+ while (true ) {
230+ // Choose parent. Candidates are internal node right_key
231+ // (current becomes its left / childA) or internal node
232+ // left_key-1 (current becomes its right / childB). Our delta()
233+ // returns CLZ (higher = more-similar = finer split), so the
234+ // CLOSER ancestor has the LARGER delta — hence ">".
235+ //
236+ // Boundary rules:
237+ // left_key == 0 → must be childA (no node -1)
238+ // right_key == n-1 → must be childB (no node n-1)
239+ const bool is_child_a = (left_key == 0 )
240+ || (right_key != N_LEAVES - 1
241+ && delta (
242+ morton_codes, right_key,
243+ morton_codes[right_key].morton_code ,
244+ right_key + 1 )
245+ > delta (
246+ morton_codes, left_key - 1 ,
247+ morton_codes[left_key - 1 ].morton_code ,
248+ left_key));
249+ const int parent = is_child_a ? right_key : left_key - 1 ;
250+
251+ auto & info = construction_infos[parent];
252+
253+ // Write the child pointer on the parent node.
254+ // childA writes .left; childB writes .right.
255+ if (is_child_a) {
256+ lbvh[parent].left = current_node;
257+ info.left_range = left_key;
287258 } else {
288- child_b = split + 1 ; // pointer to internal node
259+ lbvh[parent].right = current_node;
260+ info.right_range = right_key;
289261 }
290262
291- // Record parent-child relationships
292- lbvh[i].left = child_a;
293- lbvh[i].right = child_b;
294- construction_infos[child_a].parent = int (i);
295- construction_infos[child_b].parent = int (i);
296- construction_infos[child_a].visitation_count .store (
297- 0 , std::memory_order_relaxed);
298- construction_infos[child_b].visitation_count .store (
299- 0 , std::memory_order_relaxed);
300- }
263+ // Atomic arrival gate: the first thread to reach this parent
264+ // stops; the second thread proceeds (it now knows both children
265+ // are complete).
301266
302- // node 0 is the root and has no parent to set these values
303- if (i == 0 ) {
304- construction_infos[0 ].parent = 0 ;
305- construction_infos[0 ].visitation_count .store (
306- 0 , std::memory_order_relaxed);
307- }
308- });
309- }
310-
311- {
312- IPC_TOOLKIT_PROFILE_BLOCK (" populate_boxes" );
313- tbb::parallel_for (size_t (0 ), boxes.size (), [&](size_t i) {
314- int node_idx = construction_infos[LEAF_OFFSET + i].parent ;
315- while (true ) {
316- auto & info = construction_infos[node_idx];
317267 if (info.visitation_count ++ == 0 ) {
318268 // this is the first thread that arrived at this
319269 // node -> finished
@@ -322,23 +272,58 @@ void LBVH::init_bvh(
322272 // this is the second thread that arrived at this node,
323273 // both children are computed -> compute aabb union and
324274 // continue
325- assert (lbvh[node_idx].is_inner ());
326- const Node& child_b = lbvh[lbvh[node_idx].right ];
327- const Node& child_a = lbvh[lbvh[node_idx].left ];
328- lbvh[node_idx].aabb_min =
329- child_a.aabb_min .min (child_b.aabb_min );
330- lbvh[node_idx].aabb_max =
331- child_a.aabb_max .max (child_b.aabb_max );
275+ assert (lbvh[parent].is_inner ());
276+ const Node& child_a = lbvh[lbvh[parent].left ];
277+ const Node& child_b = lbvh[lbvh[parent].right ];
278+ lbvh[parent].aabb_min = child_a.aabb_min .min (child_b.aabb_min );
279+ lbvh[parent].aabb_max = child_a.aabb_max .max (child_b.aabb_max );
332280
333281 // Compute rightmost leaf: max of children's rightmost
334- rightmost_leaves[node_idx] = std::max (
335- rightmost_leaves[lbvh[node_idx].left ],
336- rightmost_leaves[lbvh[node_idx].right ]);
337-
338- if (node_idx == 0 ) {
339- break ; // root node
282+ rightmost_leaves[parent] = std::max (
283+ rightmost_leaves[lbvh[parent].left ],
284+ rightmost_leaves[lbvh[parent].right ]);
285+
286+ // Reconstruct the full key range for the parent.
287+ left_key = construction_infos[parent].left_range ;
288+ right_key = construction_infos[parent].right_range ;
289+ current_node = parent;
290+
291+ if (left_key == 0 && right_key == N_LEAVES - 1 ) {
292+ // only one thread should reach the root
293+ int expected = -1 ;
294+ [[maybe_unused]] bool set =
295+ root_idx.compare_exchange_strong (
296+ expected, current_node);
297+ assert (set);
298+ break ; // root AABB is complete
340299 }
341- node_idx = info.parent ;
300+ }
301+ });
302+ }
303+
304+ // --- Move the root to index 0 so traversal can start there. ---
305+ // In the Apetrei layout the root's index equals the global split position,
306+ // which is generally != 0. We swap the root node into position 0 and patch
307+ // up the single affected child pointer.
308+ //
309+ // Key invariant (Apetrei): node 0's subtree always has left_key=0, so it is
310+ // only ever written as a LEFT child — meaning no internal node ever has
311+ // right==0. Therefore swapping node 0 cannot create a spurious
312+ // is_inner_marker==0 (which would look like a leaf).
313+ const int root = root_idx.load ();
314+ if (root > 0 ) {
315+ IPC_TOOLKIT_PROFILE_BLOCK (" swap_root_to_zero" );
316+ std::swap (lbvh[0 ], lbvh[root]);
317+ std::swap (rightmost_leaves[0 ], rightmost_leaves[root]);
318+
319+ // The root (now at 0) is never any node's child, so no pointer
320+ // references R that needs rewriting to 0. The only pointers that
321+ // referenced 0 (the old node-0) must be rewritten to R. And since old
322+ // node-0 was only ever a LEFT child (see invariant above), we only need
323+ // to patch .left pointers.
324+ tbb::parallel_for (size_t (0 ), lbvh.size (), [&](size_t i) {
325+ if (lbvh[i].is_inner () && lbvh[i].left == 0 ) {
326+ lbvh[i].left = root;
342327 }
343328 });
344329 }
0 commit comments