From 2b1e1db28b0293d179e8634f7b1cf46f5896092d Mon Sep 17 00:00:00 2001
From: Ian Chamberlain <ian.h.chamberlain@gmail.com>
Date: Sun, 8 Oct 2023 21:55:36 -0400
Subject: [PATCH] Refactor matrix API and add doctests etc

Also add some doc aliases for citro3d functions. We could probably stand
to add more aliases to other wrappers too.
---
 citro3d-macros/src/lib.rs    |   2 +
 citro3d/examples/triangle.rs |  44 ++---
 citro3d/src/buffer.rs        |   1 -
 citro3d/src/lib.rs           |  48 ++---
 citro3d/src/math.rs          | 334 ++++++++++++++++++++++++++---------
 citro3d/src/shader.rs        |   2 +
 citro3d/src/uniform.rs       |  18 +-
 7 files changed, 315 insertions(+), 134 deletions(-)

diff --git a/citro3d-macros/src/lib.rs b/citro3d-macros/src/lib.rs
index 3d77b3f..4e52c88 100644
--- a/citro3d-macros/src/lib.rs
+++ b/citro3d-macros/src/lib.rs
@@ -1,3 +1,5 @@
+//! Procedural macro helpers for `citro3d`.
+
 // we're already nightly-only so might as well use unstable proc macro APIs.
 #![feature(proc_macro_span)]
 
diff --git a/citro3d/examples/triangle.rs b/citro3d/examples/triangle.rs
index 4009f5b..c631867 100644
--- a/citro3d/examples/triangle.rs
+++ b/citro3d/examples/triangle.rs
@@ -4,9 +4,11 @@
 #![feature(allocator_api)]
 
 use citro3d::macros::include_shader;
-use citro3d::math::{ClipPlane, CoordinateSystem, Matrix, Orientation, Stereoscopic};
-use citro3d::render::{self, ClearFlags};
-use citro3d::{attrib, buffer, shader, AspectRatio};
+use citro3d::math::{
+    AspectRatio, ClipPlanes, CoordinateOrientation, Matrix, ScreenOrientation, StereoDisplacement,
+};
+use citro3d::render::ClearFlags;
+use citro3d::{attrib, buffer, render, shader};
 use ctru::prelude::*;
 use ctru::services::gfx::{RawFrameBuffer, Screen, TopScreen3D};
 
@@ -167,46 +169,34 @@ fn calculate_projections() -> Projections {
     // TODO: it would be cool to allow playing around with these parameters on
     // the fly with D-pad, etc.
     let slider_val = unsafe { ctru_sys::osGet3DSliderState() };
-    let interocular_distance = slider_val / 4.0;
+    let interocular_distance = slider_val / 2.0;
 
     let vertical_fov = 40.0_f32.to_radians();
     let screen_depth = 2.0;
 
-    let clip_plane = ClipPlane {
+    let clip_planes = ClipPlanes {
         near: 0.01,
         far: 100.0,
     };
 
-    let stereoscopic = Stereoscopic::Stereo {
-        interocular_distance,
-        screen_depth,
-    };
-
-    let left_eye = Matrix::perspective_projection(
-        vertical_fov,
-        AspectRatio::TopScreen,
-        Orientation::Natural,
-        clip_plane,
-        stereoscopic,
-        CoordinateSystem::LeftHanded,
-    );
+    let stereo = StereoDisplacement::new(interocular_distance, screen_depth);
 
-    let right_eye = Matrix::perspective_projection(
+    let (left_eye, right_eye) = Matrix::stereo_projections(
         vertical_fov,
         AspectRatio::TopScreen,
-        Orientation::Natural,
-        clip_plane,
-        stereoscopic.invert(),
-        CoordinateSystem::LeftHanded,
+        ScreenOrientation::Rotated,
+        clip_planes,
+        CoordinateOrientation::LeftHanded,
+        stereo,
     );
 
     let center = Matrix::perspective_projection(
         vertical_fov,
         AspectRatio::BottomScreen,
-        Orientation::Natural,
-        clip_plane,
-        Stereoscopic::Mono,
-        CoordinateSystem::LeftHanded,
+        ScreenOrientation::Rotated,
+        clip_planes,
+        CoordinateOrientation::LeftHanded,
+        None,
     );
 
     Projections {
diff --git a/citro3d/src/buffer.rs b/citro3d/src/buffer.rs
index 534a487..b86be01 100644
--- a/citro3d/src/buffer.rs
+++ b/citro3d/src/buffer.rs
@@ -100,7 +100,6 @@ impl Info {
     ///
     /// * if `vbo_data` is not allocated with the [`ctru::linear`] allocator
     /// * if the maximum number (12) of VBOs are already registered
-    ///
     pub fn add<'this, 'vbo, 'idx, T>(
         &'this mut self,
         vbo_data: &'vbo [T],
diff --git a/citro3d/src/lib.rs b/citro3d/src/lib.rs
index 53c1350..ab10646 100644
--- a/citro3d/src/lib.rs
+++ b/citro3d/src/lib.rs
@@ -126,11 +126,37 @@ impl Instance {
         }
     }
 
-    pub fn bind_vertex_uniform(&mut self, index: uniform::Index, uniform: &impl Uniform) {
+    /// Bind a uniform to the given `index` in the vertex shader for the next draw call.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// # let _runner = test_runner::GdbRunner::default();
+    /// # use citro3d::{uniform, Matrix};
+    /// #
+    /// # let mut instance = citro3d::Instance::new().unwrap();
+    /// let idx = uniform::Index::from(0);
+    /// let mtx = Matrix::identity();
+    /// instance.bind_vertex_uniform(idx, &mtx);
+    /// ```
+    pub fn bind_vertex_uniform(&mut self, index: uniform::Index, uniform: impl Uniform) {
         uniform.bind(self, shader::Type::Vertex, index);
     }
 
-    pub fn bind_geometry_uniform(&mut self, index: uniform::Index, uniform: &impl Uniform) {
+    /// Bind a uniform to the given `index` in the geometry shader for the next draw call.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// # let _runner = test_runner::GdbRunner::default();
+    /// # use citro3d::{uniform, Matrix};
+    /// #
+    /// # let mut instance = citro3d::Instance::new().unwrap();
+    /// let idx = uniform::Index::from(0);
+    /// let mtx = Matrix::identity();
+    /// instance.bind_geometry_uniform(idx, &mtx);
+    /// ```
+    pub fn bind_geometry_uniform(&mut self, index: uniform::Index, uniform: impl Uniform) {
         uniform.bind(self, shader::Type::Geometry, index);
     }
 }
@@ -142,21 +168,3 @@ impl Drop for Instance {
         }
     }
 }
-
-#[derive(Clone, Copy, Debug)]
-#[non_exhaustive]
-pub enum AspectRatio {
-    TopScreen,
-    BottomScreen,
-    Other(f32),
-}
-
-impl From<AspectRatio> 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,
-        }
-    }
-}
diff --git a/citro3d/src/math.rs b/citro3d/src/math.rs
index 734e4f2..cf67cad 100644
--- a/citro3d/src/math.rs
+++ b/citro3d/src/math.rs
@@ -2,133 +2,303 @@
 
 use std::mem::MaybeUninit;
 
-use crate::AspectRatio;
-
 /// A 4-vector of `u8`s.
-pub struct IntVec(citro3d_sys::C3D_IVec);
+#[doc(alias = "C3D_IVec")]
+pub struct IVec(citro3d_sys::C3D_IVec);
 
 /// A 4-vector of `f32`s.
-pub struct FloatVec(citro3d_sys::C3D_FVec);
+#[doc(alias = "C3D_FVec")]
+pub struct FVec(citro3d_sys::C3D_FVec);
 
-/// A quaternion, internally represented the same way as [`FloatVec`].
-pub struct Quaternion(citro3d_sys::C3D_FQuat);
+/// 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 {
-    // TODO: does it make sense to have a helper that builds both left and right
-    // eyes for stereoscopic at the same time?
+    /// 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())
+        }
+    }
 
     /// Construct a projection matrix suitable for projecting 3D world space onto
     /// the 3DS screens.
+    ///
+    /// # Parameters
+    ///
+    /// * `vertical_fov_radians`: the vertical field of view, measured in radians
+    /// * `aspect_ratio`: The aspect ratio of the projection
+    /// * `orientation`: the orientation of the projection with respect to the screen
+    /// * `coordinates`: the handedness of the coordinate system to use
+    /// * `stereo`: if specified, the offset to displace the projection by
+    ///     for stereoscopic rendering.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use citro3d::math::*;
+    /// # use std::f32::consts::PI;
+    /// #
+    /// # let _runner = test_runner::GdbRunner::default();
+    ///
+    /// let clip_planes = ClipPlanes {
+    ///     near: 0.01,
+    ///     far: 100.0,
+    /// };
+    ///
+    /// let center = Matrix::perspective_projection(
+    ///     PI / 4.0,
+    ///     AspectRatio::BottomScreen,
+    ///     ScreenOrientation::Rotated,
+    ///     clip_planes,
+    ///     CoordinateOrientation::LeftHanded,
+    ///     None,
+    /// );
+    ///
+    /// let right_eye = Matrix::perspective_projection(
+    ///     PI / 4.0,
+    ///     AspectRatio::BottomScreen,
+    ///     ScreenOrientation::Rotated,
+    ///     clip_planes,
+    ///     CoordinateOrientation::LeftHanded,
+    ///     Some(StereoDisplacement {
+    ///         displacement: 1.0,
+    ///         screen_depth: 2.0,
+    ///     }),
+    /// );
+    /// ```
+    #[doc(alias = "Mtx_Persp")]
+    #[doc(alias = "Mtx_PerspTilt")]
     pub fn perspective_projection(
-        vertical_fov: f32,
+        vertical_fov_radians: f32,
         aspect_ratio: AspectRatio,
-        orientation: Orientation,
-        clip_plane: ClipPlane,
-        stereo: Stereoscopic,
-        coordinates: CoordinateSystem,
+        orientation: ScreenOrientation,
+        clip_plane: ClipPlanes,
+        coordinates: CoordinateOrientation,
+        stereo: Option<StereoDisplacement>,
     ) -> Self {
-        let (make_mtx_persp, make_mtx_stereo);
+        let mut result = MaybeUninit::uninit();
 
-        let initialize_mtx: &dyn Fn(_, _, _, _, _, _) -> _ = match stereo {
-            Stereoscopic::Mono => {
-                let make_mtx = match orientation {
-                    Orientation::Natural => citro3d_sys::Mtx_PerspTilt,
-                    Orientation::HardwareDefault => citro3d_sys::Mtx_Persp,
-                };
+        let left_handed = matches!(coordinates, CoordinateOrientation::LeftHanded);
 
-                make_mtx_persp = move |a, b, c, d, e, f| unsafe { make_mtx(a, b, c, d, e, f) };
-                &make_mtx_persp
+        if let Some(stereo) = stereo {
+            let initialize_mtx = orientation.perpsective_stereo_builder();
+            unsafe {
+                initialize_mtx(
+                    result.as_mut_ptr(),
+                    vertical_fov_radians,
+                    aspect_ratio.into(),
+                    clip_plane.near,
+                    clip_plane.far,
+                    stereo.displacement,
+                    stereo.screen_depth,
+                    left_handed,
+                );
             }
-            Stereoscopic::Stereo {
-                interocular_distance,
-                screen_depth,
-            } => {
-                let make_mtx = match orientation {
-                    Orientation::Natural => citro3d_sys::Mtx_PerspStereoTilt,
-                    Orientation::HardwareDefault => citro3d_sys::Mtx_PerspStereo,
-                };
-
-                make_mtx_stereo = move |a, b, c, d, e, f| unsafe {
-                    make_mtx(a, b, c, d, interocular_distance, screen_depth, e, f)
-                };
-                &make_mtx_stereo
+        } else {
+            let initialize_mtx = orientation.perspective_mono_builder();
+            unsafe {
+                initialize_mtx(
+                    result.as_mut_ptr(),
+                    vertical_fov_radians,
+                    aspect_ratio.into(),
+                    clip_plane.near,
+                    clip_plane.far,
+                    left_handed,
+                );
             }
-        };
-
-        let left_handed = matches!(coordinates, CoordinateSystem::LeftHanded);
-
-        let mut result = MaybeUninit::uninit();
-        initialize_mtx(
-            result.as_mut_ptr(),
-            vertical_fov,
-            aspect_ratio.into(),
-            clip_plane.near,
-            clip_plane.far,
-            left_handed,
-        );
+        }
 
         let inner = unsafe { result.assume_init() };
 
         Self(inner)
     }
 
+    /// Helper function to build both eyes' perspective projection matrices
+    /// at once. See [`perspective_projection`] for a description of each
+    /// parameter.
+    ///
+    /// ```
+    /// # use std::f32::consts::PI;
+    /// # use citro3d::math::*;
+    /// #
+    /// # let _runner = test_runner::GdbRunner::default();
+    ///
+    /// let (left_eye, right_eye) = Matrix::stereo_projections(
+    ///     PI / 4.0,
+    ///     AspectRatio::TopScreen,
+    ///     ScreenOrientation::Rotated,
+    ///     ClipPlanes {
+    ///         near: 0.01,
+    ///         far: 100.0,
+    ///     },
+    ///     CoordinateOrientation::LeftHanded,
+    ///     StereoDisplacement::new(0.5, 2.0),
+    /// );
+    /// ```
+    ///
+    /// [`perspective_projection`]: Self::perspective_projection
+    #[doc(alias = "Mtx_PerspStereo")]
+    #[doc(alias = "Mtx_PerspStereoTilt")]
+    pub fn stereo_projections(
+        vertical_fov_radians: f32,
+        aspect_ratio: AspectRatio,
+        orientation: ScreenOrientation,
+        clip_plane: ClipPlanes,
+        coordinates: CoordinateOrientation,
+        (left_eye, right_eye): (StereoDisplacement, StereoDisplacement),
+    ) -> (Self, Self) {
+        let left = Self::perspective_projection(
+            vertical_fov_radians,
+            aspect_ratio,
+            orientation,
+            clip_plane,
+            coordinates,
+            Some(left_eye),
+        );
+        let right = Self::perspective_projection(
+            vertical_fov_radians,
+            aspect_ratio,
+            orientation,
+            clip_plane,
+            coordinates,
+            Some(right_eye),
+        );
+        (left, right)
+    }
+
     pub(crate) fn as_raw(&self) -> *const citro3d_sys::C3D_Mtx {
         &self.0
     }
 }
 
-/// Whether to use left-handed or right-handed coordinates for calculations.
+/// The [orientation](https://en.wikipedia.org/wiki/Orientation_(geometry))
+/// (or "handedness") of the coordinate system.
 #[derive(Clone, Copy, Debug)]
-pub enum CoordinateSystem {
+pub enum CoordinateOrientation {
+    /// A left-handed coordinate system.
     LeftHanded,
+    /// A right-handed coordinate system.
     RightHanded,
 }
 
-/// Whether to rotate a projection to account for the 3DS screen configuration.
-/// Both screens on the 3DS are oriented such that the "top" of the screen is
-/// on the [left | right] ? side of the device when it's held normally, so
-/// projections must account for this extra rotation to display in the correct
-/// orientation.
+/// 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 Orientation {
-    /// Rotate the projection 90° to account for the 3DS screen rotation.
-    Natural,
-    /// Don't rotate the projection at all.
-    HardwareDefault,
+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,
 }
 
-#[derive(Clone, Copy, Debug)]
-// TODO: better name
-pub enum Stereoscopic {
-    Mono,
-    Stereo {
-        interocular_distance: f32,
-        // TODO: better name? At least docstring
-        screen_depth: f32,
-    },
-}
+impl ScreenOrientation {
+    fn perspective_mono_builder(
+        self,
+    ) -> unsafe extern "C" fn(*mut citro3d_sys::C3D_Mtx, f32, f32, f32, f32, bool) {
+        match self {
+            Self::Rotated => citro3d_sys::Mtx_PerspTilt,
+            Self::None => citro3d_sys::Mtx_Persp,
+        }
+    }
 
-impl Stereoscopic {
-    /// Flip the stereoscopic projection for the opposite eye.
-    pub fn invert(self) -> Self {
+    fn perpsective_stereo_builder(
+        self,
+    ) -> unsafe extern "C" fn(*mut citro3d_sys::C3D_Mtx, f32, f32, f32, f32, f32, f32, bool) {
         match self {
-            Self::Stereo {
-                interocular_distance,
-                screen_depth,
-            } => Self::Stereo {
-                interocular_distance: -interocular_distance,
-                screen_depth,
-            },
-            mono => mono,
+            Self::Rotated => citro3d_sys::Mtx_PerspStereoTilt,
+            Self::None => citro3d_sys::Mtx_PerspStereo,
         }
     }
+
+    // TODO: orthographic projections
+    fn ortho_builder(
+        self,
+    ) -> unsafe extern "C" fn(*mut citro3d_sys::C3D_Mtx, f32, f32, f32, f32, f32, f32, bool) {
+        match self {
+            Self::Rotated => citro3d_sys::Mtx_OrthoTilt,
+            Self::None => citro3d_sys::Mtx_Ortho,
+        }
+    }
+}
+
+/// Configuration for calculating stereoscopic projections.
+#[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 [frustum](https://en.wikipedia.org/wiki/Viewing_frustum)
+/// of a perspective projection.
 #[derive(Clone, Copy, Debug)]
-pub struct ClipPlane {
+pub struct ClipPlanes {
+    /// The z-depth of the near clip plane.
     pub near: f32,
+    /// The z-depth of the far clip plane.
     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<AspectRatio> 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,
+        }
+    }
+}
diff --git a/citro3d/src/shader.rs b/citro3d/src/shader.rs
index 9d775f8..39b9aa8 100644
--- a/citro3d/src/shader.rs
+++ b/citro3d/src/shader.rs
@@ -115,7 +115,9 @@ impl Drop for Program {
 /// The type of a shader.
 #[repr(u32)]
 pub enum Type {
+    /// A vertex shader.
     Vertex = ctru_sys::GPU_VERTEX_SHADER,
+    /// A geometry shader.
     Geometry = ctru_sys::GPU_GEOMETRY_SHADER,
 }
 
diff --git a/citro3d/src/uniform.rs b/citro3d/src/uniform.rs
index 7033a22..c4fbf8b 100644
--- a/citro3d/src/uniform.rs
+++ b/citro3d/src/uniform.rs
@@ -1,5 +1,9 @@
+//! Common definitions for binding uniforms to shaders. This is primarily
+//! done by implementing the [`Uniform`] trait for a given type.
+
 use crate::{shader, Instance, Matrix};
 
+/// The index of a uniform within a [`shader::Program`].
 #[derive(Copy, Clone, Debug)]
 pub struct Index(i8);
 
@@ -19,15 +23,21 @@ mod private {
     use crate::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
+/// shaders to be used as a uniform input to the shader.
 pub trait Uniform: private::Sealed {
-    fn bind(&self, instance: &mut Instance, shader_type: shader::Type, index: Index);
+    /// Bind the uniform to the given shader index for the given shader type.
+    /// An [`Instance`] is required to prevent concurrent binding of different
+    /// uniforms to the same index.
+    fn bind(self, instance: &mut Instance, shader_type: shader::Type, index: Index);
 }
 
-impl Uniform for Matrix {
-    fn bind(&self, _instance: &mut Instance, type_: shader::Type, index: Index) {
+impl Uniform for &Matrix {
+    #[doc(alias = "C3D_FVUnifMtx4x4")]
+    fn bind(self, _instance: &mut Instance, type_: shader::Type, index: Index) {
         unsafe { citro3d_sys::C3D_FVUnifMtx4x4(type_.into(), index.into(), self.as_raw()) }
     }
 }