bevy/crates/bevy_math/src/ops.rs
Matty 61a1530c56
Make bevy_math's libm feature use libm for all f32methods with unspecified precision (#14693)
# Objective

Closes #14474

Previously, the `libm` feature of bevy_math would just pass the same
feature flag down to glam. However, bevy_math itself had many uses of
floating-point arithmetic with unspecified precision. For example,
`f32::sin_cos` and `f32::powi` have unspecified precision, which means
that the exact details of their output are not guaranteed to be stable
across different systems and/or versions of Rust. This means that users
of bevy_math could observe slightly different behavior on different
systems if these methods were used.

The goal of this PR is to make it so that the `libm` feature flag
actually guarantees some degree of determinacy within bevy_math itself
by switching to the libm versions of these functions when the `libm`
feature is enabled.

## Solution

bevy_math now has an internal module `bevy_math::ops`, which re-exports
either the standard versions of the operations or the libm versions
depending on whether the `libm` feature is enabled. For example,
`ops::sin` compiles to `f32::sin` without the `libm` feature and to
`libm::sinf` with it.

This approach has a small shortfall, which is that `f32::powi` (integer
powers of floating point numbers) does not have an equivalent in `libm`.
On the other hand, this method is only used for squaring and cubing
numbers in bevy_math. Accordingly, this deficit is covered by the
introduction of a trait `ops::FloatPow`:
```rust
pub(crate) trait FloatPow {
    fn squared(self) -> Self;
    fn cubed(self) -> Self;
}
```

Next, each current usage of the unspecified-precision methods has been
replaced by its equivalent in `ops`, so that when `libm` is enabled, the
libm version is used instead. The exception, of course, is that
`.powi(2)`/`.powi(3)` have been replaced with `.squared()`/`.cubed()`.

Finally, the usage of the plain `f32` methods with unspecified precision
is now linted out of bevy_math (and hence disallowed in CI). For
example, using `f32::sin` within bevy_math produces a warning that tells
the user to use the `ops::sin` version instead.

## Testing

Ran existing tests. It would be nice to check some benchmarks on NURBS
things once #14677 merges. I'm happy to wait until then if the rest of
this PR is fine.

---

## Discussion

In the future, it might make sense to actually expose `bevy_math::ops`
as public if any downstream Bevy crates want to provide similar
determinacy guarantees. For now, it's all just `pub(crate)`.

This PR also only covers `f32`. If we find ourselves using `f64`
internally in parts of bevy_math for better robustness, we could extend
the module and lints to cover the `f64` versions easily enough.

I don't know how feasible it is, but it would also be nice if we could
standardize the bevy_math tests with the `libm` feature in CI, since
their success is currently platform-dependent (e.g. 8 of them fail on my
machine when run locally).

---------

Co-authored-by: IQuick 143 <IQuick143cz@gmail.com>
2024-08-12 16:13:36 +00:00

291 lines
5.8 KiB
Rust

//! This mod re-exports the correct versions of floating-point operations with
//! unspecified precision in the standard library depending on whether the `libm`
//! crate feature is enabled.
//!
//! All the functions here are named according to their versions in the standard
//! library.
#![allow(dead_code)]
#![allow(clippy::disallowed_methods)]
// Note: There are some Rust methods with unspecified precision without a `libm`
// equivalent:
// - `f32::powi` (integer powers)
// - `f32::log` (logarithm with specified base)
// - `f32::abs_sub` (actually unsure if `libm` has this, but don't use it regardless)
//
// Additionally, the following nightly API functions are not presently integrated
// into this, but they would be candidates once standardized:
// - `f32::gamma`
// - `f32::ln_gamma`
#[cfg(not(feature = "libm"))]
mod std_ops {
#[inline(always)]
pub(crate) fn powf(x: f32, y: f32) -> f32 {
f32::powf(x, y)
}
#[inline(always)]
pub(crate) fn exp(x: f32) -> f32 {
f32::exp(x)
}
#[inline(always)]
pub(crate) fn exp2(x: f32) -> f32 {
f32::exp2(x)
}
#[inline(always)]
pub(crate) fn ln(x: f32) -> f32 {
f32::ln(x)
}
#[inline(always)]
pub(crate) fn log2(x: f32) -> f32 {
f32::log2(x)
}
#[inline(always)]
pub(crate) fn log10(x: f32) -> f32 {
f32::log10(x)
}
#[inline(always)]
pub(crate) fn cbrt(x: f32) -> f32 {
f32::cbrt(x)
}
#[inline(always)]
pub(crate) fn hypot(x: f32, y: f32) -> f32 {
f32::hypot(x, y)
}
#[inline(always)]
pub(crate) fn sin(x: f32) -> f32 {
f32::sin(x)
}
#[inline(always)]
pub(crate) fn cos(x: f32) -> f32 {
f32::cos(x)
}
#[inline(always)]
pub(crate) fn tan(x: f32) -> f32 {
f32::tan(x)
}
#[inline(always)]
pub(crate) fn asin(x: f32) -> f32 {
f32::asin(x)
}
#[inline(always)]
pub(crate) fn acos(x: f32) -> f32 {
f32::acos(x)
}
#[inline(always)]
pub(crate) fn atan(x: f32) -> f32 {
f32::atan(x)
}
#[inline(always)]
pub(crate) fn atan2(x: f32, y: f32) -> f32 {
f32::atan2(x, y)
}
#[inline(always)]
pub(crate) fn sin_cos(x: f32) -> (f32, f32) {
f32::sin_cos(x)
}
#[inline(always)]
pub(crate) fn exp_m1(x: f32) -> f32 {
f32::exp_m1(x)
}
#[inline(always)]
pub(crate) fn ln_1p(x: f32) -> f32 {
f32::ln_1p(x)
}
#[inline(always)]
pub(crate) fn sinh(x: f32) -> f32 {
f32::sinh(x)
}
#[inline(always)]
pub(crate) fn cosh(x: f32) -> f32 {
f32::cosh(x)
}
#[inline(always)]
pub(crate) fn tanh(x: f32) -> f32 {
f32::tanh(x)
}
#[inline(always)]
pub(crate) fn asinh(x: f32) -> f32 {
f32::asinh(x)
}
#[inline(always)]
pub(crate) fn acosh(x: f32) -> f32 {
f32::acosh(x)
}
#[inline(always)]
pub(crate) fn atanh(x: f32) -> f32 {
f32::atanh(x)
}
}
#[cfg(feature = "libm")]
mod libm_ops {
#[inline(always)]
pub(crate) fn powf(x: f32, y: f32) -> f32 {
libm::powf(x, y)
}
#[inline(always)]
pub(crate) fn exp(x: f32) -> f32 {
libm::expf(x)
}
#[inline(always)]
pub(crate) fn exp2(x: f32) -> f32 {
libm::exp2f(x)
}
#[inline(always)]
pub(crate) fn ln(x: f32) -> f32 {
// This isn't documented in `libm` but this is actually the base e logarithm.
libm::logf(x)
}
#[inline(always)]
pub(crate) fn log2(x: f32) -> f32 {
libm::log2f(x)
}
#[inline(always)]
pub(crate) fn log10(x: f32) -> f32 {
libm::log10f(x)
}
#[inline(always)]
pub(crate) fn cbrt(x: f32) -> f32 {
libm::cbrtf(x)
}
#[inline(always)]
pub(crate) fn hypot(x: f32, y: f32) -> f32 {
libm::hypotf(x, y)
}
#[inline(always)]
pub(crate) fn sin(x: f32) -> f32 {
libm::sinf(x)
}
#[inline(always)]
pub(crate) fn cos(x: f32) -> f32 {
libm::cosf(x)
}
#[inline(always)]
pub(crate) fn tan(x: f32) -> f32 {
libm::tanf(x)
}
#[inline(always)]
pub(crate) fn asin(x: f32) -> f32 {
libm::asinf(x)
}
#[inline(always)]
pub(crate) fn acos(x: f32) -> f32 {
libm::acosf(x)
}
#[inline(always)]
pub(crate) fn atan(x: f32) -> f32 {
libm::atanf(x)
}
#[inline(always)]
pub(crate) fn atan2(x: f32, y: f32) -> f32 {
libm::atan2f(x, y)
}
#[inline(always)]
pub(crate) fn sin_cos(x: f32) -> (f32, f32) {
libm::sincosf(x)
}
#[inline(always)]
pub(crate) fn exp_m1(x: f32) -> f32 {
libm::expm1f(x)
}
#[inline(always)]
pub(crate) fn ln_1p(x: f32) -> f32 {
libm::log1pf(x)
}
#[inline(always)]
pub(crate) fn sinh(x: f32) -> f32 {
libm::sinhf(x)
}
#[inline(always)]
pub(crate) fn cosh(x: f32) -> f32 {
libm::coshf(x)
}
#[inline(always)]
pub(crate) fn tanh(x: f32) -> f32 {
libm::tanhf(x)
}
#[inline(always)]
pub(crate) fn asinh(x: f32) -> f32 {
libm::asinhf(x)
}
#[inline(always)]
pub(crate) fn acosh(x: f32) -> f32 {
libm::acoshf(x)
}
#[inline(always)]
pub(crate) fn atanh(x: f32) -> f32 {
libm::atanhf(x)
}
}
#[cfg(feature = "libm")]
pub(crate) use libm_ops::*;
#[cfg(not(feature = "libm"))]
pub(crate) use std_ops::*;
/// This extension trait covers shortfall in determinacy from the lack of a `libm` counterpart
/// to `f32::powi`. Use this for the common small exponents.
pub(crate) trait FloatPow {
fn squared(self) -> Self;
fn cubed(self) -> Self;
}
impl FloatPow for f32 {
#[inline]
fn squared(self) -> Self {
self * self
}
#[inline]
fn cubed(self) -> Self {
self * self * self
}
}