Skip to content

Commit

Permalink
Add floating-point function adapter
Browse files Browse the repository at this point in the history
The functions exposed in core::math::float::f32 automatically delegate to either
std, libm, or micromath, depending on what features are enabled. This lets code
be agnostic of which implementation of fp functions is actually used.

There is also a fallback implementing a few common and simple functions even
when none of the fp features is enabled.
  • Loading branch information
jdahlstrom committed Jan 15, 2024
1 parent 4352dfe commit 1d92d90
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 60 deletions.
1 change: 1 addition & 0 deletions core/src/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub use vec::{Vec2, Vec2i, Vec3, Vec3i, Vec4, Vec4i, Vector};
pub mod angle;
pub mod approx;
pub mod color;
pub mod float;
pub mod mat;
pub mod rand;
pub mod space;
Expand Down
43 changes: 23 additions & 20 deletions core/src/math/angle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ use core::f32::consts::{PI, TAU};
use core::fmt::{self, Debug, Display};
use core::ops::{Add, Div, Mul, Neg, Rem, Sub};

#[cfg(feature = "mm")]
use micromath::F32Ext;

use crate::math::approx::ApproxEq;
use crate::math::float::f32;
use crate::math::space::{Affine, Linear};
use crate::math::vec::{vec2, vec3, Vec2, Vec3, Vector};
use crate::math::vec::Vector;

#[cfg(feature = "fp")]
use crate::math::vec::{vec2, vec3, Vec2, Vec3};

//
// Types
Expand Down Expand Up @@ -74,7 +75,7 @@ pub fn turns(a: f32) -> Angle {
#[cfg(feature = "fp")]
pub fn asin(x: f32) -> Angle {
assert!(-1.0 <= x && x <= 1.0);
Angle(x.asin())
Angle(f32::asin(x))
}

/// Returns the arccosine of `x` as an `Angle`.
Expand All @@ -83,14 +84,15 @@ pub fn asin(x: f32) -> Angle {
///
/// # Examples
/// ```
/// # use retrofire_core::assert_approx_eq;
/// # use retrofire_core::math::angle::*;
/// assert_eq!(acos(1.0), degs(0.0));
/// assert_approx_eq!(acos(1.0), degs(0.0));
/// ```
/// # Panics
/// If `x` is outside the range [-1.0, 1.0].
#[cfg(feature = "fp")]
pub fn acos(x: f32) -> Angle {
Angle(x.acos())
Angle(f32::acos(x))
}

/// Returns the four-quadrant arctangent of `y` and `x` as an `Angle`.
Expand All @@ -106,7 +108,7 @@ pub fn acos(x: f32) -> Angle {
/// ```
#[cfg(feature = "fp")]
pub fn atan2(y: f32, x: f32) -> Angle {
Angle(y.atan2(x))
Angle(f32::atan2(y, x))
}

/// Returns a polar coordinate vector with azimuth `az` and radius `r`.
Expand Down Expand Up @@ -199,11 +201,12 @@ impl Angle {
/// Returns the sine of `self`.
/// # Examples
/// ```
/// # use retrofire_core::assert_approx_eq;
/// # use retrofire_core::math::angle::*;
/// assert_eq!(degs(30.0).sin(), 0.5)
/// assert_approx_eq!(degs(30.0).sin(), 0.5)
/// ```
pub fn sin(self) -> f32 {
self.0.sin()
f32::sin(self.0)
}
/// Returns the cosine of `self`.
/// # Examples
Expand All @@ -213,7 +216,7 @@ impl Angle {
/// assert_approx_eq!(degs(60.0).cos(), 0.5)
/// ```
pub fn cos(self) -> f32 {
self.0.cos()
f32::cos(self.0)
}
/// Simultaneously computes the sine and cosine of `self`.
/// # Examples
Expand All @@ -225,7 +228,7 @@ impl Angle {
/// assert_approx_eq!(cos, 0.0);
/// ```
pub fn sin_cos(self) -> (f32, f32) {
self.0.sin_cos()
(self.sin(), self.cos())
}
/// Returns the tangent of `self`.
/// # Examples
Expand All @@ -234,7 +237,7 @@ impl Angle {
/// assert_eq!(degs(45.0).tan(), 1.0)
/// ```
pub fn tan(self) -> f32 {
self.0.tan()
f32::tan(self.0)
}

/// Returns `self` "wrapped around" to the range `min..max`.
Expand All @@ -243,11 +246,11 @@ impl Angle {
/// ```
/// # use retrofire_core::assert_approx_eq;
/// # use retrofire_core::math::angle::*;
/// assert_approx_eq!(degs(400.0).wrap(Angle::ZERO, Angle::FULL), degs(40.0))
/// assert_approx_eq!(degs(400.0).wrap(turns(0.0), turns(1.0)), degs(40.0))
/// ```
#[must_use]
pub fn wrap(self, min: Self, max: Self) -> Self {
Self(min.0 + (self.0 - min.0).rem_euclid(max.0 - min.0))
Self(min.0 + f32::rem_euclid(self.0 - min.0, max.0 - min.0))
}
}

Expand Down Expand Up @@ -411,7 +414,7 @@ impl From<Vec2> for PolarVec {
/// ```
/// # use retrofire_core::assert_approx_eq;
/// # use retrofire_core::math::{*, angle::*};
/// // A nonnegative x and zero y maps to zero azimuth
/// // A non-negative x and zero y maps to zero azimuth
/// assert_eq!(PolarVec::from(vec2(0.0, 0.0)).az(), Angle::ZERO);
/// assert_eq!(PolarVec::from(vec2(1.0, 0.0)).az(), Angle::ZERO);
///
Expand Down Expand Up @@ -480,11 +483,11 @@ impl From<Vec3> for SphericalVec {
/// # Examples
/// ```
/// # use retrofire_core::math::{*, angle::*};
/// assert_eq!(SphericalVec::from(vec3(2.0, 0.0, 0.0)), spherical(2.0 ,degs(0.0), degs(0.0)));
/// assert_eq!(SphericalVec::from(vec3(2.0, 0.0, 0.0)), spherical(2.0, degs(0.0), degs(0.0)));
///
/// assert_eq!(SphericalVec::from(vec3(0.0, 2.0, 0.0)), spherical(2.0 ,degs(0.0), degs(90.0)));
/// assert_eq!(SphericalVec::from(vec3(0.0, 2.0, 0.0)), spherical(2.0, degs(0.0), degs(90.0)));
///
/// assert_eq!(SphericalVec::from(vec3(0.0, 0.0, 2.0)), spherical(2.0 ,degs(90.0), degs(0.0)));
/// assert_eq!(SphericalVec::from(vec3(0.0, 0.0, 2.0)), spherical(2.0, degs(90.0), degs(0.0)));
/// ```
fn from(v: Vec3) -> Self {
let [x, y, z] = v.0;
Expand Down Expand Up @@ -576,7 +579,7 @@ mod tests {
assert_approx_eq!(degs(0.0).tan(), 0.0);
assert_approx_eq!(degs(45.0).tan(), 1.0);
assert_approx_eq!(degs(135.0).tan(), -1.0);
assert_approx_eq!(degs(225.0).tan(), 1.0);
assert_approx_eq!(degs(225.0).tan(), 1.0, eps = 1e-6);
assert_approx_eq!(degs(315.0).tan(), -1.0, eps = 1e-6);
}

Expand Down
7 changes: 3 additions & 4 deletions core/src/math/approx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,9 @@ pub trait ApproxEq<Other: ?Sized = Self, Epsilon = Self> {

impl ApproxEq for f32 {
fn approx_eq_eps(&self, other: &Self, rel_eps: &Self) -> bool {
// TODO Always provide abs
let diff = self.max(*other) - self.min(*other);
let abs_self = if *self >= 0.0 { *self } else { -self };
diff <= *rel_eps * abs_self.max(1.0)
use super::float::f32;
let diff = f32::abs(self - other);
diff <= *rel_eps * f32::abs(*self).max(1.0)
}
fn relative_epsilon() -> Self {
Self::EPSILON
Expand Down
166 changes: 166 additions & 0 deletions core/src/math/float.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#[cfg(feature = "libm")]
pub mod libm {
pub use libm::fabsf as abs;
pub use libm::floorf as floor;

pub use libm::powf;
pub use libm::sqrtf as sqrt;

pub use libm::cosf as cos;
pub use libm::sinf as sin;
pub use libm::tanf as tan;

pub use libm::acosf as acos;
pub use libm::asinf as asin;
pub use libm::atan2f as atan2;

pub use libm::expf as exp;
pub use libm::log2;

pub use super::fallback::rem_euclid;
}

#[cfg(feature = "mm")]
pub mod mm {
use micromath::F32Ext as mm;

#[inline]
pub fn abs(x: f32) -> f32 {
mm::abs(x)
}
#[inline]
pub fn floor(x: f32) -> f32 {
mm::floor(x)
}
#[inline]
pub fn rem_euclid(x: f32, m: f32) -> f32 {
mm::rem_euclid(x, m)
}
/// Returns the approximate square root of `x`.
#[inline]
pub fn sqrt(x: f32) -> f32 {
let approx = mm::sqrt(x);
// One round of Newton's method
0.5 * (approx + (x / approx))
}
/// Returns the approximate reciprocal of the square root of `x`.
#[inline]
pub fn recip_sqrt(x: f32) -> f32 {
let y = mm::invsqrt(x);
// One round of Newton's method
y * (1.5 - 0.5 * x * y * y)
}
#[inline]
pub fn powf(x: f32, y: f32) -> f32 {
mm::powf(x, y)
}
#[inline]
pub fn sin(x: f32) -> f32 {
mm::sin(x)
}
#[inline]
pub fn cos(x: f32) -> f32 {
mm::cos(x)
}
#[inline]
pub fn tan(x: f32) -> f32 {
mm::tan(x)
}
#[inline]
pub fn asin(x: f32) -> f32 {
mm::asin(x)
}
#[inline]
pub fn acos(x: f32) -> f32 {
mm::acos(x)
}
#[inline]
pub fn atan2(y: f32, x: f32) -> f32 {
mm::atan2(y, x)
}
}

pub mod fallback {
/// Returns the absolute value of `x`.
#[inline]
pub fn abs(x: f32) -> f32 {
f32::from_bits(x.to_bits() & !0x8000_0000)
}
/// Returns the largest integer less than or equal to `x`.
#[inline]
pub fn floor(x: f32) -> f32 {
(x as i64 - x.is_sign_negative() as i64) as f32
}
// Returns the least non-negative remainder of `x` (mod `m`).
#[inline]
pub fn rem_euclid(x: f32, m: f32) -> f32 {
x % m + (x.is_sign_negative() as u32 as f32) * m
}
}

#[cfg(feature = "mm")]
pub use mm as f32;

#[cfg(all(feature = "libm", not(feature = "mm")))]
pub use libm as f32;

#[cfg(all(feature = "std", not(feature = "mm"), not(feature = "libm")))]
#[allow(non_camel_case_types)]
pub type f32 = core::primitive::f32;

#[cfg(not(feature = "fp"))]
pub use fallback as f32;

#[cfg(test)]
mod tests {
use core::f32::consts::PI;

#[cfg(feature = "libm")]
#[test]
fn libm_functions() {
use super::libm;
assert_eq!(libm::cos(PI), -1.0);
assert_eq!(libm::sqrt(9.0), 3.0);
}

#[cfg(feature = "mm")]
#[test]
fn mm_functions() {
use super::mm;
use crate::assert_approx_eq;

assert_eq!(mm::cos(PI), -1.0);
assert_eq!(mm::sqrt(9.0), 3.0025);
assert_eq!(mm::sqrt(16.0), 4.0);

assert_approx_eq!(mm::recip_sqrt(9.0), 0.333, eps = 1e-3);
assert_approx_eq!(mm::recip_sqrt(16.0), 0.25, eps = 1e-3);
}

#[cfg(feature = "std")]
#[test]
fn std_functions() {
use super::f32;
assert_eq!(f32::cos(PI), -1.0);
assert_eq!(f32::sqrt(9.0), 3.0);
}

#[cfg(not(feature = "fp"))]
#[test]
fn fallback_functions() {
use crate::assert_approx_eq;

assert_eq!(f32::floor(1.23), 1.0);
assert_eq!(f32::floor(0.0), 0.0);
assert_eq!(f32::floor(-1.23), -2.0);

assert_eq!(f32::abs(1.23), 1.23);
assert_eq!(f32::abs(0.0), 0.0);
assert_eq!(f32::abs(-1.23), 1.23);

assert_approx_eq!(f32::rem_euclid(1.23, 4.0), 1.23);
assert_approx_eq!(f32::rem_euclid(4.0, 4.0), 0.0);
assert_approx_eq!(f32::rem_euclid(5.67, 4.0), 1.67);
assert_approx_eq!(f32::rem_euclid(-1.23, 4.0), 2.77);
}
}
19 changes: 8 additions & 11 deletions core/src/math/mat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ use core::fmt::{self, Debug, Formatter};
use core::marker::PhantomData;
use core::ops::Range;

#[cfg(feature = "mm")]
use micromath::F32Ext;

use crate::math::space::{Affine, Linear, Proj4, Real};
use crate::math::vec::{splat, Vec2i, Vec3, Vec4, Vector};
use crate::math::space::{Proj4, Real};
use crate::math::vec::{Vec2i, Vec3, Vec4, Vector};
use crate::render::{NdcToScreen, ViewToProjective};

/// A linear transform from one space (or basis) to another.
Expand Down Expand Up @@ -262,13 +259,13 @@ impl<Src, Dst> Mat4x4<RealToReal<3, Src, Dst>> {
/// * Panics in debug mode.
/// * Does not panic in release mode, but the result may be inaccurate
/// or contain `Inf`s or `NaN`s.
#[cfg(feature = "fp")] // TODO Only requires abs
#[must_use]
pub fn inverse(&self) -> Mat4x4<RealToReal<3, Dst, Src>> {
use super::float::f32;
if cfg!(debug_assertions) {
let det = self.determinant();
assert!(
det.abs() > f32::EPSILON,
f32::abs(det) > f32::EPSILON,
"a singular, near-singular, or non-finite matrix does not \
have a well-defined inverse (determinant = {det})"
);
Expand Down Expand Up @@ -303,8 +300,8 @@ impl<Src, Dst> Mat4x4<RealToReal<3, Src, Dst>> {
for idx in 0..4 {
let pivot = (idx..4)
.max_by(|&r1, &r2| {
let v1 = this.0[r1][idx].abs();
let v2 = this.0[r2][idx].abs();
let v1 = f32::abs(this.0[r1][idx]);
let v2 = f32::abs(this.0[r2][idx]);
v1.partial_cmp(&v2).unwrap()
})
.unwrap();
Expand Down Expand Up @@ -495,7 +492,7 @@ pub fn orient_z(new_z: Vec3, x: Vec3) -> Mat4x4<RealToReal<3>> {

#[cfg(feature = "fp")]
fn orient(new_y: Vec3, new_z: Vec3) -> Mat4x4<RealToReal<3>> {
use crate::math::ApproxEq;
use crate::math::{vec::splat, ApproxEq};
assert!(!new_y.approx_eq(&splat(0.0)));
assert!(!new_z.approx_eq(&splat(0.0)));

Expand Down Expand Up @@ -617,7 +614,7 @@ pub fn viewport(bounds: Range<Vec2i>) -> Mat4x4<NdcToScreen> {
#[cfg(test)]
mod tests {
use crate::assert_approx_eq;
use crate::math::{degs, vec3};
use crate::math::{degs, vec::splat, vec3};

use super::*;

Expand Down
Loading

0 comments on commit 1d92d90

Please sign in to comment.