From f770f504b0ad6082edde86907cace6f071603e65 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Thu, 19 Oct 2023 23:33:15 -0400 Subject: [PATCH] Add approximate equal impls and tests More doctests instead of libtests. For now, just FVec4, but FVec4 and matrices coming next. --- citro3d/Cargo.toml | 3 +- citro3d/src/math/fvec.rs | 95 +++++++++++++++++++++++++++---------- citro3d/src/math/matrix.rs | 5 ++ citro3d/src/math/ops.rs | 97 +++++++++++++++++++++++++++++++------- 4 files changed, 157 insertions(+), 43 deletions(-) diff --git a/citro3d/Cargo.toml b/citro3d/Cargo.toml index c70473f..0762a4d 100644 --- a/citro3d/Cargo.toml +++ b/citro3d/Cargo.toml @@ -6,12 +6,13 @@ version = "0.1.0" edition = "2021" [dependencies] -citro3d-macros = { version = "0.1.0", path = "../citro3d-macros" } bitflags = "1.3.2" bytemuck = { version = "1.10.0", features = ["extern_crate_std"] } +citro3d-macros = { version = "0.1.0", path = "../citro3d-macros" } citro3d-sys = { git = "https://github.com/rust3ds/citro3d-rs.git" } ctru-rs = { git = "https://github.com/rust3ds/ctru-rs.git" } ctru-sys = { git = "https://github.com/rust3ds/ctru-rs.git" } +float-cmp = "0.9.0" libc = "0.2.125" [dev-dependencies] diff --git a/citro3d/src/math/fvec.rs b/citro3d/src/math/fvec.rs index 5b2df95..ba77954 100644 --- a/citro3d/src/math/fvec.rs +++ b/citro3d/src/math/fvec.rs @@ -15,45 +15,96 @@ pub type FVec4 = FVec<4>; impl fmt::Debug for FVec { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let inner = unsafe { self.0.__bindgen_anon_1 }; - f.debug_tuple(std::any::type_name::()) - .field(&inner) - .finish() + let type_name = std::any::type_name::().split("::").last().unwrap(); + f.debug_tuple(type_name).field(&inner).finish() } } impl FVec4 { /// Create a new [`FVec4`] from its components. + #[doc(alias = "FVec4_New")] pub fn new(x: f32, y: f32, z: f32, w: f32) -> Self { Self(unsafe { citro3d_sys::FVec4_New(x, y, z, w) }) } /// Create a new [`FVec4`], setting each component to `v`. + /// + /// # Example + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::FVec4; + /// # use float_cmp::assert_approx_eq; + /// let v = FVec4::splat(1.0); + /// assert_approx_eq!(FVec4, v, FVec4::new(1.0, 1.0, 1.0, 1.0)); + /// ``` pub fn splat(v: f32) -> Self { Self::new(v, v, v, v) } - /// The vector's `w` component (sometimes also called the `r` component of `ijk[r]`). + /// The vector's `w` component (sometimes also called `r` for the real + /// component of a quaternion `ijk[r]`). #[doc(alias = "r")] pub fn w(&self) -> f32 { unsafe { self.0.__bindgen_anon_1.w } } /// Divide the vector's XYZ components by its W component. + /// # Example + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::FVec4; + /// # use float_cmp::assert_approx_eq; + /// let v = FVec4::new(2.0, 4.0, 6.0, 2.0); + /// assert_approx_eq!( + /// FVec4, + /// v.perspective_divide(), + /// FVec4::new(1.0, 2.0, 3.0, 1.0) + /// ); + /// ``` + #[doc(alias = "FVec4_PerspDivide")] pub fn perspective_divide(&self) -> Self { Self(unsafe { citro3d_sys::FVec4_PerspDivide(self.0) }) } /// The dot product of two vectors. + /// # Example + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::FVec4; + /// # use float_cmp::assert_approx_eq; + /// let v1 = FVec4::new(1.0, 2.0, 3.0, 4.0); + /// let v2 = FVec4::new(1.0, 0.5, 1.0, 0.5); + /// assert_approx_eq!(f32, v1.dot(&v2), 7.0); + /// ``` + #[doc(alias = "FVec4_Dot")] pub fn dot(&self, rhs: &Self) -> f32 { unsafe { citro3d_sys::FVec4_Dot(self.0, rhs.0) } } /// The magnitude of the vector. + /// # Example + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::FVec4; + /// # use float_cmp::assert_approx_eq; + /// let v = FVec4::splat(1.0); + /// assert_approx_eq!(f32, v.magnitude(), 2.0); + /// ``` + #[doc(alias = "FVec4_Magnitude")] pub fn magnitude(&self) -> f32 { unsafe { citro3d_sys::FVec4_Magnitude(self.0) } } /// Normalize the vector to a magnitude of `1.0`. + /// # Example + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::FVec4; + /// # use float_cmp::assert_approx_eq; + /// let v = FVec4::new(1.0, 2.0, 2.0, 4.0); + /// assert_approx_eq!(FVec4, v, FVec4::new(0.1, 0.4, 0.4, 0.8)); + /// ``` + #[doc(alias = "FVec3_Normalize")] pub fn normalize(&self) -> Self { Self(unsafe { citro3d_sys::FVec3_Normalize(self.0) }) } @@ -61,6 +112,7 @@ impl FVec4 { impl FVec3 { /// Create a new [`FVec3`] from its components. + #[doc(alias = "FVec3_New")] pub fn new(x: f32, y: f32, z: f32) -> Self { Self(unsafe { citro3d_sys::FVec3_New(x, y, z) }) } @@ -71,26 +123,31 @@ impl FVec3 { } /// The distance between two points in 3D space. + #[doc(alias = "FVec3_Distance")] pub fn distance(&self, rhs: &Self) -> f32 { unsafe { citro3d_sys::FVec3_Distance(self.0, rhs.0) } } /// The cross product of two 3D vectors. + #[doc(alias = "FVec3_Cross")] pub fn cross(&self, rhs: &Self) -> Self { Self(unsafe { citro3d_sys::FVec3_Cross(self.0, rhs.0) }) } /// The dot product of two vectors. + #[doc(alias = "FVec3_Dot")] pub fn dot(&self, rhs: &Self) -> f32 { unsafe { citro3d_sys::FVec3_Dot(self.0, rhs.0) } } /// The magnitude of the vector. + #[doc(alias = "FVec3_Magnitude")] pub fn magnitude(&self) -> f32 { unsafe { citro3d_sys::FVec3_Magnitude(self.0) } } /// Normalize the vector to a magnitude of `1.0`. + #[doc(alias = "FVec3_Normalize")] pub fn normalize(&self) -> Self { Self(unsafe { citro3d_sys::FVec3_Normalize(self.0) }) } @@ -119,33 +176,21 @@ mod tests { use super::*; - // TODO: These could probably all be doctests instead it's just sort of a pain #[test] fn fvec4() { - let l = FVec4::new(2.0, 2.0, 2.0, 2.0); - - assert_eq!(l, FVec4::splat(2.0)); - - for component in [l.x(), l.y(), l.z(), l.w()] { - assert_approx_eq!(f32, component, 2.0); - } - - assert_eq!(l.perspective_divide(), FVec4::splat(1.0)); - - let dot = l.dot(&FVec4::splat(3.0)); - assert_approx_eq!(f32, dot, 24.0); - - assert_approx_eq!(f32, l.magnitude(), 4.0); - - let norm = l.normalize(); - assert_approx_eq!(f32, norm.magnitude(), 1.0); - for component in [l.y(), l.z(), l.w()] { - assert_approx_eq!(f32, component, l.x()); - } + let v = FVec4::new(1.0, 2.0, 3.0, 4.0); + let actual = [v.x(), v.y(), v.z(), v.w()]; + let expected = [1.0, 2.0, 3.0, 4.0]; + assert_approx_eq!(&[f32], &actual, &expected); } #[test] fn fvec3() { + let v = FVec3::new(1.0, 2.0, 3.0); + let actual = [v.x(), v.y(), v.z()]; + let expected = [1.0, 2.0, 3.0]; + assert_approx_eq!(&[f32], &actual, &expected); + let l = FVec3::new(2.0, 2.0, 2.0); assert_eq!(l, FVec3::splat(2.0)); diff --git a/citro3d/src/math/matrix.rs b/citro3d/src/math/matrix.rs index 79faabb..7215611 100644 --- a/citro3d/src/math/matrix.rs +++ b/citro3d/src/math/matrix.rs @@ -41,6 +41,11 @@ mod private { /// Trim the matrix down to only the rows and columns we care about, /// since the inner representation is always 4x4. + /// + /// NOTE: this probably shouldn't be used in hot paths since it copies + /// the underlying storage. For some use cases slicing might be better, + /// although the underlying slice would always contain extra values for + /// matrices smaller than 4x4. pub(crate) fn as_rows(&self) -> [[f32; N]; M] { let rows = unsafe { self.0.r }.map(|row| -> [f32; N] { // Rows are stored in WZYX order, so we slice from back to front. diff --git a/citro3d/src/math/ops.rs b/citro3d/src/math/ops.rs index 8204ca4..e66ff82 100644 --- a/citro3d/src/math/ops.rs +++ b/citro3d/src/math/ops.rs @@ -2,11 +2,16 @@ use std::borrow::Borrow; use std::mem::MaybeUninit; use std::ops::{Add, Deref, Div, Mul, Neg, Sub}; +use float_cmp::ApproxEq; + use super::{FVec, FVec3, FVec4, Matrix, Matrix3, Matrix4}; +// region: FVec4 math operators + impl Add for FVec4 { type Output = Self; + #[doc(alias = "FVec4_Add")] fn add(self, rhs: Self) -> Self::Output { Self(unsafe { citro3d_sys::FVec4_Add(self.0, rhs.0) }) } @@ -15,6 +20,7 @@ impl Add for FVec4 { impl Sub for FVec4 { type Output = Self; + #[doc(alias = "FVec4_Subtract")] fn sub(self, rhs: Self) -> Self::Output { Self(unsafe { citro3d_sys::FVec4_Subtract(self.0, rhs.0) }) } @@ -23,6 +29,7 @@ impl Sub for FVec4 { impl Neg for FVec4 { type Output = Self; + #[doc(alias = "FVec4_Negate")] fn neg(self) -> Self::Output { Self(unsafe { citro3d_sys::FVec4_Negate(self.0) }) } @@ -31,14 +38,20 @@ impl Neg for FVec4 { impl Mul for FVec4 { type Output = Self; + #[doc(alias = "FVec4_Scale")] fn mul(self, rhs: f32) -> Self::Output { Self(unsafe { citro3d_sys::FVec4_Scale(self.0, rhs) }) } } +// endregion + +// region: FVec3 math operators + impl Add for FVec3 { type Output = Self; + #[doc(alias = "FVec3_Add")] fn add(self, rhs: Self) -> Self::Output { Self(unsafe { citro3d_sys::FVec3_Add(self.0, rhs.0) }) } @@ -47,6 +60,7 @@ impl Add for FVec3 { impl Sub for FVec3 { type Output = Self; + #[doc(alias = "FVec3_Subtract")] fn sub(self, rhs: Self) -> Self::Output { Self(unsafe { citro3d_sys::FVec3_Subtract(self.0, rhs.0) }) } @@ -55,6 +69,7 @@ impl Sub for FVec3 { impl Neg for FVec3 { type Output = Self; + #[doc(alias = "FVec3_Negate")] fn neg(self) -> Self::Output { Self(unsafe { citro3d_sys::FVec3_Negate(self.0) }) } @@ -63,11 +78,14 @@ impl Neg for FVec3 { impl Mul for FVec3 { type Output = Self; + #[doc(alias = "FVec3_Scale")] fn mul(self, rhs: f32) -> Self::Output { Self(unsafe { citro3d_sys::FVec3_Scale(self.0, rhs) }) } } +// endregion + impl Div for FVec where FVec: Mul, @@ -81,12 +99,29 @@ where impl PartialEq for FVec { fn eq(&self, other: &Self) -> bool { - unsafe { self.0.c == other.0.c } + let range = (4 - N)..; + unsafe { self.0.c[range.clone()] == other.0.c[range] } } } impl Eq for FVec {} +impl ApproxEq for FVec +where + f32: ApproxEq, +{ + type Margin = Margin; + + fn approx_eq>(self, other: Self, margin: M) -> bool { + let margin = margin.into(); + let range = (4 - N)..; + let (lhs, rhs) = unsafe { (&self.0.c[range.clone()], &other.0.c[range]) }; + lhs.approx_eq(rhs, margin) + } +} + +// region: Matrix math operators + impl, const M: usize, const N: usize> Add for &Matrix { type Output = ::Target; @@ -155,6 +190,8 @@ impl Mul for &Matrix<4, 3> { } } +// endregion + impl, const M: usize, const N: usize> PartialEq for Matrix { fn eq(&self, other: &Rhs) -> bool { self.as_rows() == other.borrow().as_rows() @@ -163,8 +200,34 @@ impl, const M: usize, const N: usize> PartialEq for Matri impl Eq for Matrix {} +impl ApproxEq for &Matrix +where + Margin: Copy + Default, + f32: ApproxEq, +{ + type Margin = Margin; + + fn approx_eq>(self, other: Self, margin: Marg) -> bool { + let margin = margin.into(); + let lhs = self.as_rows(); + let rhs = other.as_rows(); + + for row in 0..M { + for col in 0..N { + if !lhs[row][col].approx_eq(rhs[row][col], margin) { + return false; + } + } + } + + true + } +} + #[cfg(test)] mod tests { + use float_cmp::assert_approx_eq; + use super::*; #[test] @@ -172,11 +235,11 @@ mod tests { let l = FVec3::splat(1.0); let r = FVec3::splat(2.0); - assert_eq!(l + r, FVec3::splat(3.0)); - assert_eq!(l - r, FVec3::splat(-1.0)); - assert_eq!(-l, FVec3::splat(-1.0)); - assert_eq!(l * 1.5, FVec3::splat(1.5)); - assert_eq!(l / 2.0, FVec3::splat(0.5)); + assert_approx_eq!(FVec3, l + r, FVec3::splat(3.0)); + assert_approx_eq!(FVec3, l - r, FVec3::splat(-1.0)); + assert_approx_eq!(FVec3, -l, FVec3::splat(-1.0)); + assert_approx_eq!(FVec3, l * 1.5, FVec3::splat(1.5)); + assert_approx_eq!(FVec3, l / 2.0, FVec3::splat(0.5)); } #[test] @@ -184,11 +247,11 @@ mod tests { let l = FVec4::splat(1.0); let r = FVec4::splat(2.0); - assert_eq!(l + r, FVec4::splat(3.0)); - assert_eq!(l - r, FVec4::splat(-1.0)); - assert_eq!(-l, FVec4::splat(-1.0)); - assert_eq!(l * 1.5, FVec4::splat(1.5)); - assert_eq!(l / 2.0, FVec4::splat(0.5)); + assert_approx_eq!(FVec4, l + r, FVec4::splat(3.0)); + assert_approx_eq!(FVec4, l - r, FVec4::splat(-1.0)); + assert_approx_eq!(FVec4, -l, FVec4::splat(-1.0)); + assert_approx_eq!(FVec4, l * 1.5, FVec4::splat(1.5)); + assert_approx_eq!(FVec4, l / 2.0, FVec4::splat(0.5)); } #[test] @@ -197,9 +260,9 @@ mod tests { let r = Matrix3::identity(); let (l, r) = (&l, &r); - assert_eq!(l * r, l); - assert_eq!(l + r, Matrix3::diagonal(2.0, 3.0, 4.0)); - assert_eq!(l - r, Matrix3::diagonal(0.0, 1.0, 2.0)); + assert_approx_eq!(&Matrix3, &(l * r), l); + assert_approx_eq!(&Matrix3, &(l + r), &Matrix3::diagonal(2.0, 3.0, 4.0)); + assert_approx_eq!(&Matrix3, &(l - r), &Matrix3::diagonal(0.0, 1.0, 2.0)); } #[test] @@ -208,8 +271,8 @@ mod tests { let r = Matrix4::identity(); let (l, r) = (&l, &r); - assert_eq!(l * r, l); - assert_eq!(l + r, Matrix4::diagonal(2.0, 3.0, 4.0, 5.0)); - assert_eq!(l - r, Matrix4::diagonal(0.0, 1.0, 2.0, 3.0)); + assert_approx_eq!(&Matrix4, &(l * r), l); + assert_approx_eq!(&Matrix4, &(l + r), &Matrix4::diagonal(2.0, 3.0, 4.0, 5.0)); + assert_approx_eq!(&Matrix4, &(l - r), &Matrix4::diagonal(0.0, 1.0, 2.0, 3.0)); } }