diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c447bbc..5452463 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,13 +5,12 @@ on: branches: - main pull_request: - branches: - - main workflow_dispatch: jobs: lint: strategy: + fail-fast: false matrix: toolchain: # Run against a "known good" nightly. Rustc version is 1 day behind the toolchain date @@ -48,6 +47,7 @@ jobs: test: strategy: + fail-fast: false matrix: toolchain: - nightly-2023-06-01 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; diff --git a/citro3d/Cargo.toml b/citro3d/Cargo.toml index 306b2d2..4147662 100644 --- a/citro3d/Cargo.toml +++ b/citro3d/Cargo.toml @@ -6,13 +6,32 @@ version = "0.1.0" edition = "2021" [dependencies] -citro3d-macros = { version = "0.1.0", path = "../citro3d-macros" } +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" } +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] 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/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/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.rs b/citro3d/src/math.rs index 479ef71..949bb42 100644 --- a/citro3d/src/math.rs +++ b/citro3d/src/math.rs @@ -1,185 +1,24 @@ //! Safe wrappers for working with matrix and vector types provided by `citro3d`. -use std::mem::MaybeUninit; +// TODO: bench FFI calls into `inline statics` generated by bindgen, vs +// reimplementing some of those calls. Many of them are pretty trivial impls +mod fvec; +mod matrix; +mod ops; mod projection; -pub use projection::{Orthographic, Perspective, 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")] 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); - -/// A 4x4 row-major matrix of `f32`s. -#[doc(alias = "C3D_Mtx")] -pub struct Matrix(citro3d_sys::C3D_Mtx); - -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(out.assume_init()) - } - } - - /// 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()) - } - } - - pub(crate) fn as_raw(&self) -> *const citro3d_sys::C3D_Mtx { - &self.0 - } -} - -// 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 new file mode 100644 index 0000000..183f412 --- /dev/null +++ b/citro3d/src/math/fvec.rs @@ -0,0 +1,266 @@ +//! Floating-point vectors. + +use std::fmt; + +/// A vector of `f32`s. +#[derive(Clone, Copy)] +pub struct FVec(pub(crate) 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.__bindgen_anon_1 }; + let type_name = std::any::type_name::().split("::").last().unwrap(); + f.debug_tuple(type_name).field(&inner).finish() + } +} + +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) }) + } + + /// Create a new [`FVec4`], setting each component to `v`. + /// + /// # Example + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::FVec4; + /// # use approx::assert_abs_diff_eq; + /// let v = FVec4::splat(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) + } + + /// Divide the vector's XYZ components by its W component. + /// + /// # Example + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::math::FVec4; + /// # use approx::assert_abs_diff_eq; + /// let v = FVec4::new(2.0, 4.0, 6.0, 2.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 { + 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 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_abs_diff_eq!(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 approx::assert_abs_diff_eq; + /// let v = FVec4::splat(1.0); + /// assert_abs_diff_eq!(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 approx::assert_abs_diff_eq; + /// let v = FVec4::new(1.0, 2.0, 2.0, 4.0); + /// 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::FVec4_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 { + 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 { + 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 { + 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 { + 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(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 { + Self(unsafe { citro3d_sys::FVec3_Normalize(self.0) }) + } +} + +#[cfg(test)] +mod tests { + use approx::assert_abs_diff_eq; + + use super::*; + + #[test] + fn fvec4() { + 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_abs_diff_eq!(&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_abs_diff_eq!(&actual[..], &expected[..]); + } +} diff --git a/citro3d/src/math/matrix.rs b/citro3d/src/math/matrix.rs new file mode 100644 index 0000000..7215611 --- /dev/null +++ b/citro3d/src/math/matrix.rs @@ -0,0 +1,214 @@ +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 + } + + /// 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. + // 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 inner = self.as_rows().map(|mut row| { + // Rows are stored in WZYX order which is opposite of how most people + // probably expect, so reverse each row in-place for debug printing + row.reverse(); + row + }); + + let type_name = std::any::type_name::().split("::").last().unwrap(); + f.debug_tuple(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 new file mode 100644 index 0000000..311c473 --- /dev/null +++ b/citro3d/src/math/ops.rs @@ -0,0 +1,285 @@ +use std::borrow::Borrow; +use std::mem::MaybeUninit; +use std::ops::{Add, Deref, Div, Mul, Neg, Sub}; + +#[cfg(feature = "approx")] +use approx::AbsDiffEq; + +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) }) + } +} + +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) }) + } +} + +impl Neg for FVec4 { + type Output = Self; + + #[doc(alias = "FVec4_Negate")] + fn neg(self) -> Self::Output { + Self(unsafe { citro3d_sys::FVec4_Negate(self.0) }) + } +} + +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) }) + } +} + +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) }) + } +} + +impl Neg for FVec3 { + type Output = Self; + + #[doc(alias = "FVec3_Negate")] + fn neg(self) -> Self::Output { + Self(unsafe { citro3d_sys::FVec3_Negate(self.0) }) + } +} + +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, +{ + type Output = >::Output; + + fn div(self, rhs: f32) -> Self::Output { + self * (1.0 / rhs) + } +} + +impl PartialEq for FVec { + fn eq(&self, other: &Self) -> bool { + let range = (4 - N)..; + unsafe { self.0.c[range.clone()] == other.0.c[range] } + } +} + +impl Eq for FVec {} + +#[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 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.abs_diff_eq(rhs, epsilon) + } +} + +// region: Matrix math operators + +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) }) + } +} + +// endregion + +impl, const M: usize, const N: usize> PartialEq for Matrix { + fn eq(&self, other: &Rhs) -> bool { + self.as_rows() == other.borrow().as_rows() + } +} + +impl Eq for Matrix {} + +#[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 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].abs_diff_eq(&rhs[row][col], epsilon) { + return false; + } + } + } + + true + } +} + +#[cfg(test)] +mod tests { + use approx::assert_abs_diff_eq; + + use super::*; + + #[test] + fn fvec3() { + let l = FVec3::splat(1.0); + let r = FVec3::splat(2.0); + + 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 fvec4() { + let l = FVec4::splat(1.0); + let r = FVec4::splat(2.0); + + 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] + fn matrix3() { + let l = Matrix3::diagonal(1.0, 2.0, 3.0); + let r = Matrix3::identity(); + let (l, r) = (&l, &r); + + 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] + fn matrix4() { + let l = Matrix4::diagonal(1.0, 2.0, 3.0, 4.0); + let r = Matrix4::identity(); + let (l, r) = (&l, &r); + + 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)); + } +} diff --git a/citro3d/src/math/projection.rs b/citro3d/src/math/projection.rs index 6833fb9..46313b9 100644 --- a/citro3d/src/math/projection.rs +++ b/citro3d/src/math/projection.rs @@ -1,15 +1,13 @@ use std::mem::MaybeUninit; use std::ops::Range; -use super::{ - AspectRatio, ClipPlanes, CoordinateOrientation, Matrix, 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. /// [`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, @@ -75,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")] @@ -122,7 +121,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 +136,7 @@ impl Projection { } } -impl From> for Matrix { +impl From> for Matrix4 { fn from(projection: Projection) -> Self { let Perspective { vertical_fov_radians, @@ -182,7 +181,7 @@ impl From> for Matrix { } } - unsafe { Self(result.assume_init()) } + unsafe { Self::new(result.assume_init()) } } } @@ -206,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 { @@ -233,7 +232,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 +257,138 @@ impl From> for Matrix { clip_planes_z.far, projection.coordinates.is_left_handed(), ); - Self(out.assume_init()) + Self::new(out.assume_init()) } } } + +// 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 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(), + ); + } } }