Skip to content

Commit 861e778

Browse files
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). ![image](https://github.com/user-attachments/assets/59d1cf1f-d136-437f-8ad6-fdae8ca7d57a) ## Testing ``` cargo test -p bevy_math ```
1 parent 18712f3 commit 861e778

File tree

1 file changed

+514
-11
lines changed

1 file changed

+514
-11
lines changed

0 commit comments

Comments
 (0)