Skip to content

Commit

Permalink
Add no_std Support to bevy_math (#15810)
Browse files Browse the repository at this point in the history
# Objective

- Contributes to #15460

## Solution

- Added two new features, `std` (default) and `alloc`, gating `std` and
`alloc` behind them respectively.
- Added missing `f32` functions to `std_ops` as required. These `f32`
methods have been added to the `clippy.toml` deny list to aid in
`no_std` development.

## Testing

- CI
- `cargo clippy -p bevy_math --no-default-features --features libm
--target "x86_64-unknown-none"`
- `cargo test -p bevy_math --no-default-features --features libm`
- `cargo test -p bevy_math --no-default-features --features "libm,
alloc"`
- `cargo test -p bevy_math --no-default-features --features "libm,
alloc, std"`
- `cargo test -p bevy_math --no-default-features --features "std"`

## Notes

The following items require the `alloc` feature to be enabled:

- `CubicBSpline`
- `CubicBezier`
- `CubicCardinalSpline`
- `CubicCurve`
- `CubicGenerator`
- `CubicHermite`
- `CubicNurbs`
- `CyclicCubicGenerator`
- `RationalCurve`
- `RationalGenerator`
- `BoxedPolygon`
- `BoxedPolyline2d`
- `BoxedPolyline3d`
- `SampleCurve`
- `SampleAutoCurve`
- `UnevenSampleCurve`
- `UnevenSampleAutoCurve`
- `EvenCore`
- `UnevenCore`
- `ChunkedUnevenCore`

This requirement could be relaxed in certain cases, but I had erred on
the side of gating rather than modifying. Since `no_std` is a new set of
platforms we are adding support to, and the `alloc` feature is enabled
by default, this is not a breaking change.

---------

Co-authored-by: Benjamin Brienen <[email protected]>
Co-authored-by: Matty <[email protected]>
Co-authored-by: Joona Aalto <[email protected]>
  • Loading branch information
4 people authored Dec 3, 2024
1 parent aa600ae commit a8b9c94
Show file tree
Hide file tree
Showing 33 changed files with 627 additions and 268 deletions.
41 changes: 31 additions & 10 deletions crates/bevy_math/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@ keywords = ["bevy"]
rust-version = "1.81.0"

[dependencies]
glam = { version = "0.29", features = ["bytemuck"] }
glam = { version = "0.29", default-features = false, features = ["bytemuck"] }
derive_more = { version = "1", default-features = false, features = [
"error",
"from",
"display",
"into",
] }
itertools = "0.13.0"
serde = { version = "1", features = ["derive"], optional = true }
itertools = { version = "0.13.0", default-features = false }
serde = { version = "1", default-features = false, features = [
"derive",
], optional = true }
libm = { version = "0.2", optional = true }
approx = { version = "0.5", optional = true }
rand = { version = "0.8", features = [
"alloc",
], default-features = false, optional = true }
approx = { version = "0.5", default-features = false, optional = true }
rand = { version = "0.8", default-features = false, optional = true }
rand_distr = { version = "0.4.3", optional = true }
smallvec = { version = "1.11" }
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [
Expand All @@ -36,11 +36,30 @@ approx = "0.5"
rand = "0.8"
rand_chacha = "0.3"
# Enable the approx feature when testing.
bevy_math = { path = ".", version = "0.15.0-dev", features = ["approx"] }
glam = { version = "0.29", features = ["approx"] }
bevy_math = { path = ".", version = "0.15.0-dev", default-features = false, features = [
"approx",
] }
glam = { version = "0.29", default-features = false, features = ["approx"] }

[features]
default = ["rand", "bevy_reflect", "curve"]
default = ["std", "rand", "bevy_reflect", "curve"]
std = [
"alloc",
"glam/std",
"derive_more/std",
"itertools/use_std",
"serde?/std",
"approx?/std",
"rand?/std",
"rand_distr?/std",
]
alloc = [
"itertools/use_alloc",
"serde?/alloc",
"rand?/alloc",
"rand_distr?/alloc",
]

serialize = ["dep:serde", "glam/serde"]
# Enable approx for glam types to approximate floating point equality comparisons and assertions
approx = ["dep:approx", "glam/approx"]
Expand All @@ -57,6 +76,8 @@ debug_glam_assert = ["glam/debug-glam-assert"]
rand = ["dep:rand", "dep:rand_distr", "glam/rand"]
# Include code related to the Curve trait
curve = []
# Enable bevy_reflect (requires std)
bevy_reflect = ["dep:bevy_reflect", "std"]

[lints]
workspace = true
Expand Down
9 changes: 9 additions & 0 deletions crates/bevy_math/clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,13 @@ disallowed-methods = [
{ path = "f32::asinh", reason = "use ops::asinh instead for libm determinism" },
{ path = "f32::acosh", reason = "use ops::acosh instead for libm determinism" },
{ path = "f32::atanh", reason = "use ops::atanh instead for libm determinism" },
# These methods have defined precision, but are only available from the standard library,
# not in core. Using these substitutes allows for no_std compatibility.
{ path = "f32::rem_euclid", reason = "use ops::rem_euclid instead for no_std compatibility" },
{ path = "f32::abs", reason = "use ops::abs instead for no_std compatibility" },
{ path = "f32::sqrt", reason = "use ops::sqrt instead for no_std compatibility" },
{ path = "f32::copysign", reason = "use ops::copysign instead for no_std compatibility" },
{ path = "f32::round", reason = "use ops::round instead for no_std compatibility" },
{ path = "f32::floor", reason = "use ops::floor instead for no_std compatibility" },
{ path = "f32::fract", reason = "use ops::fract instead for no_std compatibility" },
]
21 changes: 11 additions & 10 deletions crates/bevy_math/src/bounding/bounded2d/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod primitive_impls;

use super::{BoundingVolume, IntersectsVolume};
use crate::{
ops,
prelude::{Mat2, Rot2, Vec2},
FloatPow, Isometry2d,
};
Expand Down Expand Up @@ -296,12 +297,12 @@ mod aabb2d_tests {
min: Vec2::new(-1., -1.),
max: Vec2::new(1., 1.),
};
assert!((aabb.visible_area() - 4.).abs() < f32::EPSILON);
assert!(ops::abs(aabb.visible_area() - 4.) < f32::EPSILON);
let aabb = Aabb2d {
min: Vec2::new(0., 0.),
max: Vec2::new(1., 0.5),
};
assert!((aabb.visible_area() - 0.5).abs() < f32::EPSILON);
assert!(ops::abs(aabb.visible_area() - 0.5) < f32::EPSILON);
}

#[test]
Expand Down Expand Up @@ -488,7 +489,7 @@ impl BoundingCircle {
}
}

BoundingCircle::new(isometry * center, radius_squared.sqrt())
BoundingCircle::new(isometry * center, ops::sqrt(radius_squared))
}

/// Get the radius of the bounding circle
Expand Down Expand Up @@ -539,7 +540,7 @@ impl BoundingVolume for BoundingCircle {
#[inline(always)]
fn contains(&self, other: &Self) -> bool {
let diff = self.radius() - other.radius();
self.center.distance_squared(other.center) <= diff.squared().copysign(diff)
self.center.distance_squared(other.center) <= ops::copysign(diff.squared(), diff)
}

#[inline(always)]
Expand Down Expand Up @@ -614,14 +615,14 @@ mod bounding_circle_tests {
use super::BoundingCircle;
use crate::{
bounding::{BoundingVolume, IntersectsVolume},
Vec2,
ops, Vec2,
};

#[test]
fn area() {
let circle = BoundingCircle::new(Vec2::ONE, 5.);
// Since this number is messy we check it with a higher threshold
assert!((circle.visible_area() - 78.5398).abs() < 0.001);
assert!(ops::abs(circle.visible_area() - 78.5398) < 0.001);
}

#[test]
Expand All @@ -647,7 +648,7 @@ mod bounding_circle_tests {
let b = BoundingCircle::new(Vec2::new(1., -4.), 1.);
let merged = a.merge(&b);
assert!((merged.center - Vec2::new(1., 0.5)).length() < f32::EPSILON);
assert!((merged.radius() - 5.5).abs() < f32::EPSILON);
assert!(ops::abs(merged.radius() - 5.5) < f32::EPSILON);
assert!(merged.contains(&a));
assert!(merged.contains(&b));
assert!(!a.contains(&merged));
Expand Down Expand Up @@ -679,7 +680,7 @@ mod bounding_circle_tests {
fn grow() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
let padded = a.grow(1.25);
assert!((padded.radius() - 6.25).abs() < f32::EPSILON);
assert!(ops::abs(padded.radius() - 6.25) < f32::EPSILON);
assert!(padded.contains(&a));
assert!(!a.contains(&padded));
}
Expand All @@ -688,7 +689,7 @@ mod bounding_circle_tests {
fn shrink() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
let shrunk = a.shrink(0.5);
assert!((shrunk.radius() - 4.5).abs() < f32::EPSILON);
assert!(ops::abs(shrunk.radius() - 4.5) < f32::EPSILON);
assert!(a.contains(&shrunk));
assert!(!shrunk.contains(&a));
}
Expand All @@ -697,7 +698,7 @@ mod bounding_circle_tests {
fn scale_around_center() {
let a = BoundingCircle::new(Vec2::ONE, 5.);
let scaled = a.scale_around_center(2.);
assert!((scaled.radius() - 10.).abs() < f32::EPSILON);
assert!(ops::abs(scaled.radius() - 10.) < f32::EPSILON);
assert!(!a.contains(&scaled));
assert!(scaled.contains(&a));
}
Expand Down
33 changes: 21 additions & 12 deletions crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
use crate::{
ops,
primitives::{
Annulus, Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector,
CircularSegment, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon,
Rhombus, Segment2d, Triangle2d,
Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d,
Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d,
},
Dir2, Isometry2d, Mat2, Rot2, Vec2,
};
use core::f32::consts::{FRAC_PI_2, PI, TAU};

#[cfg(feature = "alloc")]
use crate::primitives::{BoxedPolygon, BoxedPolyline2d};

use smallvec::SmallVec;

use super::{Aabb2d, Bounded2d, BoundingCircle};
Expand Down Expand Up @@ -41,11 +43,11 @@ fn arc_bounding_points(arc: Arc2d, rotation: impl Into<Rot2>) -> SmallVec<[Vec2;
// The half-angles are measured from a starting point of π/2, being the angle of Vec2::Y.
// Compute the normalized angles of the endpoints with the rotation taken into account, and then
// check if we are looking for an angle that is between or outside them.
let left_angle = (FRAC_PI_2 + arc.half_angle + rotation.as_radians()).rem_euclid(TAU);
let right_angle = (FRAC_PI_2 - arc.half_angle + rotation.as_radians()).rem_euclid(TAU);
let left_angle = ops::rem_euclid(FRAC_PI_2 + arc.half_angle + rotation.as_radians(), TAU);
let right_angle = ops::rem_euclid(FRAC_PI_2 - arc.half_angle + rotation.as_radians(), TAU);
let inverted = left_angle < right_angle;
for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] {
let angle = extremum.to_angle().rem_euclid(TAU);
let angle = ops::rem_euclid(extremum.to_angle(), TAU);
// If inverted = true, then right_angle > left_angle, so we are looking for an angle that is not between them.
// There's a chance that this condition fails due to rounding error, if the endpoint angle is juuuust shy of the axis.
// But in that case, the endpoint itself is within rounding error of the axis and will define the bounds just fine.
Expand Down Expand Up @@ -286,6 +288,7 @@ impl<const N: usize> Bounded2d for Polyline2d<N> {
}
}

#[cfg(feature = "alloc")]
impl Bounded2d for BoxedPolyline2d {
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
Aabb2d::from_point_cloud(isometry, &self.vertices)
Expand Down Expand Up @@ -348,7 +351,8 @@ impl Bounded2d for Rectangle {
// Compute the AABB of the rotated rectangle by transforming the half-extents
// by an absolute rotation matrix.
let (sin, cos) = isometry.rotation.sin_cos();
let abs_rot_mat = Mat2::from_cols_array(&[cos.abs(), sin.abs(), sin.abs(), cos.abs()]);
let abs_rot_mat =
Mat2::from_cols_array(&[ops::abs(cos), ops::abs(sin), ops::abs(sin), ops::abs(cos)]);
let half_size = abs_rot_mat * self.half_size;

Aabb2d::new(isometry.translation, half_size)
Expand All @@ -371,6 +375,7 @@ impl<const N: usize> Bounded2d for Polygon<N> {
}
}

#[cfg(feature = "alloc")]
impl Bounded2d for BoxedPolygon {
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
Aabb2d::from_point_cloud(isometry, &self.vertices)
Expand Down Expand Up @@ -470,6 +475,7 @@ mod tests {
// Arcs and circular segments have the same bounding shapes so they share test cases.
fn arc_and_segment() {
struct TestCase {
#[allow(unused)]
name: &'static str,
arc: Arc2d,
translation: Vec2,
Expand All @@ -487,7 +493,7 @@ mod tests {
}

// The apothem of an arc covering 1/6th of a circle.
let apothem = f32::sqrt(3.0) / 2.0;
let apothem = ops::sqrt(3.0) / 2.0;
let tests = [
// Test case: a basic minor arc
TestCase {
Expand Down Expand Up @@ -558,7 +564,7 @@ mod tests {
aabb_min: Vec2::ZERO,
aabb_max: Vec2::splat(1.0),
bounding_circle_center: Vec2::splat(0.5),
bounding_circle_radius: f32::sqrt(2.0) / 2.0,
bounding_circle_radius: ops::sqrt(2.0) / 2.0,
},
// Test case: a basic major arc
TestCase {
Expand Down Expand Up @@ -597,6 +603,7 @@ mod tests {
];

for test in tests {
#[cfg(feature = "std")]
println!("subtest case: {}", test.name);
let segment: CircularSegment = test.arc.into();

Expand All @@ -622,6 +629,7 @@ mod tests {
#[test]
fn circular_sector() {
struct TestCase {
#[allow(unused)]
name: &'static str,
arc: Arc2d,
translation: Vec2,
Expand All @@ -639,8 +647,8 @@ mod tests {
}

// The apothem of an arc covering 1/6th of a circle.
let apothem = f32::sqrt(3.0) / 2.0;
let inv_sqrt_3 = f32::sqrt(3.0).recip();
let apothem = ops::sqrt(3.0) / 2.0;
let inv_sqrt_3 = ops::sqrt(3.0).recip();
let tests = [
// Test case: An sector whose arc is minor, but whose bounding circle is not the circumcircle of the endpoints and center
TestCase {
Expand Down Expand Up @@ -717,7 +725,7 @@ mod tests {
aabb_min: Vec2::ZERO,
aabb_max: Vec2::splat(1.0),
bounding_circle_center: Vec2::splat(0.5),
bounding_circle_radius: f32::sqrt(2.0) / 2.0,
bounding_circle_radius: ops::sqrt(2.0) / 2.0,
},
TestCase {
name: "5/6th circle untransformed",
Expand Down Expand Up @@ -753,6 +761,7 @@ mod tests {
];

for test in tests {
#[cfg(feature = "std")]
println!("subtest case: {}", test.name);
let sector: CircularSector = test.arc.into();

Expand Down
19 changes: 12 additions & 7 deletions crates/bevy_math/src/bounding/bounded3d/extrusion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ use crate::{
bounding::{BoundingCircle, BoundingVolume},
ops,
primitives::{
BoxedPolygon, BoxedPolyline2d, Capsule2d, Cuboid, Cylinder, Ellipse, Extrusion, Line2d,
Polygon, Polyline2d, Primitive2d, Rectangle, RegularPolygon, Segment2d, Triangle2d,
Capsule2d, Cuboid, Cylinder, Ellipse, Extrusion, Line2d, Polygon, Polyline2d, Primitive2d,
Rectangle, RegularPolygon, Segment2d, Triangle2d,
},
Isometry2d, Isometry3d, Quat, Rot2,
};

#[cfg(feature = "alloc")]
use crate::primitives::{BoxedPolygon, BoxedPolyline2d};

use crate::{bounding::Bounded2d, primitives::Circle};

use super::{Aabb3d, Bounded3d, BoundingSphere};
Expand All @@ -26,7 +29,7 @@ impl BoundedExtrusion for Circle {
let top = (segment_dir * half_depth).abs();

let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO);
let half_size = self.radius * Vec3A::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt());
let half_size = self.radius * Vec3A::new(ops::sqrt(e.x), ops::sqrt(e.y), ops::sqrt(e.z));

Aabb3d {
min: isometry.translation - half_size - top,
Expand Down Expand Up @@ -56,8 +59,8 @@ impl BoundedExtrusion for Ellipse {
let m = -axis.x / axis.y;
let signum = axis.signum();

let y = signum.y * b * b / (b * b + m * m * a * a).sqrt();
let x = signum.x * a * (1. - y * y / b / b).sqrt();
let y = signum.y * b * b / ops::sqrt(b * b + m * m * a * a);
let x = signum.x * a * ops::sqrt(1. - y * y / b / b);
isometry.rotation * Vec3A::new(x, y, 0.)
});

Expand Down Expand Up @@ -104,6 +107,7 @@ impl<const N: usize> BoundedExtrusion for Polyline2d<N> {
}
}

#[cfg(feature = "alloc")]
impl BoundedExtrusion for BoxedPolyline2d {
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
Expand Down Expand Up @@ -144,6 +148,7 @@ impl<const N: usize> BoundedExtrusion for Polygon<N> {
}
}

#[cfg(feature = "alloc")]
impl BoundedExtrusion for BoxedPolygon {
fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into<Isometry3d>) -> Aabb3d {
let isometry = isometry.into();
Expand Down Expand Up @@ -301,7 +306,7 @@ mod tests {

let bounding_sphere = extrusion.bounding_sphere(isometry);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), 8f32.sqrt());
assert_eq!(bounding_sphere.radius(), ops::sqrt(8f32));
}

#[test]
Expand Down Expand Up @@ -444,7 +449,7 @@ mod tests {

let bounding_sphere = extrusion.bounding_sphere(isometry);
assert_eq!(bounding_sphere.center, translation.into());
assert_eq!(bounding_sphere.radius(), 8f32.sqrt());
assert_eq!(bounding_sphere.radius(), ops::sqrt(8f32));
}

#[test]
Expand Down
Loading

0 comments on commit a8b9c94

Please sign in to comment.