Skip to content

Background fixed update #7

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 9 commits into
base: main
Choose a base branch
from
Open

Conversation

Vrixyz
Copy link

@Vrixyz Vrixyz commented Dec 24, 2024

Objective

I published https://github.com/vrixyz/bevy_fixed_update_task ; This PR allows to tweak schedules used by the plugin.

See https://github.com/Vrixyz/bevy_fixed_update_task/blob/main/examples/interpolate_custom_schedule.rs for an example integration.

Changelog

  • Ability to pass specific schedules to plugins to avoid relying on FixedUpdate.
  • Ability to opt out of built-in ease_[translation|rotation|scale]_lerp, useful when your time management does not rely on Res<Time<Fixed>.

Migration Guide

  • Plugins must be constructed now, default() is supported.
Previous description for historical context

Objective

bevy's FixedUpdate runs synchronously when Time<Virtual> exceeds Time<Fixed> + its accumulated time. This can lead to lag if the fixed update logic takes too long.

When the FixedUpdate doesn't run every frame, we can take advantage of that by running it in parallel.

Solution

This PR is quite similar to how bevy's fixed update works, but eagerly extracts ECS data into a background task, to synchronize it only when we exceed Time<Fixed> + its accumulated time.

The implementation doesn't use Time<Fixed> but a component approach TimeStep + TaskToRender.

  • I took a component approach due to reusing bevy_rapier work, but currently it supports only 1 entity having those components, due to relying on scheduling order for compatibility with bevy_transform_ interpolation. I think a component approach is cleaner long term, but definitely error prone right now.
    • ⚠️ I believe that's the biggest "implementation flaw" right now.
  • by relying on scheduling for extracting data and writing back, I can rely on the non-interpolated transforms in schedules from background_fixed_schedule.
  • I didn't rely on Fixed because I think we could have a different implementation involving a FixedUpdate being run in advance of the Virtual time, resulting in a "negative accumulated time".

image

mermaid

Unfortunately mermaid has a few bugs and github doesn't rely on latest mermaid, you can paste that is https://mermaid.live for a better formatting:

gantt
    title Background fixed update
    dateFormat  X
    axisFormat %L
    section Frames
        Frame 1                             :f1, 0, 0.16s
        Frame 2                             :f2,after f1, 0.16s
        Frame 3                             :f3,after f2, 0.16s
    section Bevy ecs
        start fixed update                  :s1, 0, 0.001s
        Extract data                        :after s1, 0.01s
        Should we finish the fixed update?  :c1,after f1, 0.001s
        Should we finish the fixed update?  :c2,after f2, 0.001s
        Write back data                     :w1,after c2, 0.01s
        start fixed update                  :s2,after w1, 0.001s
        Extract data                        :e2,after s2, 0.01s
    section Background thread
        Background task                     :after e1, 0.25s
        Background task                     :after e2, 0.25s

Demo

This recording simulates (through sleep) a constant task time of 0.5 second.

We can see that at a too high fixed update rate, the rendering is struggling. but at a lower fixed framerate, the rendering has a steady 60FPS.

Using bevy's synchronous FixedUpdate would result in a 0.5 lag at every fixed update. While an extreme example, it illustrates quite well the advantages of this approach.

Screencast.from.12-24-2024.03_58_11.PM.webm

This approach also could allow some "time deviation" between virtual time and "task" time, to avoid a "catch back" phase (where the time speed appears to go quicker). The objective is to allow user to customize the "fixed steps" to consume before spawning the task.

How to test

Run the example with bevy/multi_threaded to have the same behaviour as the above demo. Add in bevy/trace_tracy feature and profile with tracy to have a better visualization of what's going on.

cargo run --example interpolate_custom_schedule --features bevy/multi_threaded

Background task drawbacks

  • Eagerly extracting data from ECS comes at the cost that input registered between extraction and task handling is not taken into account.
    • This could be mitigated by estimating the time needed and delay the extraction, but:
      • bevy_transform_interpolation modifies the Transform outside of expected FixedUpdate, so we'd need some special handling
      • it's a risk of delaying the fixed update rate, which would in turn impact interpolation
    • This could be mitigated by eagerly writing back data, but the simulation would be ahead of Time<Virtual>, and it's non trivial to make work with interpolation.

@Vrixyz Vrixyz changed the title Background task Background fixed update Dec 26, 2024
Comment on lines 83 to 134
fn ease_translation_lerp(
mut query: Query<(&mut Transform, &TranslationEasingState)>,
time: Query<(&TaskToRenderTime, &Timestep)>,
) {
let Ok((time, timestep)) = time.get_single() else {
return;
};
let overstep = (time.diff.max(0.0) / timestep.timestep.as_secs_f64()).min(1.0) as f32;
query.iter_mut().for_each(|(mut transform, interpolation)| {
if let (Some(start), Some(end)) = (interpolation.start, interpolation.end) {
transform.translation = start.lerp(end, overstep);
}
});
}

/// Eases the rotations of entities with spherical linear interpolation.
fn ease_rotation_slerp(
mut query: Query<(&mut Transform, &RotationEasingState)>,
time: Query<(&TaskToRenderTime, &Timestep)>,
) {
let Ok((time, timestep)) = time.get_single() else {
return;
};
let overstep = (time.diff.max(0.0) / timestep.timestep.as_secs_f64()).min(1.0) as f32;

query
.par_iter_mut()
.for_each(|(mut transform, interpolation)| {
if let (Some(start), Some(end)) = (interpolation.start, interpolation.end) {
// Note: `slerp` will always take the shortest path, but when the two rotations are more than
// 180 degrees apart, this can cause visual artifacts as the rotation "flips" to the other side.
transform.rotation = start.slerp(end, overstep);
}
});
}

/// Eases the scales of entities with linear interpolation.
fn ease_scale_lerp(
mut query: Query<(&mut Transform, &ScaleEasingState)>,
time: Query<(&TaskToRenderTime, &Timestep)>,
) {
let Ok((time, timestep)) = time.get_single() else {
return;
};
let overstep = (time.diff.max(0.0) / timestep.timestep.as_secs_f64()).min(1.0) as f32;

query.iter_mut().for_each(|(mut transform, interpolation)| {
if let (Some(start), Some(end)) = (interpolation.start, interpolation.end) {
transform.scale = start.lerp(end, overstep);
}
});
}
Copy link
Author

Choose a reason for hiding this comment

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

Those systems could use a helper function to only retrieve the overstep(), the rest of the code is similar to the easing plugin.

Copy link
Author

Choose a reason for hiding this comment

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

This comment is still relevant, as it could be more practical to opt-out of Res<Time<Fixed>> time management. That said, it's a minor inconvenience.

Vrixyz added a commit to Vrixyz/bevy_fixed_update_task that referenced this pull request Jan 7, 2025
@Vrixyz Vrixyz marked this pull request as ready for review January 17, 2025 09:10
@@ -83,7 +83,7 @@ use bevy::prelude::*;
/// Then, add the [`TransformExtrapolationPlugin`] to the app with the velocity sources:
///
/// ```
/// use bevy::{ecs::query::QueryData, prelude::*};
/// use bevy::{ecs::query::QueryData, time::TimePlugin, prelude::*};
Copy link
Author

Choose a reason for hiding this comment

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

Those are slightly out of scope for this PR, but needed for bevy 0.15.1 due to #7.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Interpolation over non FixedUpdate frames
1 participant