Skip to content

Make bevy_math's libm feature use libm for all f32methods with unspecified precision#14693

Merged
alice-i-cecile merged 15 commits intobevyengine:mainfrom
mweatherley:libm-support
Aug 12, 2024
Merged

Make bevy_math's libm feature use libm for all f32methods with unspecified precision#14693
alice-i-cecile merged 15 commits intobevyengine:mainfrom
mweatherley:libm-support

Conversation

@mweatherley
Copy link
Copy Markdown
Contributor

@mweatherley mweatherley commented Aug 10, 2024

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:

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).

@mweatherley mweatherley added C-Bug An unexpected or incorrect behavior C-Feature A new feature, making something new possible A-Math Fundamental domain-agnostic mathematical operations D-Straightforward Simple bug fixes and API improvements, docs, test and examples X-Contentious There are nontrivial implications that should be thought through labels Aug 10, 2024
@mweatherley mweatherley requested a review from Jondolf August 10, 2024 13:40
@mweatherley
Copy link
Copy Markdown
Contributor Author

Marking this as contentious because of increased maintenance burden and potential confusion for new contributors, but I think that doing something like this is a good idea if we actually want to support the libm feature.

Co-authored-by: IQuick 143 <IQuick143cz@gmail.com>
#[cfg(not(feature = "libm"))]
mod std_ops {
#[inline(always)]
pub(crate) fn powf(x: f32, y: f32) -> f32 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just me wondering:
Could this be done with something like:

mod std_ops {
    pub use f32::powf; ///etc...
}

or would that not work??

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, but it also fails to unify the naming of the std and libm versions, which differ in many (most?) cases.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do pub use libm::sqrtf as sqrt and so on

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could do pub use libm::thingf as thing, I think.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True! Do you know if that would dodge the lints? I just organized them this way because it's how glam does it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, not sure. If it breaks the lints, then it's not worth it and should be kept how it is now

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm; I tried it and it literally just doesn't work, but maybe I'm just being silly. i.e., pub use f32::powf; in a module literally just doesn't compile. I dunno how much this is worth fussing over anyway; if these later become public then the whole module thing will probably have to be reorganized regardless.

let point = circle.sample_boundary(&mut rng);

let angle = f32::atan(point.y / point.x) + PI / 2.0;
let angle = ops::atan(point.y / point.x) + PI / 2.0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a prime candidate for atan2.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but it's currently written with the angle lying in the range [0, PI] in mind, so changing it to atan2 basically requires rewriting it, which I'd rather not do right now.

Comment on lines +270 to +280
#[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;
}
Copy link
Copy Markdown
Contributor

@Jondolf Jondolf Aug 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to make these public? It would be useful at least for me in Avian and some other libs. I have the same issues as bevy_math prior to this PR, and it would be valuable to have these available.

Edit: Oh you mentioned this in the PR description already, my bad

Copy link
Copy Markdown
Contributor

@Jondolf Jondolf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I think it's very important to ensure that the core math library can be made fully cross-platform deterministic, and I like the lints to enforce this.

Things to consider for follow-ups:

  • I would really like these ops to be public. I have similar needs for my physics engine, collision detection stuff, and some other libraries, and it would be great if I didn't have to duplicate everything.
  • f64 versions would be valuable.
  • We could consider extending this to some other Bevy crates that may have an impact on determinism. Not sure if there's a need yet though...

@mweatherley mweatherley added the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Aug 11, 2024
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Aug 12, 2024
Merged via the queue into bevyengine:main with commit 61a1530 Aug 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Math Fundamental domain-agnostic mathematical operations C-Bug An unexpected or incorrect behavior C-Feature A new feature, making something new possible D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Contentious There are nontrivial implications that should be thought through

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bevy_math's libm feature should probably apply to bevy_math's internal code

4 participants