Skip to main content

packed_spatial_index/index2d_soa/
raycast.rs

1use std::{collections::BinaryHeap, ops::ControlFlow};
2
3use wide::f64x4;
4
5#[cfg(target_arch = "x86_64")]
6use crate::leftpack::leftpack4;
7use crate::{
8    config::{DEFAULT_NEIGHBOR_QUEUE_CAPACITY, DEFAULT_SEARCH_STACK_CAPACITY},
9    geometry::Box2D,
10    neighbors::{NeighborNodeState, NeighborState, NeighborWorkspace},
11    ray::Ray2D,
12    traversal::{SearchWorkspace, upper_bound_level},
13};
14
15use super::{SimdIndex2D, load4};
16
17impl SimdIndex2D {
18    #[inline]
19    fn box_at_soa(&self, pos: usize) -> Box2D {
20        Box2D::new(
21            self.min_xs[pos],
22            self.min_ys[pos],
23            self.max_xs[pos],
24            self.max_ys[pos],
25        )
26    }
27
28    /// SoA/SIMD ordered closest-hit raycast (2D). Same result as
29    /// [`Index2D::raycast_closest_with`](crate::Index2D::raycast_closest_with), with the
30    /// slab test evaluated four (or eight, on AVX-512) children at a time.
31    ///
32    /// Axis-parallel rays are handled by the `wide::f64x4` path with a masked slab
33    /// test to avoid `0 * inf = NaN` at box faces.
34    pub fn raycast_closest_with(
35        &self,
36        ray: Ray2D,
37        workspace: &mut NeighborWorkspace,
38    ) -> Option<(usize, f64)> {
39        #[cfg(target_arch = "x86_64")]
40        {
41            if std::is_x86_feature_detected!("avx512f") {
42                // The plain AVX-512 slab is multiply-only (fastest, but not NaN-safe for
43                // axis-parallel rays); the masked variant handles a zero direction.
44                // SAFETY: only reached after confirming avx512f is available.
45                return unsafe {
46                    if ray.has_zero_direction() {
47                        self.raycast_closest_avx512_masked(ray, workspace)
48                    } else {
49                        self.raycast_closest_avx512(ray, workspace)
50                    }
51                };
52            }
53        }
54        self.raycast_closest_wide(ray, workspace)
55    }
56
57    fn raycast_closest_wide(
58        &self,
59        ray: Ray2D,
60        workspace: &mut NeighborWorkspace,
61    ) -> Option<(usize, f64)> {
62        let queue = &mut workspace.node_queue;
63        queue.clear();
64        if self.num_items == 0 || ray.max_distance < 0.0 || ray.max_distance.is_nan() {
65            return None;
66        }
67        let root = self.min_xs.len() - 1;
68        let root_t = ray.enter_t(self.box_at_soa(root))?;
69        let mut best_t = ray.max_distance;
70        let mut best_index = None;
71        queue.push(NeighborNodeState::new(root, root_t));
72
73        let ox = f64x4::splat(ray.origin.x);
74        let oy = f64x4::splat(ray.origin.y);
75        let ix = f64x4::splat(ray.inv_dir_x);
76        let iy = f64x4::splat(ray.inv_dir_y);
77        let zero = f64x4::splat(0.0);
78        let maxd = f64x4::splat(ray.max_distance);
79        let pos_inf = f64x4::splat(f64::INFINITY);
80        let neg_inf = f64x4::splat(f64::NEG_INFINITY);
81        // See the 3D `raycast_closest_wide`: a zero-direction axis is handled with
82        // `blend` (inclusive inside-test) to stay NaN-safe at a box face.
83        let (zx, zy) = (ray.dir_x == 0.0, ray.dir_y == 0.0);
84        let axis = |mn: f64x4, mx: f64x4, o: f64x4, inv: f64x4, degenerate: bool| {
85            if degenerate {
86                let inside = mn.simd_le(o) & o.simd_le(mx);
87                (
88                    inside.blend(neg_inf, pos_inf),
89                    inside.blend(pos_inf, neg_inf),
90                )
91            } else {
92                let t1 = (mn - o) * inv;
93                let t2 = (mx - o) * inv;
94                (t1.fast_min(t2), t1.fast_max(t2))
95            }
96        };
97
98        while let Some(node) = queue.pop() {
99            if node.dist >= best_t {
100                break;
101            }
102            let upper = upper_bound_level(&self.level_bounds, node.index);
103            let end = (node.index + self.node_size).min(self.level_bounds[upper]);
104            let is_leaf = node.index < self.num_items;
105
106            let mut pos = node.index;
107            while pos + 4 <= end {
108                let (nx, fx) = axis(
109                    load4(&self.min_xs, pos),
110                    load4(&self.max_xs, pos),
111                    ox,
112                    ix,
113                    zx,
114                );
115                let (ny, fy) = axis(
116                    load4(&self.min_ys, pos),
117                    load4(&self.max_ys, pos),
118                    oy,
119                    iy,
120                    zy,
121                );
122                let near = nx.fast_max(ny).fast_max(zero);
123                let far = fx.fast_min(fy).fast_min(maxd);
124                let bits = near.simd_le(far).to_bitmask();
125                if bits != 0 {
126                    let tn = near.to_array();
127                    // `k` indexes `tn` and selects the mask bit, so a range loop is clearest.
128                    #[allow(clippy::needless_range_loop)]
129                    for k in 0..4 {
130                        if bits & (1 << k) != 0 && tn[k] < best_t {
131                            if is_leaf {
132                                best_t = tn[k];
133                                best_index = Some(self.indices[pos + k]);
134                            } else {
135                                queue.push(NeighborNodeState::new(self.indices[pos + k], tn[k]));
136                            }
137                        }
138                    }
139                }
140                pos += 4;
141            }
142            while pos < end {
143                if let Some(t) = ray.enter_t(self.box_at_soa(pos))
144                    && t < best_t
145                {
146                    if is_leaf {
147                        best_t = t;
148                        best_index = Some(self.indices[pos]);
149                    } else {
150                        queue.push(NeighborNodeState::new(self.indices[pos], t));
151                    }
152                }
153                pos += 1;
154            }
155        }
156
157        best_index.map(|index| (index, best_t))
158    }
159
160    /// AVX-512 closest-hit (2D): eight children per slab test.
161    #[cfg(target_arch = "x86_64")]
162    #[target_feature(enable = "avx512f")]
163    unsafe fn raycast_closest_avx512(
164        &self,
165        ray: Ray2D,
166        workspace: &mut NeighborWorkspace,
167    ) -> Option<(usize, f64)> {
168        use std::arch::x86_64::*;
169
170        let queue = &mut workspace.node_queue;
171        queue.clear();
172        if self.num_items == 0 || ray.max_distance < 0.0 || ray.max_distance.is_nan() {
173            return None;
174        }
175        let root = self.min_xs.len() - 1;
176        let root_t = ray.enter_t(self.box_at_soa(root))?;
177        let mut best_t = ray.max_distance;
178        let mut best_index = None;
179        queue.push(NeighborNodeState::new(root, root_t));
180
181        let ox = _mm512_set1_pd(ray.origin.x);
182        let oy = _mm512_set1_pd(ray.origin.y);
183        let ix = _mm512_set1_pd(ray.inv_dir_x);
184        let iy = _mm512_set1_pd(ray.inv_dir_y);
185        let zero = _mm512_setzero_pd();
186        let maxd = _mm512_set1_pd(ray.max_distance);
187
188        while let Some(node) = queue.pop() {
189            if node.dist >= best_t {
190                break;
191            }
192            let upper = upper_bound_level(&self.level_bounds, node.index);
193            let end = (node.index + self.node_size).min(self.level_bounds[upper]);
194            let is_leaf = node.index < self.num_items;
195
196            let mut pos = node.index;
197            while pos + 8 <= end {
198                // SAFETY: `pos + 8 <= end <= len`, so all eight lanes are in bounds.
199                let (mnx, mxx, mny, mxy) = unsafe {
200                    (
201                        _mm512_loadu_pd(self.min_xs.as_ptr().add(pos)),
202                        _mm512_loadu_pd(self.max_xs.as_ptr().add(pos)),
203                        _mm512_loadu_pd(self.min_ys.as_ptr().add(pos)),
204                        _mm512_loadu_pd(self.max_ys.as_ptr().add(pos)),
205                    )
206                };
207                let t1x = _mm512_mul_pd(_mm512_sub_pd(mnx, ox), ix);
208                let t2x = _mm512_mul_pd(_mm512_sub_pd(mxx, ox), ix);
209                let t1y = _mm512_mul_pd(_mm512_sub_pd(mny, oy), iy);
210                let t2y = _mm512_mul_pd(_mm512_sub_pd(mxy, oy), iy);
211                let near = _mm512_max_pd(
212                    _mm512_max_pd(_mm512_min_pd(t1x, t2x), _mm512_min_pd(t1y, t2y)),
213                    zero,
214                );
215                let far = _mm512_min_pd(
216                    _mm512_min_pd(_mm512_max_pd(t1x, t2x), _mm512_max_pd(t1y, t2y)),
217                    maxd,
218                );
219                let mut bits: u8 = _mm512_cmp_pd_mask::<_CMP_LE_OQ>(near, far);
220                if bits != 0 {
221                    let mut tn = [0.0f64; 8];
222                    // SAFETY: `tn` holds eight `f64`, matching the 512-bit store.
223                    unsafe { _mm512_storeu_pd(tn.as_mut_ptr(), near) };
224                    while bits != 0 {
225                        let k = bits.trailing_zeros() as usize;
226                        bits &= bits - 1;
227                        if tn[k] < best_t {
228                            if is_leaf {
229                                best_t = tn[k];
230                                best_index = Some(self.indices[pos + k]);
231                            } else {
232                                queue.push(NeighborNodeState::new(self.indices[pos + k], tn[k]));
233                            }
234                        }
235                    }
236                }
237                pos += 8;
238            }
239            while pos < end {
240                if let Some(t) = ray.enter_t(self.box_at_soa(pos))
241                    && t < best_t
242                {
243                    if is_leaf {
244                        best_t = t;
245                        best_index = Some(self.indices[pos]);
246                    } else {
247                        queue.push(NeighborNodeState::new(self.indices[pos], t));
248                    }
249                }
250                pos += 1;
251            }
252        }
253
254        best_index.map(|index| (index, best_t))
255    }
256
257    /// AVX-512 closest-hit (2D) for axis-parallel rays: a zero-direction axis is handled
258    /// with `_mm512_mask_blend_pd` over an inclusive inside-test, so it is NaN-safe at a
259    /// box face. Only invoked for rays with a zero direction component.
260    #[cfg(target_arch = "x86_64")]
261    #[target_feature(enable = "avx512f")]
262    unsafe fn raycast_closest_avx512_masked(
263        &self,
264        ray: Ray2D,
265        workspace: &mut NeighborWorkspace,
266    ) -> Option<(usize, f64)> {
267        use std::arch::x86_64::*;
268
269        let queue = &mut workspace.node_queue;
270        queue.clear();
271        if self.num_items == 0 || ray.max_distance < 0.0 || ray.max_distance.is_nan() {
272            return None;
273        }
274        let root = self.min_xs.len() - 1;
275        let root_t = ray.enter_t(self.box_at_soa(root))?;
276        let mut best_t = ray.max_distance;
277        let mut best_index = None;
278        queue.push(NeighborNodeState::new(root, root_t));
279
280        let ox = _mm512_set1_pd(ray.origin.x);
281        let oy = _mm512_set1_pd(ray.origin.y);
282        let ix = _mm512_set1_pd(ray.inv_dir_x);
283        let iy = _mm512_set1_pd(ray.inv_dir_y);
284        let zero = _mm512_setzero_pd();
285        let maxd = _mm512_set1_pd(ray.max_distance);
286        let pos_inf = _mm512_set1_pd(f64::INFINITY);
287        let neg_inf = _mm512_set1_pd(f64::NEG_INFINITY);
288        let (zx, zy) = (ray.dir_x == 0.0, ray.dir_y == 0.0);
289
290        let axis = |mn, mx, o, inv, degenerate: bool| {
291            if degenerate {
292                let inside = _mm512_cmp_pd_mask::<_CMP_LE_OQ>(mn, o)
293                    & _mm512_cmp_pd_mask::<_CMP_LE_OQ>(o, mx);
294                (
295                    _mm512_mask_blend_pd(inside, pos_inf, neg_inf),
296                    _mm512_mask_blend_pd(inside, neg_inf, pos_inf),
297                )
298            } else {
299                let t1 = _mm512_mul_pd(_mm512_sub_pd(mn, o), inv);
300                let t2 = _mm512_mul_pd(_mm512_sub_pd(mx, o), inv);
301                (_mm512_min_pd(t1, t2), _mm512_max_pd(t1, t2))
302            }
303        };
304
305        while let Some(node) = queue.pop() {
306            if node.dist >= best_t {
307                break;
308            }
309            let upper = upper_bound_level(&self.level_bounds, node.index);
310            let end = (node.index + self.node_size).min(self.level_bounds[upper]);
311            let is_leaf = node.index < self.num_items;
312
313            let mut pos = node.index;
314            while pos + 8 <= end {
315                // SAFETY: `pos + 8 <= end <= len`, so all eight lanes are in bounds.
316                let (nx, fx, ny, fy) = unsafe {
317                    let (mnx, mxx, mny, mxy) = (
318                        _mm512_loadu_pd(self.min_xs.as_ptr().add(pos)),
319                        _mm512_loadu_pd(self.max_xs.as_ptr().add(pos)),
320                        _mm512_loadu_pd(self.min_ys.as_ptr().add(pos)),
321                        _mm512_loadu_pd(self.max_ys.as_ptr().add(pos)),
322                    );
323                    let (nx, fx) = axis(mnx, mxx, ox, ix, zx);
324                    let (ny, fy) = axis(mny, mxy, oy, iy, zy);
325                    (nx, fx, ny, fy)
326                };
327                let near = _mm512_max_pd(_mm512_max_pd(nx, ny), zero);
328                let far = _mm512_min_pd(_mm512_min_pd(fx, fy), maxd);
329                let mut bits: u8 = _mm512_cmp_pd_mask::<_CMP_LE_OQ>(near, far);
330                if bits != 0 {
331                    let mut tn = [0.0f64; 8];
332                    // SAFETY: `tn` holds eight `f64`, matching the 512-bit store.
333                    unsafe { _mm512_storeu_pd(tn.as_mut_ptr(), near) };
334                    while bits != 0 {
335                        let k = bits.trailing_zeros() as usize;
336                        bits &= bits - 1;
337                        if tn[k] < best_t {
338                            if is_leaf {
339                                best_t = tn[k];
340                                best_index = Some(self.indices[pos + k]);
341                            } else {
342                                queue.push(NeighborNodeState::new(self.indices[pos + k], tn[k]));
343                            }
344                        }
345                    }
346                }
347                pos += 8;
348            }
349            while pos < end {
350                if let Some(t) = ray.enter_t(self.box_at_soa(pos))
351                    && t < best_t
352                {
353                    if is_leaf {
354                        best_t = t;
355                        best_index = Some(self.indices[pos]);
356                    } else {
357                        queue.push(NeighborNodeState::new(self.indices[pos], t));
358                    }
359                }
360                pos += 1;
361            }
362        }
363        best_index.map(|index| (index, best_t))
364    }
365
366    /// Return the nearest item whose box the ray segment enters, as
367    /// `(item index, entry t)`, or `None` when the segment hits nothing.
368    ///
369    /// Nodes are visited front-to-back by entry distance and pruned once a
370    /// closer hit is known, so the cost is roughly independent of
371    /// `max_distance` after the first hit. `t` is `0.0` when the ray origin
372    /// starts inside the item's box.
373    pub fn raycast_closest(&self, ray: Ray2D) -> Option<(usize, f64)> {
374        let mut workspace = NeighborWorkspace::new();
375        self.raycast_closest_with(ray, &mut workspace)
376    }
377
378    /// Return the indices of all items whose boxes the ray segment touches.
379    pub fn raycast(&self, ray: Ray2D) -> Vec<usize> {
380        let mut results = Vec::new();
381        self.raycast_into(ray, &mut results);
382        results
383    }
384
385    /// Raycast with a reusable result buffer.
386    pub fn raycast_into(&self, ray: Ray2D, results: &mut Vec<usize>) {
387        let mut stack = Vec::with_capacity(DEFAULT_SEARCH_STACK_CAPACITY);
388        self.raycast_into_stack(ray, results, &mut stack);
389    }
390
391    /// Raycast with reusable result and traversal buffers.
392    pub fn raycast_with<'a>(&self, ray: Ray2D, workspace: &'a mut SearchWorkspace) -> &'a [usize] {
393        self.raycast_into_stack(ray, &mut workspace.results, &mut workspace.stack);
394        &workspace.results
395    }
396
397    /// Buffer-explicit raycast (mirrors `search_into_stack`). The per-node slab
398    /// test is vectorized: AVX-512 (eight children at a time) for non-degenerate
399    /// rays where available, otherwise `wide::f64x4`. Axis-parallel rays always
400    /// take the `wide` path, whose `blend` kernel is NaN-safe at box faces.
401    #[doc(hidden)]
402    pub fn raycast_into_stack(&self, ray: Ray2D, results: &mut Vec<usize>, stack: &mut Vec<usize>) {
403        #[cfg(target_arch = "x86_64")]
404        {
405            if !ray.has_zero_direction() {
406                if std::is_x86_feature_detected!("avx512f") {
407                    // SAFETY: reached only after confirming avx512f is available.
408                    unsafe { self.raycast_collect_avx512(ray, results, stack) };
409                    return;
410                }
411                if std::is_x86_feature_detected!("avx2") {
412                    // SAFETY: reached only after confirming avx2 is available.
413                    unsafe { self.raycast_collect_avx2(ray, results, stack) };
414                    return;
415                }
416            }
417        }
418        self.raycast_collect_wide(ray, results, stack);
419    }
420
421    /// Force the `wide` all-hits raycast path (doc-hidden; for benchmarks/tests).
422    #[doc(hidden)]
423    pub fn raycast_wide_into(&self, ray: Ray2D, results: &mut Vec<usize>) {
424        let mut stack = Vec::new();
425        self.raycast_collect_wide(ray, results, &mut stack);
426    }
427
428    /// Force the AVX2 all-hits raycast path (doc-hidden; for benchmarks/tests).
429    #[doc(hidden)]
430    pub fn raycast_avx2_into(&self, ray: Ray2D, results: &mut Vec<usize>) {
431        let mut stack = Vec::new();
432        #[cfg(target_arch = "x86_64")]
433        {
434            if !ray.has_zero_direction() && std::is_x86_feature_detected!("avx2") {
435                // SAFETY: guarded by the avx2 feature check.
436                unsafe { self.raycast_collect_avx2(ray, results, &mut stack) };
437                return;
438            }
439        }
440        self.raycast_collect_wide(ray, results, &mut stack);
441    }
442
443    fn raycast_collect_wide(&self, ray: Ray2D, results: &mut Vec<usize>, stack: &mut Vec<usize>) {
444        results.clear();
445        stack.clear();
446        if self.num_items == 0 || ray.max_distance < 0.0 || ray.max_distance.is_nan() {
447            return;
448        }
449
450        let ox = f64x4::splat(ray.origin.x);
451        let oy = f64x4::splat(ray.origin.y);
452        let ix = f64x4::splat(ray.inv_dir_x);
453        let iy = f64x4::splat(ray.inv_dir_y);
454        let zero = f64x4::splat(0.0);
455        let maxd = f64x4::splat(ray.max_distance);
456        let pos_inf = f64x4::splat(f64::INFINITY);
457        let neg_inf = f64x4::splat(f64::NEG_INFINITY);
458        // A zero-direction axis imposes no `t` bound when the origin is inside
459        // (inclusive, so a ray on a face still hits) and an empty interval
460        // otherwise, computed with `blend` to dodge the `0 * inf = NaN` of the
461        // multiply path.
462        let (zx, zy) = (ray.dir_x == 0.0, ray.dir_y == 0.0);
463        let axis = |mn: f64x4, mx: f64x4, o: f64x4, inv: f64x4, degenerate: bool| {
464            if degenerate {
465                let inside = mn.simd_le(o) & o.simd_le(mx);
466                (
467                    inside.blend(neg_inf, pos_inf),
468                    inside.blend(pos_inf, neg_inf),
469                )
470            } else {
471                let t1 = (mn - o) * inv;
472                let t2 = (mx - o) * inv;
473                (t1.fast_min(t2), t1.fast_max(t2))
474            }
475        };
476
477        let mut node_index = self.min_xs.len() - 1;
478        let mut level = self.level_bounds.len() - 1;
479        loop {
480            let end = (node_index + self.node_size).min(self.level_bounds[level]);
481            let is_leaf = node_index < self.num_items;
482            let child_level = level.wrapping_sub(1);
483
484            let mut pos = node_index;
485            while pos + 4 <= end {
486                let (nx, fx) = axis(
487                    load4(&self.min_xs, pos),
488                    load4(&self.max_xs, pos),
489                    ox,
490                    ix,
491                    zx,
492                );
493                let (ny, fy) = axis(
494                    load4(&self.min_ys, pos),
495                    load4(&self.max_ys, pos),
496                    oy,
497                    iy,
498                    zy,
499                );
500                let near = nx.fast_max(ny).fast_max(zero);
501                let far = fx.fast_min(fy).fast_min(maxd);
502                let mut bits = near.simd_le(far).to_bitmask();
503                while bits != 0 {
504                    let k = bits.trailing_zeros() as usize;
505                    bits &= bits - 1;
506                    let index = self.indices[pos + k];
507                    if is_leaf {
508                        results.push(index);
509                    } else {
510                        stack.push(index);
511                        stack.push(child_level);
512                    }
513                }
514                pos += 4;
515            }
516            while pos < end {
517                if ray.intersects_box(self.box_at_soa(pos)) {
518                    let index = self.indices[pos];
519                    if is_leaf {
520                        results.push(index);
521                    } else {
522                        stack.push(index);
523                        stack.push(child_level);
524                    }
525                }
526                pos += 1;
527            }
528
529            if stack.len() > 1 {
530                level = stack.pop().unwrap();
531                node_index = stack.pop().unwrap();
532            } else {
533                return;
534            }
535        }
536    }
537
538    /// AVX-512 all-hits slab test, eight children at a time. Only called for
539    /// non-degenerate rays (no zero direction component), so the multiply-only
540    /// slab is NaN-safe.
541    #[cfg(target_arch = "x86_64")]
542    #[target_feature(enable = "avx512f")]
543    unsafe fn raycast_collect_avx512(
544        &self,
545        ray: Ray2D,
546        results: &mut Vec<usize>,
547        stack: &mut Vec<usize>,
548    ) {
549        use std::arch::x86_64::*;
550
551        results.clear();
552        stack.clear();
553        if self.num_items == 0 || ray.max_distance < 0.0 || ray.max_distance.is_nan() {
554            return;
555        }
556
557        let ox = _mm512_set1_pd(ray.origin.x);
558        let oy = _mm512_set1_pd(ray.origin.y);
559        let ix = _mm512_set1_pd(ray.inv_dir_x);
560        let iy = _mm512_set1_pd(ray.inv_dir_y);
561        let zero = _mm512_setzero_pd();
562        let maxd = _mm512_set1_pd(ray.max_distance);
563
564        let mut node_index = self.min_xs.len() - 1;
565        let mut level = self.level_bounds.len() - 1;
566        loop {
567            let end = (node_index + self.node_size).min(self.level_bounds[level]);
568            let is_leaf = node_index < self.num_items;
569            let child_level = level.wrapping_sub(1);
570            if is_leaf {
571                results.reserve(end - node_index);
572            }
573
574            let mut pos = node_index;
575            while pos + 8 <= end {
576                // SAFETY: `pos + 8 <= end <= len`, so all eight lanes are in bounds.
577                let (mnx, mxx, mny, mxy) = unsafe {
578                    (
579                        _mm512_loadu_pd(self.min_xs.as_ptr().add(pos)),
580                        _mm512_loadu_pd(self.max_xs.as_ptr().add(pos)),
581                        _mm512_loadu_pd(self.min_ys.as_ptr().add(pos)),
582                        _mm512_loadu_pd(self.max_ys.as_ptr().add(pos)),
583                    )
584                };
585                let t1x = _mm512_mul_pd(_mm512_sub_pd(mnx, ox), ix);
586                let t2x = _mm512_mul_pd(_mm512_sub_pd(mxx, ox), ix);
587                let t1y = _mm512_mul_pd(_mm512_sub_pd(mny, oy), iy);
588                let t2y = _mm512_mul_pd(_mm512_sub_pd(mxy, oy), iy);
589                let near = _mm512_max_pd(
590                    _mm512_max_pd(_mm512_min_pd(t1x, t2x), _mm512_min_pd(t1y, t2y)),
591                    zero,
592                );
593                let far = _mm512_min_pd(
594                    _mm512_min_pd(_mm512_max_pd(t1x, t2x), _mm512_max_pd(t1y, t2y)),
595                    maxd,
596                );
597                let mut bits: u8 = _mm512_cmp_pd_mask::<_CMP_LE_OQ>(near, far);
598                if is_leaf {
599                    // VPCOMPRESSQ pack the hit indices (capacity reserved above).
600                    // SAFETY: `pos + 8 <= end <= indices.len()`; `results` has
601                    // `end - node_index` slack.
602                    unsafe {
603                        let dst = results.as_mut_ptr().add(results.len()) as *mut i64;
604                        let vidx = _mm512_loadu_epi64(self.indices.as_ptr().add(pos) as *const i64);
605                        _mm512_mask_compressstoreu_epi64(dst, bits, vidx);
606                        results.set_len(results.len() + bits.count_ones() as usize);
607                    }
608                } else {
609                    while bits != 0 {
610                        let k = bits.trailing_zeros() as usize;
611                        bits &= bits - 1;
612                        stack.push(self.indices[pos + k]);
613                        stack.push(child_level);
614                    }
615                }
616                pos += 8;
617            }
618            while pos < end {
619                if ray.intersects_box(self.box_at_soa(pos)) {
620                    let index = self.indices[pos];
621                    if is_leaf {
622                        results.push(index);
623                    } else {
624                        stack.push(index);
625                        stack.push(child_level);
626                    }
627                }
628                pos += 1;
629            }
630
631            if stack.len() > 1 {
632                level = stack.pop().unwrap();
633                node_index = stack.pop().unwrap();
634            } else {
635                return;
636            }
637        }
638    }
639
640    /// AVX2 all-hits raycast (4-wide slab test, AVX2 left-pack leaf collection).
641    #[cfg(target_arch = "x86_64")]
642    #[target_feature(enable = "avx2")]
643    unsafe fn raycast_collect_avx2(
644        &self,
645        ray: Ray2D,
646        results: &mut Vec<usize>,
647        stack: &mut Vec<usize>,
648    ) {
649        use std::arch::x86_64::*;
650
651        results.clear();
652        stack.clear();
653        if self.num_items == 0 || ray.max_distance < 0.0 || ray.max_distance.is_nan() {
654            return;
655        }
656
657        let ox = _mm256_set1_pd(ray.origin.x);
658        let oy = _mm256_set1_pd(ray.origin.y);
659        let ix = _mm256_set1_pd(ray.inv_dir_x);
660        let iy = _mm256_set1_pd(ray.inv_dir_y);
661        let zero = _mm256_setzero_pd();
662        let maxd = _mm256_set1_pd(ray.max_distance);
663
664        let mut node_index = self.min_xs.len() - 1;
665        let mut level = self.level_bounds.len() - 1;
666        loop {
667            let end = (node_index + self.node_size).min(self.level_bounds[level]);
668            let is_leaf = node_index < self.num_items;
669            let child_level = level.wrapping_sub(1);
670            if is_leaf {
671                results.reserve(end - node_index + 4);
672            }
673
674            let mut pos = node_index;
675            while pos + 4 <= end {
676                // SAFETY: `pos + 4 <= end <= len`, so all four lanes are in bounds.
677                let (mnx, mxx, mny, mxy) = unsafe {
678                    (
679                        _mm256_loadu_pd(self.min_xs.as_ptr().add(pos)),
680                        _mm256_loadu_pd(self.max_xs.as_ptr().add(pos)),
681                        _mm256_loadu_pd(self.min_ys.as_ptr().add(pos)),
682                        _mm256_loadu_pd(self.max_ys.as_ptr().add(pos)),
683                    )
684                };
685                let t1x = _mm256_mul_pd(_mm256_sub_pd(mnx, ox), ix);
686                let t2x = _mm256_mul_pd(_mm256_sub_pd(mxx, ox), ix);
687                let t1y = _mm256_mul_pd(_mm256_sub_pd(mny, oy), iy);
688                let t2y = _mm256_mul_pd(_mm256_sub_pd(mxy, oy), iy);
689                let near = _mm256_max_pd(
690                    _mm256_max_pd(_mm256_min_pd(t1x, t2x), _mm256_min_pd(t1y, t2y)),
691                    zero,
692                );
693                let far = _mm256_min_pd(
694                    _mm256_min_pd(_mm256_max_pd(t1x, t2x), _mm256_max_pd(t1y, t2y)),
695                    maxd,
696                );
697                let mut bits = _mm256_movemask_pd(_mm256_cmp_pd::<_CMP_LE_OQ>(near, far)) as usize;
698                if is_leaf {
699                    if bits != 0 {
700                        // SAFETY: `pos + 4 <= end <= indices.len()`; `results` has
701                        // `end - node_index + 4` slack reserved.
702                        unsafe {
703                            let added = leftpack4(
704                                self.indices.as_ptr().add(pos),
705                                bits as u32,
706                                results.as_mut_ptr().add(results.len()),
707                            );
708                            results.set_len(results.len() + added);
709                        }
710                    }
711                } else {
712                    while bits != 0 {
713                        let k = bits.trailing_zeros() as usize;
714                        bits &= bits - 1;
715                        stack.push(self.indices[pos + k]);
716                        stack.push(child_level);
717                    }
718                }
719                pos += 4;
720            }
721            while pos < end {
722                if ray.intersects_box(self.box_at_soa(pos)) {
723                    let index = self.indices[pos];
724                    if is_leaf {
725                        results.push(index);
726                    } else {
727                        stack.push(index);
728                        stack.push(child_level);
729                    }
730                }
731                pos += 1;
732            }
733
734            if stack.len() > 1 {
735                level = stack.pop().unwrap();
736                node_index = stack.pop().unwrap();
737            } else {
738                return;
739            }
740        }
741    }
742}
743
744impl SimdIndex2D {
745    /// Visit items in nondecreasing entry-`t` order along the ray segment.
746    ///
747    /// The visitor receives `(item index, entry t)`. Return
748    /// [`ControlFlow::Break`] to stop early - for example after the first N
749    /// occluders. `t` is `0.0` when the ray origin starts inside a box.
750    pub fn visit_raycast<B, F>(&self, ray: Ray2D, mut visitor: F) -> ControlFlow<B>
751    where
752        F: FnMut(usize, f64) -> ControlFlow<B>,
753    {
754        let mut queue = BinaryHeap::with_capacity(DEFAULT_NEIGHBOR_QUEUE_CAPACITY);
755        if self.num_items == 0 {
756            return ControlFlow::Continue(());
757        }
758
759        let mut node_index = self.min_xs.len() - 1;
760        loop {
761            let upper = upper_bound_level(&self.level_bounds, node_index);
762            let end = (node_index + self.node_size).min(self.level_bounds[upper]);
763            let is_leaf = node_index < self.num_items;
764
765            for pos in node_index..end {
766                if let Some(t) = ray.enter_t(self.box_at_soa(pos)) {
767                    queue.push(NeighborState::new(self.indices[pos], is_leaf, t));
768                }
769            }
770
771            let mut continue_search = false;
772            while let Some(state) = queue.pop() {
773                if state.is_leaf {
774                    visitor(state.index, state.dist)?;
775                } else {
776                    node_index = state.index;
777                    continue_search = true;
778                    break;
779                }
780            }
781            if !continue_search {
782                return ControlFlow::Continue(());
783            }
784        }
785    }
786}