-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit 861e778
authored
Add unit structs for each ease function (#18739)
## Objective
Allow users to directly call ease functions rather than going through
the `EaseFunction` struct. This is less verbose and more efficient when
the user doesn't need the data-driven aspects of `EaseFunction`.
## Background
`EaseFunction` is a flexible and data-driven way to apply easing. But
that has a price when a user just wants to call a specific ease
function:
```rust
EaseFunction::SmoothStep.sample(t);
```
This is a bit verbose, but also surprisingly inefficient. It calls the
general `EaseFunction::eval`, which won't be inlined and adds an
unnecessary branch. It can also increase code size since it pulls in all
ease functions even though the user might only require one. As far as I
can tell this is true even with `opt-level = 3` and `lto = "fat"`.
```asm
; EaseFunction::SmoothStep.sample_unchecked(t)
lea rcx, [rip + __unnamed_2] ; Load the disciminant for EaseFunction::SmoothStep.
movaps xmm1, xmm0
jmp bevy_math::curve::easing::EaseFunction::eval
```
## Solution
This PR adds a struct for each ease function. Most are unit structs, but
a couple have parameters:
```rust
SmoothStep.sample(t);
Elastic(50.0).sample(t);
Steps(4, JumpAt::Start).sample(t)
```
The structs implement the `Curve<f32>` trait. This means they fit into
the broader `Curve` system, and the user can choose between `sample`,
`sample_unchecked`, and `sample_clamped`. The internals are a simple
function call so the compiler can easily estimate the cost of inlining:
```asm
; SmoothStep.sample_unchecked(t)
movaps xmm1, xmm0
addss xmm1, xmm0
movss xmm2, dword ptr [rip + __real@40400000] ; 3.0
subss xmm2, xmm1
mulss xmm2, xmm0
mulss xmm0, xmm2
```
In a microbenchmark this is around 4x faster. If inlining permits
auto-vectorization then it's 20-50x faster, but that's a niche case.
Adding unit structs is also a small boost to discoverability - the unit
struct can be found in VS Code via "Go To Symbol" -> "smoothstep", which
doesn't find `EaseFunction::SmoothStep`.
### Concerns
- While the unit structs have advantages, they add a lot of API surface
area.
- Another option would have been to expose the underlying functions.
- But functions can't implement the `Curve` trait.
- And the underlying functions are unclamped, which could be a footgun.
- Or there have to be three functions to cover unchecked/checked/clamped
variants.
- The unit structs can't be used with `EasingCurve`, which requires
`EaseFunction`.
- This might confuse users and limit optimisation.
- Wrong: `EasingCurve::new(a, b, SmoothStep)`.
- Right: `EasingCurve::new(a, b, EaseFunction::SmoothStep)`.
- In theory `EasingCurve` could be changed to support any `Curve<f32>`
or a more limited trait.
- But that's likely to be a breaking change and raises questions around
reflection and reliability.
- The unit structs don't have serialization.
- I don't know much about the motivations/requirements for
serialization.
- Each unit struct duplicates the documentation of `EaseFunction`.
- This is convenient for the user, but awkward for anyone updating the
code.
- Maybe better if each unit struct points to the matching
`EaseFunction`.
- Might also make the module page less intimidating (see screenshot).

## Testing
```
cargo test -p bevy_math
```1 parent 18712f3 commit 861e778Copy full SHA for 861e778
File tree
Expand file treeCollapse file tree
1 file changed
+514
-11
lines changedFilter options
- crates/bevy_math/src/curve
Expand file treeCollapse file tree
1 file changed
+514
-11
lines changed
0 commit comments