Skip to main content

alice_physics/
spatial.rs

1//! Spatial Hash Grid
2//!
3//! A hash-based spatial acceleration structure for neighbor queries.
4//! Used by [`crate::fluid`] for SPH neighbor search and available
5//! for cloth self-collision or any particle-based simulation.
6//!
7//! # How It Works
8//!
9//! The grid divides space into uniform cells. Each particle is inserted
10//! into the cell corresponding to its position. Neighbor queries examine
11//! the 3x3x3 neighborhood of cells around the query point.
12//!
13//! Author: Moroya Sakamoto
14
15use crate::math::{Fix128, Vec3Fix};
16
17#[cfg(not(feature = "std"))]
18use alloc::vec;
19#[cfg(not(feature = "std"))]
20use alloc::vec::Vec;
21
22/// Spatial hash grid for O(n) neighbor queries in particle simulations.
23///
24/// Divides space into a uniform 3D grid of `grid_dim^3` cells. Uses a
25/// CSR (Compressed Sparse Row) flat-buffer layout: `indices` holds all
26/// particle indices packed contiguously, and `cell_offsets[h]..cell_offsets[h+1]`
27/// gives the slice for cell `h`. This eliminates per-cell heap allocations
28/// and improves cache locality for neighbor queries.
29///
30/// # Build flow
31///
32/// 1. Call `clear()` to reset counts.
33/// 2. Call `insert()` for each particle.
34/// 3. Call `build()` to finalize the CSR layout.
35/// 4. Call `query_neighbors_into()` for lookups.
36pub struct SpatialGrid {
37    inv_cell_size: Fix128,
38    /// Raw cell size (units of 2^-64) when `1 / cell_size` is not
39    /// representable (`|raw| <= 2`); the cell coordinate is then the exact
40    /// floor quotient `x_raw / cell_raw`.
41    tiny_cell_raw: Option<i128>,
42    /// Flat particle index buffer (CSR values).
43    indices: Vec<usize>,
44    /// Cell start offsets in `indices`; length = total_cells + 1.
45    cell_offsets: Vec<usize>,
46    /// Per-cell counts used during the two-pass build.
47    counts: Vec<usize>,
48    grid_dim: usize,
49    grid_half: i64,
50    total_cells: usize,
51}
52
53impl SpatialGrid {
54    /// Create a new spatial grid.
55    ///
56    /// - `cell_size`: Side length of each cubic cell (should match the
57    ///   interaction radius for best performance).
58    /// - `grid_dim`: Number of cells along each axis. Total cells = `grid_dim^3`.
59    #[must_use]
60    pub fn new(cell_size: Fix128, grid_dim: usize) -> Self {
61        let inv_cell = if cell_size.is_zero() {
62            Fix128::ONE
63        } else {
64            Fix128::ONE / cell_size
65        };
66        let cell_raw = fix_raw(cell_size);
67        let tiny_cell_raw = (cell_raw != 0 && cell_raw.unsigned_abs() <= 2).then_some(cell_raw);
68        let grid_half = (grid_dim as i64) / 2;
69        let total_cells = grid_dim * grid_dim * grid_dim;
70        Self {
71            inv_cell_size: inv_cell,
72            tiny_cell_raw,
73            indices: Vec::new(),
74            cell_offsets: vec![0; total_cells + 1],
75            counts: vec![0; total_cells],
76            grid_dim,
77            grid_half,
78            total_cells,
79        }
80    }
81
82    /// Reset the grid for a fresh build pass.
83    pub fn clear(&mut self) {
84        self.counts.fill(0);
85        self.cell_offsets.fill(0);
86        self.indices.clear();
87    }
88
89    /// Cell coordinate along one axis:
90    /// `clamp(floor(x / cell_size) + grid_dim / 2, 0, grid_dim - 1)`.
91    ///
92    /// Total over the whole `Fix128` range, identical in debug and release:
93    ///
94    /// - `|x / cell_size| < 2^62` (every position that fits in a grid one can
95    ///   allocate): `floor` is taken from `x * (1 / cell_size)` exactly as
96    ///   before, so these cells are bit-unchanged.
97    /// - `|x / cell_size| >= 2^62`: the scaled coordinate (or the
98    ///   `+ grid_dim / 2` offset) does not fit in `i64`; the cell is the
99    ///   border cell on the side of the sign of `x / cell_size` (`0` or
100    ///   `grid_dim - 1`), which is what the clamp gives for the exact value
101    ///   because `grid_dim / 2 < 2^62` for any allocatable grid.
102    /// - `|cell_size| <= 2^-63` (`1 / cell_size` not representable): the cell
103    ///   is the exact floor quotient of the raw values.
104    ///
105    /// Requires `grid_dim > 0`.
106    #[inline(always)]
107    fn axis_cell(&self, x: Fix128) -> usize {
108        let gd = self.grid_dim as i64;
109        let half = self.grid_half;
110        let x_raw = fix_raw(x);
111        if let Some(c_raw) = self.tiny_cell_raw {
112            // floor(x_raw / c_raw); `checked_div` fails only for i128::MIN / -1
113            // whose quotient is +2^127 (beyond every cell)
114            let q = match x_raw.checked_div(c_raw) {
115                Some(q) if x_raw % c_raw != 0 && ((x_raw < 0) != (c_raw < 0)) => q - 1,
116                Some(q) => q,
117                None => return self.grid_dim - 1,
118            };
119            return q
120                .saturating_add(i128::from(half))
121                .clamp(0, i128::from(gd - 1)) as usize;
122        }
123        let inv_raw = fix_raw(self.inv_cell_size);
124        // |x * inv| < 2^62  <=>  |x_raw| * |inv_raw| < 2^(62 + 128)
125        if mul_u128_high(x_raw.unsigned_abs(), inv_raw.unsigned_abs()) < (1u128 << 62) {
126            return ((x * self.inv_cell_size).hi.saturating_add(half)).clamp(0, gd - 1) as usize;
127        }
128        if (x_raw < 0) != (inv_raw < 0) {
129            0
130        } else {
131            self.grid_dim - 1
132        }
133    }
134
135    /// Compute the cell index for a given position.
136    ///
137    /// Each axis is `clamp(floor(x / cell_size) + grid_dim / 2, 0, grid_dim - 1)`
138    /// for every `Fix128` input: positions far outside the grid (up to the
139    /// ends of the `Fix128` range, or with a cell size down to `2^-64`) land
140    /// in the border cell on their side, never panic and never wrap, in both
141    /// debug and release builds.
142    ///
143    /// A grid built with `grid_dim = 0` has no cells; every position hashes
144    /// to `0`, and callers already guard `h < total_cells` (which is also
145    /// `0`) before using the result.
146    #[inline(always)]
147    #[must_use]
148    pub fn hash(&self, pos: Vec3Fix) -> usize {
149        if self.grid_dim == 0 {
150            return 0;
151        }
152        let ix = self.axis_cell(pos.x);
153        let iy = self.axis_cell(pos.y);
154        let iz = self.axis_cell(pos.z);
155        ix + iy * self.grid_dim + iz * self.grid_dim * self.grid_dim
156    }
157
158    /// Record a particle insertion (pass 1: count only).
159    ///
160    /// Call `build()` after all insertions to finalize the CSR layout.
161    pub fn insert(&mut self, idx: usize, pos: Vec3Fix) {
162        let h = self.hash(pos);
163        if h < self.total_cells {
164            self.counts[h] += 1;
165            // Store (h, idx) temporarily in `indices` as interleaved pairs.
166            self.indices.push(h);
167            self.indices.push(idx);
168        }
169    }
170
171    /// Finalize the CSR layout after all `insert()` calls.
172    ///
173    /// Converts the temporary (cell, particle) pairs into a proper
174    /// prefix-sum offset table and sorted flat index buffer.
175    pub fn build(&mut self) {
176        // Prefix-sum counts → cell_offsets
177        let mut running = 0usize;
178        for h in 0..self.total_cells {
179            self.cell_offsets[h] = running;
180            running += self.counts[h];
181            self.counts[h] = 0; // reuse as write cursor below
182        }
183        self.cell_offsets[self.total_cells] = running;
184
185        // Scatter particle indices into a final sorted buffer
186        let n_pairs = self.indices.len() / 2;
187        let mut final_buf = vec![0usize; running];
188        for k in 0..n_pairs {
189            let h = self.indices[k * 2];
190            let idx = self.indices[k * 2 + 1];
191            let slot = self.cell_offsets[h] + self.counts[h];
192            final_buf[slot] = idx;
193            self.counts[h] += 1;
194        }
195        self.indices = final_buf;
196    }
197
198    /// Collect all particle indices in the 3x3x3 neighborhood of `pos`.
199    ///
200    /// Results are appended to `neighbors` (which is cleared first).
201    // LIMITATION(COV-PART-052): `_radius_sq` is reserved for future distance filtering.
202    /// `_radius_sq` is reserved for future distance filtering.
203    pub fn query_neighbors_into(
204        &self,
205        pos: Vec3Fix,
206        _radius_sq: Fix128,
207        neighbors: &mut Vec<usize>,
208    ) {
209        neighbors.clear();
210        if self.grid_dim == 0 {
211            return;
212        }
213        let cx = self.axis_cell(pos.x) as i32;
214        let cy = self.axis_cell(pos.y) as i32;
215        let cz = self.axis_cell(pos.z) as i32;
216
217        for dz in -1..=1 {
218            for dy in -1..=1 {
219                for dx in -1..=1 {
220                    let nx = cx + dx;
221                    let ny = cy + dy;
222                    let nz = cz + dz;
223                    if nx < 0 || ny < 0 || nz < 0 {
224                        continue;
225                    }
226                    let nx = nx as usize;
227                    let ny = ny as usize;
228                    let nz = nz as usize;
229                    if nx >= self.grid_dim || ny >= self.grid_dim || nz >= self.grid_dim {
230                        continue;
231                    }
232
233                    let h = nx + ny * self.grid_dim + nz * self.grid_dim * self.grid_dim;
234                    let start = self.cell_offsets[h];
235                    let end = self.cell_offsets[h + 1];
236                    for &idx in &self.indices[start..end] {
237                        neighbors.push(idx);
238                    }
239                }
240            }
241        }
242    }
243}
244
245/// The signed 128-bit raw value of a `Fix128` (units of 2^-64).
246#[inline(always)]
247fn fix_raw(v: Fix128) -> i128 {
248    (i128::from(v.hi) << 64) | i128::from(v.lo)
249}
250
251/// High 128 bits of the 256-bit product `a * b`.
252#[inline(always)]
253fn mul_u128_high(a: u128, b: u128) -> u128 {
254    const M: u128 = u64::MAX as u128;
255    let (a1, a0) = (a >> 64, a & M);
256    let (b1, b0) = (b >> 64, b & M);
257    let ll = a0 * b0;
258    let lh = a0 * b1;
259    let hl = a1 * b0;
260    let hh = a1 * b1;
261    let mid = (ll >> 64) + (lh & M) + (hl & M);
262    hh + (lh >> 64) + (hl >> 64) + (mid >> 64)
263}
264
265// ============================================================================
266// Tests
267// ============================================================================
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_spatial_grid_basic() {
275        let mut grid = SpatialGrid::new(Fix128::from_ratio(1, 5), 32);
276        grid.insert(0, Vec3Fix::ZERO);
277        grid.insert(1, Vec3Fix::from_f32(0.1, 0.0, 0.0));
278        grid.insert(2, Vec3Fix::from_f32(10.0, 0.0, 0.0));
279        grid.build();
280
281        let h_sq = Fix128::from_ratio(1, 5) * Fix128::from_ratio(1, 5);
282        let mut neighbors = Vec::new();
283        grid.query_neighbors_into(Vec3Fix::ZERO, h_sq, &mut neighbors);
284        assert!(neighbors.contains(&0), "Should find self");
285        assert!(neighbors.contains(&1), "Should find nearby particle");
286    }
287
288    #[test]
289    fn test_spatial_grid_clear() {
290        let mut grid = SpatialGrid::new(Fix128::ONE, 8);
291        grid.insert(0, Vec3Fix::ZERO);
292        grid.build();
293        grid.clear();
294
295        let mut neighbors = Vec::new();
296        grid.query_neighbors_into(Vec3Fix::ZERO, Fix128::ONE, &mut neighbors);
297        assert!(neighbors.is_empty(), "Grid should be empty after clear");
298    }
299
300    #[test]
301    fn test_spatial_grid_hash_determinism() {
302        let grid = SpatialGrid::new(Fix128::from_ratio(1, 5), 32);
303        let pos = Vec3Fix::from_f32(1.5, -2.3, 0.7);
304        let h1 = grid.hash(pos);
305        let h2 = grid.hash(pos);
306        assert_eq!(h1, h2, "Hash must be deterministic");
307    }
308
309    fn on_x(x: Fix128) -> Vec3Fix {
310        Vec3Fix::new(x, Fix128::ZERO, Fix128::ZERO)
311    }
312
313    /// oracle: a zero cell size is treated as 1, so on a grid of 4
314    /// (`grid_dim / 2 = 2`) `x = 3/2` falls in cell `floor(3/2) + 2 = 3` and
315    /// `x = −3/2` in `−2 + 2 = 0` (`y = z = 0` give cell 2 on those axes).
316    #[test]
317    fn zero_cell_size_hashes_as_unit_cells() {
318        let g = SpatialGrid::new(Fix128::ZERO, 4);
319        let base = 2 * 4 + 2 * 16;
320        assert_eq!(g.hash(on_x(Fix128::from_ratio(3, 2))), 3 + base);
321        assert_eq!(g.hash(on_x(Fix128::from_ratio(-3, 2))), base);
322    }
323
324    /// oracle: for a cell of 2 raw units (`2⁻⁶³`, its reciprocal is not
325    /// representable) the cell is the floor quotient of the raw values plus
326    /// 2: raw 3 → `1 + 2 = 3`, raw −1 → `−1 + 2 = 1`, raw −3 → `−2 + 2 = 0`.
327    /// A cell of −1 raw unit and the most negative position (whose quotient
328    /// `+2¹²⁷` does not fit) gives the last cell.
329    #[test]
330    fn tiny_cells_use_the_exact_raw_floor_quotient() {
331        let g = SpatialGrid::new(Fix128::from_raw(0, 2), 4);
332        let base = 2 * 4 + 2 * 16;
333        assert_eq!(g.hash(on_x(Fix128::from_raw(0, 3))), 3 + base);
334        assert_eq!(g.hash(on_x(Fix128::from_raw(-1, u64::MAX))), 1 + base);
335        assert_eq!(g.hash(on_x(Fix128::from_raw(-1, u64::MAX - 2))), base);
336        let neg = SpatialGrid::new(Fix128::from_raw(-1, u64::MAX), 4);
337        assert_eq!(neg.hash(on_x(Fix128::from_raw(i64::MIN, 0))), 3 + base);
338    }
339
340    /// oracle: with cell 1 a coordinate of `±2⁶²` (beyond `2⁶²` scaled) lands
341    /// in the border cell of its sign; with cell −1 the sign flips. A grid
342    /// of 0 cells hashes everything to 0 and finds no neighbours.
343    #[test]
344    fn far_coordinates_and_an_empty_grid() {
345        let big = Fix128::from_int(1 << 62);
346        let g = SpatialGrid::new(Fix128::ONE, 4);
347        let base = 2 * 4 + 2 * 16;
348        assert_eq!(g.hash(on_x(big)), 3 + base);
349        assert_eq!(g.hash(on_x(-big)), base);
350        let flipped = SpatialGrid::new(-Fix128::ONE, 4);
351        assert_eq!(flipped.hash(on_x(big)), base);
352        let empty = SpatialGrid::new(Fix128::ONE, 0);
353        assert_eq!(empty.hash(on_x(big)), 0);
354        let mut out = vec![7];
355        empty.query_neighbors_into(Vec3Fix::ZERO, Fix128::ONE, &mut out);
356        assert!(out.is_empty());
357    }
358}