Skip to content

Add unit structs for each ease function #18739

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

greeble-dev
Copy link
Contributor

@greeble-dev greeble-dev commented Apr 6, 2025

Objective

When a user wants a specific ease function, make it less verbose and more efficient.

Background

EaseFunction is a flexible and data-driven way to apply easing. But that has a price when a user only wants a specific ease function:

EaseFunction::SmoothStep.sample(t);

This is a little bit verbose, but the main problem is efficiency - it calls the general EaseFunction::eval, which won't be inlined. That means an unnecessary branch inside eval, and can 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".

; 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

The PR adds unit structs that implement Curve<f32> for each ease function. This is less verbose, and the compiler can make accurate predictions about inlining.

SmoothStep.sample(t);
; 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, but could be less in real code. 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.
  • 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

Testing

cargo test -p bevy_math

@IQuick143 IQuick143 added A-Math Fundamental domain-agnostic mathematical operations D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward S-Needs-Design This issue requires design work to think about how it would best be accomplished and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Apr 6, 2025
@greeble-dev greeble-dev changed the title Make specific ease functions less verbose and more inlineable Add unit structs for each ease function Apr 7, 2025
@greeble-dev greeble-dev marked this pull request as ready for review April 7, 2025 14:12
Copy link
Contributor

@ElliottjPierce ElliottjPierce left a comment

Choose a reason for hiding this comment

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

This is pretty much exactly what I've been hoping for. Thanks for doing it!

As for the extra API surface, this is 100% worth it. It gives significant performance wins in my noise lib, basically for free. Also note that not all easing functions are zero-sized, differentiable, etc. We could build on this with enums like SmallEaseFunction, DifferentiableEaseFunction, and SmoothEaseFunction (where the derivative is 0 at t=0 and t=1). That's what I'd use ultimately for my noise lib to allow users to change the ease at runtime. It would also open up the possibility for a Smooth trait (WIP name).

So the API surface is not only worth it, but it's a blocker for these tools.

As for the docs, for now, I see no problem duplicating it. But if we add the other 3 enums above, this would become annoying. It doesn't have to be this pr, but I would either have the enum docs point back to the unit struct docs, or create a macro for each function that expands to the desired docs. Or just dump it all in module docs and refer to that. I've seen other crates do that sometimes.

Anyway, that's my thoughts. Excited to see this land!

});
}

#[test]
fn unit_structs_match_function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't a bad thing to have, but all the functions just call each other, so I don't really see the need for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added it to defend against copy and paste errors, although in retrospect the test was vulnerable to the same error.

I've updated with a macro based test that should be less vulnerable. Also open to removing it if there's a consensus that it's unnecessary.

@greeble-dev greeble-dev added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Needs-Design This issue requires design work to think about how it would best be accomplished labels May 5, 2025
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 D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants