Make bevy_math's libm feature use libm for all f32methods with unspecified precision#14693
Conversation
|
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 |
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 { |
There was a problem hiding this comment.
Just me wondering:
Could this be done with something like:
mod std_ops {
pub use f32::powf; ///etc...
}
or would that not work??
There was a problem hiding this comment.
I'm not sure, but it also fails to unify the naming of the std and libm versions, which differ in many (most?) cases.
There was a problem hiding this comment.
You can do pub use libm::sqrtf as sqrt and so on
There was a problem hiding this comment.
You could do pub use libm::thingf as thing, I think.
There was a problem hiding this comment.
True! Do you know if that would dodge the lints? I just organized them this way because it's how glam does it.
There was a problem hiding this comment.
Hmm, not sure. If it breaks the lints, then it's not worth it and should be kept how it is now
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Seems like a prime candidate for atan2.
There was a problem hiding this comment.
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.
| #[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; | ||
| } |
There was a problem hiding this comment.
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
Jondolf
left a comment
There was a problem hiding this comment.
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...
Objective
Closes #14474
Previously, the
libmfeature 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_cosandf32::powihave 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
libmfeature flag actually guarantees some degree of determinacy within bevy_math itself by switching to the libm versions of these functions when thelibmfeature 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 thelibmfeature is enabled. For example,ops::sincompiles tof32::sinwithout thelibmfeature and tolibm::sinfwith it.This approach has a small shortfall, which is that
f32::powi(integer powers of floating point numbers) does not have an equivalent inlibm. 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 traitops::FloatPow:Next, each current usage of the unspecified-precision methods has been replaced by its equivalent in
ops, so that whenlibmis 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
f32methods with unspecified precision is now linted out of bevy_math (and hence disallowed in CI). For example, usingf32::sinwithin bevy_math produces a warning that tells the user to use theops::sinversion 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::opsas public if any downstream Bevy crates want to provide similar determinacy guarantees. For now, it's all justpub(crate).This PR also only covers
f32. If we find ourselves usingf64internally in parts of bevy_math for better robustness, we could extend the module and lints to cover thef64versions 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
libmfeature in CI, since their success is currently platform-dependent (e.g. 8 of them fail on my machine when run locally).