From a120178a6c33e2d3cc0d38a88e2c2a6bebe109d1 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sat, 7 Oct 2023 00:56:36 -0400 Subject: [PATCH 01/12] Implement FVec types + operators --- citro3d/src/math.rs | 11 ++- citro3d/src/math/fvec.rs | 177 +++++++++++++++++++++++++++++++++++++++ citro3d/src/math/ops.rs | 114 +++++++++++++++++++++++++ 3 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 citro3d/src/math/fvec.rs create mode 100644 citro3d/src/math/ops.rs diff --git a/citro3d/src/math.rs b/citro3d/src/math.rs index 479ef71..22634ab 100644 --- a/citro3d/src/math.rs +++ b/citro3d/src/math.rs @@ -1,19 +1,22 @@ //! Safe wrappers for working with matrix and vector types provided by `citro3d`. +// TODO: bench FFI calls into `inline statics` generated by bindgen, vs +// reimplementing some of those calls. Many of them are pretty trivial impls + use std::mem::MaybeUninit; mod projection; pub use projection::{Orthographic, Perspective, Projection}; +mod fvec; +mod ops; + +pub use fvec::{FVec, FVec3, FVec4}; /// A 4-vector of `u8`s. #[doc(alias = "C3D_IVec")] pub struct IVec(citro3d_sys::C3D_IVec); -/// A 4-vector of `f32`s. -#[doc(alias = "C3D_FVec")] -pub struct FVec(citro3d_sys::C3D_FVec); - /// A quaternion, internally represented the same way as [`FVec`]. #[doc(alias = "C3D_FQuat")] pub struct FQuat(citro3d_sys::C3D_FQuat); diff --git a/citro3d/src/math/fvec.rs b/citro3d/src/math/fvec.rs new file mode 100644 index 0000000..b1d7f12 --- /dev/null +++ b/citro3d/src/math/fvec.rs @@ -0,0 +1,177 @@ +//! Floating-point vectors. + +use std::fmt; + +/// A vector of `f32`s. +#[derive(Clone, Copy)] +pub struct FVec(pub(super) citro3d_sys::C3D_FVec); + +/// A 3-vector of `f32`s. +pub type FVec3 = FVec<3>; + +/// A 4-vector of `f32`s. +pub type FVec4 = FVec<4>; + +impl fmt::Debug for FVec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let inner = unsafe { self.0.c }; + f.debug_tuple(std::any::type_name::()) + .field(&inner) + .finish() + } +} + +impl PartialEq for FVec +where + Rhs: Copy, + Self: From, +{ + fn eq(&self, other: &Rhs) -> bool { + unsafe { self.0.c == Self::from(*other).0.c } + } +} + +impl Eq for FVec {} + +impl FVec4 { + /// Create a new [`FVec4`] from its components. + 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`. + 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]`). + #[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. + pub fn perspective_divide(&self) -> Self { + Self(unsafe { citro3d_sys::FVec4_PerspDivide(self.0) }) + } + + /// The dot product of two vectors. + pub fn dot(&self, rhs: &Self) -> f32 { + unsafe { citro3d_sys::FVec3_Dot(self.0, rhs.0) } + } + + /// The magnitude of the vector. + pub fn magnitude(&self) -> f32 { + unsafe { citro3d_sys::FVec3_Magnitude(self.0) } + } + + /// Normalize the vector to a magnitude of `1.0`. + pub fn normalize(&self) -> Self { + Self(unsafe { citro3d_sys::FVec3_Normalize(self.0) }) + } +} + +impl FVec3 { + /// Create a new [`FVec3`] from its components. + pub fn new(x: f32, y: f32, z: f32) -> Self { + Self(unsafe { citro3d_sys::FVec3_New(x, y, z) }) + } + + /// Create a new [`FVec3`], setting each component to the given `v`. + pub fn splat(v: f32) -> Self { + Self::new(v, v, v) + } + + /// The distance between two points in 3D space. + pub fn distance(&self, rhs: &Self) -> f32 { + unsafe { citro3d_sys::FVec3_Distance(self.0, rhs.0) } + } + + /// The cross product of two 3D vectors. + pub fn cross(&self, rhs: &Self) -> Self { + Self(unsafe { citro3d_sys::FVec3_Cross(self.0, rhs.0) }) + } + + /// The dot product of two vectors. + pub fn dot(&self, rhs: &Self) -> f32 { + unsafe { citro3d_sys::FVec3_Dot(self.0, rhs.0) } + } + + /// The magnitude of the vector. + pub fn magnitude(&self) -> f32 { + unsafe { citro3d_sys::FVec3_Magnitude(self.0) } + } + + /// Normalize the vector to a magnitude of `1.0`. + pub fn normalize(&self) -> Self { + Self(unsafe { citro3d_sys::FVec3_Normalize(self.0) }) + } +} + +impl FVec { + /// The vector's `x` component (sometimes also called the `i` component of `ijk[r]`). + pub fn x(&self) -> f32 { + unsafe { self.0.__bindgen_anon_1.x } + } + + /// The vector's `y` component (sometimes also called the `j` component of `ijk[r]`). + pub fn y(&self) -> f32 { + unsafe { self.0.__bindgen_anon_1.y } + } + + /// The vector's `i` component (sometimes also called the `k` component of `ijk[r]`). + pub fn z(&self) -> f32 { + unsafe { self.0.__bindgen_anon_1.z } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[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!((component - 2.0).abs() < f32::EPSILON); + } + + assert_eq!(l.perspective_divide(), FVec4::splat(1.0)); + + let dot = l.dot(&FVec4::splat(3.0)); + assert!((dot - 24.0).abs() < f32::EPSILON); + + assert!((l.magnitude() - 8.0).abs() < f32::EPSILON); + + let norm = l.normalize(); + assert!((norm.magnitude() - 1.0).abs() < f32::EPSILON); + for component in [l.y(), l.z(), l.w()] { + assert!((component - l.x()).abs() < f32::EPSILON); + } + } + + #[test] + fn fvec3() { + let l = FVec3::new(2.0, 2.0, 2.0); + + assert_eq!(l, FVec3::splat(2.0)); + + for component in [l.x(), l.y(), l.z()] { + assert!((component - 2.0).abs() < f32::EPSILON); + } + + let dot = l.dot(&FVec3::splat(3.0)); + assert!((dot - 18.0).abs() < f32::EPSILON); + + assert!((l.magnitude() - 8.0).abs() < f32::EPSILON); + + let norm = l.normalize(); + assert!((norm.magnitude() - 1.0).abs() < f32::EPSILON); + for component in [l.y(), l.z()] { + assert!((l.x() - component).abs() < f32::EPSILON); + } + } +} diff --git a/citro3d/src/math/ops.rs b/citro3d/src/math/ops.rs new file mode 100644 index 0000000..dffe352 --- /dev/null +++ b/citro3d/src/math/ops.rs @@ -0,0 +1,114 @@ +use std::borrow::Borrow; +use std::ops::{Add, Div, Mul, Neg, Sub}; + +use super::{FVec, FVec3, FVec4}; + +impl> Add for FVec4 { + type Output = Self; + + fn add(self, rhs: Rhs) -> Self::Output { + Self(unsafe { citro3d_sys::FVec4_Add(self.0, rhs.borrow().0) }) + } +} + +impl> Sub for FVec4 { + type Output = Self; + + fn sub(self, rhs: Rhs) -> Self::Output { + Self(unsafe { citro3d_sys::FVec4_Add(self.0, rhs.borrow().0) }) + } +} + +impl Neg for FVec4 { + type Output = Self; + + fn neg(self) -> Self::Output { + Self(unsafe { citro3d_sys::FVec4_Negate(self.0) }) + } +} + +impl Mul for FVec4 { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + Self(unsafe { citro3d_sys::FVec4_Scale(self.0, rhs) }) + } +} + +impl> Add for FVec3 { + type Output = Self; + + fn add(self, rhs: Rhs) -> Self::Output { + Self(unsafe { citro3d_sys::FVec3_Add(self.0, rhs.borrow().0) }) + } +} + +impl> Sub for FVec3 { + type Output = Self; + + fn sub(self, rhs: Rhs) -> Self::Output { + Self(unsafe { citro3d_sys::FVec3_Add(self.0, rhs.borrow().0) }) + } +} + +impl Neg for FVec3 { + type Output = Self; + + fn neg(self) -> Self::Output { + Self(unsafe { citro3d_sys::FVec3_Negate(self.0) }) + } +} + +impl Mul for FVec3 { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + Self(unsafe { citro3d_sys::FVec3_Scale(self.0, rhs) }) + } +} + +impl Div for FVec +where + FVec: Mul, +{ + type Output = >::Output; + + fn div(self, rhs: f32) -> Self::Output { + self * (1.0 / rhs) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::op_ref)] + + use super::*; + + #[test] + fn vec3_ops() { + 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(3.0)); + assert_eq!(l - r, FVec3::splat(-1.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)); + } + + #[test] + fn vec4_ops() { + 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(3.0)); + assert_eq!(l - r, FVec4::splat(-1.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)); + } +} From 1a67ba0d00b09b3b06048046f4b08ced01d21541 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sat, 7 Oct 2023 01:01:53 -0400 Subject: [PATCH 02/12] Run CI against non-main branches as well --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c447bbc..5617ee5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,8 +5,6 @@ on: branches: - main pull_request: - branches: - - main workflow_dispatch: jobs: From 163a42cc47f72552721df38f1f81f374742c145b Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sat, 7 Oct 2023 01:11:24 -0400 Subject: [PATCH 03/12] Fixup some mistaken API usage and tests --- .github/workflows/ci.yml | 2 ++ citro3d/src/math/fvec.rs | 8 ++++---- citro3d/src/math/ops.rs | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5617ee5..5452463 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ on: jobs: lint: strategy: + fail-fast: false matrix: toolchain: # Run against a "known good" nightly. Rustc version is 1 day behind the toolchain date @@ -46,6 +47,7 @@ jobs: test: strategy: + fail-fast: false matrix: toolchain: - nightly-2023-06-01 diff --git a/citro3d/src/math/fvec.rs b/citro3d/src/math/fvec.rs index b1d7f12..c6624c6 100644 --- a/citro3d/src/math/fvec.rs +++ b/citro3d/src/math/fvec.rs @@ -57,12 +57,12 @@ impl FVec4 { /// The dot product of two vectors. pub fn dot(&self, rhs: &Self) -> f32 { - unsafe { citro3d_sys::FVec3_Dot(self.0, rhs.0) } + unsafe { citro3d_sys::FVec4_Dot(self.0, rhs.0) } } /// The magnitude of the vector. pub fn magnitude(&self) -> f32 { - unsafe { citro3d_sys::FVec3_Magnitude(self.0) } + unsafe { citro3d_sys::FVec4_Magnitude(self.0) } } /// Normalize the vector to a magnitude of `1.0`. @@ -144,7 +144,7 @@ mod tests { let dot = l.dot(&FVec4::splat(3.0)); assert!((dot - 24.0).abs() < f32::EPSILON); - assert!((l.magnitude() - 8.0).abs() < f32::EPSILON); + assert!((l.magnitude() - 4.0).abs() < f32::EPSILON); let norm = l.normalize(); assert!((norm.magnitude() - 1.0).abs() < f32::EPSILON); @@ -166,7 +166,7 @@ mod tests { let dot = l.dot(&FVec3::splat(3.0)); assert!((dot - 18.0).abs() < f32::EPSILON); - assert!((l.magnitude() - 8.0).abs() < f32::EPSILON); + assert!((l.magnitude() - f32::sqrt(12.0)).abs() < f32::EPSILON); let norm = l.normalize(); assert!((norm.magnitude() - 1.0).abs() < f32::EPSILON); diff --git a/citro3d/src/math/ops.rs b/citro3d/src/math/ops.rs index dffe352..7a4f486 100644 --- a/citro3d/src/math/ops.rs +++ b/citro3d/src/math/ops.rs @@ -15,7 +15,7 @@ impl> Sub for FVec4 { type Output = Self; fn sub(self, rhs: Rhs) -> Self::Output { - Self(unsafe { citro3d_sys::FVec4_Add(self.0, rhs.borrow().0) }) + Self(unsafe { citro3d_sys::FVec4_Subtract(self.0, rhs.borrow().0) }) } } @@ -47,7 +47,7 @@ impl> Sub for FVec3 { type Output = Self; fn sub(self, rhs: Rhs) -> Self::Output { - Self(unsafe { citro3d_sys::FVec3_Add(self.0, rhs.borrow().0) }) + Self(unsafe { citro3d_sys::FVec3_Subtract(self.0, rhs.borrow().0) }) } } From 8cd28b7ca5bcb82c0b3fde804de69db7d6d3129a Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sat, 7 Oct 2023 01:51:32 -0400 Subject: [PATCH 04/12] Use float-cmp for nicer assertions --- citro3d/Cargo.toml | 1 + citro3d/src/math/fvec.rs | 22 ++++++++++++---------- citro3d/src/math/ops.rs | 4 ++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/citro3d/Cargo.toml b/citro3d/Cargo.toml index 306b2d2..c70473f 100644 --- a/citro3d/Cargo.toml +++ b/citro3d/Cargo.toml @@ -15,4 +15,5 @@ ctru-sys = { git = "https://github.com/rust3ds/ctru-rs.git" } libc = "0.2.125" [dev-dependencies] +float-cmp = "0.9.0" test-runner = { git = "https://github.com/rust3ds/test-runner.git" } diff --git a/citro3d/src/math/fvec.rs b/citro3d/src/math/fvec.rs index c6624c6..fce145d 100644 --- a/citro3d/src/math/fvec.rs +++ b/citro3d/src/math/fvec.rs @@ -128,7 +128,9 @@ impl FVec { #[cfg(test)] mod tests { use super::*; + use float_cmp::assert_approx_eq; + // 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); @@ -136,20 +138,20 @@ mod tests { assert_eq!(l, FVec4::splat(2.0)); for component in [l.x(), l.y(), l.z(), l.w()] { - assert!((component - 2.0).abs() < f32::EPSILON); + 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!((dot - 24.0).abs() < f32::EPSILON); + assert_approx_eq!(f32, dot, 24.0); - assert!((l.magnitude() - 4.0).abs() < f32::EPSILON); + assert_approx_eq!(f32, l.magnitude(), 4.0); let norm = l.normalize(); - assert!((norm.magnitude() - 1.0).abs() < f32::EPSILON); + assert_approx_eq!(f32, norm.magnitude(), 1.0); for component in [l.y(), l.z(), l.w()] { - assert!((component - l.x()).abs() < f32::EPSILON); + assert_approx_eq!(f32, component, l.x()); } } @@ -160,18 +162,18 @@ mod tests { assert_eq!(l, FVec3::splat(2.0)); for component in [l.x(), l.y(), l.z()] { - assert!((component - 2.0).abs() < f32::EPSILON); + assert_approx_eq!(f32, component, 2.0); } let dot = l.dot(&FVec3::splat(3.0)); - assert!((dot - 18.0).abs() < f32::EPSILON); + assert_approx_eq!(f32, dot, 18.0); - assert!((l.magnitude() - f32::sqrt(12.0)).abs() < f32::EPSILON); + assert_approx_eq!(f32, l.magnitude(), f32::sqrt(12.0)); let norm = l.normalize(); - assert!((norm.magnitude() - 1.0).abs() < f32::EPSILON); + assert_approx_eq!(f32, norm.magnitude(), 1.0); for component in [l.y(), l.z()] { - assert!((l.x() - component).abs() < f32::EPSILON); + assert_approx_eq!(f32, l.x(), component); } } } diff --git a/citro3d/src/math/ops.rs b/citro3d/src/math/ops.rs index 7a4f486..3be3095 100644 --- a/citro3d/src/math/ops.rs +++ b/citro3d/src/math/ops.rs @@ -85,7 +85,7 @@ mod tests { use super::*; #[test] - fn vec3_ops() { + fn vec3() { let l = FVec3::splat(1.0); let r = FVec3::splat(2.0); @@ -99,7 +99,7 @@ mod tests { } #[test] - fn vec4_ops() { + fn vec4() { let l = FVec4::splat(1.0); let r = FVec4::splat(2.0); From 4e0fc5a409501225f4f4aa3167fe052dbcd3b952 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sun, 15 Oct 2023 21:30:46 -0400 Subject: [PATCH 05/12] Define matrix as MxN with const generics --- citro3d/examples/triangle.rs | 8 +++--- citro3d/src/math.rs | 45 +++++++++++++++++++++++++++------- citro3d/src/math/projection.rs | 14 +++++------ citro3d/src/uniform.rs | 21 +++++++++++++--- 4 files changed, 64 insertions(+), 24 deletions(-) diff --git a/citro3d/examples/triangle.rs b/citro3d/examples/triangle.rs index b367906..f8585e4 100644 --- a/citro3d/examples/triangle.rs +++ b/citro3d/examples/triangle.rs @@ -4,7 +4,7 @@ #![feature(allocator_api)] use citro3d::macros::include_shader; -use citro3d::math::{AspectRatio, ClipPlanes, Matrix, Projection, StereoDisplacement}; +use citro3d::math::{AspectRatio, ClipPlanes, Matrix4, Projection, StereoDisplacement}; use citro3d::render::ClearFlags; use citro3d::{attrib, buffer, render, shader}; use ctru::prelude::*; @@ -158,9 +158,9 @@ where } struct Projections { - left_eye: Matrix, - right_eye: Matrix, - center: Matrix, + left_eye: Matrix4, + right_eye: Matrix4, + center: Matrix4, } fn calculate_projections() -> Projections { diff --git a/citro3d/src/math.rs b/citro3d/src/math.rs index 22634ab..976ddc7 100644 --- a/citro3d/src/math.rs +++ b/citro3d/src/math.rs @@ -21,11 +21,40 @@ pub struct IVec(citro3d_sys::C3D_IVec); #[doc(alias = "C3D_FQuat")] pub struct FQuat(citro3d_sys::C3D_FQuat); +mod mtx { + /// An `M`x`N` row-major matrix of `f32`s. + #[doc(alias = "C3D_Mtx")] + pub struct Matrix(citro3d_sys::C3D_Mtx); + + impl Matrix { + const ROW_SIZE: () = assert!(M == 3 || M == 4); + const COLUMN_SIZE: () = assert!(N > 0 && N <= 4); + + // This constructor validates, at compile time, that the + // constructed matrix is 3xN or 4xN matrix, where 0 < N ≤ 4. + // We put this struct in a submodule to enforce that nothing creates + // a Matrix without calling this constructor. + #[allow(clippy::let_unit_value)] + pub(crate) fn new(value: citro3d_sys::C3D_Mtx) -> Self { + let () = Self::ROW_SIZE; + let () = Self::COLUMN_SIZE; + Self(value) + } + + pub(crate) fn as_raw(&self) -> *const citro3d_sys::C3D_Mtx { + &self.0 + } + } +} + +pub use mtx::Matrix; + +/// A 3x3 row-major matrix of `f32`s. +pub type Matrix3 = Matrix<3, 3>; /// A 4x4 row-major matrix of `f32`s. -#[doc(alias = "C3D_Mtx")] -pub struct Matrix(citro3d_sys::C3D_Mtx); +pub type Matrix4 = Matrix<4, 4>; -impl Matrix { +impl Matrix { /// Construct the zero matrix. #[doc(alias = "Mtx_Zeros")] pub fn zero() -> Self { @@ -33,23 +62,21 @@ impl Matrix { let mut out = MaybeUninit::uninit(); unsafe { citro3d_sys::Mtx_Zeros(out.as_mut_ptr()); - Self(out.assume_init()) + Self::new(out.assume_init()) } } +} +impl Matrix { /// Construct the identity matrix. #[doc(alias = "Mtx_Identity")] pub fn identity() -> Self { let mut out = MaybeUninit::uninit(); unsafe { citro3d_sys::Mtx_Identity(out.as_mut_ptr()); - Self(out.assume_init()) + Self::new(out.assume_init()) } } - - pub(crate) fn as_raw(&self) -> *const citro3d_sys::C3D_Mtx { - &self.0 - } } // region: Projection configuration diff --git a/citro3d/src/math/projection.rs b/citro3d/src/math/projection.rs index 6833fb9..44bc6e1 100644 --- a/citro3d/src/math/projection.rs +++ b/citro3d/src/math/projection.rs @@ -2,14 +2,14 @@ use std::mem::MaybeUninit; use std::ops::Range; use super::{ - AspectRatio, ClipPlanes, CoordinateOrientation, Matrix, ScreenOrientation, StereoDisplacement, + AspectRatio, ClipPlanes, CoordinateOrientation, Matrix4, ScreenOrientation, StereoDisplacement, }; /// Configuration for a 3D [projection](https://en.wikipedia.org/wiki/3D_projection). /// See specific `Kind` implementations for constructors, e.g. /// [`Projection::perspective`] and [`Projection::orthographic`]. /// -/// To use the resulting projection, convert it to a [`Matrix`] with [`From`]/[`Into`]. +/// To use the resulting projection, convert it to a [`Matrix`](super::Matrix) with [`From`]/[`Into`]. #[derive(Clone, Debug)] pub struct Projection { coordinates: CoordinateOrientation, @@ -122,7 +122,7 @@ impl Projection { self, left_eye: StereoDisplacement, right_eye: StereoDisplacement, - ) -> (Matrix, Matrix) { + ) -> (Matrix4, Matrix4) { // TODO: we might be able to avoid this clone if there was a conversion // from &Self to Matrix instead of Self... but it's probably fine for now let left = self.clone().stereo(left_eye); @@ -137,7 +137,7 @@ impl Projection { } } -impl From> for Matrix { +impl From> for Matrix4 { fn from(projection: Projection) -> Self { let Perspective { vertical_fov_radians, @@ -182,7 +182,7 @@ impl From> for Matrix { } } - unsafe { Self(result.assume_init()) } + unsafe { Self::new(result.assume_init()) } } } @@ -233,7 +233,7 @@ impl Projection { } } -impl From> for Matrix { +impl From> for Matrix4 { fn from(projection: Projection) -> Self { let make_mtx = match projection.rotation { ScreenOrientation::Rotated => citro3d_sys::Mtx_OrthoTilt, @@ -258,7 +258,7 @@ impl From> for Matrix { clip_planes_z.far, projection.coordinates.is_left_handed(), ); - Self(out.assume_init()) + Self::new(out.assume_init()) } } } diff --git a/citro3d/src/uniform.rs b/citro3d/src/uniform.rs index c6a1d75..22c7092 100644 --- a/citro3d/src/uniform.rs +++ b/citro3d/src/uniform.rs @@ -24,7 +24,8 @@ mod private { use crate::math::Matrix; pub trait Sealed {} - impl Sealed for &Matrix {} + + impl Sealed for &Matrix {} } /// A shader uniform. This trait is implemented for types that can be bound to @@ -36,9 +37,21 @@ pub trait Uniform: private::Sealed { fn bind(self, instance: &mut Instance, shader_type: shader::Type, index: Index); } -impl Uniform for &Matrix { - #[doc(alias = "C3D_FVUnifMtx4x4")] +impl Uniform for &Matrix { + #[doc(alias = "C34_FVUnifMtxNx4")] + #[doc(alias = "C34_FVUnifMtx4x4")] + #[doc(alias = "C34_FVUnifMtx3x4")] + #[doc(alias = "C34_FVUnifMtx2x4")] fn bind(self, _instance: &mut Instance, type_: shader::Type, index: Index) { - unsafe { citro3d_sys::C3D_FVUnifMtx4x4(type_.into(), index.into(), self.as_raw()) } + unsafe { + citro3d_sys::C3D_FVUnifMtxNx4( + type_.into(), + index.into(), + self.as_raw(), + // UNWRAP: it should be impossible for end users to construct + // a matrix with M > i32::MAX + M.try_into().unwrap(), + ); + } } } From e2abfa300a8ffb920188ef4c83d28e995f6065ef Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sun, 15 Oct 2023 23:15:23 -0400 Subject: [PATCH 06/12] Implement more matrix operations Also simplify FVec operations since it's a Copy type, but keep using references for Matrix implementations. Use type-safe matrix multiplication with other matrices and vectors. --- citro3d/src/math.rs | 126 ++++++++++++++++++++++++++++++++++ citro3d/src/math/fvec.rs | 17 +---- citro3d/src/math/ops.rs | 142 +++++++++++++++++++++++++++++++++------ 3 files changed, 251 insertions(+), 34 deletions(-) diff --git a/citro3d/src/math.rs b/citro3d/src/math.rs index 976ddc7..64fa613 100644 --- a/citro3d/src/math.rs +++ b/citro3d/src/math.rs @@ -22,10 +22,22 @@ pub struct IVec(citro3d_sys::C3D_IVec); pub struct FQuat(citro3d_sys::C3D_FQuat); mod mtx { + use std::fmt; + /// An `M`x`N` row-major matrix of `f32`s. #[doc(alias = "C3D_Mtx")] + #[derive(Clone)] pub struct Matrix(citro3d_sys::C3D_Mtx); + impl fmt::Debug for Matrix { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let inner = unsafe { self.0.r.map(|v| v.c) }; + f.debug_tuple(std::any::type_name::()) + .field(&inner) + .finish() + } + } + impl Matrix { const ROW_SIZE: () = assert!(M == 3 || M == 4); const COLUMN_SIZE: () = assert!(N > 0 && N <= 4); @@ -44,6 +56,14 @@ mod mtx { pub(crate) fn as_raw(&self) -> *const citro3d_sys::C3D_Mtx { &self.0 } + + pub(crate) fn into_raw(self) -> citro3d_sys::C3D_Mtx { + self.0 + } + + pub(crate) fn as_mut(&mut self) -> *mut citro3d_sys::C3D_Mtx { + &mut self.0 + } } } @@ -65,9 +85,72 @@ impl Matrix { Self::new(out.assume_init()) } } + + /// Transpose the matrix, swapping rows and columns. + pub fn transpose(mut self) -> Matrix { + unsafe { + citro3d_sys::Mtx_Transpose(self.as_mut()); + } + Matrix::new(self.into_raw()) + } + + // region: Matrix transformations + // + // NOTE: the `bRightSide` arg common to many of these APIs flips the order of + // operations so that a transformation occurs as self(T) instead of T(self). + // For now I'm not sure if that's a common use case, but if needed we could + // probably have some kinda wrapper type that does transformations in the + // opposite order, or an enum arg for these APIs or something. + + /// Translate a transformation matrix by the given amounts in the X, Y, and Z + /// directions. + pub fn translate(&mut self, x: f32, y: f32, z: f32) { + unsafe { citro3d_sys::Mtx_Translate(self.as_mut(), x, y, z, false) } + } + + /// Scale a transformation matrix by the given amounts in the X, Y, and Z directions. + pub fn scale(&mut self, x: f32, y: f32, z: f32) { + unsafe { citro3d_sys::Mtx_Scale(self.as_mut(), x, y, z) } + } + + /// Rotate a transformation matrix by the given angle around the given axis. + pub fn rotate(&mut self, axis: FVec3, angle: f32) { + unsafe { citro3d_sys::Mtx_Rotate(self.as_mut(), axis.0, angle, false) } + } + + /// Rotate a transformation matrix by the given angle around the X axis. + pub fn rotate_x(&mut self, angle: f32) { + unsafe { citro3d_sys::Mtx_RotateX(self.as_mut(), angle, false) } + } + + /// Rotate a transformation matrix by the given angle around the Y axis. + pub fn rotate_y(&mut self, angle: f32) { + unsafe { citro3d_sys::Mtx_RotateY(self.as_mut(), angle, false) } + } + + /// Rotate a transformation matrix by the given angle around the Z axis. + pub fn rotate_z(&mut self, angle: f32) { + unsafe { citro3d_sys::Mtx_RotateZ(self.as_mut(), angle, false) } + } + + // endregion } impl Matrix { + /// Find the inverse of the matrix. + /// + /// # Errors + /// + /// If the matrix has no inverse, it will be returned unchanged as an [`Err`]. + pub fn inverse(mut self) -> Result { + let determinant = unsafe { citro3d_sys::Mtx_Inverse(self.as_mut()) }; + if determinant == 0.0 { + Err(self) + } else { + Ok(self) + } + } + /// Construct the identity matrix. #[doc(alias = "Mtx_Identity")] pub fn identity() -> Self { @@ -79,6 +162,49 @@ impl Matrix { } } +impl Matrix3 { + /// Construct a 3x3 matrix with the given values on the diagonal. + pub fn diagonal(x: f32, y: f32, z: f32) -> Self { + let mut out = MaybeUninit::uninit(); + unsafe { + citro3d_sys::Mtx_Diagonal(out.as_mut_ptr(), x, y, z, 0.0); + Self::new(out.assume_init()) + } + } +} + +impl Matrix4 { + /// Construct a 4x4 matrix with the given values on the diagonal. + pub fn diagonal(x: f32, y: f32, z: f32, w: f32) -> Self { + let mut out = MaybeUninit::uninit(); + unsafe { + citro3d_sys::Mtx_Diagonal(out.as_mut_ptr(), x, y, z, w); + Self::new(out.assume_init()) + } + } + + /// Construct a 3D transformation matrix for a camera, given its position, + /// target, and upward direction. + pub fn looking_at( + camera_position: FVec3, + camera_target: FVec3, + camera_up: FVec3, + coordinates: CoordinateOrientation, + ) -> Self { + let mut out = MaybeUninit::uninit(); + unsafe { + citro3d_sys::Mtx_LookAt( + out.as_mut_ptr(), + camera_position.0, + camera_target.0, + camera_up.0, + coordinates.is_left_handed(), + ); + Self::new(out.assume_init()) + } + } +} + // region: Projection configuration // // TODO: maybe move into `mod projection`, or hoist `projection::*` into here. diff --git a/citro3d/src/math/fvec.rs b/citro3d/src/math/fvec.rs index fce145d..76d1c2d 100644 --- a/citro3d/src/math/fvec.rs +++ b/citro3d/src/math/fvec.rs @@ -4,7 +4,7 @@ use std::fmt; /// A vector of `f32`s. #[derive(Clone, Copy)] -pub struct FVec(pub(super) citro3d_sys::C3D_FVec); +pub struct FVec(pub(crate) citro3d_sys::C3D_FVec); /// A 3-vector of `f32`s. pub type FVec3 = FVec<3>; @@ -21,18 +21,6 @@ impl fmt::Debug for FVec { } } -impl PartialEq for FVec -where - Rhs: Copy, - Self: From, -{ - fn eq(&self, other: &Rhs) -> bool { - unsafe { self.0.c == Self::from(*other).0.c } - } -} - -impl Eq for FVec {} - impl FVec4 { /// Create a new [`FVec4`] from its components. pub fn new(x: f32, y: f32, z: f32, w: f32) -> Self { @@ -127,9 +115,10 @@ impl FVec { #[cfg(test)] mod tests { - use super::*; use float_cmp::assert_approx_eq; + use super::*; + // TODO: These could probably all be doctests instead it's just sort of a pain #[test] fn fvec4() { diff --git a/citro3d/src/math/ops.rs b/citro3d/src/math/ops.rs index 3be3095..0cb51a4 100644 --- a/citro3d/src/math/ops.rs +++ b/citro3d/src/math/ops.rs @@ -1,21 +1,22 @@ use std::borrow::Borrow; -use std::ops::{Add, Div, Mul, Neg, Sub}; +use std::mem::MaybeUninit; +use std::ops::{Add, Deref, Div, Mul, Neg, Sub}; -use super::{FVec, FVec3, FVec4}; +use super::{FVec, FVec3, FVec4, Matrix, Matrix3, Matrix4}; -impl> Add for FVec4 { +impl Add for FVec4 { type Output = Self; - fn add(self, rhs: Rhs) -> Self::Output { - Self(unsafe { citro3d_sys::FVec4_Add(self.0, rhs.borrow().0) }) + fn add(self, rhs: Self) -> Self::Output { + Self(unsafe { citro3d_sys::FVec4_Add(self.0, rhs.0) }) } } -impl> Sub for FVec4 { +impl Sub for FVec4 { type Output = Self; - fn sub(self, rhs: Rhs) -> Self::Output { - Self(unsafe { citro3d_sys::FVec4_Subtract(self.0, rhs.borrow().0) }) + fn sub(self, rhs: Self) -> Self::Output { + Self(unsafe { citro3d_sys::FVec4_Subtract(self.0, rhs.0) }) } } @@ -35,19 +36,19 @@ impl Mul for FVec4 { } } -impl> Add for FVec3 { +impl Add for FVec3 { type Output = Self; - fn add(self, rhs: Rhs) -> Self::Output { - Self(unsafe { citro3d_sys::FVec3_Add(self.0, rhs.borrow().0) }) + fn add(self, rhs: Self) -> Self::Output { + Self(unsafe { citro3d_sys::FVec3_Add(self.0, rhs.0) }) } } -impl> Sub for FVec3 { +impl Sub for FVec3 { type Output = Self; - fn sub(self, rhs: Rhs) -> Self::Output { - Self(unsafe { citro3d_sys::FVec3_Subtract(self.0, rhs.borrow().0) }) + fn sub(self, rhs: Self) -> Self::Output { + Self(unsafe { citro3d_sys::FVec3_Subtract(self.0, rhs.0) }) } } @@ -78,11 +79,94 @@ where } } +impl PartialEq for FVec { + fn eq(&self, other: &Self) -> bool { + unsafe { self.0.c == other.0.c } + } +} + +impl Eq for FVec {} + +impl, const M: usize, const N: usize> Add for &Matrix { + type Output = ::Target; + + fn add(self, rhs: Rhs) -> Self::Output { + let mut out = MaybeUninit::uninit(); + unsafe { + citro3d_sys::Mtx_Add(out.as_mut_ptr(), self.as_raw(), rhs.borrow().as_raw()); + Matrix::new(out.assume_init()) + } + } +} + +impl, const M: usize, const N: usize> Sub for &Matrix { + type Output = ::Target; + + fn sub(self, rhs: Rhs) -> Self::Output { + let mut out = MaybeUninit::uninit(); + unsafe { + citro3d_sys::Mtx_Subtract(out.as_mut_ptr(), self.as_raw(), rhs.borrow().as_raw()); + Matrix::new(out.assume_init()) + } + } +} + +impl Mul<&Matrix> for &Matrix { + type Output = Matrix; + + fn mul(self, rhs: &Matrix) -> Self::Output { + let mut out = MaybeUninit::uninit(); + unsafe { + citro3d_sys::Mtx_Multiply(out.as_mut_ptr(), self.as_raw(), rhs.as_raw()); + Matrix::new(out.assume_init()) + } + } +} + +impl Mul> for &Matrix { + type Output = Matrix; + + fn mul(self, rhs: Matrix) -> Self::Output { + self * &rhs + } +} + +impl Mul for &Matrix3 { + type Output = FVec3; + + fn mul(self, rhs: FVec3) -> Self::Output { + FVec(unsafe { citro3d_sys::Mtx_MultiplyFVec3(self.as_raw(), rhs.0) }) + } +} + +impl Mul for &Matrix4 { + type Output = FVec4; + + fn mul(self, rhs: FVec4) -> Self::Output { + FVec(unsafe { citro3d_sys::Mtx_MultiplyFVec4(self.as_raw(), rhs.0) }) + } +} + +impl Mul for &Matrix<4, 3> { + type Output = FVec4; + + fn mul(self, rhs: FVec3) -> Self::Output { + FVec(unsafe { citro3d_sys::Mtx_MultiplyFVecH(self.as_raw(), rhs.0) }) + } +} + +impl, const M: usize, const N: usize> PartialEq for Matrix { + fn eq(&self, other: &Rhs) -> bool { + unsafe { (*self.as_raw()).m == (*other.borrow().as_raw()).m } + } +} + +impl Eq for Matrix {} + #[cfg(test)] mod tests { - #![allow(clippy::op_ref)] - use super::*; + use crate::math::{Matrix3, Matrix4}; #[test] fn vec3() { @@ -90,9 +174,7 @@ mod tests { let r = FVec3::splat(2.0); assert_eq!(l + r, FVec3::splat(3.0)); - assert_eq!(l + &r, FVec3::splat(3.0)); assert_eq!(l - r, FVec3::splat(-1.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)); @@ -104,11 +186,31 @@ mod tests { let r = FVec4::splat(2.0); assert_eq!(l + r, FVec4::splat(3.0)); - assert_eq!(l + &r, FVec4::splat(3.0)); assert_eq!(l - r, FVec4::splat(-1.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)); } + + #[test] + fn matrix3() { + let l = Matrix3::diagonal(1.0, 2.0, 3.0); + let r = Matrix3::identity(); + let (l, r) = (&l, &r); + + assert_eq!(l * r, r); + assert_eq!(l + r, Matrix3::diagonal(2.0, 3.0, 4.0)); + assert_eq!(l - r, Matrix3::diagonal(0.0, 1.0, 2.0)); + } + + #[test] + fn matrix4() { + let l = Matrix4::diagonal(1.0, 2.0, 3.0, 4.0); + let r = Matrix4::identity(); + let (l, r) = (&l, &r); + + assert_eq!(l * r, r); + 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)); + } } From 8b7e8fe71b64729cfcbeba0cb7ed9cd94b50a742 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sun, 15 Oct 2023 23:48:25 -0400 Subject: [PATCH 07/12] Refactor matrix into new module Fix up a couple test bugs and do some other refactor / cleanup --- citro3d/src/math.rs | 331 +-------------------------------- citro3d/src/math/fvec.rs | 2 +- citro3d/src/math/matrix.rs | 202 ++++++++++++++++++++ citro3d/src/math/ops.rs | 5 +- citro3d/src/math/projection.rs | 135 +++++++++++++- 5 files changed, 344 insertions(+), 331 deletions(-) create mode 100644 citro3d/src/math/matrix.rs diff --git a/citro3d/src/math.rs b/citro3d/src/math.rs index 64fa613..949bb42 100644 --- a/citro3d/src/math.rs +++ b/citro3d/src/math.rs @@ -3,15 +3,17 @@ // TODO: bench FFI calls into `inline statics` generated by bindgen, vs // reimplementing some of those calls. Many of them are pretty trivial impls -use std::mem::MaybeUninit; - -mod projection; - -pub use projection::{Orthographic, Perspective, Projection}; mod fvec; +mod matrix; mod ops; +mod projection; pub use fvec::{FVec, FVec3, FVec4}; +pub use matrix::{Matrix, Matrix3, Matrix4}; +pub use projection::{ + AspectRatio, ClipPlanes, CoordinateOrientation, Orthographic, Perspective, Projection, + ScreenOrientation, StereoDisplacement, +}; /// A 4-vector of `u8`s. #[doc(alias = "C3D_IVec")] @@ -20,322 +22,3 @@ pub struct IVec(citro3d_sys::C3D_IVec); /// A quaternion, internally represented the same way as [`FVec`]. #[doc(alias = "C3D_FQuat")] pub struct FQuat(citro3d_sys::C3D_FQuat); - -mod mtx { - use std::fmt; - - /// An `M`x`N` row-major matrix of `f32`s. - #[doc(alias = "C3D_Mtx")] - #[derive(Clone)] - pub struct Matrix(citro3d_sys::C3D_Mtx); - - impl fmt::Debug for Matrix { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let inner = unsafe { self.0.r.map(|v| v.c) }; - f.debug_tuple(std::any::type_name::()) - .field(&inner) - .finish() - } - } - - impl Matrix { - const ROW_SIZE: () = assert!(M == 3 || M == 4); - const COLUMN_SIZE: () = assert!(N > 0 && N <= 4); - - // This constructor validates, at compile time, that the - // constructed matrix is 3xN or 4xN matrix, where 0 < N ≤ 4. - // We put this struct in a submodule to enforce that nothing creates - // a Matrix without calling this constructor. - #[allow(clippy::let_unit_value)] - pub(crate) fn new(value: citro3d_sys::C3D_Mtx) -> Self { - let () = Self::ROW_SIZE; - let () = Self::COLUMN_SIZE; - Self(value) - } - - pub(crate) fn as_raw(&self) -> *const citro3d_sys::C3D_Mtx { - &self.0 - } - - pub(crate) fn into_raw(self) -> citro3d_sys::C3D_Mtx { - self.0 - } - - pub(crate) fn as_mut(&mut self) -> *mut citro3d_sys::C3D_Mtx { - &mut self.0 - } - } -} - -pub use mtx::Matrix; - -/// A 3x3 row-major matrix of `f32`s. -pub type Matrix3 = Matrix<3, 3>; -/// A 4x4 row-major matrix of `f32`s. -pub type Matrix4 = Matrix<4, 4>; - -impl Matrix { - /// Construct the zero matrix. - #[doc(alias = "Mtx_Zeros")] - pub fn zero() -> Self { - // TODO: should this also be Default::default()? - let mut out = MaybeUninit::uninit(); - unsafe { - citro3d_sys::Mtx_Zeros(out.as_mut_ptr()); - Self::new(out.assume_init()) - } - } - - /// Transpose the matrix, swapping rows and columns. - pub fn transpose(mut self) -> Matrix { - unsafe { - citro3d_sys::Mtx_Transpose(self.as_mut()); - } - Matrix::new(self.into_raw()) - } - - // region: Matrix transformations - // - // NOTE: the `bRightSide` arg common to many of these APIs flips the order of - // operations so that a transformation occurs as self(T) instead of T(self). - // For now I'm not sure if that's a common use case, but if needed we could - // probably have some kinda wrapper type that does transformations in the - // opposite order, or an enum arg for these APIs or something. - - /// Translate a transformation matrix by the given amounts in the X, Y, and Z - /// directions. - pub fn translate(&mut self, x: f32, y: f32, z: f32) { - unsafe { citro3d_sys::Mtx_Translate(self.as_mut(), x, y, z, false) } - } - - /// Scale a transformation matrix by the given amounts in the X, Y, and Z directions. - pub fn scale(&mut self, x: f32, y: f32, z: f32) { - unsafe { citro3d_sys::Mtx_Scale(self.as_mut(), x, y, z) } - } - - /// Rotate a transformation matrix by the given angle around the given axis. - pub fn rotate(&mut self, axis: FVec3, angle: f32) { - unsafe { citro3d_sys::Mtx_Rotate(self.as_mut(), axis.0, angle, false) } - } - - /// Rotate a transformation matrix by the given angle around the X axis. - pub fn rotate_x(&mut self, angle: f32) { - unsafe { citro3d_sys::Mtx_RotateX(self.as_mut(), angle, false) } - } - - /// Rotate a transformation matrix by the given angle around the Y axis. - pub fn rotate_y(&mut self, angle: f32) { - unsafe { citro3d_sys::Mtx_RotateY(self.as_mut(), angle, false) } - } - - /// Rotate a transformation matrix by the given angle around the Z axis. - pub fn rotate_z(&mut self, angle: f32) { - unsafe { citro3d_sys::Mtx_RotateZ(self.as_mut(), angle, false) } - } - - // endregion -} - -impl Matrix { - /// Find the inverse of the matrix. - /// - /// # Errors - /// - /// If the matrix has no inverse, it will be returned unchanged as an [`Err`]. - pub fn inverse(mut self) -> Result { - let determinant = unsafe { citro3d_sys::Mtx_Inverse(self.as_mut()) }; - if determinant == 0.0 { - Err(self) - } else { - Ok(self) - } - } - - /// Construct the identity matrix. - #[doc(alias = "Mtx_Identity")] - pub fn identity() -> Self { - let mut out = MaybeUninit::uninit(); - unsafe { - citro3d_sys::Mtx_Identity(out.as_mut_ptr()); - Self::new(out.assume_init()) - } - } -} - -impl Matrix3 { - /// Construct a 3x3 matrix with the given values on the diagonal. - pub fn diagonal(x: f32, y: f32, z: f32) -> Self { - let mut out = MaybeUninit::uninit(); - unsafe { - citro3d_sys::Mtx_Diagonal(out.as_mut_ptr(), x, y, z, 0.0); - Self::new(out.assume_init()) - } - } -} - -impl Matrix4 { - /// Construct a 4x4 matrix with the given values on the diagonal. - pub fn diagonal(x: f32, y: f32, z: f32, w: f32) -> Self { - let mut out = MaybeUninit::uninit(); - unsafe { - citro3d_sys::Mtx_Diagonal(out.as_mut_ptr(), x, y, z, w); - Self::new(out.assume_init()) - } - } - - /// Construct a 3D transformation matrix for a camera, given its position, - /// target, and upward direction. - pub fn looking_at( - camera_position: FVec3, - camera_target: FVec3, - camera_up: FVec3, - coordinates: CoordinateOrientation, - ) -> Self { - let mut out = MaybeUninit::uninit(); - unsafe { - citro3d_sys::Mtx_LookAt( - out.as_mut_ptr(), - camera_position.0, - camera_target.0, - camera_up.0, - coordinates.is_left_handed(), - ); - Self::new(out.assume_init()) - } - } -} - -// region: Projection configuration -// -// TODO: maybe move into `mod projection`, or hoist `projection::*` into here. -// it will probably mostly depend on how big all the matrices/vec impls get. -// Also worth considering is whether `mod projection` should be pub. - -/// The [orientation](https://en.wikipedia.org/wiki/Orientation_(geometry)) -/// (or "handedness") of the coordinate system. Coordinates are always +Y-up, -/// +X-right. -#[derive(Clone, Copy, Debug)] -pub enum CoordinateOrientation { - /// A left-handed coordinate system. +Z points into the screen. - LeftHanded, - /// A right-handed coordinate system. +Z points out of the screen. - RightHanded, -} - -impl CoordinateOrientation { - pub(crate) fn is_left_handed(self) -> bool { - matches!(self, Self::LeftHanded) - } -} - -impl Default for CoordinateOrientation { - /// This is an opinionated default, but [`RightHanded`](Self::RightHanded) - /// seems to be the preferred coordinate system for most - /// [examples](https://github.com/devkitPro/3ds-examples) - /// from upstream, and is also fairly common in other applications. - fn default() -> Self { - Self::RightHanded - } -} - -/// Whether to rotate a projection to account for the 3DS screen orientation. -/// Both screens on the 3DS are oriented such that the "top-left" of the screen -/// in framebuffer coordinates is the physical bottom-left of the screen -/// (i.e. the "width" is smaller than the "height"). -#[derive(Clone, Copy, Debug)] -pub enum ScreenOrientation { - /// Rotate 90° clockwise to account for the 3DS screen rotation. Most - /// applications will use this variant. - Rotated, - /// Do not apply any extra rotation to the projection. - None, -} - -impl Default for ScreenOrientation { - fn default() -> Self { - Self::Rotated - } -} - -/// Configuration for calculating stereoscopic projections. -// TODO: not totally happy with this name + API yet, but it works for now. -#[derive(Clone, Copy, Debug)] -pub struct StereoDisplacement { - /// The horizontal offset of the eye from center. Negative values - /// correspond to the left eye, and positive values to the right eye. - pub displacement: f32, - /// The position of the screen, which determines the focal length. Objects - /// closer than this depth will appear to pop out of the screen, and objects - /// further than this will appear inside the screen. - pub screen_depth: f32, -} - -impl StereoDisplacement { - /// Construct displacement for the left and right eyes simulataneously. - /// The given `interocular_distance` describes the distance between the two - /// rendered "eyes". A negative value will be treated the same as a positive - /// value of the same magnitude. - /// - /// See struct documentation for details about the - /// [`screen_depth`](Self::screen_depth) parameter. - pub fn new(interocular_distance: f32, screen_depth: f32) -> (Self, Self) { - let displacement = interocular_distance.abs() / 2.0; - - let left_eye = Self { - displacement: -displacement, - screen_depth, - }; - let right_eye = Self { - displacement, - screen_depth, - }; - - (left_eye, right_eye) - } -} - -/// Configuration for the clipping planes of a projection. -/// -/// For [`Perspective`] projections, this is used for the near and far clip planes -/// of the [view frustum](https://en.wikipedia.org/wiki/Viewing_frustum). -/// -/// For [`Orthographic`] projections, this is used for the Z clipping planes of -/// the projection. -/// -/// Note that the `near` value should always be less than `far`, regardless of -/// [`CoordinateOrientation`]. In other words, these values will be negated -/// when used with a [`RightHanded`](CoordinateOrientation::RightHanded) -/// orientation. -#[derive(Clone, Copy, Debug)] -pub struct ClipPlanes { - /// The Z-depth of the near clip plane, usually close or equal to zero. - pub near: f32, - /// The Z-depth of the far clip plane, usually greater than zero. - pub far: f32, -} - -/// The aspect ratio of a projection plane. -#[derive(Clone, Copy, Debug)] -#[non_exhaustive] -pub enum AspectRatio { - /// The aspect ratio of the 3DS' top screen (per-eye). - #[doc(alias = "C3D_AspectRatioTop")] - TopScreen, - /// The aspect ratio of the 3DS' bottom screen. - #[doc(alias = "C3D_AspectRatioBot")] - BottomScreen, - /// A custom aspect ratio (should be calcualted as `width / height`). - Other(f32), -} - -impl From for f32 { - fn from(ratio: AspectRatio) -> Self { - match ratio { - AspectRatio::TopScreen => citro3d_sys::C3D_AspectRatioTop as f32, - AspectRatio::BottomScreen => citro3d_sys::C3D_AspectRatioBot as f32, - AspectRatio::Other(ratio) => ratio, - } - } -} - -// endregion diff --git a/citro3d/src/math/fvec.rs b/citro3d/src/math/fvec.rs index 76d1c2d..5b2df95 100644 --- a/citro3d/src/math/fvec.rs +++ b/citro3d/src/math/fvec.rs @@ -14,7 +14,7 @@ pub type FVec4 = FVec<4>; impl fmt::Debug for FVec { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let inner = unsafe { self.0.c }; + let inner = unsafe { self.0.__bindgen_anon_1 }; f.debug_tuple(std::any::type_name::()) .field(&inner) .finish() diff --git a/citro3d/src/math/matrix.rs b/citro3d/src/math/matrix.rs new file mode 100644 index 0000000..f863784 --- /dev/null +++ b/citro3d/src/math/matrix.rs @@ -0,0 +1,202 @@ +use std::mem::MaybeUninit; + +pub use private::Matrix; + +use super::{CoordinateOrientation, FVec3}; + +mod private { + use std::fmt; + + /// An `M`x`N` row-major matrix of `f32`s. + #[doc(alias = "C3D_Mtx")] + #[derive(Clone)] + pub struct Matrix(citro3d_sys::C3D_Mtx); + + impl Matrix { + const ROW_SIZE: () = assert!(M == 3 || M == 4); + const COLUMN_SIZE: () = assert!(N > 0 && N <= 4); + + // This constructor validates, at compile time, that the + // constructed matrix is 3xN or 4xN matrix, where 0 < N ≤ 4. + // We put this struct in a submodule to enforce that nothing creates + // a Matrix without calling this constructor. + #[allow(clippy::let_unit_value)] + pub(crate) fn new(value: citro3d_sys::C3D_Mtx) -> Self { + let () = Self::ROW_SIZE; + let () = Self::COLUMN_SIZE; + Self(value) + } + + pub(crate) fn as_raw(&self) -> *const citro3d_sys::C3D_Mtx { + &self.0 + } + + pub(crate) fn into_raw(self) -> citro3d_sys::C3D_Mtx { + self.0 + } + + pub(crate) fn as_mut(&mut self) -> *mut citro3d_sys::C3D_Mtx { + &mut self.0 + } + } + + impl fmt::Debug for Matrix { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let rows = unsafe { self.0.r }.map(|row| { + // UNWRAP: N ≤ 4, so slicing to a smaller array should always work + let mut row: [f32; N] = unsafe { row.c[..N].try_into() }.unwrap(); + // Rows are stored in WZYX order which is opposite of how most people + // probably expect, so we reverse each row in-place as well. + row.reverse(); + row + }); + + // UNWRAP: M ≤ 4, so slicing to a smaller array should always work + let inner: [_; M] = rows[..M].try_into().unwrap(); + + f.debug_tuple(std::any::type_name::()) + .field(&inner) + .finish() + } + } +} + +/// A 3x3 row-major matrix of `f32`s. +pub type Matrix3 = Matrix<3, 3>; +/// A 4x4 row-major matrix of `f32`s. +pub type Matrix4 = Matrix<4, 4>; + +impl Matrix { + /// Construct the zero matrix. + #[doc(alias = "Mtx_Zeros")] + pub fn zero() -> Self { + // TODO: should this also be Default::default()? + let mut out = MaybeUninit::uninit(); + unsafe { + citro3d_sys::Mtx_Zeros(out.as_mut_ptr()); + Self::new(out.assume_init()) + } + } + + /// Transpose the matrix, swapping rows and columns. + #[doc(alias = "Mtx_Transpose")] + pub fn transpose(mut self) -> Matrix { + unsafe { + citro3d_sys::Mtx_Transpose(self.as_mut()); + } + Matrix::new(self.into_raw()) + } + + // region: Matrix transformations + // + // NOTE: the `bRightSide` arg common to many of these APIs flips the order of + // operations so that a transformation occurs as self(T) instead of T(self). + // For now I'm not sure if that's a common use case, but if needed we could + // probably have some kinda wrapper type that does transformations in the + // opposite order, or an enum arg for these APIs or something. + + /// Translate a transformation matrix by the given amounts in the X, Y, and Z + /// directions. + pub fn translate(&mut self, x: f32, y: f32, z: f32) { + unsafe { citro3d_sys::Mtx_Translate(self.as_mut(), x, y, z, false) } + } + + /// Scale a transformation matrix by the given amounts in the X, Y, and Z directions. + pub fn scale(&mut self, x: f32, y: f32, z: f32) { + unsafe { citro3d_sys::Mtx_Scale(self.as_mut(), x, y, z) } + } + + /// Rotate a transformation matrix by the given angle around the given axis. + pub fn rotate(&mut self, axis: FVec3, angle: f32) { + unsafe { citro3d_sys::Mtx_Rotate(self.as_mut(), axis.0, angle, false) } + } + + /// Rotate a transformation matrix by the given angle around the X axis. + pub fn rotate_x(&mut self, angle: f32) { + unsafe { citro3d_sys::Mtx_RotateX(self.as_mut(), angle, false) } + } + + /// Rotate a transformation matrix by the given angle around the Y axis. + pub fn rotate_y(&mut self, angle: f32) { + unsafe { citro3d_sys::Mtx_RotateY(self.as_mut(), angle, false) } + } + + /// Rotate a transformation matrix by the given angle around the Z axis. + pub fn rotate_z(&mut self, angle: f32) { + unsafe { citro3d_sys::Mtx_RotateZ(self.as_mut(), angle, false) } + } + + // endregion +} + +impl Matrix { + /// Find the inverse of the matrix. + /// + /// # Errors + /// + /// If the matrix has no inverse, it will be returned unchanged as an [`Err`]. + #[doc(alias = "Mtx_Inverse")] + pub fn inverse(mut self) -> Result { + let determinant = unsafe { citro3d_sys::Mtx_Inverse(self.as_mut()) }; + if determinant == 0.0 { + Err(self) + } else { + Ok(self) + } + } + + /// Construct the identity matrix. + #[doc(alias = "Mtx_Identity")] + pub fn identity() -> Self { + let mut out = MaybeUninit::uninit(); + unsafe { + citro3d_sys::Mtx_Identity(out.as_mut_ptr()); + Self::new(out.assume_init()) + } + } +} + +impl Matrix3 { + /// Construct a 3x3 matrix with the given values on the diagonal. + #[doc(alias = "Mtx_Diagonal")] + pub fn diagonal(x: f32, y: f32, z: f32) -> Self { + let mut out = MaybeUninit::uninit(); + unsafe { + citro3d_sys::Mtx_Diagonal(out.as_mut_ptr(), x, y, z, 0.0); + Self::new(out.assume_init()) + } + } +} + +impl Matrix4 { + /// Construct a 4x4 matrix with the given values on the diagonal. + #[doc(alias = "Mtx_Diagonal")] + pub fn diagonal(x: f32, y: f32, z: f32, w: f32) -> Self { + let mut out = MaybeUninit::uninit(); + unsafe { + citro3d_sys::Mtx_Diagonal(out.as_mut_ptr(), x, y, z, w); + Self::new(out.assume_init()) + } + } + + /// Construct a 3D transformation matrix for a camera, given its position, + /// target, and upward direction. + pub fn looking_at( + camera_position: FVec3, + camera_target: FVec3, + camera_up: FVec3, + coordinates: CoordinateOrientation, + ) -> Self { + let mut out = MaybeUninit::uninit(); + unsafe { + citro3d_sys::Mtx_LookAt( + out.as_mut_ptr(), + camera_position.0, + camera_target.0, + camera_up.0, + coordinates.is_left_handed(), + ); + Self::new(out.assume_init()) + } + } +} diff --git a/citro3d/src/math/ops.rs b/citro3d/src/math/ops.rs index 0cb51a4..e8c648a 100644 --- a/citro3d/src/math/ops.rs +++ b/citro3d/src/math/ops.rs @@ -166,7 +166,6 @@ impl Eq for Matrix {} #[cfg(test)] mod tests { use super::*; - use crate::math::{Matrix3, Matrix4}; #[test] fn vec3() { @@ -198,7 +197,7 @@ mod tests { let r = Matrix3::identity(); let (l, r) = (&l, &r); - assert_eq!(l * r, 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)); } @@ -209,7 +208,7 @@ mod tests { let r = Matrix4::identity(); let (l, r) = (&l, &r); - assert_eq!(l * r, 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)); } diff --git a/citro3d/src/math/projection.rs b/citro3d/src/math/projection.rs index 44bc6e1..278a776 100644 --- a/citro3d/src/math/projection.rs +++ b/citro3d/src/math/projection.rs @@ -1,9 +1,7 @@ use std::mem::MaybeUninit; use std::ops::Range; -use super::{ - AspectRatio, ClipPlanes, CoordinateOrientation, Matrix4, ScreenOrientation, StereoDisplacement, -}; +use super::Matrix4; /// Configuration for a 3D [projection](https://en.wikipedia.org/wiki/3D_projection). /// See specific `Kind` implementations for constructors, e.g. @@ -262,3 +260,134 @@ impl From> for Matrix4 { } } } + +// region: Projection configuration + +/// The [orientation](https://en.wikipedia.org/wiki/Orientation_(geometry)) +/// (or "handedness") of the coordinate system. Coordinates are always +Y-up, +/// +X-right. +#[derive(Clone, Copy, Debug)] +pub enum CoordinateOrientation { + /// A left-handed coordinate system. +Z points into the screen. + LeftHanded, + /// A right-handed coordinate system. +Z points out of the screen. + RightHanded, +} + +impl CoordinateOrientation { + pub(crate) fn is_left_handed(self) -> bool { + matches!(self, Self::LeftHanded) + } +} + +impl Default for CoordinateOrientation { + /// This is an opinionated default, but [`RightHanded`](Self::RightHanded) + /// seems to be the preferred coordinate system for most + /// [examples](https://github.com/devkitPro/3ds-examples) + /// from upstream, and is also fairly common in other applications. + fn default() -> Self { + Self::RightHanded + } +} + +/// Whether to rotate a projection to account for the 3DS screen orientation. +/// Both screens on the 3DS are oriented such that the "top-left" of the screen +/// in framebuffer coordinates is the physical bottom-left of the screen +/// (i.e. the "width" is smaller than the "height"). +#[derive(Clone, Copy, Debug)] +pub enum ScreenOrientation { + /// Rotate 90° clockwise to account for the 3DS screen rotation. Most + /// applications will use this variant. + Rotated, + /// Do not apply any extra rotation to the projection. + None, +} + +impl Default for ScreenOrientation { + fn default() -> Self { + Self::Rotated + } +} + +/// Configuration for calculating stereoscopic projections. +// TODO: not totally happy with this name + API yet, but it works for now. +#[derive(Clone, Copy, Debug)] +pub struct StereoDisplacement { + /// The horizontal offset of the eye from center. Negative values + /// correspond to the left eye, and positive values to the right eye. + pub displacement: f32, + /// The position of the screen, which determines the focal length. Objects + /// closer than this depth will appear to pop out of the screen, and objects + /// further than this will appear inside the screen. + pub screen_depth: f32, +} + +impl StereoDisplacement { + /// Construct displacement for the left and right eyes simulataneously. + /// The given `interocular_distance` describes the distance between the two + /// rendered "eyes". A negative value will be treated the same as a positive + /// value of the same magnitude. + /// + /// See struct documentation for details about the + /// [`screen_depth`](Self::screen_depth) parameter. + pub fn new(interocular_distance: f32, screen_depth: f32) -> (Self, Self) { + let displacement = interocular_distance.abs() / 2.0; + + let left_eye = Self { + displacement: -displacement, + screen_depth, + }; + let right_eye = Self { + displacement, + screen_depth, + }; + + (left_eye, right_eye) + } +} + +/// Configuration for the clipping planes of a projection. +/// +/// For [`Perspective`] projections, this is used for the near and far clip planes +/// of the [view frustum](https://en.wikipedia.org/wiki/Viewing_frustum). +/// +/// For [`Orthographic`] projections, this is used for the Z clipping planes of +/// the projection. +/// +/// Note that the `near` value should always be less than `far`, regardless of +/// [`CoordinateOrientation`]. In other words, these values will be negated +/// when used with a [`RightHanded`](CoordinateOrientation::RightHanded) +/// orientation. +#[derive(Clone, Copy, Debug)] +pub struct ClipPlanes { + /// The Z-depth of the near clip plane, usually close or equal to zero. + pub near: f32, + /// The Z-depth of the far clip plane, usually greater than zero. + pub far: f32, +} + +/// The aspect ratio of a projection plane. +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +pub enum AspectRatio { + /// The aspect ratio of the 3DS' top screen (per-eye). + #[doc(alias = "C3D_AspectRatioTop")] + TopScreen, + /// The aspect ratio of the 3DS' bottom screen. + #[doc(alias = "C3D_AspectRatioBot")] + BottomScreen, + /// A custom aspect ratio (should be calcualted as `width / height`). + Other(f32), +} + +impl From for f32 { + fn from(ratio: AspectRatio) -> Self { + match ratio { + AspectRatio::TopScreen => citro3d_sys::C3D_AspectRatioTop as f32, + AspectRatio::BottomScreen => citro3d_sys::C3D_AspectRatioBot as f32, + AspectRatio::Other(ratio) => ratio, + } + } +} + +// endregion From 4cc9a649fcd039a0ed415581049929b4f3310003 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Mon, 16 Oct 2023 00:28:11 -0400 Subject: [PATCH 08/12] More fixes for debug print + matrix equality --- citro3d/src/math/matrix.rs | 27 +++++++++++++++++---------- citro3d/src/math/ops.rs | 2 +- citro3d/src/math/projection.rs | 9 +++++---- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/citro3d/src/math/matrix.rs b/citro3d/src/math/matrix.rs index f863784..79faabb 100644 --- a/citro3d/src/math/matrix.rs +++ b/citro3d/src/math/matrix.rs @@ -38,25 +38,32 @@ mod private { pub(crate) fn as_mut(&mut self) -> *mut citro3d_sys::C3D_Mtx { &mut self.0 } + + /// Trim the matrix down to only the rows and columns we care about, + /// since the inner representation is always 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. + // UNWRAP: N ≤ 4, so slicing to a smaller array should always work + unsafe { row.c[(4 - N)..].try_into() }.unwrap() + }); + + // UNWRAP: M ≤ 4, so slicing to a smaller array should always work + rows[..M].try_into().unwrap() + } } impl fmt::Debug for Matrix { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let rows = unsafe { self.0.r }.map(|row| { - // UNWRAP: N ≤ 4, so slicing to a smaller array should always work - let mut row: [f32; N] = unsafe { row.c[..N].try_into() }.unwrap(); + let inner = self.as_rows().map(|mut row| { // Rows are stored in WZYX order which is opposite of how most people - // probably expect, so we reverse each row in-place as well. + // probably expect, so reverse each row in-place for debug printing row.reverse(); row }); - // UNWRAP: M ≤ 4, so slicing to a smaller array should always work - let inner: [_; M] = rows[..M].try_into().unwrap(); - - 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() } } } diff --git a/citro3d/src/math/ops.rs b/citro3d/src/math/ops.rs index e8c648a..8204ca4 100644 --- a/citro3d/src/math/ops.rs +++ b/citro3d/src/math/ops.rs @@ -157,7 +157,7 @@ impl Mul for &Matrix<4, 3> { impl, const M: usize, const N: usize> PartialEq for Matrix { fn eq(&self, other: &Rhs) -> bool { - unsafe { (*self.as_raw()).m == (*other.borrow().as_raw()).m } + self.as_rows() == other.borrow().as_rows() } } diff --git a/citro3d/src/math/projection.rs b/citro3d/src/math/projection.rs index 278a776..46313b9 100644 --- a/citro3d/src/math/projection.rs +++ b/citro3d/src/math/projection.rs @@ -73,10 +73,11 @@ impl Projection { /// far: 100.0, /// }; /// - /// let bottom: Matrix = + /// let bottom: Matrix4 = /// Projection::perspective(PI / 4.0, AspectRatio::BottomScreen, clip_planes).into(); /// - /// let top: Matrix = Projection::perspective(PI / 4.0, AspectRatio::TopScreen, clip_planes).into(); + /// let top: Matrix4 = + /// Projection::perspective(PI / 4.0, AspectRatio::TopScreen, clip_planes).into(); /// ``` #[doc(alias = "Mtx_Persp")] #[doc(alias = "Mtx_PerspTilt")] @@ -204,9 +205,9 @@ impl Projection { /// /// ``` /// # let _runner = test_runner::GdbRunner::default(); - /// # use citro3d::math::{Projection, ClipPlanes, Matrix}; + /// # use citro3d::math::{Projection, ClipPlanes, Matrix4}; /// # - /// let mtx: Matrix = Projection::orthographic( + /// let mtx: Matrix4 = Projection::orthographic( /// 0.0..240.0, /// 0.0..400.0, /// ClipPlanes { From f770f504b0ad6082edde86907cace6f071603e65 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Thu, 19 Oct 2023 23:33:15 -0400 Subject: [PATCH 09/12] 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)); } } From 3174e66e7ef9becdfd21b04a962bcc36c282e2c9 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sat, 21 Oct 2023 13:55:40 -0400 Subject: [PATCH 10/12] Swap to `approx` for comparisons Hide this impl behind a feature, which is always enabled for testing but disabled by default for downstream crates. This feels a bit tidier to me. --- citro3d/Cargo.toml | 21 ++++- citro3d/src/lib.rs | 9 +- citro3d/src/math/fvec.rs | 191 +++++++++++++++++++++++++-------------- citro3d/src/math/ops.rs | 81 +++++++++-------- 4 files changed, 193 insertions(+), 109 deletions(-) diff --git a/citro3d/Cargo.toml b/citro3d/Cargo.toml index 0762a4d..4147662 100644 --- a/citro3d/Cargo.toml +++ b/citro3d/Cargo.toml @@ -6,15 +6,32 @@ version = "0.1.0" edition = "2021" [dependencies] +approx = { version = "0.5.1", optional = true } 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" +document-features = "0.2.7" libc = "0.2.125" +[features] +default = [] +## Enable this feature to use the `approx` crate for comparing vectors and matrices. +approx = ["dep:approx"] + [dev-dependencies] -float-cmp = "0.9.0" test-runner = { git = "https://github.com/rust3ds/test-runner.git" } + +[dev-dependencies.citro3d] +# Basically, this works like `cargo 3ds test --features ...` for building tests +# https://github.com/rust-lang/cargo/issues/2911#issuecomment-749580481 +path = "." +features = ["approx"] + +[package.metadata.docs.rs] +all-features = true +default-target = "armv6k-nintendo-3ds" +targs = [] +cargo-args = ["-Z", "build-std"] diff --git a/citro3d/src/lib.rs b/citro3d/src/lib.rs index 0ba340d..985db2a 100644 --- a/citro3d/src/lib.rs +++ b/citro3d/src/lib.rs @@ -1,6 +1,13 @@ -//! Safe Rust bindings to `citro3d`. #![feature(custom_test_frameworks)] #![test_runner(test_runner::run_gdb)] +#![feature(doc_cfg)] +#![feature(doc_auto_cfg)] + +//! Safe Rust bindings to `citro3d`. This crate wraps `citro3d-sys` to provide +//! safer APIs for graphics programs targeting the 3DS. +//! +//! ## Feature flags +#![doc = document_features::document_features!()] pub mod attrib; pub mod buffer; diff --git a/citro3d/src/math/fvec.rs b/citro3d/src/math/fvec.rs index ba77954..5813424 100644 --- a/citro3d/src/math/fvec.rs +++ b/citro3d/src/math/fvec.rs @@ -20,8 +20,41 @@ impl fmt::Debug for FVec { } } +impl FVec { + /// The vector's `x` component (also called the `i` component of `ijk[r]`). + #[doc(alias = "i")] + pub fn x(self) -> f32 { + unsafe { self.0.__bindgen_anon_1.x } + } + + /// The vector's `y` component (also called the `j` component of `ijk[r]`). + #[doc(alias = "j")] + pub fn y(self) -> f32 { + unsafe { self.0.__bindgen_anon_1.y } + } + + /// The vector's `i` component (also called the `k` component of `ijk[r]`). + #[doc(alias = "k")] + pub fn z(self) -> f32 { + unsafe { self.0.__bindgen_anon_1.z } + } +} + impl FVec4 { + /// The vector's `w` component (also called `r` for the real component of `ijk[r]`). + #[doc(alias = "r")] + pub fn w(self) -> f32 { + unsafe { self.0.__bindgen_anon_1.w } + } + /// Create a new [`FVec4`] from its components. + /// + /// # Example + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::FVec4; + /// let v = FVec4::new(1.0, 2.0, 3.0, 4.0); + /// ``` #[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) }) @@ -33,146 +66,185 @@ impl FVec4 { /// ``` /// # let _runner = test_runner::GdbRunner::default(); /// # use citro3d::math::FVec4; - /// # use float_cmp::assert_approx_eq; + /// # use approx::assert_abs_diff_eq; /// let v = FVec4::splat(1.0); - /// assert_approx_eq!(FVec4, v, FVec4::new(1.0, 1.0, 1.0, 1.0)); + /// assert_abs_diff_eq!(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 `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; + /// # use approx::assert_abs_diff_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) - /// ); + /// assert_abs_diff_eq!(v.perspective_divide(), FVec4::new(1.0, 2.0, 3.0, 1.0)); /// ``` #[doc(alias = "FVec4_PerspDivide")] - pub fn perspective_divide(&self) -> Self { + 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; + /// # use approx::assert_abs_diff_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); + /// assert_abs_diff_eq!(v1.dot(v2), 7.0); /// ``` #[doc(alias = "FVec4_Dot")] - pub fn dot(&self, rhs: &Self) -> f32 { + 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; + /// # use approx::assert_abs_diff_eq; /// let v = FVec4::splat(1.0); - /// assert_approx_eq!(f32, v.magnitude(), 2.0); + /// assert_abs_diff_eq!(v.magnitude(), 2.0); /// ``` #[doc(alias = "FVec4_Magnitude")] - pub fn magnitude(&self) -> f32 { + 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; + /// # use approx::assert_abs_diff_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)); + /// assert_abs_diff_eq!(v, FVec4::new(0.1, 0.4, 0.4, 0.8)); /// ``` #[doc(alias = "FVec3_Normalize")] - pub fn normalize(&self) -> Self { + pub fn normalize(self) -> Self { Self(unsafe { citro3d_sys::FVec3_Normalize(self.0) }) } } impl FVec3 { /// Create a new [`FVec3`] from its components. + /// + /// # Example + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::FVec3; + /// let v = FVec3::new(1.0, 2.0, 3.0); + /// ``` #[doc(alias = "FVec3_New")] pub fn new(x: f32, y: f32, z: f32) -> Self { Self(unsafe { citro3d_sys::FVec3_New(x, y, z) }) } /// Create a new [`FVec3`], setting each component to the given `v`. + /// + /// # Example + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::FVec3; + /// let v = FVec3::splat(1.0); + /// ``` pub fn splat(v: f32) -> Self { Self::new(v, v, v) } /// The distance between two points in 3D space. + /// + /// # Example + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::FVec3; + /// # use approx::assert_abs_diff_eq; + /// let l = FVec3::new(1.0, 3.0, 4.0); + /// let r = FVec3::new(0.0, 1.0, 2.0); + /// + /// assert_abs_diff_eq!(l.distance(r), 3.0); + /// ``` #[doc(alias = "FVec3_Distance")] - pub fn distance(&self, rhs: &Self) -> f32 { + pub fn distance(self, rhs: Self) -> f32 { unsafe { citro3d_sys::FVec3_Distance(self.0, rhs.0) } } /// The cross product of two 3D vectors. + /// + /// # Example + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::FVec3; + /// # use approx::assert_abs_diff_eq; + /// let l = FVec3::new(1.0, 0.0, 0.0); + /// let r = FVec3::new(0.0, 1.0, 0.0); + /// assert_abs_diff_eq!(l.cross(r), FVec3::new(0.0, 0.0, 1.0)); + /// ``` #[doc(alias = "FVec3_Cross")] - pub fn cross(&self, rhs: &Self) -> Self { + pub fn cross(self, rhs: Self) -> Self { Self(unsafe { citro3d_sys::FVec3_Cross(self.0, rhs.0) }) } /// The dot product of two vectors. + /// + /// # Example + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::FVec3; + /// # use approx::assert_abs_diff_eq; + /// let l = FVec3::new(1.0, 2.0, 3.0); + /// let r = FVec3::new(3.0, 2.0, 1.0); + /// assert_abs_diff_eq!(l.dot(r), 10.0); + /// ``` #[doc(alias = "FVec3_Dot")] - pub fn dot(&self, rhs: &Self) -> f32 { + pub fn dot(self, rhs: Self) -> f32 { unsafe { citro3d_sys::FVec3_Dot(self.0, rhs.0) } } /// The magnitude of the vector. + /// + /// # Example + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::FVec3; + /// # use approx::assert_abs_diff_eq; + /// let v = FVec3::splat(3.0f32.sqrt()); + /// assert_abs_diff_eq!(v.magnitude(), 3.0); + /// ``` #[doc(alias = "FVec3_Magnitude")] - pub fn magnitude(&self) -> f32 { + pub fn magnitude(self) -> f32 { unsafe { citro3d_sys::FVec3_Magnitude(self.0) } } /// Normalize the vector to a magnitude of `1.0`. + /// + /// # Example + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::FVec3; + /// # use approx::assert_abs_diff_eq; + /// let v = FVec3::splat(2.0); + /// assert_abs_diff_eq!(v.normalize(), FVec3::splat(1.0)); + /// ``` #[doc(alias = "FVec3_Normalize")] - pub fn normalize(&self) -> Self { + pub fn normalize(self) -> Self { Self(unsafe { citro3d_sys::FVec3_Normalize(self.0) }) } } -impl FVec { - /// The vector's `x` component (sometimes also called the `i` component of `ijk[r]`). - pub fn x(&self) -> f32 { - unsafe { self.0.__bindgen_anon_1.x } - } - - /// The vector's `y` component (sometimes also called the `j` component of `ijk[r]`). - pub fn y(&self) -> f32 { - unsafe { self.0.__bindgen_anon_1.y } - } - - /// The vector's `i` component (sometimes also called the `k` component of `ijk[r]`). - pub fn z(&self) -> f32 { - unsafe { self.0.__bindgen_anon_1.z } - } -} - #[cfg(test)] mod tests { - use float_cmp::assert_approx_eq; + use approx::assert_abs_diff_eq; use super::*; @@ -181,7 +253,7 @@ mod tests { 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); + assert_abs_diff_eq!(&actual[..], &expected[..]); } #[test] @@ -189,25 +261,6 @@ mod tests { 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)); - - for component in [l.x(), l.y(), l.z()] { - assert_approx_eq!(f32, component, 2.0); - } - - let dot = l.dot(&FVec3::splat(3.0)); - assert_approx_eq!(f32, dot, 18.0); - - assert_approx_eq!(f32, l.magnitude(), f32::sqrt(12.0)); - - let norm = l.normalize(); - assert_approx_eq!(f32, norm.magnitude(), 1.0); - for component in [l.y(), l.z()] { - assert_approx_eq!(f32, l.x(), component); - } + assert_abs_diff_eq!(&actual[..], &expected[..]); } } diff --git a/citro3d/src/math/ops.rs b/citro3d/src/math/ops.rs index e66ff82..311c473 100644 --- a/citro3d/src/math/ops.rs +++ b/citro3d/src/math/ops.rs @@ -2,7 +2,8 @@ use std::borrow::Borrow; use std::mem::MaybeUninit; use std::ops::{Add, Deref, Div, Mul, Neg, Sub}; -use float_cmp::ApproxEq; +#[cfg(feature = "approx")] +use approx::AbsDiffEq; use super::{FVec, FVec3, FVec4, Matrix, Matrix3, Matrix4}; @@ -106,17 +107,20 @@ impl PartialEq for FVec { impl Eq for FVec {} -impl ApproxEq for FVec -where - f32: ApproxEq, -{ - type Margin = Margin; +#[cfg(feature = "approx")] +impl AbsDiffEq for FVec { + type Epsilon = f32; + + fn default_epsilon() -> Self::Epsilon { + // See https://docs.rs/almost/latest/almost/#why-another-crate + // for rationale of using this over just EPSILON + f32::EPSILON.sqrt() + } - fn approx_eq>(self, other: Self, margin: M) -> bool { - let margin = margin.into(); + fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { let range = (4 - N)..; let (lhs, rhs) = unsafe { (&self.0.c[range.clone()], &other.0.c[range]) }; - lhs.approx_eq(rhs, margin) + lhs.abs_diff_eq(rhs, epsilon) } } @@ -200,21 +204,24 @@ 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; +#[cfg(feature = "approx")] +#[doc(cfg(feature = "approx"))] +impl AbsDiffEq for Matrix { + type Epsilon = f32; + + fn default_epsilon() -> Self::Epsilon { + // See https://docs.rs/almost/latest/almost/#why-another-crate + // for rationale of using this over just EPSILON + f32::EPSILON.sqrt() + } - fn approx_eq>(self, other: Self, margin: Marg) -> bool { - let margin = margin.into(); + fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { 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) { + if !lhs[row][col].abs_diff_eq(&rhs[row][col], epsilon) { return false; } } @@ -226,32 +233,32 @@ where #[cfg(test)] mod tests { - use float_cmp::assert_approx_eq; + use approx::assert_abs_diff_eq; use super::*; #[test] - fn vec3() { + fn fvec3() { let l = FVec3::splat(1.0); let r = FVec3::splat(2.0); - 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)); + assert_abs_diff_eq!(l + r, FVec3::splat(3.0)); + assert_abs_diff_eq!(l - r, FVec3::splat(-1.0)); + assert_abs_diff_eq!(-l, FVec3::splat(-1.0)); + assert_abs_diff_eq!(l * 1.5, FVec3::splat(1.5)); + assert_abs_diff_eq!(l / 2.0, FVec3::splat(0.5)); } #[test] - fn vec4() { + fn fvec4() { let l = FVec4::splat(1.0); let r = FVec4::splat(2.0); - 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)); + assert_abs_diff_eq!(l + r, FVec4::splat(3.0)); + assert_abs_diff_eq!(l - r, FVec4::splat(-1.0)); + assert_abs_diff_eq!(-l, FVec4::splat(-1.0)); + assert_abs_diff_eq!(l * 1.5, FVec4::splat(1.5)); + assert_abs_diff_eq!(l / 2.0, FVec4::splat(0.5)); } #[test] @@ -260,9 +267,9 @@ mod tests { let r = Matrix3::identity(); let (l, r) = (&l, &r); - 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)); + assert_abs_diff_eq!(&(l * r), l); + assert_abs_diff_eq!(&(l + r), &Matrix3::diagonal(2.0, 3.0, 4.0)); + assert_abs_diff_eq!(&(l - r), &Matrix3::diagonal(0.0, 1.0, 2.0)); } #[test] @@ -271,8 +278,8 @@ mod tests { let r = Matrix4::identity(); let (l, r) = (&l, &r); - 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)); + assert_abs_diff_eq!(&(l * r), l); + assert_abs_diff_eq!(&(l + r), &Matrix4::diagonal(2.0, 3.0, 4.0, 5.0)); + assert_abs_diff_eq!(&(l - r), &Matrix4::diagonal(0.0, 1.0, 2.0, 3.0)); } } From fdbd17b2187a74db913d733bdb2fdfb1c71dd625 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Tue, 21 Nov 2023 11:45:05 -0500 Subject: [PATCH 11/12] Link shim-3ds for `test --lib` --- citro3d-sys/Cargo.toml | 3 +++ citro3d-sys/src/lib.rs | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/citro3d-sys/Cargo.toml b/citro3d-sys/Cargo.toml index 62b31d8..797e9cb 100644 --- a/citro3d-sys/Cargo.toml +++ b/citro3d-sys/Cargo.toml @@ -14,3 +14,6 @@ ctru-sys = { git = "https://github.com/rust3ds/ctru-rs.git" } bindgen = { version = "0.68.1", features = ["experimental"] } cc = "1.0.83" doxygen-rs = "0.4.2" + +[dev-dependencies] +shim-3ds = { git = "https://github.com/rust3ds/shim-3ds.git" } diff --git a/citro3d-sys/src/lib.rs b/citro3d-sys/src/lib.rs index ed84f5e..c2867a5 100644 --- a/citro3d-sys/src/lib.rs +++ b/citro3d-sys/src/lib.rs @@ -9,3 +9,7 @@ include!(concat!(env!("OUT_DIR"), "/bindings.rs")); pub mod gx; pub use gx::*; + +// Prevent linking errors from the standard `test` library when running `cargo 3ds test --lib`. +#[cfg(test)] +extern crate shim_3ds; From e1bba068d74efd81deeb30401457af3991c579ae Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Tue, 21 Nov 2023 12:02:14 -0500 Subject: [PATCH 12/12] Fix normalize doctests to use proper math --- citro3d/src/math/fvec.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/citro3d/src/math/fvec.rs b/citro3d/src/math/fvec.rs index 5813424..183f412 100644 --- a/citro3d/src/math/fvec.rs +++ b/citro3d/src/math/fvec.rs @@ -128,11 +128,11 @@ impl FVec4 { /// # use citro3d::math::FVec4; /// # use approx::assert_abs_diff_eq; /// let v = FVec4::new(1.0, 2.0, 2.0, 4.0); - /// assert_abs_diff_eq!(v, FVec4::new(0.1, 0.4, 0.4, 0.8)); + /// assert_abs_diff_eq!(v.normalize(), FVec4::new(0.2, 0.4, 0.4, 0.8)); /// ``` #[doc(alias = "FVec3_Normalize")] pub fn normalize(self) -> Self { - Self(unsafe { citro3d_sys::FVec3_Normalize(self.0) }) + Self(unsafe { citro3d_sys::FVec4_Normalize(self.0) }) } } @@ -233,8 +233,8 @@ impl FVec3 { /// # let _runner = test_runner::GdbRunner::default(); /// # use citro3d::math::FVec3; /// # use approx::assert_abs_diff_eq; - /// let v = FVec3::splat(2.0); - /// assert_abs_diff_eq!(v.normalize(), FVec3::splat(1.0)); + /// let v = FVec3::splat(1.0); + /// assert_abs_diff_eq!(v.normalize(), FVec3::splat(1.0 / 3.0_f32.sqrt())); /// ``` #[doc(alias = "FVec3_Normalize")] pub fn normalize(self) -> Self {