Skip to main content

brepkit_math/
simd.rs

1//! SIMD-friendly batch math operations.
2//!
3//! When the `simd` feature is enabled, these functions structure computations
4//! to encourage auto-vectorization by the compiler. On `wasm32` with
5//! `-C target-feature=+simd128`, LLVM will use SIMD instructions.
6
7use crate::mat::Mat4;
8use crate::vec::{Point3, Vec3};
9
10/// Batch transform: apply a [`Mat4`] to an array of points.
11///
12/// Processing points in batches enables auto-vectorization.
13pub fn batch_transform_points(mat: &Mat4, points: &[Point3], out: &mut Vec<Point3>) {
14    out.clear();
15    out.reserve(points.len());
16    for p in points {
17        out.push(mat.mul_point(*p));
18    }
19}
20
21/// Batch dot products: compute `dot(a[i], b[i])` for parallel arrays.
22///
23/// # Panics
24///
25/// Debug-asserts that `a` and `b` have equal length.
26#[must_use]
27pub fn batch_dot(a: &[Vec3], b: &[Vec3]) -> Vec<f64> {
28    debug_assert_eq!(a.len(), b.len());
29    a.iter().zip(b.iter()).map(|(ai, bi)| ai.dot(*bi)).collect()
30}
31
32/// Batch cross products: compute `cross(a[i], b[i])` for parallel arrays.
33///
34/// # Panics
35///
36/// Debug-asserts that `a` and `b` have equal length.
37#[must_use]
38pub fn batch_cross(a: &[Vec3], b: &[Vec3]) -> Vec<Vec3> {
39    debug_assert_eq!(a.len(), b.len());
40    a.iter()
41        .zip(b.iter())
42        .map(|(ai, bi)| ai.cross(*bi))
43        .collect()
44}
45
46/// Batch normalize: normalize an array of vectors.
47///
48/// Zero-length vectors are left as-is.
49#[must_use]
50pub fn batch_normalize(vecs: &[Vec3]) -> Vec<Vec3> {
51    vecs.iter().map(|v| v.normalize().unwrap_or(*v)).collect()
52}
53
54/// Batch squared distances between corresponding point pairs.
55///
56/// # Panics
57///
58/// Debug-asserts that `a` and `b` have equal length.
59#[must_use]
60pub fn batch_distance_sq(a: &[Point3], b: &[Point3]) -> Vec<f64> {
61    debug_assert_eq!(a.len(), b.len());
62    a.iter()
63        .zip(b.iter())
64        .map(|(ai, bi)| {
65            let d = *ai - *bi;
66            d.length_squared()
67        })
68        .collect()
69}
70
71#[cfg(test)]
72#[allow(clippy::expect_used, clippy::unwrap_used)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn batch_transform_matches_individual() {
78        let mat = Mat4::translation(1.0, 2.0, 3.0) * Mat4::rotation_z(std::f64::consts::FRAC_PI_4);
79        let points = vec![
80            Point3::new(1.0, 0.0, 0.0),
81            Point3::new(0.0, 1.0, 0.0),
82            Point3::new(0.0, 0.0, 1.0),
83            Point3::new(3.0, 4.0, 5.0),
84        ];
85
86        let mut batch_out = Vec::new();
87        batch_transform_points(&mat, &points, &mut batch_out);
88
89        for (i, p) in points.iter().enumerate() {
90            let expected = mat.mul_point(*p);
91            assert!(
92                (batch_out[i].x() - expected.x()).abs() < 1e-14,
93                "x mismatch at {i}"
94            );
95            assert!(
96                (batch_out[i].y() - expected.y()).abs() < 1e-14,
97                "y mismatch at {i}"
98            );
99            assert!(
100                (batch_out[i].z() - expected.z()).abs() < 1e-14,
101                "z mismatch at {i}"
102            );
103        }
104    }
105
106    #[test]
107    fn batch_dot_matches_individual() {
108        let a = vec![
109            Vec3::new(1.0, 2.0, 3.0),
110            Vec3::new(4.0, 5.0, 6.0),
111            Vec3::new(-1.0, 0.0, 1.0),
112        ];
113        let b = vec![
114            Vec3::new(4.0, 5.0, 6.0),
115            Vec3::new(1.0, 2.0, 3.0),
116            Vec3::new(2.0, 3.0, 4.0),
117        ];
118
119        let results = batch_dot(&a, &b);
120
121        for (i, (ai, bi)) in a.iter().zip(b.iter()).enumerate() {
122            let expected = ai.dot(*bi);
123            assert!((results[i] - expected).abs() < 1e-14, "dot mismatch at {i}");
124        }
125    }
126
127    #[test]
128    fn batch_distance_matches_individual() {
129        let a = vec![
130            Point3::new(1.0, 2.0, 3.0),
131            Point3::new(0.0, 0.0, 0.0),
132            Point3::new(-1.0, -2.0, -3.0),
133        ];
134        let b = vec![
135            Point3::new(4.0, 6.0, 3.0),
136            Point3::new(1.0, 1.0, 1.0),
137            Point3::new(1.0, 2.0, 3.0),
138        ];
139
140        let results = batch_distance_sq(&a, &b);
141
142        for (i, (ai, bi)) in a.iter().zip(b.iter()).enumerate() {
143            let d = *ai - *bi;
144            let expected = d.length_squared();
145            assert!(
146                (results[i] - expected).abs() < 1e-14,
147                "distance_sq mismatch at {i}"
148            );
149        }
150    }
151}