From 55685fcd7f31a214e9929384ecfa3874c72292b2 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Sun, 7 Sep 2025 00:11:45 +0200 Subject: [PATCH 1/9] test: add tests for Matrix*:transform_*() --- tests/core/cg.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/tests/core/cg.rs b/tests/core/cg.rs index b061efbba..a0d41a53e 100644 --- a/tests/core/cg.rs +++ b/tests/core/cg.rs @@ -1,4 +1,4 @@ -use na::{Matrix3, Matrix4, Point2, Point3, Vector2, Vector3}; +use na::{Matrix3, Matrix4, Orthographic3, Perspective3, Point2, Point3, Vector2, Vector3}; /// See Example 3.4 of "Graphics and Visualization: Principles & Algorithms" /// by Theoharis, Papaioannou, Platis, Patrikalakis. @@ -57,3 +57,58 @@ fn test_scaling_wrt_point_3() { assert!(result == expected); } + +#[test] +fn test_perspective_transform_vector() { + let vector = Vector3::new(1.0, 2.0, 3.0); + let perspective = Perspective3::new(2.0, 45.0, 1.0, 1000.0); + + let transformed = perspective.as_matrix().transform_vector(&vector); + + let multiplied = perspective.as_matrix() * vector.to_homogeneous(); + let multiplied = multiplied / multiplied.w; + + assert_relative_eq!(transformed, perspective.project_vector(&vector)); + assert_relative_eq!(transformed.push(1.0), multiplied); +} + +#[test] +fn test_perspective_transform_point3() { + let point = Point3::new(1.0, 2.0, 3.0); + let perspective = Perspective3::new(2.0, 45.0, 1.0, 1000.0); + + let transformed = perspective.as_matrix().transform_point(&point); + + let multiplied = perspective.as_matrix() * point.to_homogeneous(); + let multiplied = multiplied / multiplied.w; + + assert_relative_eq!(transformed, perspective.project_point(&point)); + assert_relative_eq!(transformed.coords.push(1.0), multiplied); +} + +#[test] +fn test_orthographic_transform_vector() { + let vector = Vector3::new(1.0, 2.0, 3.0); + let orthographic = Orthographic3::from_fov(2.0, 45.0, 1.0, 1000.0); + + let transformed = orthographic.as_matrix().transform_vector(&vector); + + let multiplied = orthographic.as_matrix() * vector.to_homogeneous(); + + assert_relative_eq!(transformed, orthographic.project_vector(&vector)); + assert_relative_eq!(transformed.push(0.0), multiplied); +} + +#[test] +fn test_orthographic_transform_point3() { + let point = Point3::new(1.0, 2.0, 3.0); + let orthographic = Orthographic3::from_fov(2.0, 45.0, 1.0, 1000.0); + + let transformed = orthographic.as_matrix().transform_point(&point); + + let multiplied = orthographic.as_matrix() * point.to_homogeneous(); + let multiplied = multiplied / multiplied.w; + + assert_relative_eq!(transformed, orthographic.project_point(&point)); + assert_relative_eq!(transformed.coords.push(1.0), multiplied); +} From bd9b9138b87d5f7884dfb77596ae1e1b1107579a Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Mon, 8 Sep 2025 02:48:17 +0200 Subject: [PATCH 2/9] fix: negate `z` component in Perspective3::project_vector() This makes Perspective3::project_vector() consistent with the same perspective projection applied by just multiplying the underlying matrix by source vector converted to homogeneous coordinates. --- src/geometry/perspective.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/geometry/perspective.rs b/src/geometry/perspective.rs index af59f66e9..2a4920e49 100644 --- a/src/geometry/perspective.rs +++ b/src/geometry/perspective.rs @@ -271,7 +271,7 @@ impl Perspective3 { Vector3::new( self.matrix[(0, 0)].clone() * p[0].clone() * inverse_denom.clone(), self.matrix[(1, 1)].clone() * p[1].clone() * inverse_denom, - self.matrix[(2, 2)].clone(), + -self.matrix[(2, 2)].clone(), ) } From 7323d189d98278f327c897ad414c6e6796cca766 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Sun, 7 Sep 2025 02:45:16 +0200 Subject: [PATCH 3/9] test: add tests for {Orthographic3, Perspective3}::project_vector() --- tests/geometry/projection.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/geometry/projection.rs b/tests/geometry/projection.rs index 1e0c9fd51..487199209 100644 --- a/tests/geometry/projection.rs +++ b/tests/geometry/projection.rs @@ -35,7 +35,7 @@ fn perspective_matrix_point_transformation() { #[cfg(feature = "proptest-support")] mod proptest_tests { - use na::{Orthographic3, Perspective3}; + use na::{Orthographic3, Perspective3, Point3}; use crate::proptest::*; use proptest::{prop_assert, proptest}; @@ -60,5 +60,31 @@ mod proptest_tests { prop_assert!(relative_eq!(pt, unprojected, epsilon = 1.0e-7)) } + + #[test] + fn perspective_project_vector(pt in point3(), vec in vector2()) { + let proj = Perspective3::new(800.0 / 600.0, 3.14 / 2.0, 1.0, 1000.0); + + let proj_pt = proj.project_point(&pt); + let proj_vec = proj.project_vector(&vec.push(pt.z)); + let proj_pt2 = proj.project_point(&(pt + vec.push(0.0))); + + let proj_pt_plus_proj_vec = Point3::from((proj_pt.xy() + proj_vec.xy()).coords.push(proj_pt.z)); + + prop_assert!(relative_eq!(proj_pt_plus_proj_vec, proj_pt2, epsilon = 1.0e-7)) + } + + #[test] + fn orthographic_project_vector(pt in point3(), vec in vector3()) { + let proj = Orthographic3::new(1.0, 2.0, -3.0, -2.5, 10.0, 900.0); + + let proj_pt = proj.project_point(&pt); + let proj_vec = proj.project_vector(&vec); + let proj_pt2 = proj.project_point(&(pt + vec)); + + let proj_pt_plus_proj_vec = proj_pt + proj_vec; + + prop_assert!(relative_eq!(proj_pt_plus_proj_vec, proj_pt2, epsilon = 1.0e-7)) + } } } From 5723f793931cd11b44303acca63f7c4180216d79 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Mon, 8 Sep 2025 09:47:00 +0200 Subject: [PATCH 4/9] test: `codegen-units = 1` for benchmarks During benchmarking I found that `codegen-units` with default value leads to inconsistent results across recompilations (clean vs. incremental). Also, sometimes it leads to a significant performance degradation of benchmarks unrelated to code changes. --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 6f31a54f5..49781efa5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -176,6 +176,7 @@ required-features = ["rand"] [profile.bench] lto = true +codegen-units = 1 [package.metadata.docs.rs] # Enable all the features when building the docs on docs.rs From 7b7a81bd2d102f43c541008a9dce3d035b6f92bd Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Sat, 6 Sep 2025 23:10:28 +0200 Subject: [PATCH 5/9] test: add benchmarks for Matrix*:transform*() --- benches/core/cg.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++ benches/core/mod.rs | 2 ++ benches/lib.rs | 1 + 3 files changed, 58 insertions(+) create mode 100644 benches/core/cg.rs diff --git a/benches/core/cg.rs b/benches/core/cg.rs new file mode 100644 index 000000000..e8b602bfa --- /dev/null +++ b/benches/core/cg.rs @@ -0,0 +1,55 @@ +use na::{Matrix3, Matrix4, Orthographic3, Point2, Point3, Vector2, Vector3}; +use rand::{Rng, SeedableRng}; +use rand_isaac::IsaacRng; + +#[path = "../common/macros.rs"] +mod macros; + +bench_binop_ref!( + mat3_transform_vector2, + Matrix3, + Vector2, + transform_vector +); +bench_binop_ref!( + mat4_transform_vector3, + Matrix4, + Vector3, + transform_vector +); +bench_binop_ref!( + mat3_transform_point2, + Matrix3, + Point2, + transform_point +); +bench_binop_ref!( + mat4_transform_point3, + Matrix4, + Point3, + transform_point +); + +fn mat4_transform_vector3_no_division(bench: &mut criterion::Criterion) { + let mut rng = IsaacRng::seed_from_u64(0); + let orthographic = Orthographic3::from_fov( + rng.random_range(0.5..2.0), + rng.random_range(30.0..90.0), + rng.random_range(0.5..1.5), + rng.random_range(2.0..1000.0), + ) + .to_homogeneous(); + let vector = rng.random(); + bench.bench_function("mat4_transform_vector3_no_division", move |bh| { + bh.iter(|| orthographic.transform_vector(&vector)) + }); +} + +criterion_group!( + cg, + mat3_transform_vector2, + mat4_transform_vector3, + mat3_transform_point2, + mat4_transform_point3, + mat4_transform_vector3_no_division, +); diff --git a/benches/core/mod.rs b/benches/core/mod.rs index eda9ddaac..213442298 100644 --- a/benches/core/mod.rs +++ b/benches/core/mod.rs @@ -1,5 +1,7 @@ +pub use self::cg::cg; pub use self::matrix::matrix; pub use self::vector::vector; +mod cg; mod matrix; mod vector; diff --git a/benches/lib.rs b/benches/lib.rs index df866cca3..db2585fd9 100644 --- a/benches/lib.rs +++ b/benches/lib.rs @@ -21,6 +21,7 @@ fn reproducible_dmatrix(nrows: usize, ncols: usize) -> DMatrix { } criterion_main!( + core::cg, core::matrix, core::vector, geometry::quaternion, From 9fba307686391624ba24c65d8cf53889a3acaafd Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Mon, 8 Sep 2025 04:44:44 +0200 Subject: [PATCH 6/9] feat: AoSoA SIMD Matrix*:simd_transform_*() --- src/base/cg.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ tests/core/cg.rs | 44 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/src/base/cg.rs b/src/base/cg.rs index 8e961e72c..4a672e79c 100644 --- a/src/base/cg.rs +++ b/src/base/cg.rs @@ -20,6 +20,7 @@ use crate::geometry::{ }; use simba::scalar::{ClosedAddAssign, ClosedMulAssign, RealField}; +use simba::simd::SimdRealField; /// # Translation and scaling in any dimension impl OMatrix @@ -423,6 +424,32 @@ where } } +/// # Transformation of vectors and points +impl, S: Storage> SquareMatrix +where + DefaultAllocator: Allocator + + Allocator> + + Allocator, DimNameDiff>, +{ + /// Transforms the given vector, assuming the matrix `self` uses homogeneous coordinates. + #[inline] + pub fn simd_transform_vector( + &self, + v: &OVector>, + ) -> OVector> { + let transform = self.generic_view( + (0, 0), + (DimNameDiff::::name(), DimNameDiff::::name()), + ); + let normalizer = + self.generic_view((D::DIM - 1, 0), (Const::<1>, DimNameDiff::::name())); + let n = normalizer.tr_dot(v); + + let n = n.clone().select(n.simd_ne(T::zero()), T::one()); + transform * (v / n) + } +} + impl, Const<3>>> SquareMatrix, S> { /// Transforms the given point, assuming the matrix `self` uses homogeneous coordinates. #[inline] @@ -440,6 +467,20 @@ impl, Const<3>>> SquareMatrix, } } +impl, Const<3>>> SquareMatrix, S> { + /// Transforms the given point, assuming the matrix `self` uses homogeneous coordinates. + #[inline] + pub fn simd_transform_point(&self, pt: &Point) -> Point { + let transform = self.fixed_view::<2, 2>(0, 0); + let translation = self.fixed_view::<2, 1>(0, 2); + let normalizer = self.fixed_view::<1, 2>(2, 0); + let n = normalizer.tr_dot(&pt.coords) + unsafe { self.get_unchecked((2, 2)).clone() }; + + let n = n.clone().select(n.simd_ne(T::zero()), T::one()); + (transform * pt + translation) / n + } +} + impl, Const<4>>> SquareMatrix, S> { /// Transforms the given point, assuming the matrix `self` uses homogeneous coordinates. #[inline] @@ -456,3 +497,17 @@ impl, Const<4>>> SquareMatrix, } } } + +impl, Const<4>>> SquareMatrix, S> { + /// Transforms the given point, assuming the matrix `self` uses homogeneous coordinates. + #[inline] + pub fn simd_transform_point(&self, pt: &Point) -> Point { + let transform = self.fixed_view::<3, 3>(0, 0); + let translation = self.fixed_view::<3, 1>(0, 3); + let normalizer = self.fixed_view::<1, 3>(3, 0); + let n = normalizer.tr_dot(&pt.coords) + unsafe { self.get_unchecked((3, 3)).clone() }; + + let n = n.clone().select(n.simd_ne(T::zero()), T::one()); + (transform * pt + translation) / n + } +} diff --git a/tests/core/cg.rs b/tests/core/cg.rs index a0d41a53e..5908c8f27 100644 --- a/tests/core/cg.rs +++ b/tests/core/cg.rs @@ -72,6 +72,17 @@ fn test_perspective_transform_vector() { assert_relative_eq!(transformed.push(1.0), multiplied); } +#[test] +fn test_perspective_simd_transform_vector() { + let vector = Vector3::new(1.0, 2.0, 3.0); + let perspective = Perspective3::new(2.0, 45.0, 1.0, 1000.0); + + assert_relative_eq!( + perspective.as_matrix().simd_transform_vector(&vector), + perspective.as_matrix().transform_vector(&vector) + ); +} + #[test] fn test_perspective_transform_point3() { let point = Point3::new(1.0, 2.0, 3.0); @@ -86,6 +97,17 @@ fn test_perspective_transform_point3() { assert_relative_eq!(transformed.coords.push(1.0), multiplied); } +#[test] +fn test_perspective_simd_transform_point3() { + let point = Point3::new(1.0, 2.0, 3.0); + let perspective = Perspective3::new(2.0, 45.0, 1.0, 1000.0); + + assert_relative_eq!( + perspective.as_matrix().simd_transform_point(&point), + perspective.as_matrix().transform_point(&point) + ); +} + #[test] fn test_orthographic_transform_vector() { let vector = Vector3::new(1.0, 2.0, 3.0); @@ -99,6 +121,17 @@ fn test_orthographic_transform_vector() { assert_relative_eq!(transformed.push(0.0), multiplied); } +#[test] +fn test_orthographic_simd_transform_vector() { + let vector = Vector3::new(1.0, 2.0, 3.0); + let orthographic = Orthographic3::from_fov(2.0, 45.0, 1.0, 1000.0); + + assert_relative_eq!( + orthographic.as_matrix().simd_transform_vector(&vector), + orthographic.as_matrix().transform_vector(&vector) + ); +} + #[test] fn test_orthographic_transform_point3() { let point = Point3::new(1.0, 2.0, 3.0); @@ -112,3 +145,14 @@ fn test_orthographic_transform_point3() { assert_relative_eq!(transformed, orthographic.project_point(&point)); assert_relative_eq!(transformed.coords.push(1.0), multiplied); } + +#[test] +fn test_orthographic_simd_transform_point3() { + let point = Point3::new(1.0, 2.0, 3.0); + let orthographic = Orthographic3::from_fov(2.0, 45.0, 1.0, 1000.0); + + assert_relative_eq!( + orthographic.as_matrix().simd_transform_point(&point), + orthographic.as_matrix().transform_point(&point) + ); +} From 2df9b17346d42797ec7c2b091301fccee477fc49 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Mon, 8 Sep 2025 09:23:22 +0200 Subject: [PATCH 7/9] test: add SIMD benchmarks for Matrix*::simd_transform_*() Following benchmarks were perfomed on Linux after `cpupower frequency-set --governor performance` and with default RUSTFLAGS. AMD Ryzen 9 5950X: mat3_transform_vector2 time: [314.35 ps 314.44 ps 314.56 ps] Found 11 outliers among 100 measurements (11.00%) 3 (3.00%) high mild 8 (8.00%) high severe mat4_transform_vector3 time: [440.44 ps 440.95 ps 441.45 ps] Found 13 outliers among 100 measurements (13.00%) 1 (1.00%) high mild 12 (12.00%) high severe mat3_transform_point2 time: [314.40 ps 314.48 ps 314.60 ps] Found 9 outliers among 100 measurements (9.00%) 4 (4.00%) high mild 5 (5.00%) high severe mat4_transform_point3 time: [436.98 ps 437.03 ps 437.08 ps] Found 8 outliers among 100 measurements (8.00%) 1 (1.00%) low mild 2 (2.00%) high mild 5 (5.00%) high severe mat3_transform_vector2_x4wide time: [422.74 ps 422.85 ps 422.98 ps] Found 9 outliers among 100 measurements (9.00%) 1 (1.00%) low mild 3 (3.00%) high mild 5 (5.00%) high severe mat4_transform_vector3_x4wide time: [635.36 ps 635.49 ps 635.63 ps] Found 7 outliers among 100 measurements (7.00%) 1 (1.00%) low mild 4 (4.00%) high mild 2 (2.00%) high severe mat3_transform_point2_x4wide time: [422.21 ps 422.31 ps 422.47 ps] Found 12 outliers among 100 measurements (12.00%) 2 (2.00%) low mild 4 (4.00%) high mild 6 (6.00%) high severe mat4_transform_point3_x4wide time: [635.19 ps 635.27 ps 635.37 ps] Found 10 outliers among 100 measurements (10.00%) 1 (1.00%) low severe 4 (4.00%) high mild 5 (5.00%) high severe mat4_transform_vector3_no_division time: [439.86 ps 439.96 ps 440.07 ps] Found 13 outliers among 100 measurements (13.00%) 2 (2.00%) low severe 1 (1.00%) low mild 4 (4.00%) high mild 6 (6.00%) high severe Intel Core i7-8565U: mat3_transform_vector2 time: [294.41 ps 294.52 ps 294.63 ps] Found 7 outliers among 100 measurements (7.00%) 1 (1.00%) low severe 4 (4.00%) high mild 2 (2.00%) high severe mat4_transform_vector3 time: [583.88 ps 587.06 ps 592.15 ps] Found 20 outliers among 100 measurements (20.00%) 4 (4.00%) low severe 1 (1.00%) low mild 3 (3.00%) high mild 12 (12.00%) high severe mat3_transform_point2 time: [309.23 ps 309.52 ps 309.88 ps] Found 9 outliers among 100 measurements (9.00%) 2 (2.00%) low severe 2 (2.00%) high mild 5 (5.00%) high severe mat4_transform_point3 time: [557.59 ps 558.26 ps 559.52 ps] Found 15 outliers among 100 measurements (15.00%) 3 (3.00%) low severe 7 (7.00%) low mild 3 (3.00%) high mild 2 (2.00%) high severe mat3_transform_vector2_x4wide time: [557.77 ps 558.22 ps 558.75 ps] Found 3 outliers among 100 measurements (3.00%) 1 (1.00%) low severe 1 (1.00%) low mild 1 (1.00%) high severe mat4_transform_vector3_x4wide time: [801.89 ps 802.37 ps 802.89 ps] Found 8 outliers among 100 measurements (8.00%) 1 (1.00%) low severe 4 (4.00%) high mild 3 (3.00%) high severe mat3_transform_point2_x4wide time: [574.76 ps 575.10 ps 575.44 ps] Found 12 outliers among 100 measurements (12.00%) 2 (2.00%) low severe 3 (3.00%) low mild 1 (1.00%) high mild 6 (6.00%) high severe mat4_transform_point3_x4wide time: [801.65 ps 802.63 ps 803.94 ps] Found 9 outliers among 100 measurements (9.00%) 1 (1.00%) low severe 2 (2.00%) low mild 2 (2.00%) high mild 4 (4.00%) high severe mat4_transform_vector3_no_division time: [568.86 ps 569.60 ps 570.52 ps] Found 3 outliers among 100 measurements (3.00%) 2 (2.00%) high mild 1 (1.00%) high severe Apple M2 Max: mat3_transform_vector2 time: [316.93 ps 317.90 ps 318.91 ps] Found 3 outliers among 100 measurements (3.00%) 2 (2.00%) high mild 1 (1.00%) high severe mat4_transform_vector3 time: [383.72 ps 383.77 ps 383.83 ps] Found 9 outliers among 100 measurements (9.00%) 5 (5.00%) high mild 4 (4.00%) high severe mat3_transform_point2 time: [317.60 ps 318.44 ps 319.31 ps] Found 3 outliers among 100 measurements (3.00%) 1 (1.00%) low mild 2 (2.00%) high mild mat4_transform_point3 time: [383.75 ps 383.78 ps 383.81 ps] Found 4 outliers among 100 measurements (4.00%) 1 (1.00%) high mild 3 (3.00%) high severe mat3_transform_vector2_x4wide time: [308.93 ps 309.36 ps 309.80 ps] Found 2 outliers among 100 measurements (2.00%) 1 (1.00%) high mild 1 (1.00%) high severe mat4_transform_vector3_x4wide time: [460.46 ps 460.50 ps 460.55 ps] Found 16 outliers among 100 measurements (16.00%) 6 (6.00%) low mild 5 (5.00%) high mild 5 (5.00%) high severe mat3_transform_point2_x4wide time: [308.88 ps 309.27 ps 309.69 ps] Found 2 outliers among 100 measurements (2.00%) 2 (2.00%) high mild mat4_transform_point3_x4wide time: [460.48 ps 460.52 ps 460.57 ps] Found 5 outliers among 100 measurements (5.00%) 3 (3.00%) high mild 2 (2.00%) high severe mat4_transform_vector3_no_division time: [383.77 ps 383.86 ps 383.98 ps] Found 9 outliers among 100 measurements (9.00%) 4 (4.00%) high mild 5 (5.00%) high severe Broadcom BCM2711 (Raspberry Pi 4): mat3_transform_vector2 time: [1.1169 ns 1.1172 ns 1.1175 ns] Found 11 outliers among 100 measurements (11.00%) 4 (4.00%) high mild 7 (7.00%) high severe mat4_transform_vector3 time: [1.6819 ns 1.6821 ns 1.6824 ns] Found 11 outliers among 100 measurements (11.00%) 8 (8.00%) high mild 3 (3.00%) high severe mat3_transform_point2 time: [1.1168 ns 1.1169 ns 1.1171 ns] Found 10 outliers among 100 measurements (10.00%) 7 (7.00%) high mild 3 (3.00%) high severe mat4_transform_point3 time: [1.6818 ns 1.6820 ns 1.6823 ns] Found 8 outliers among 100 measurements (8.00%) 5 (5.00%) high mild 3 (3.00%) high severe mat3_transform_vector2_x4wide time: [2.2615 ns 2.2619 ns 2.2624 ns] Found 8 outliers among 100 measurements (8.00%) 5 (5.00%) high mild 3 (3.00%) high severe mat4_transform_vector3_x4wide time: [3.3941 ns 3.3947 ns 3.3954 ns] Found 4 outliers among 100 measurements (4.00%) 2 (2.00%) high mild 2 (2.00%) high severe mat3_transform_point2_x4wide time: [2.2615 ns 2.2619 ns 2.2622 ns] Found 8 outliers among 100 measurements (8.00%) 7 (7.00%) high mild 1 (1.00%) high severe mat4_transform_point3_x4wide time: [3.3943 ns 3.3957 ns 3.3973 ns] Found 11 outliers among 100 measurements (11.00%) 1 (1.00%) low mild 4 (4.00%) high mild 6 (6.00%) high severe mat4_transform_vector3_no_division time: [1.9711 ns 1.9719 ns 1.9728 ns] Found 11 outliers among 100 measurements (11.00%) 5 (5.00%) high mild 6 (6.00%) high severe rustc -vV rustc 1.89.0 (29483883e 2025-08-04) (Fedora 1.89.0-2.fc42) binary: rustc commit-hash: 29483883eed69d5fb4db01964cdf2af4d86e9cb2 commit-date: 2025-08-04 host: x86_64-unknown-linux-gnu release: 1.89.0 LLVM version: 20.1.8 rustc 1.89.0 (29483883e 2025-08-04) (Fedora 1.89.0-2.fc42) binary: rustc commit-hash: 29483883eed69d5fb4db01964cdf2af4d86e9cb2 commit-date: 2025-08-04 host: aarch64-unknown-linux-gnu release: 1.89.0 LLVM version: 20.1.8 --- Cargo.toml | 3 ++- benches/core/cg.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 49781efa5..d1dbb1e23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,6 +98,7 @@ arbitrary = ["quickcheck"] proptest-support = ["proptest"] slow-tests = [] rkyv-safe-deser = ["rkyv-serialize", "rkyv/validation"] +wide-tests = ["simba/wide", "simba/rand"] [dependencies] nalgebra-macros = { version = "0.3.0", path = "nalgebra-macros", optional = true } @@ -168,7 +169,7 @@ required-features = ["compare"] name = "nalgebra_bench" harness = false path = "benches/lib.rs" -required-features = ["rand"] +required-features = ["rand", "wide-tests"] #[profile.bench] #opt-level = 0 diff --git a/benches/core/cg.rs b/benches/core/cg.rs index e8b602bfa..e6b690697 100644 --- a/benches/core/cg.rs +++ b/benches/core/cg.rs @@ -1,6 +1,7 @@ use na::{Matrix3, Matrix4, Orthographic3, Point2, Point3, Vector2, Vector3}; use rand::{Rng, SeedableRng}; use rand_isaac::IsaacRng; +use simba::simd::WideF32x4; #[path = "../common/macros.rs"] mod macros; @@ -30,6 +31,31 @@ bench_binop_ref!( transform_point ); +bench_binop_ref!( + mat3_transform_vector2_x4wide, + Matrix3, + Vector2, + simd_transform_vector +); +bench_binop_ref!( + mat4_transform_vector3_x4wide, + Matrix4, + Vector3, + simd_transform_vector +); +bench_binop_ref!( + mat3_transform_point2_x4wide, + Matrix3, + Point2, + simd_transform_point +); +bench_binop_ref!( + mat4_transform_point3_x4wide, + Matrix4, + Point3, + simd_transform_point +); + fn mat4_transform_vector3_no_division(bench: &mut criterion::Criterion) { let mut rng = IsaacRng::seed_from_u64(0); let orthographic = Orthographic3::from_fov( @@ -51,5 +77,9 @@ criterion_group!( mat4_transform_vector3, mat3_transform_point2, mat4_transform_point3, + mat3_transform_vector2_x4wide, + mat4_transform_vector3_x4wide, + mat3_transform_point2_x4wide, + mat4_transform_point3_x4wide, mat4_transform_vector3_no_division, ); From 0b614acc0fbec7df97e8d16940d1dd9c778f6e46 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Mon, 8 Sep 2025 06:53:02 +0200 Subject: [PATCH 8/9] docs: document results of Matrix*::transform*() and Perspective3:project_*() --- src/base/cg.rs | 18 ++++++++++++++++++ src/geometry/perspective.rs | 9 +++++++++ 2 files changed, 27 insertions(+) diff --git a/src/base/cg.rs b/src/base/cg.rs index 4a672e79c..392f20852 100644 --- a/src/base/cg.rs +++ b/src/base/cg.rs @@ -403,6 +403,9 @@ where + Allocator, DimNameDiff>, { /// Transforms the given vector, assuming the matrix `self` uses homogeneous coordinates. + /// + /// Each component of the resulting vector is divided by the last component of the homogeneous + /// coordinates if it is not zero or returned unchanged otherwise. #[inline] pub fn transform_vector( &self, @@ -432,6 +435,9 @@ where + Allocator, DimNameDiff>, { /// Transforms the given vector, assuming the matrix `self` uses homogeneous coordinates. + /// + /// Each component of the resulting vector is divided by the last component of the homogeneous + /// coordinates if it is not zero or returned unchanged otherwise. #[inline] pub fn simd_transform_vector( &self, @@ -452,6 +458,9 @@ where impl, Const<3>>> SquareMatrix, S> { /// Transforms the given point, assuming the matrix `self` uses homogeneous coordinates. + /// + /// Each component of the resulting point is divided by the `z` component if it is not zero + /// or returned unchanged otherwise. #[inline] pub fn transform_point(&self, pt: &Point) -> Point { let transform = self.fixed_view::<2, 2>(0, 0); @@ -469,6 +478,9 @@ impl, Const<3>>> SquareMatrix, impl, Const<3>>> SquareMatrix, S> { /// Transforms the given point, assuming the matrix `self` uses homogeneous coordinates. + /// + /// Each component of the resulting point is divided by the `z` component if it is not zero + /// or returned unchanged otherwise. #[inline] pub fn simd_transform_point(&self, pt: &Point) -> Point { let transform = self.fixed_view::<2, 2>(0, 0); @@ -483,6 +495,9 @@ impl, Const<3>>> SquareMatrix, Const<4>>> SquareMatrix, S> { /// Transforms the given point, assuming the matrix `self` uses homogeneous coordinates. + /// + /// Each component of the resulting vector is divided by the `w` component if it is not zero + /// or returned unchanged otherwise. #[inline] pub fn transform_point(&self, pt: &Point) -> Point { let transform = self.fixed_view::<3, 3>(0, 0); @@ -500,6 +515,9 @@ impl, Const<4>>> SquareMatrix, impl, Const<4>>> SquareMatrix, S> { /// Transforms the given point, assuming the matrix `self` uses homogeneous coordinates. + /// + /// Each component of the resulting vector is divided by the `w` component if it is not zero + /// or returned unchanged otherwise. #[inline] pub fn simd_transform_point(&self, pt: &Point) -> Point { let transform = self.fixed_view::<3, 3>(0, 0); diff --git a/src/geometry/perspective.rs b/src/geometry/perspective.rs index 2a4920e49..bef735542 100644 --- a/src/geometry/perspective.rs +++ b/src/geometry/perspective.rs @@ -233,6 +233,8 @@ impl Perspective3 { // TODO: when we get specialization, specialize the Mul impl instead. /// Projects a point. Faster than matrix multiplication. + /// + /// Each component of the resulting point will be NaN or Inf if `p.z` is zero. #[inline] #[must_use] pub fn project_point(&self, p: &Point3) -> Point3 { @@ -261,6 +263,13 @@ impl Perspective3 { // TODO: when we get specialization, specialize the Mul impl instead. /// Projects a vector. Faster than matrix multiplication. + /// + /// `x` and `y` components of the result is a projection as a 2D vector. + /// + /// Components `x` and `y` of the resulting vector will be NaN or Inf if `p.z` is zero. + /// + /// `z` component of the resulting vector always equals to `-self.m33` and does + /// not depend on the components of `p`. #[inline] #[must_use] pub fn project_vector(&self, p: &Vector) -> Vector3 From fbba6576272f89b17f4471a5805d3db0b30066c3 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Tue, 9 Sep 2025 17:06:09 +0200 Subject: [PATCH 9/9] test: SIMD test for Matrix*::simd_transform_vector() --- .github/workflows/nalgebra-ci-build.yml | 4 +- tests/core/cg.rs | 51 +++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nalgebra-ci-build.yml b/.github/workflows/nalgebra-ci-build.yml index 1acd93e15..b3bf13ebc 100644 --- a/.github/workflows/nalgebra-ci-build.yml +++ b/.github/workflows/nalgebra-ci-build.yml @@ -64,7 +64,7 @@ jobs: override: true - uses: actions/checkout@v4 - name: check - run: cargo check --features arbitrary,rand,serde-serialize,sparse,debug,io,compare,libm,proptest-support,slow-tests,rkyv-safe-deser,rayon; + run: cargo check --features arbitrary,rand,serde-serialize,sparse,debug,io,compare,libm,proptest-support,slow-tests,wide-tests,rkyv-safe-deser,rayon; test-nalgebra: runs-on: ubuntu-latest # env: @@ -81,7 +81,7 @@ jobs: override: true - uses: actions/checkout@v4 - name: test - run: cargo test --features arbitrary,rand,serde-serialize,sparse,debug,io,compare,libm,proptest-support,slow-tests,rkyv-safe-deser,rayon; + run: cargo test --features arbitrary,rand,serde-serialize,sparse,debug,io,compare,libm,proptest-support,slow-tests,wide-tests,rkyv-safe-deser,rayon; test-nalgebra-glm: runs-on: ubuntu-latest steps: diff --git a/tests/core/cg.rs b/tests/core/cg.rs index 5908c8f27..2f1233934 100644 --- a/tests/core/cg.rs +++ b/tests/core/cg.rs @@ -1,5 +1,10 @@ use na::{Matrix3, Matrix4, Orthographic3, Perspective3, Point2, Point3, Vector2, Vector3}; +#[cfg(feature = "wide-tests")] +use na::{Rotation3, SimdValue, Translation3}; +#[cfg(feature = "wide-tests")] +use simba::simd::WideF32x4; + /// See Example 3.4 of "Graphics and Visualization: Principles & Algorithms" /// by Theoharis, Papaioannou, Platis, Patrikalakis. #[test] @@ -156,3 +161,49 @@ fn test_orthographic_simd_transform_point3() { orthographic.as_matrix().transform_point(&point) ); } + +#[cfg(feature = "wide-tests")] +#[test] +fn test_transform_vector_x4wide() { + let v1 = Vector3::new(0.0, 1.0, 2.0); + let v2 = Vector3::new(3.0, 4.0, 5.0); + let v3 = Vector3::new(6.0, 7.0, 8.0); + let v4 = Vector3::new(9.0, 10.0, 11.0); + + let wide_v = Vector3::new( + WideF32x4::from_arr([v1.x, v2.x, v3.x, v4.x]), + WideF32x4::from_arr([v1.y, v2.y, v3.y, v4.y]), + WideF32x4::from_arr([v1.z, v2.z, v3.z, v4.z]), + ); + + let m1 = Perspective3::new(2.0, 45.0, 1.0, 1000.0).to_homogeneous(); + let m2 = Orthographic3::from_fov(2.0, 45.0, 1.0, 1000.0).to_homogeneous(); + let m3 = Rotation3::from_axis_angle(&Vector3::y_axis(), 2.5).to_homogeneous(); + let m4 = Translation3::new(1.0, 2.0, 3.0).to_homogeneous(); + + let wide_m = Matrix4::new( + WideF32x4::from_arr([m1.m11, m2.m11, m3.m11, m4.m11]), + WideF32x4::from_arr([m1.m12, m2.m12, m3.m12, m4.m12]), + WideF32x4::from_arr([m1.m13, m2.m13, m3.m13, m4.m13]), + WideF32x4::from_arr([m1.m14, m2.m14, m3.m14, m4.m14]), + WideF32x4::from_arr([m1.m21, m2.m21, m3.m21, m4.m21]), + WideF32x4::from_arr([m1.m22, m2.m22, m3.m22, m4.m22]), + WideF32x4::from_arr([m1.m23, m2.m23, m3.m23, m4.m23]), + WideF32x4::from_arr([m1.m24, m2.m24, m3.m24, m4.m24]), + WideF32x4::from_arr([m1.m31, m2.m31, m3.m31, m4.m31]), + WideF32x4::from_arr([m1.m32, m2.m32, m3.m32, m4.m32]), + WideF32x4::from_arr([m1.m33, m2.m33, m3.m33, m4.m33]), + WideF32x4::from_arr([m1.m34, m2.m34, m3.m34, m4.m34]), + WideF32x4::from_arr([m1.m41, m2.m41, m3.m41, m4.m41]), + WideF32x4::from_arr([m1.m42, m2.m42, m3.m42, m4.m42]), + WideF32x4::from_arr([m1.m43, m2.m43, m3.m43, m4.m43]), + WideF32x4::from_arr([m1.m44, m2.m44, m3.m44, m4.m44]), + ); + + let wide_v = wide_m.simd_transform_vector(&wide_v); + + assert_eq!(wide_v.extract(0), m1.transform_vector(&v1)); + assert_eq!(wide_v.extract(1), m2.transform_vector(&v2)); + assert_eq!(wide_v.extract(2), m3.transform_vector(&v3)); + assert_eq!(wide_v.extract(3), m4.transform_vector(&v4)); +}