From 79698f5c2507fe8b15151cf6977f4e39cda973dc Mon Sep 17 00:00:00 2001
From: Thierry Berger <contact@thierryberger.com>
Date: Tue, 24 Dec 2024 10:49:04 +0100
Subject: [PATCH 1/8] background simulation task + interpolation (wip wip)

---
 Cargo.toml                                    |  16 +-
 examples/extrapolation.rs                     |   2 +-
 examples/interpolate_custom_schedule.rs       | 285 +++++++
 examples/interpolate_custom_schedule_retry.rs | 722 ++++++++++++++++++
 examples/interpolation.rs                     |   8 -
 src/background_fixed_schedule.rs              | 423 ++++++++++
 src/interpolation.rs                          |  37 +-
 src/lib.rs                                    |  64 +-
 8 files changed, 1520 insertions(+), 37 deletions(-)
 create mode 100644 examples/interpolate_custom_schedule.rs
 create mode 100644 examples/interpolate_custom_schedule_retry.rs
 create mode 100644 src/background_fixed_schedule.rs

diff --git a/Cargo.toml b/Cargo.toml
index d35dd9f..f3342c2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,15 +11,27 @@ readme = "README.md"
 keywords = ["gamedev", "easing", "bevy"]
 categories = ["game-development"]
 
+[features]
+default = ["x11"]
+x11 = ["bevy/x11"]
+
 [dependencies]
-bevy = { version = "0.15.0-rc", default-features = false }
+bevy = { version = "0.15", default-features = false }
+crossbeam-channel = "0.5"
+profiling = "1.0"
+rand = "0.8"
 
 [dev-dependencies]
-bevy = { version = "0.15.0-rc", default-features = false, features = [
+bevy = { version = "0.15", default-features = false, features = [
     "bevy_asset",
     "bevy_render",
+    "bevy_window",
     "bevy_text",
     "bevy_ui",
     "bevy_winit",
     "default_font",
+    "bevy_gizmos",
 ] }
+crossbeam-channel = "0.5"
+profiling = "1.0"
+rand = "0.8"
diff --git a/examples/extrapolation.rs b/examples/extrapolation.rs
index 2fc61b2..852f1e1 100644
--- a/examples/extrapolation.rs
+++ b/examples/extrapolation.rs
@@ -83,7 +83,7 @@ impl Plugin for TransformExtrapolationPlugin {
         // Add the `TransformEasingPlugin` if it hasn't been added yet.
         // It performs the actual easing based on the start and end states set by the extrapolation.
         if !app.is_plugin_added::<TransformEasingPlugin>() {
-            app.add_plugins(TransformEasingPlugin);
+            app.add_plugins(TransformEasingPlugin::default());
         }
     }
 }
diff --git a/examples/interpolate_custom_schedule.rs b/examples/interpolate_custom_schedule.rs
new file mode 100644
index 0000000..d50f20f
--- /dev/null
+++ b/examples/interpolate_custom_schedule.rs
@@ -0,0 +1,285 @@
+//! This example showcases how `Transform` interpolation can be used to make movement
+//! appear smooth at fixed timesteps.
+//!
+//! `Transform` interpolation updates `Transform` at every frame in between
+//! fixed ticks to smooth out the visual result. The interpolation is done
+//! from the previous positions to the current positions, which keeps movement smooth,
+//! but has the downside of making movement feel slightly delayed as the rendered
+//! result lags slightly behind the true positions.
+//!
+//! For an example of how transform extrapolation could be implemented instead,
+//! see `examples/extrapolation.rs`.
+
+use bevy::{
+    color::palettes::{
+        css::{ORANGE, RED, WHITE},
+        tailwind::{CYAN_400, RED_400},
+    },
+    ecs::schedule::ScheduleLabel,
+    prelude::*,
+};
+use bevy_transform_interpolation::{
+    background_fixed_schedule::{
+        AngularVelocity, BackgroundFixedUpdatePlugin, LinearVelocity, PostWriteBack, PreWriteBack,
+        TaskResults, TaskToRenderTime, Timestep, ToMove,
+    },
+    prelude::*,
+    RotationEasingState, ScaleEasingState, TransformEasingSet, TranslationEasingState,
+};
+
+use std::time::Duration;
+
+const MOVEMENT_SPEED: f32 = 250.0;
+const ROTATION_SPEED: f32 = 2.0;
+
+fn main() {
+    let mut app = App::new();
+
+    let easing_plugin = TransformEasingPlugin {
+        schedule_fixed_first: PreWriteBack.intern(),
+        schedule_fixed_last: PostWriteBack.intern(),
+        schedule_fixed_loop: bevy::app::prelude::RunFixedMainLoop.intern(),
+        after_fixed_main_loop: RunFixedMainLoopSystem::AfterFixedMainLoop.intern(),
+        update_easing_values: false,
+    };
+    let interpolation_plugin = TransformInterpolationPlugin {
+        schedule_fixed_first: PreWriteBack.intern(),
+        schedule_fixed_last: PostWriteBack.intern(),
+        interpolate_translation_all: false,
+        interpolate_rotation_all: false,
+        interpolate_scale_all: false,
+    };
+
+    // Add the `TransformInterpolationPlugin` to the app to enable transform interpolation.
+    app.add_plugins((
+        DefaultPlugins,
+        BackgroundFixedUpdatePlugin,
+        easing_plugin,
+        interpolation_plugin,
+    ));
+
+    // Set the fixed timestep to just 5 Hz for demonstration purposes.
+
+    // Setup the scene and UI, and update text in `Update`.
+    app.add_systems(Startup, (setup, setup_text)).add_systems(
+        bevy::app::prelude::RunFixedMainLoop,
+        (
+            change_timestep,
+            update_timestep_text,
+            update_diff_to_render_text,
+        ),
+    );
+
+    // This runs every frame to poll if our task was done.
+
+    app.add_systems(
+        bevy::app::prelude::RunFixedMainLoop,
+        (ease_translation_lerp, ease_rotation_slerp, ease_scale_lerp)
+            .in_set(TransformEasingSet::Ease),
+    );
+
+    // Run the app.
+    app.run();
+}
+/// Eases the translations of entities with linear interpolation.
+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);
+        }
+    });
+}
+
+fn setup(
+    mut commands: Commands,
+    mut materials: ResMut<Assets<ColorMaterial>>,
+    mut meshes: ResMut<Assets<Mesh>>,
+) {
+    // Spawn a camera.
+    commands.spawn(Camera2d);
+
+    let mesh = meshes.add(Rectangle::from_length(60.0));
+
+    commands.spawn((
+        TaskToRenderTime::default(),
+        Timestep {
+            timestep: Duration::from_secs_f32(0.5),
+        },
+        TaskResults::default(),
+    ));
+
+    // This entity uses transform interpolation.
+    commands.spawn((
+        Name::new("Interpolation"),
+        Mesh2d(mesh.clone()),
+        MeshMaterial2d(materials.add(Color::from(CYAN_400)).clone()),
+        Transform::from_xyz(-500.0, 60.0, 0.0),
+        TransformInterpolation,
+        LinearVelocity(Vec2::new(MOVEMENT_SPEED, 0.0)),
+        AngularVelocity(ROTATION_SPEED),
+        ToMove,
+    ));
+
+    // This entity is simulated in `FixedUpdate` without any smoothing.
+    commands.spawn((
+        Name::new("No Interpolation"),
+        Mesh2d(mesh.clone()),
+        MeshMaterial2d(materials.add(Color::from(RED_400)).clone()),
+        Transform::from_xyz(-500.0, -60.0, 0.0),
+        LinearVelocity(Vec2::new(MOVEMENT_SPEED, 0.0)),
+        AngularVelocity(ROTATION_SPEED),
+        ToMove,
+    ));
+}
+
+/// Changes the timestep of the simulation when the up or down arrow keys are pressed.
+fn change_timestep(mut time: Query<&mut Timestep>, keyboard_input: Res<ButtonInput<KeyCode>>) {
+    let mut time = time.single_mut();
+    if keyboard_input.pressed(KeyCode::ArrowUp) {
+        let new_timestep = (time.timestep.as_secs_f64() * 0.9).max(1.0 / 255.0);
+        time.timestep = Duration::from_secs_f64(new_timestep);
+    }
+    if keyboard_input.pressed(KeyCode::ArrowDown) {
+        let new_timestep = (time.timestep.as_secs_f64() * 1.1)
+            .min(1.0)
+            .max(1.0 / 255.0);
+        time.timestep = Duration::from_secs_f64(new_timestep);
+    }
+}
+
+#[derive(Component)]
+struct TimestepText;
+
+#[derive(Component)]
+struct TaskToRenderTimeText;
+
+fn setup_text(mut commands: Commands) {
+    let font = TextFont {
+        font_size: 20.0,
+        ..default()
+    };
+
+    commands
+        .spawn((
+            Text::new("Fixed Hz: "),
+            TextColor::from(WHITE),
+            font.clone(),
+            Node {
+                position_type: PositionType::Absolute,
+                top: Val::Px(10.0),
+                left: Val::Px(10.0),
+                ..default()
+            },
+        ))
+        .with_child((TimestepText, TextSpan::default()));
+
+    commands.spawn((
+        Text::new("Change Timestep With Up/Down Arrow"),
+        TextColor::from(WHITE),
+        font.clone(),
+        Node {
+            position_type: PositionType::Absolute,
+            top: Val::Px(10.0),
+            right: Val::Px(10.0),
+            ..default()
+        },
+    ));
+
+    commands.spawn((
+        Text::new("Interpolation"),
+        TextColor::from(CYAN_400),
+        font.clone(),
+        Node {
+            position_type: PositionType::Absolute,
+            top: Val::Px(50.0),
+            left: Val::Px(10.0),
+            ..default()
+        },
+    ));
+
+    commands.spawn((
+        Text::new("No Interpolation"),
+        TextColor::from(RED_400),
+        font.clone(),
+        Node {
+            position_type: PositionType::Absolute,
+            top: Val::Px(75.0),
+            left: Val::Px(10.0),
+            ..default()
+        },
+    ));
+
+    commands
+        .spawn((
+            Text::new("Diff to render time: "),
+            TextColor::from(WHITE),
+            font.clone(),
+            Node {
+                position_type: PositionType::Absolute,
+                top: Val::Px(100.0),
+                left: Val::Px(10.0),
+                ..default()
+            },
+        ))
+        .with_child((TaskToRenderTimeText, TextSpan::default()));
+}
+
+fn update_timestep_text(
+    mut text: Single<&mut TextSpan, With<TimestepText>>,
+    time: Query<&Timestep>,
+) {
+    let timestep = time.single().timestep.as_secs_f32().recip();
+    text.0 = format!("{timestep:.2}");
+}
+
+fn update_diff_to_render_text(
+    mut text: Single<&mut TextSpan, With<TaskToRenderTimeText>>,
+    task_to_render: Single<&TaskToRenderTime>,
+) {
+    text.0 = format!("{:.2}", task_to_render.diff);
+}
diff --git a/examples/interpolate_custom_schedule_retry.rs b/examples/interpolate_custom_schedule_retry.rs
new file mode 100644
index 0000000..b4bd70e
--- /dev/null
+++ b/examples/interpolate_custom_schedule_retry.rs
@@ -0,0 +1,722 @@
+//! This example showcases how `Transform` interpolation can be used to make movement
+//! appear smooth at fixed timesteps.
+//!
+//! `Transform` interpolation updates `Transform` at every frame in between
+//! fixed ticks to smooth out the visual result. The interpolation is done
+//! from the previous positions to the current positions, which keeps movement smooth,
+//! but has the downside of making movement feel slightly delayed as the rendered
+//! result lags slightly behind the true positions.
+//!
+//! For an example of how transform extrapolation could be implemented instead,
+//! see `examples/extrapolation.rs`.
+
+use bevy::{
+    color::palettes::{
+        css::WHITE,
+        tailwind::{CYAN_400, RED_400},
+    },
+    ecs::schedule::{LogLevel, ScheduleBuildSettings, ScheduleLabel},
+    prelude::*,
+    tasks::AsyncComputeTaskPool,
+};
+use bevy_transform_interpolation::{
+    prelude::*, RotationEasingState, ScaleEasingState, TransformEasingSet, TranslationEasingState,
+};
+use crossbeam_channel::Receiver;
+use rand::{thread_rng, Rng};
+use std::{collections::VecDeque, slice::IterMut, time::Duration};
+
+const MOVEMENT_SPEED: f32 = 250.0;
+const ROTATION_SPEED: f32 = 2.0;
+
+// TODO: update this time to use it correctly.
+// See https://github.com/bevyengine/bevy/blob/d4b07a51149c4cc69899f7424df473ff817fe324/crates/bevy_time/src/fixed.rs#L241
+
+fn main() {
+    let mut app = App::new();
+
+    // Add the `TransformInterpolationPlugin` to the app to enable transform interpolation.
+    app.add_plugins(DefaultPlugins);
+
+    // Set the fixed timestep to just 5 Hz for demonstration purposes.
+
+    // Setup the scene and UI, and update text in `Update`.
+    app.add_systems(Startup, (setup, setup_text)).add_systems(
+        bevy::app::prelude::RunFixedMainLoop,
+        (
+            change_timestep,
+            update_timestep_text,
+            update_diff_to_render_text,
+        ),
+    );
+
+    // This runs every frame to poll if our task was done.
+    app.add_systems(
+        bevy::app::prelude::RunFixedMainLoop, // TODO: use a specific schedule for this, à la bevy's FixedMainLoop
+        task_schedule::FixedMain::run_schedule,
+    );
+
+    // this handles checking for task completion, firing writeback schedules and spawning a new task.
+    app.edit_schedule(task_schedule::FixedMain, |schedule| {
+        schedule
+            .add_systems(task_schedule::HandleTask::run_schedule)
+            .set_build_settings(ScheduleBuildSettings {
+                ambiguity_detection: LogLevel::Error,
+                ..default()
+            });
+    });
+
+    // those schedules are part of FixedMain
+    app.init_schedule(task_schedule::PreWriteBack);
+    app.edit_schedule(task_schedule::WriteBack, |schedule| {
+        schedule
+            .add_systems((handle_task,))
+            .set_build_settings(ScheduleBuildSettings {
+                ambiguity_detection: LogLevel::Error,
+                ..default()
+            });
+    });
+    app.edit_schedule(task_schedule::PostWriteBack, |schedule| {
+        schedule.set_build_settings(ScheduleBuildSettings {
+            ambiguity_detection: LogLevel::Error,
+            ..default()
+        });
+    });
+
+    app.add_systems(
+        bevy::app::prelude::RunFixedMainLoop,
+        (ease_translation_lerp, ease_rotation_slerp, ease_scale_lerp)
+            .in_set(TransformEasingSet::Ease),
+    );
+    // this will spawn a new task if needed.
+    app.edit_schedule(task_schedule::MaybeSpawnTask, |schedule| {
+        schedule
+            .add_systems(spawn_task)
+            .set_build_settings(ScheduleBuildSettings {
+                ambiguity_detection: LogLevel::Error,
+                ..default()
+            });
+    });
+
+    // Run the app.
+    app.run();
+}
+/// Eases the translations of entities with linear interpolation.
+fn ease_translation_lerp(
+    mut query: Query<(&mut Transform, &TranslationEasingState)>,
+    time: Query<(&TaskToRenderTime, &Timestep, &LastTaskTimings)>,
+) {
+    let Ok((time, timestep, last_task_timing)) = time.get_single() else {
+        return;
+    };
+    let overstep = (time.diff.max(0.0)
+        / (timestep.timestep - last_task_timing.render_time_elapsed_during_the_simulation)
+            .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);
+        }
+    });
+}
+
+/// The linear velocity of an entity indicating its movement speed and direction.
+#[derive(Component, Debug, Deref, DerefMut, Clone)]
+pub struct LinearVelocity(Vec2);
+
+/// The angular velocity of an entity indicating its rotation speed.
+#[derive(Component, Debug, Deref, DerefMut, Clone)]
+pub struct AngularVelocity(f32);
+
+#[derive(Component, Debug, Clone)]
+struct ToMove;
+
+fn setup(
+    mut commands: Commands,
+    mut materials: ResMut<Assets<ColorMaterial>>,
+    mut meshes: ResMut<Assets<Mesh>>,
+) {
+    // Spawn a camera.
+    commands.spawn(Camera2d);
+
+    let mesh = meshes.add(Rectangle::from_length(60.0));
+
+    commands.spawn((
+        TaskToRenderTime::default(),
+        Timestep {
+            timestep: Duration::from_secs_f32(0.5),
+        },
+        TaskResults::default(),
+    ));
+
+    // This entity uses transform interpolation.
+    commands.spawn((
+        Name::new("Interpolation"),
+        Mesh2d(mesh.clone()),
+        MeshMaterial2d(materials.add(Color::from(CYAN_400)).clone()),
+        Transform::from_xyz(-500.0, 60.0, 0.0),
+        TransformInterpolation,
+        LinearVelocity(Vec2::new(MOVEMENT_SPEED, 0.0)),
+        AngularVelocity(ROTATION_SPEED),
+        ToMove,
+    ));
+
+    // This entity is simulated in `FixedUpdate` without any smoothing.
+    commands.spawn((
+        Name::new("No Interpolation"),
+        Mesh2d(mesh.clone()),
+        MeshMaterial2d(materials.add(Color::from(RED_400)).clone()),
+        Transform::from_xyz(-500.0, -60.0, 0.0),
+        LinearVelocity(Vec2::new(MOVEMENT_SPEED, 0.0)),
+        AngularVelocity(ROTATION_SPEED),
+        ToMove,
+    ));
+}
+
+/// Changes the timestep of the simulation when the up or down arrow keys are pressed.
+fn change_timestep(mut time: Query<&mut Timestep>, keyboard_input: Res<ButtonInput<KeyCode>>) {
+    let mut time = time.single_mut();
+    if keyboard_input.pressed(KeyCode::ArrowUp) {
+        let new_timestep = (time.timestep.as_secs_f64() * 0.9).max(1.0 / 255.0);
+        time.timestep = Duration::from_secs_f64(new_timestep);
+    }
+    if keyboard_input.pressed(KeyCode::ArrowDown) {
+        let new_timestep = (time.timestep.as_secs_f64() * 1.1)
+            .min(1.0)
+            .max(1.0 / 255.0);
+        time.timestep = Duration::from_secs_f64(new_timestep);
+    }
+}
+
+/// Flips the movement directions of objects when they reach the left or right side of the screen.
+fn flip_movement_direction(query: IterMut<(&mut Transform, &mut LinearVelocity)>) {
+    for (transform, lin_vel) in query {
+        if transform.translation.x > 500.0 && lin_vel.0.x > 0.0 {
+            lin_vel.0 = Vec2::new(-MOVEMENT_SPEED, 0.0);
+        } else if transform.translation.x < -500.0 && lin_vel.0.x < 0.0 {
+            lin_vel.0 = Vec2::new(MOVEMENT_SPEED, 0.0);
+        }
+    }
+}
+
+/// Moves entities based on their `LinearVelocity`.
+fn movement(query: IterMut<(&mut Transform, &mut LinearVelocity)>, delta: Duration) {
+    let delta_secs = delta.as_secs_f32();
+    for (transform, lin_vel) in query {
+        transform.translation += lin_vel.extend(0.0) * delta_secs;
+    }
+}
+
+/// Rotates entities based on their `AngularVelocity`.
+fn rotate(query: IterMut<(&mut Transform, &mut AngularVelocity)>, delta: Duration) {
+    let delta_secs = delta.as_secs_f32();
+    for (transform, ang_vel) in query {
+        transform.rotate_local_z(ang_vel.0 * delta_secs);
+    }
+}
+
+#[derive(Component)]
+struct TimestepText;
+
+#[derive(Component)]
+struct TaskToRenderTimeText;
+
+fn setup_text(mut commands: Commands) {
+    let font = TextFont {
+        font_size: 20.0,
+        ..default()
+    };
+
+    commands
+        .spawn((
+            Text::new("Fixed Hz: "),
+            TextColor::from(WHITE),
+            font.clone(),
+            Node {
+                position_type: PositionType::Absolute,
+                top: Val::Px(10.0),
+                left: Val::Px(10.0),
+                ..default()
+            },
+        ))
+        .with_child((TimestepText, TextSpan::default()));
+
+    commands.spawn((
+        Text::new("Change Timestep With Up/Down Arrow"),
+        TextColor::from(WHITE),
+        font.clone(),
+        Node {
+            position_type: PositionType::Absolute,
+            top: Val::Px(10.0),
+            right: Val::Px(10.0),
+            ..default()
+        },
+    ));
+
+    commands.spawn((
+        Text::new("Interpolation"),
+        TextColor::from(CYAN_400),
+        font.clone(),
+        Node {
+            position_type: PositionType::Absolute,
+            top: Val::Px(50.0),
+            left: Val::Px(10.0),
+            ..default()
+        },
+    ));
+
+    commands.spawn((
+        Text::new("No Interpolation"),
+        TextColor::from(RED_400),
+        font.clone(),
+        Node {
+            position_type: PositionType::Absolute,
+            top: Val::Px(75.0),
+            left: Val::Px(10.0),
+            ..default()
+        },
+    ));
+
+    commands
+        .spawn((
+            Text::new("Diff to render time: "),
+            TextColor::from(WHITE),
+            font.clone(),
+            Node {
+                position_type: PositionType::Absolute,
+                top: Val::Px(100.0),
+                left: Val::Px(10.0),
+                ..default()
+            },
+        ))
+        .with_child((TaskToRenderTimeText, TextSpan::default()));
+}
+
+fn update_timestep_text(
+    mut text: Single<&mut TextSpan, With<TimestepText>>,
+    time: Query<&Timestep>,
+) {
+    let timestep = time.single().timestep.as_secs_f32().recip();
+    text.0 = format!("{timestep:.2}");
+}
+
+fn update_diff_to_render_text(
+    mut text: Single<&mut TextSpan, With<TaskToRenderTimeText>>,
+    task_to_render: Single<&TaskToRenderTime>,
+) {
+    text.0 = format!("{:.2}", task_to_render.diff);
+}
+
+pub mod task_schedule {
+
+    use bevy::{
+        ecs::schedule::ScheduleLabel,
+        log::{info, trace},
+        prelude::{SystemSet, World},
+        time::Time,
+    };
+
+    use crate::TaskToRenderTime;
+
+    #[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
+    pub enum FixedMainLoop {
+        Before,
+        During,
+        After,
+    }
+
+    /// Executes before the task result is propagated to the ECS.
+    #[derive(ScheduleLabel, Clone, Copy, Debug, PartialEq, Eq, Hash)]
+    pub struct PreWriteBack;
+
+    /// Propagates the task result to the ECS.
+    #[derive(ScheduleLabel, Clone, Copy, Debug, PartialEq, Eq, Hash)]
+    pub struct WriteBack;
+
+    /// Called after the propagation of the task result to the ECS.
+    #[derive(ScheduleLabel, Clone, Copy, Debug, PartialEq, Eq, Hash)]
+    pub struct PostWriteBack;
+
+    /// Called once to start a task, then after receiving each task result.
+    #[derive(ScheduleLabel, Clone, Copy, Debug, PartialEq, Eq, Hash)]
+    pub struct MaybeSpawnTask;
+
+    /// Schedule running [`PreWriteBack`], [`WriteBack`] and [`PostWriteBack`]
+    /// only if it received its data from the [`super::WorkTask`] present in the single Entity containing it.
+    ///
+    /// This Schedule overrides [`Res<Time>`][Time] to be the task's time ([`Time<Fixed<MyTaskTime>>`]).
+    ///
+    /// It's also responsible for spawning a new [`super::WorkTask`].
+    ///
+    /// This Schedule does not support multiple Entities with the same `Task` component.
+    // TODO: Schedule as entities might be able to support multiple entities?
+    ///
+    /// This works similarly to [`bevy's FixedMain`][bevy::app::FixedMain],
+    /// but it is not blocked by the render loop.
+    #[derive(Debug, Hash, PartialEq, Eq, Clone, ScheduleLabel)]
+    pub struct FixedMain;
+
+    impl FixedMain {
+        /// A system that runs the [`SingleTaskSchedule`] if the task was done.
+        pub fn run_schedule(world: &mut World) {
+            world
+                .run_system_cached(crate::finish_task_and_store_result)
+                .unwrap();
+
+            // Compute difference between task and render time.
+            let clock = world.resource::<Time>().as_generic();
+            let mut query = world.query::<(&mut TaskToRenderTime, &super::Timestep)>();
+            let (mut task_to_render_time, timestep) = query.single_mut(world);
+            task_to_render_time.diff += clock.delta().as_secs_f64();
+            // should we apply deferred commands?
+            if task_to_render_time.diff <= timestep.timestep.as_secs_f64() {
+                // Task is too far ahead, we should not read the simulation.
+                return;
+            }
+            let simulated_time = {
+                let mut query = world.query::<&crate::TaskResults>();
+                let task_result = query.single(world).results.front();
+                task_result.map(|task_result| task_result.result.simulated_time)
+            };
+            let Some(simulated_time) = simulated_time else {
+                let mut query = world.query::<&crate::LastTaskTimings>();
+                if query.get_single(world).is_err() {
+                    world.run_schedule(MaybeSpawnTask);
+                }
+                return;
+            };
+            let mut query = world.query::<&mut TaskToRenderTime>();
+            let mut task_to_render_time = query.single_mut(world);
+            task_to_render_time.diff -= simulated_time.as_secs_f64();
+            let _ = world.try_schedule_scope(FixedMain, |world, schedule| {
+                // Advance simulation.
+                trace!("Running FixedMain schedule");
+                schedule.run(world);
+
+                // If physics is paused, reset delta time to stop simulation
+                // unless users manually advance `Time<Physics>`.
+                /*if is_paused {
+                    world
+                        .resource_mut::<Time<Physics>>()
+                        .advance_by(Duration::ZERO);
+                }
+                */
+            });
+            // PROBLEM: This is outside of our fixed update, so we're reading the interpolated transforms.
+            // This is unacceptable because that's not our ground truth.
+            //world.run_schedule(MaybeSpawnTask);
+        }
+    }
+
+    /// Schedule handling a single task.
+    #[derive(Debug, Hash, PartialEq, Eq, Clone, ScheduleLabel)]
+    pub struct HandleTask;
+
+    impl HandleTask {
+        pub fn run_schedule(world: &mut World) {
+            let _ = world.try_schedule_scope(PreWriteBack, |world, schedule| {
+                schedule.run(world);
+            });
+            let _ = world.try_schedule_scope(WriteBack, |world, schedule| {
+                schedule.run(world);
+            });
+            let _ = world.try_schedule_scope(MaybeSpawnTask, |world, schedule| {
+                schedule.run(world);
+            });
+            let _ = world.try_schedule_scope(PostWriteBack, |world, schedule| {
+                schedule.run(world);
+            });
+        }
+    }
+}
+
+///
+/// The task inside this component is polled by the system [`handle_tasks`].
+///
+/// Any changes to [`Transform`]s being modified by the task will be overridden when the task finishes.
+///
+/// This component is removed when the task is done
+#[derive(Component, Debug)]
+pub struct WorkTask {
+    /// The time in seconds at which we started the simulation, as reported by the used render time [`Time::elapsed`].
+    pub started_at_render_time: Duration,
+    /// Amount of frames elapsed since the simulation started.
+    pub update_frames_elapsed: u32,
+    /// The channel end to receive the simulation result.
+    pub recv: Receiver<TaskResultRaw>,
+}
+
+/// The result of a task to be handled.
+#[derive(Debug, Default)]
+pub struct TaskResultRaw {
+    pub transforms: Vec<(Entity, Transform, LinearVelocity, AngularVelocity)>,
+    /// The duration in seconds **simulated** by the simulation.
+    ///
+    /// This is different from the real time it took to simulate the physics.
+    ///
+    /// It is needed to synchronize the simulation with the render time.
+    pub simulated_time: Duration,
+}
+
+/// The result of a task to be handled.
+#[derive(Debug, Default)]
+pub struct TaskResult {
+    pub result: TaskResultRaw,
+    pub render_time_elapsed_during_the_simulation: Duration,
+    /// The time at which we started the simulation, as reported by the used render time [`Time::elapsed`].
+    pub started_at_render_time: Duration,
+    /// Amount of frames elapsed since the simulation started.
+    pub update_frames_elapsed: u32,
+}
+/// The result of last task result, helpful for interpolation.
+#[derive(Debug, Default, Component)]
+pub struct LastTaskTimings {
+    pub render_time_elapsed_during_the_simulation: Duration,
+    /// The time at which we started the simulation, as reported by the used render time [`Time::elapsed`].
+    pub started_at_render_time: Duration,
+}
+
+#[derive(Debug, Default, Component)]
+pub struct RealTransform(pub Transform);
+
+/// The result of a task to be handled.
+#[derive(Debug, Default, Component)]
+pub struct TaskResults {
+    /// The results of the tasks.
+    ///
+    /// This is a queue because we might be spawning a new task while another has not been processed yet.
+    ///
+    /// To avoid overwriting the results, we keep them in a queue.
+    pub results: VecDeque<TaskResult>,
+}
+
+/// Difference between tasks and rendering time
+#[derive(Component, Default, Reflect, Clone)]
+pub struct TaskToRenderTime {
+    /// Difference in seconds between tasks and rendering time.
+    ///
+    /// We don't use [`Duration`] because it can be negative.
+    pub diff: f64,
+    /// Amount of rendering frames last task took.
+    pub last_task_frame_count: u32,
+}
+
+/// Difference between tasks and rendering time
+#[derive(Component, Default, Reflect, Clone)]
+pub struct Timestep {
+    pub timestep: Duration,
+}
+
+/// This system spawns a [`WorkTask`] is none are ongoing.
+/// The task simulate computationally intensive work that potentially spans multiple frames/ticks.
+///
+/// A separate system, [`handle_tasks`], will poll the spawned tasks on subsequent
+/// frames/ticks, and use the results to spawn cubes
+pub(crate) fn spawn_task(
+    mut commands: Commands,
+    q_context: Query<(
+        Entity,
+        &TaskToRenderTime,
+        &Timestep,
+        Has<WorkTask>,
+        &TaskResults,
+    )>,
+    q_transforms: Query<(Entity, &mut Transform, &LinearVelocity, &AngularVelocity), With<ToMove>>,
+    virtual_time: Res<Time<Virtual>>,
+) {
+    let Ok((entity_ctx, task_to_render_time, timestep, has_work, results)) = q_context.get_single()
+    else {
+        info!("No correct entity found.");
+        return;
+    };
+    if has_work {
+        info!("A task is ongoing.");
+        return;
+    }
+    let timestep = timestep.timestep;
+
+    // We are not impacting task to render diff yet, because the task has not run yet.
+    // Ideally, this should be driven from user code.
+    let mut sim_to_render_time = task_to_render_time.clone();
+
+    let mut substep_count = 1;
+    /*while sim_to_render_time.diff > timestep.as_secs_f64() {
+        sim_to_render_time.diff -= timestep.as_secs_f64();
+        substep_count += 1;
+    }
+    if substep_count == 0 {
+        info!("No substeps needed.");
+        return;
+    }*/
+
+    let mut transforms_to_move: Vec<(Entity, Transform, LinearVelocity, AngularVelocity)> =
+        q_transforms
+            .iter()
+            .map(|(entity, transform, lin_vel, ang_vel)| {
+                (entity, transform.clone(), lin_vel.clone(), ang_vel.clone())
+            })
+            .collect();
+    let (sender, recv) = crossbeam_channel::unbounded();
+
+    let thread_pool = AsyncComputeTaskPool::get();
+    thread_pool
+        .spawn(async move {
+            let simulated_time = timestep * substep_count;
+
+            info!(
+                "Let's spawn a simulation task for time: {:?}",
+                simulated_time
+            );
+            profiling::scope!("Task ongoing");
+            // Simulate an expensive task
+
+            let to_simulate = simulated_time.as_millis() as u64;
+            std::thread::sleep(Duration::from_millis(thread_rng().gen_range(100..101)));
+
+            // Move entities in a fixed amount of time. The movement should appear smooth for interpolated entities.
+            flip_movement_direction(
+                transforms_to_move
+                    .iter_mut()
+                    .map(|(_, transform, lin_vel, _)| (transform, lin_vel))
+                    .collect::<Vec<_>>()
+                    .iter_mut(),
+            );
+            movement(
+                transforms_to_move
+                    .iter_mut()
+                    .map(|(_, transform, lin_vel, _)| (transform, lin_vel))
+                    .collect::<Vec<_>>()
+                    .iter_mut(),
+                simulated_time,
+            );
+            rotate(
+                transforms_to_move
+                    .iter_mut()
+                    .map(|(_, transform, _, ang_vel)| (transform, ang_vel))
+                    .collect::<Vec<_>>()
+                    .iter_mut(),
+                simulated_time,
+            );
+            let mut result = TaskResultRaw::default();
+            result.transforms = transforms_to_move;
+            result.simulated_time = simulated_time;
+            let _ = sender.send(result);
+        })
+        .detach();
+
+    commands.entity(entity_ctx).insert(WorkTask {
+        recv,
+        started_at_render_time: virtual_time.elapsed(),
+        update_frames_elapsed: 0,
+    });
+}
+
+/// This system queries for `Task<RapierSimulation>` component. It polls the
+/// task, if it has finished, it removes the [`WorkTask`] component from the entity,
+/// and adds a [`TaskResult`] component.
+///
+/// This expects only 1 task at a time.
+pub(crate) fn finish_task_and_store_result(
+    mut commands: Commands,
+    time: Res<Time<Virtual>>,
+    mut q_tasks: Query<(Entity, &mut WorkTask, &mut TaskResults)>,
+) {
+    let Ok((e, mut task, mut results)) = q_tasks.get_single_mut() else {
+        return;
+    };
+    task.update_frames_elapsed += 1;
+
+    let mut handle_result = |task_result: TaskResultRaw| {
+        commands.entity(e).remove::<WorkTask>();
+        results.results.push_back(TaskResult {
+            result: task_result,
+            render_time_elapsed_during_the_simulation: dbg!(time.elapsed())
+                - dbg!(task.started_at_render_time),
+            started_at_render_time: task.started_at_render_time,
+            update_frames_elapsed: task.update_frames_elapsed,
+        });
+        info!("Task finished!");
+    };
+    // TODO: configure this somehow.
+    if task.update_frames_elapsed > 60 {
+        // Do not tolerate more delay over the rendering: block on the result of the simulation.
+        if let Some(result) = task.recv.recv().ok() {
+            handle_result(result);
+        }
+    } else {
+        if let Some(result) = task.recv.try_recv().ok() {
+            handle_result(result);
+        }
+    }
+}
+
+pub(crate) fn handle_task(
+    mut commands: Commands,
+    mut task_results: Query<(Entity, &mut TaskResults, &mut TaskToRenderTime)>,
+    mut q_transforms: Query<(&mut RealTransform, &mut LinearVelocity)>,
+) {
+    for (e, mut results, mut task_to_render) in task_results.iter_mut() {
+        let Some(task) = results.results.pop_front() else {
+            continue;
+        };
+        commands.entity(e).insert(LastTaskTimings {
+            render_time_elapsed_during_the_simulation: task
+                .render_time_elapsed_during_the_simulation,
+            started_at_render_time: task.started_at_render_time,
+        });
+        // Apply transform changes.
+        info!(
+            "handle_task: simulated_time: {:?}",
+            task.result.simulated_time
+        );
+        for (entity, new_transform, new_lin_vel, _) in task.result.transforms.iter() {
+            if let Ok((mut transform, mut lin_vel)) = q_transforms.get_mut(*entity) {
+                transform.0 = *new_transform;
+                *lin_vel = new_lin_vel.clone();
+            }
+        }
+        //let diff_this_frame = dbg!(task.render_time_elapsed_during_the_simulation.as_secs_f64())
+        //    - dbg!(task.result.simulated_time.as_secs_f64());
+        //task_to_render.diff += dbg!(diff_this_frame);
+        //task_to_render.diff += dbg!(diff_this_frame);
+        task_to_render.last_task_frame_count = task.update_frames_elapsed;
+    }
+}
diff --git a/examples/interpolation.rs b/examples/interpolation.rs
index b99e672..4276cf8 100644
--- a/examples/interpolation.rs
+++ b/examples/interpolation.rs
@@ -45,14 +45,6 @@ fn main() {
     app.run();
 }
 
-/// The linear velocity of an entity indicating its movement speed and direction.
-#[derive(Component, Deref, DerefMut)]
-struct LinearVelocity(Vec2);
-
-/// The angular velocity of an entity indicating its rotation speed.
-#[derive(Component, Deref, DerefMut)]
-struct AngularVelocity(f32);
-
 fn setup(
     mut commands: Commands,
     mut materials: ResMut<Assets<ColorMaterial>>,
diff --git a/src/background_fixed_schedule.rs b/src/background_fixed_schedule.rs
new file mode 100644
index 0000000..4eb9209
--- /dev/null
+++ b/src/background_fixed_schedule.rs
@@ -0,0 +1,423 @@
+use bevy::ecs::schedule::{LogLevel, ScheduleBuildSettings, ScheduleLabel};
+use bevy::prelude::*;
+use bevy::tasks::AsyncComputeTaskPool;
+use bevy::{log::trace, prelude::World, time::Time};
+use crossbeam_channel::Receiver;
+use rand::{thread_rng, Rng};
+use std::slice::IterMut;
+use std::{collections::VecDeque, time::Duration};
+
+/// The linear velocity of an entity indicating its movement speed and direction.
+#[derive(Component, Debug, Clone, Deref, DerefMut)]
+pub struct LinearVelocity(pub Vec2);
+
+/// The angular velocity of an entity indicating its rotation speed.
+#[derive(Component, Debug, Clone, Deref, DerefMut)]
+pub struct AngularVelocity(pub f32);
+
+#[derive(Component, Debug, Clone)]
+pub struct ToMove;
+
+/// Flips the movement directions of objects when they reach the left or right side of the screen.
+fn flip_movement_direction(query: IterMut<(&mut Transform, &mut LinearVelocity)>) {
+    for (transform, lin_vel) in query {
+        if transform.translation.x > 500.0 && lin_vel.0.x > 0.0 {
+            lin_vel.0 = Vec2::new(-lin_vel.x.abs(), 0.0);
+        } else if transform.translation.x < -500.0 && lin_vel.0.x < 0.0 {
+            lin_vel.0 = Vec2::new(lin_vel.x.abs(), 0.0);
+        }
+    }
+}
+
+/// Moves entities based on their `LinearVelocity`.
+fn movement(query: IterMut<(&mut Transform, &mut LinearVelocity)>, delta: Duration) {
+    let delta_secs = delta.as_secs_f32();
+    for (transform, lin_vel) in query {
+        transform.translation += lin_vel.extend(0.0) * delta_secs;
+    }
+}
+
+/// Rotates entities based on their `AngularVelocity`.
+fn rotate(query: IterMut<(&mut Transform, &mut AngularVelocity)>, delta: Duration) {
+    let delta_secs = delta.as_secs_f32();
+    for (transform, ang_vel) in query {
+        transform.rotate_local_z(ang_vel.0 * delta_secs);
+    }
+}
+
+///
+/// The task inside this component is polled by the system [`handle_tasks`].
+///
+/// Any changes to [`Transform`]s being modified by the task will be overridden when the task finishes.
+///
+/// This component is removed when the task is done
+#[derive(Component, Debug)]
+pub struct WorkTask {
+    /// The time in seconds at which we started the simulation, as reported by the used render time [`Time::elapsed`].
+    pub started_at_render_time: Duration,
+    /// Amount of frames elapsed since the simulation started.
+    pub update_frames_elapsed: u32,
+    /// The channel end to receive the simulation result.
+    pub recv: Receiver<TaskResultRaw>,
+}
+
+/// The result of a task to be handled.
+#[derive(Debug, Default)]
+pub struct TaskResultRaw {
+    pub transforms: Vec<(Entity, Transform, LinearVelocity, AngularVelocity)>,
+    /// The duration in seconds **simulated** by the simulation.
+    ///
+    /// This is different from the real time it took to simulate the physics.
+    ///
+    /// It is needed to synchronize the simulation with the render time.
+    pub simulated_time: Duration,
+}
+
+/// The result of a task to be handled.
+#[derive(Debug, Default)]
+pub struct TaskResult {
+    pub result: TaskResultRaw,
+    pub render_time_elapsed_during_the_simulation: Duration,
+    /// The time at which we started the simulation, as reported by the used render time [`Time::elapsed`].
+    pub started_at_render_time: Duration,
+    /// Amount of frames elapsed since the simulation started.
+    pub update_frames_elapsed: u32,
+}
+
+/// The result of a task to be handled.
+#[derive(Debug, Default, Component)]
+pub struct TaskResults {
+    /// The results of the tasks.
+    ///
+    /// This is a queue because we might be spawning a new task while another has not been processed yet.
+    ///
+    /// To avoid overwriting the results, we keep them in a queue.
+    pub results: VecDeque<TaskResult>,
+}
+
+pub struct BackgroundFixedUpdatePlugin;
+
+impl Plugin for BackgroundFixedUpdatePlugin {
+    fn build(&self, app: &mut App) {
+        app.add_systems(
+            bevy::app::prelude::RunFixedMainLoop, // TODO: use a specific schedule for this, à la bevy's FixedMainLoop
+            FixedMain::run_schedule,
+        );
+
+        // this handles checking for task completion, firing writeback schedules and spawning a new task.
+        app.edit_schedule(FixedMain, |schedule| {
+            schedule
+                .add_systems(HandleTask::run_schedule)
+                .set_build_settings(ScheduleBuildSettings {
+                    ambiguity_detection: LogLevel::Error,
+                    ..default()
+                });
+        });
+
+        // those schedules are part of FixedMain
+        app.init_schedule(PreWriteBack);
+        app.edit_schedule(WriteBack, |schedule| {
+            schedule
+                .add_systems(handle_task)
+                .set_build_settings(ScheduleBuildSettings {
+                    ambiguity_detection: LogLevel::Error,
+                    ..default()
+                });
+        });
+        app.edit_schedule(SpawnTask, |schedule| {
+            schedule
+                .add_systems(spawn_task)
+                .set_build_settings(ScheduleBuildSettings {
+                    ambiguity_detection: LogLevel::Error,
+                    ..default()
+                });
+        });
+        app.edit_schedule(PostWriteBack, |schedule| {
+            schedule.set_build_settings(ScheduleBuildSettings {
+                ambiguity_detection: LogLevel::Error,
+                ..default()
+            });
+        });
+    }
+}
+
+/// Difference between tasks and rendering time
+#[derive(Component, Default, Reflect, Clone)]
+pub struct TaskToRenderTime {
+    /// Difference in seconds between tasks and rendering time.
+    ///
+    /// We don't use [`Duration`] because it can be negative.
+    pub diff: f64,
+    /// Amount of rendering frames last task took.
+    pub last_task_frame_count: u32,
+}
+
+/// Difference between tasks and rendering time
+#[derive(Component, Default, Reflect, Clone)]
+pub struct Timestep {
+    pub timestep: Duration,
+}
+
+#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub enum FixedMainLoop {
+    Before,
+    During,
+    After,
+}
+
+/// Executes before the task result is propagated to the ECS.
+#[derive(ScheduleLabel, Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct PreWriteBack;
+
+/// Propagates the task result to the ECS.
+#[derive(ScheduleLabel, Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct WriteBack;
+
+/// Spawn a new background task.
+#[derive(ScheduleLabel, Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct SpawnTask;
+
+/// Called after the propagation of the task result to the ECS.
+#[derive(ScheduleLabel, Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct PostWriteBack;
+
+/// Schedule running [`PreWriteBack`], [`WriteBack`] and [`PostWriteBack`]
+/// only if it received its data from the [`WorkTask`] present in the single Entity containing it.
+///
+/// This Schedule overrides [`Res<Time>`][Time] to be the task's time ([`Time<Fixed<MyTaskTime>>`]).
+///
+/// It's also responsible for spawning a new [`WorkTask`].
+///
+/// This Schedule does not support multiple Entities with the same `Task` component.
+// TODO: Schedule as entities might be able to support multiple entities?
+///
+/// This works similarly to [`bevy's FixedMain`][bevy::app::FixedMain],
+/// but it is not blocked by the render loop.
+#[derive(Debug, Hash, PartialEq, Eq, Clone, ScheduleLabel)]
+pub struct FixedMain;
+
+impl FixedMain {
+    /// A system that runs the [`SingleTaskSchedule`] if the task was done.
+    pub fn run_schedule(world: &mut World, mut has_run_at_least_once: Local<bool>) {
+        if !*has_run_at_least_once {
+            world.run_system_cached(spawn_task);
+            *has_run_at_least_once = true;
+            return;
+        }
+        world
+            .run_system_cached(finish_task_and_store_result)
+            .unwrap();
+
+        // Compute difference between task and render time.
+        let clock = world.resource::<Time>().as_generic();
+        let mut query = world.query::<(&mut TaskToRenderTime, &Timestep)>();
+        let (mut task_to_render_time, timestep) = query.single_mut(world);
+        task_to_render_time.diff += clock.delta().as_secs_f64();
+        if task_to_render_time.diff < timestep.timestep.as_secs_f64() {
+            // Task is too far ahead, we should not read the simulation.
+            //world.run_system_cached(spawn_task);
+            info!("Task is too far ahead, we should not read the simulation.");
+            return;
+        }
+        let simulated_time = {
+            let mut query = world.query::<&TaskResults>();
+            let task_result = query.single(world).results.front();
+            task_result.map(|task_result| task_result.result.simulated_time)
+        };
+        let Some(simulated_time) = simulated_time else {
+            //world.run_system_cached(spawn_task);
+            info!("No task result found.");
+            return;
+        };
+        let mut query = world.query::<&mut TaskToRenderTime>();
+        let mut task_to_render_time = query.single_mut(world);
+        task_to_render_time.diff -= simulated_time.as_secs_f64();
+        let _ = world.try_schedule_scope(FixedMain, |world, schedule| {
+            // Advance simulation.
+            info!("Running FixedMain schedule");
+            schedule.run(world);
+
+            // If physics is paused, reset delta time to stop simulation
+            // unless users manually advance `Time<Physics>`.
+            /*if is_paused {
+                world
+                    .resource_mut::<Time<Physics>>()
+                    .advance_by(Duration::ZERO);
+            }
+            */
+        });
+    }
+}
+
+/// Schedule handling a single task.
+#[derive(Debug, Hash, PartialEq, Eq, Clone, ScheduleLabel)]
+pub struct HandleTask;
+
+impl HandleTask {
+    pub fn run_schedule(world: &mut World) {
+        let _ = world.try_schedule_scope(PreWriteBack, |world, schedule| {
+            schedule.run(world);
+        });
+        let _ = world.try_schedule_scope(WriteBack, |world, schedule| {
+            schedule.run(world);
+        });
+        let _ = world.try_schedule_scope(SpawnTask, |world, schedule| {
+            schedule.run(world);
+        });
+        let _ = world.try_schedule_scope(PostWriteBack, |world, schedule| {
+            schedule.run(world);
+        });
+    }
+}
+
+/// This system spawns a [`WorkTask`] is none are ongoing.
+/// The task simulate computationally intensive work that potentially spans multiple frames/ticks.
+///
+/// A separate system, [`handle_tasks`], will poll the spawned tasks on subsequent
+/// frames/ticks, and use the results to spawn cubes
+pub fn spawn_task(
+    mut commands: Commands,
+    q_context: Query<(Entity, &TaskToRenderTime, &Timestep, Has<WorkTask>)>,
+    q_transforms: Query<(Entity, &Transform, &LinearVelocity, &AngularVelocity), With<ToMove>>,
+    virtual_time: Res<Time<Virtual>>,
+) {
+    let Ok((entity_ctx, task_to_render_time, timestep, has_work)) = q_context.get_single() else {
+        info!("No correct entity found.");
+        return;
+    };
+    if has_work {
+        info!("A task is ongoing.");
+        return;
+    }
+    let timestep = timestep.timestep;
+
+    // TODO: tweak this on user side, to allow the simulation to catch up with the render time.
+    let mut substep_count = 1;
+
+    let mut transforms_to_move: Vec<(Entity, Transform, LinearVelocity, AngularVelocity)> =
+        q_transforms
+            .iter()
+            .map(|(entity, transform, lin_vel, ang_vel)| {
+                (entity, transform.clone(), lin_vel.clone(), ang_vel.clone())
+            })
+            .collect();
+    let (sender, recv) = crossbeam_channel::unbounded();
+
+    let thread_pool = AsyncComputeTaskPool::get();
+    thread_pool
+        .spawn(async move {
+            let simulated_time = timestep * substep_count;
+
+            info!(
+                "Let's spawn a simulation task for time: {:?}",
+                simulated_time
+            );
+            profiling::scope!("Rapier physics simulation");
+            // Simulate an expensive task
+
+            let to_simulate = simulated_time.as_millis() as u64;
+            std::thread::sleep(Duration::from_millis(thread_rng().gen_range(200..201)));
+
+            // Move entities in a fixed amount of time. The movement should appear smooth for interpolated entities.
+            flip_movement_direction(
+                transforms_to_move
+                    .iter_mut()
+                    .map(|(_, transform, lin_vel, _)| (transform, lin_vel))
+                    .collect::<Vec<_>>()
+                    .iter_mut(),
+            );
+            movement(
+                transforms_to_move
+                    .iter_mut()
+                    .map(|(_, transform, lin_vel, _)| (transform, lin_vel))
+                    .collect::<Vec<_>>()
+                    .iter_mut(),
+                simulated_time,
+            );
+            rotate(
+                transforms_to_move
+                    .iter_mut()
+                    .map(|(_, transform, _, ang_vel)| (transform, ang_vel))
+                    .collect::<Vec<_>>()
+                    .iter_mut(),
+                simulated_time,
+            );
+            let mut result = TaskResultRaw::default();
+            result.transforms = transforms_to_move;
+            result.simulated_time = simulated_time;
+            let _ = sender.send(result);
+        })
+        .detach();
+
+    commands.entity(entity_ctx).insert(WorkTask {
+        recv,
+        started_at_render_time: virtual_time.elapsed(),
+        update_frames_elapsed: 0,
+    });
+}
+
+/// This system queries for `Task<RapierSimulation>` component. It polls the
+/// task, if it has finished, it removes the [`WorkTask`] component from the entity,
+/// and adds a [`TaskResult`] component.
+///
+/// This expects only 1 task at a time.
+pub(crate) fn finish_task_and_store_result(
+    mut commands: Commands,
+    time: Res<Time<Virtual>>,
+    mut q_tasks: Query<(Entity, &mut WorkTask, &mut TaskResults)>,
+) {
+    let Ok((e, mut task, mut results)) = q_tasks.get_single_mut() else {
+        return;
+    };
+    task.update_frames_elapsed += 1;
+
+    let mut handle_result = |task_result: TaskResultRaw| {
+        commands.entity(e).remove::<WorkTask>();
+        results.results.push_back(TaskResult {
+            result: task_result,
+            render_time_elapsed_during_the_simulation: dbg!(time.elapsed())
+                - dbg!(task.started_at_render_time),
+            started_at_render_time: task.started_at_render_time,
+            update_frames_elapsed: task.update_frames_elapsed,
+        });
+        info!("Task finished!");
+    };
+    // TODO: configure this somehow.
+    if task.update_frames_elapsed > 60 {
+        // Do not tolerate more delay over the rendering: block on the result of the simulation.
+        if let Some(result) = task.recv.recv().ok() {
+            handle_result(result);
+        }
+    } else {
+        if let Some(result) = task.recv.try_recv().ok() {
+            handle_result(result);
+        }
+    }
+}
+
+pub(crate) fn handle_task(
+    mut task_results: Query<(&mut TaskResults, &mut TaskToRenderTime)>,
+    mut q_transforms: Query<(&mut Transform, &mut LinearVelocity)>,
+) {
+    for (mut results, mut task_to_render) in task_results.iter_mut() {
+        let Some(task) = results.results.pop_front() else {
+            continue;
+        };
+        // Apply transform changes.
+        info!(
+            "handle_task: simulated_time: {:?}",
+            task.result.simulated_time
+        );
+        for (entity, new_transform, new_lin_vel, _) in task.result.transforms.iter() {
+            if let Ok((mut transform, mut lin_vel)) = q_transforms.get_mut(*entity) {
+                *transform = *new_transform;
+                *lin_vel = new_lin_vel.clone();
+            }
+        }
+        //let diff_this_frame = dbg!(task.render_time_elapsed_during_the_simulation.as_secs_f64())
+        //    - dbg!(task.result.simulated_time.as_secs_f64());
+        //task_to_render.diff += dbg!(diff_this_frame);
+        //task_to_render.diff += dbg!(diff_this_frame);
+        task_to_render.last_task_frame_count = task.update_frames_elapsed;
+    }
+}
diff --git a/src/interpolation.rs b/src/interpolation.rs
index 5691296..fda46ad 100644
--- a/src/interpolation.rs
+++ b/src/interpolation.rs
@@ -5,7 +5,11 @@
 #![allow(clippy::type_complexity)]
 
 use crate::*;
-use bevy::prelude::*;
+use bevy::{
+    app::FixedMain,
+    ecs::schedule::{InternedScheduleLabel, ScheduleLabel},
+    prelude::*,
+};
 
 /// A plugin for [`Transform`] interpolation, making movement in [`FixedUpdate`] appear smooth.
 ///
@@ -104,7 +108,7 @@ use bevy::prelude::*;
 /// Because good extrapolation requires velocity, it is currently not a built-in feature for `bevy_transform_interpolation`.
 /// However, it is relatively straightforward to implement your own extrapolation system on top of the [`TransformEasingPlugin`].
 /// An example of this can be found in `examples/extrapolation.rs`.
-#[derive(Debug, Default)]
+#[derive(Debug)]
 pub struct TransformInterpolationPlugin {
     /// If `true`, translation will be interpolated for all entities with the [`Transform`] component by default.
     ///
@@ -118,6 +122,21 @@ pub struct TransformInterpolationPlugin {
     ///
     /// This can be overridden for individual entities by adding the [`NoScaleInterpolation`] or [`NoTransformInterpolation`] component.
     pub interpolate_scale_all: bool,
+
+    pub schedule_fixed_first: InternedScheduleLabel,
+    pub schedule_fixed_last: InternedScheduleLabel,
+}
+
+impl Default for TransformInterpolationPlugin {
+    fn default() -> Self {
+        Self {
+            interpolate_translation_all: false,
+            interpolate_rotation_all: false,
+            interpolate_scale_all: false,
+            schedule_fixed_first: FixedFirst.intern(),
+            schedule_fixed_last: FixedLast.intern(),
+        }
+    }
 }
 
 impl TransformInterpolationPlugin {
@@ -125,11 +144,12 @@ impl TransformInterpolationPlugin {
     ///
     /// This can be overridden for individual entities by adding the [`NoTransformInterpolation`] component,
     /// or the individual [`NoTranslationInterpolation`], [`NoRotationInterpolation`], and [`NoScaleInterpolation`] components.
-    pub const fn interpolate_all() -> Self {
+    pub fn interpolate_all() -> Self {
         Self {
             interpolate_translation_all: true,
             interpolate_rotation_all: true,
             interpolate_scale_all: true,
+            ..Default::default()
         }
     }
 }
@@ -147,7 +167,7 @@ impl Plugin for TransformInterpolationPlugin {
         )>();
 
         app.add_systems(
-            FixedFirst,
+            self.schedule_fixed_first,
             (
                 complete_translation_easing,
                 complete_rotation_easing,
@@ -156,10 +176,9 @@ impl Plugin for TransformInterpolationPlugin {
                 .chain()
                 .before(TransformEasingSet::Reset),
         );
-
         // Update the start state of the interpolation at the start of the fixed timestep.
         app.add_systems(
-            FixedFirst,
+            self.schedule_fixed_first,
             (
                 update_translation_interpolation_start,
                 update_rotation_interpolation_start,
@@ -171,7 +190,7 @@ impl Plugin for TransformInterpolationPlugin {
 
         // Update the end state of the interpolation at the end of the fixed timestep.
         app.add_systems(
-            FixedLast,
+            self.schedule_fixed_last,
             (
                 update_translation_interpolation_end,
                 update_rotation_interpolation_end,
@@ -197,7 +216,7 @@ impl Plugin for TransformInterpolationPlugin {
     fn finish(&self, app: &mut App) {
         // Add the `TransformEasingPlugin` if it hasn't been added yet.
         if !app.is_plugin_added::<TransformEasingPlugin>() {
-            app.add_plugins(TransformEasingPlugin);
+            app.add_plugins(TransformEasingPlugin::default());
         }
     }
 }
@@ -360,6 +379,8 @@ fn update_translation_interpolation_end(
 ) {
     for (transform, mut easing) in &mut query {
         easing.end = Some(transform.translation);
+        info!("update_translation_interpolation_end");
+        info!("{easing:?}");
     }
 }
 
diff --git a/src/lib.rs b/src/lib.rs
index 434d6d6..40511a9 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -116,6 +116,7 @@
 
 #![allow(clippy::needless_doctest_main)]
 
+pub mod background_fixed_schedule;
 pub mod interpolation;
 
 /// The prelude.
@@ -133,7 +134,11 @@ pub mod prelude {
 use interpolation::*;
 
 use bevy::{
-    ecs::{component::Tick, system::SystemChangeTick},
+    ecs::{
+        component::Tick,
+        schedule::{InternedScheduleLabel, InternedSystemSet, ScheduleLabel},
+        system::SystemChangeTick,
+    },
     prelude::*,
 };
 
@@ -145,8 +150,27 @@ use bevy::{
 ///
 /// To actually perform automatic easing, an easing backend that updates the `start` and `end` states must be used.
 /// The [`TransformInterpolationPlugin`] is provided for transform interpolation, but custom backends can also be implemented.
-#[derive(Debug, Default)]
-pub struct TransformEasingPlugin;
+#[derive(Debug)]
+pub struct TransformEasingPlugin {
+    pub schedule_fixed_first: InternedScheduleLabel,
+    pub schedule_fixed_last: InternedScheduleLabel,
+    pub schedule_fixed_loop: InternedScheduleLabel,
+    pub after_fixed_main_loop: InternedSystemSet,
+    /// If set to `true`, the plugin adds systems to update the easing values in [`Ease`].
+    pub update_easing_values: bool,
+}
+
+impl Default for TransformEasingPlugin {
+    fn default() -> Self {
+        Self {
+            schedule_fixed_first: FixedFirst.intern(),
+            schedule_fixed_last: FixedLast.intern(),
+            schedule_fixed_loop: RunFixedMainLoop.intern(),
+            after_fixed_main_loop: RunFixedMainLoopSystem::AfterFixedMainLoop.intern(),
+            update_easing_values: true,
+        }
+    }
+}
 
 impl Plugin for TransformEasingPlugin {
     fn build(&self, app: &mut App) {
@@ -161,27 +185,27 @@ impl Plugin for TransformEasingPlugin {
 
         // Reset easing states and update start values at the start of the fixed timestep.
         app.configure_sets(
-            FixedFirst,
+            self.schedule_fixed_first,
             (TransformEasingSet::Reset, TransformEasingSet::UpdateStart).chain(),
         );
 
         // Update end values at the end of the fixed timestep.
-        app.configure_sets(FixedLast, TransformEasingSet::UpdateEnd);
+        app.configure_sets(self.schedule_fixed_last, TransformEasingSet::UpdateEnd);
 
         // Perform transform easing right after the fixed timestep, before `Update`.
         app.configure_sets(
-            RunFixedMainLoop,
+            self.schedule_fixed_loop,
             (
                 TransformEasingSet::Ease,
                 TransformEasingSet::UpdateEasingTick,
             )
                 .chain()
-                .in_set(RunFixedMainLoopSystem::AfterFixedMainLoop),
+                .in_set(self.after_fixed_main_loop),
         );
 
         // Reset easing states.
         app.add_systems(
-            FixedFirst,
+            self.schedule_fixed_first,
             (
                 reset_translation_easing,
                 reset_rotation_easing,
@@ -192,20 +216,22 @@ impl Plugin for TransformEasingPlugin {
         );
 
         app.add_systems(
-            RunFixedMainLoop,
+            self.schedule_fixed_loop,
             reset_easing_states_on_transform_change.before(TransformEasingSet::Ease),
         );
 
-        // Perform easing.
-        app.add_systems(
-            RunFixedMainLoop,
-            (ease_translation_lerp, ease_rotation_slerp, ease_scale_lerp)
-                .in_set(TransformEasingSet::Ease),
-        );
+        if self.update_easing_values {
+            // Perform easing.
+            app.add_systems(
+                self.schedule_fixed_loop,
+                (ease_translation_lerp, ease_rotation_slerp, ease_scale_lerp)
+                    .in_set(TransformEasingSet::Ease),
+            );
+        }
 
         // Update the last easing tick.
         app.add_systems(
-            RunFixedMainLoop,
+            self.schedule_fixed_loop,
             update_last_easing_tick.in_set(TransformEasingSet::UpdateEasingTick),
         );
     }
@@ -346,6 +372,7 @@ pub fn reset_easing_states_on_transform_change(
 /// Resets the `start` and `end` states for translation interpolation.
 fn reset_translation_easing(mut query: Query<&mut TranslationEasingState>) {
     for mut easing in &mut query {
+        info!("reset_translation_easing");
         easing.start = None;
         easing.end = None;
     }
@@ -373,10 +400,11 @@ fn ease_translation_lerp(
     time: Res<Time<Fixed>>,
 ) {
     let overstep = time.overstep_fraction();
-
+    info!("ease_translation_lerp; overstep: {:?}", overstep);
     query.iter_mut().for_each(|(mut transform, interpolation)| {
         if let (Some(start), Some(end)) = (interpolation.start, interpolation.end) {
-            transform.translation = start.lerp(end, overstep);
+            info!("{:?} - {:?}", start, end);
+            transform.translation = start.lerp(end, overstep.min(1.0));
         }
     });
 }

From e6c73ab46d9c28bb24c949b1e803636ef858784b Mon Sep 17 00:00:00 2001
From: Thierry Berger <contact@thierryberger.com>
Date: Tue, 24 Dec 2024 16:03:26 +0100
Subject: [PATCH 2/8] cleaner implementation with user space task

---
 Cargo.toml                              |   1 +
 examples/interpolate_custom_schedule.rs | 147 ++++++++++++++-
 src/background_fixed_schedule.rs        | 234 +++++++++++-------------
 3 files changed, 249 insertions(+), 133 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index f3342c2..eee261c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,7 @@ bevy = { version = "0.15", default-features = false }
 crossbeam-channel = "0.5"
 profiling = "1.0"
 rand = "0.8"
+dyn-clone = "1.0"
 
 [dev-dependencies]
 bevy = { version = "0.15", default-features = false, features = [
diff --git a/examples/interpolate_custom_schedule.rs b/examples/interpolate_custom_schedule.rs
index d50f20f..7b3cc5f 100644
--- a/examples/interpolate_custom_schedule.rs
+++ b/examples/interpolate_custom_schedule.rs
@@ -20,12 +20,13 @@ use bevy::{
 };
 use bevy_transform_interpolation::{
     background_fixed_schedule::{
-        AngularVelocity, BackgroundFixedUpdatePlugin, LinearVelocity, PostWriteBack, PreWriteBack,
-        TaskResults, TaskToRenderTime, Timestep, ToMove,
+        BackgroundFixedUpdatePlugin, PostWriteBack, PreWriteBack, TaskResults, TaskToRenderTime,
+        TaskWorker, Timestep,
     },
     prelude::*,
     RotationEasingState, ScaleEasingState, TransformEasingSet, TranslationEasingState,
 };
+use task_user::{AngularVelocity, LinearVelocity, TaskWorkerTraitImpl, ToMove};
 
 use std::time::Duration;
 
@@ -53,7 +54,7 @@ fn main() {
     // Add the `TransformInterpolationPlugin` to the app to enable transform interpolation.
     app.add_plugins((
         DefaultPlugins,
-        BackgroundFixedUpdatePlugin,
+        BackgroundFixedUpdatePlugin::<task_user::TaskWorkerTraitImpl>::default(),
         easing_plugin,
         interpolation_plugin,
     ));
@@ -150,7 +151,10 @@ fn setup(
         Timestep {
             timestep: Duration::from_secs_f32(0.5),
         },
-        TaskResults::default(),
+        TaskResults::<TaskWorkerTraitImpl>::default(),
+        TaskWorker {
+            worker: TaskWorkerTraitImpl {},
+        },
     ));
 
     // This entity uses transform interpolation.
@@ -283,3 +287,138 @@ fn update_diff_to_render_text(
 ) {
     text.0 = format!("{:.2}", task_to_render.diff);
 }
+
+pub mod task_user {
+    use std::{slice::IterMut, time::Duration};
+
+    use bevy::prelude::*;
+    use bevy_transform_interpolation::background_fixed_schedule::TaskWorkerTrait;
+    use rand::{thread_rng, Rng};
+
+    #[derive(Debug, Clone, Default)]
+    pub struct TaskWorkerTraitImpl;
+
+    impl TaskWorkerTrait for TaskWorkerTraitImpl {
+        type TaskExtractedData = TaskExtractedData;
+        type TaskResultPure = Vec<(Entity, Transform, LinearVelocity, AngularVelocity)>;
+
+        fn work(
+            &self,
+            mut input: TaskExtractedData,
+            timestep: Duration,
+            substep_count: u32,
+        ) -> Vec<(Entity, Transform, LinearVelocity, AngularVelocity)> {
+            let simulated_time = timestep * substep_count;
+            let to_simulate = simulated_time.as_millis() as u64;
+            // Simulate an expensive task
+            std::thread::sleep(Duration::from_millis(thread_rng().gen_range(200..201)));
+
+            // Move entities in a fixed amount of time. The movement should appear smooth for interpolated entities.
+            flip_movement_direction(
+                input
+                    .data
+                    .iter_mut()
+                    .map(|(_, transform, lin_vel, _)| (transform, lin_vel))
+                    .collect::<Vec<_>>()
+                    .iter_mut(),
+            );
+            movement(
+                input
+                    .data
+                    .iter_mut()
+                    .map(|(_, transform, lin_vel, _)| (transform, lin_vel))
+                    .collect::<Vec<_>>()
+                    .iter_mut(),
+                simulated_time,
+            );
+            rotate(
+                input
+                    .data
+                    .iter_mut()
+                    .map(|(_, transform, _, ang_vel)| (transform, ang_vel))
+                    .collect::<Vec<_>>()
+                    .iter_mut(),
+                simulated_time,
+            );
+            input.data
+        }
+
+        fn extract(&self, world: &mut World) -> TaskExtractedData {
+            // TODO: use a system rather than a world.
+            let mut query = world.query_filtered::<
+                            (Entity, &Transform, &LinearVelocity, &AngularVelocity),
+                            With<ToMove>,
+                        >();
+
+            let transforms_to_move: Vec<(Entity, Transform, LinearVelocity, AngularVelocity)> =
+                query
+                    .iter(world)
+                    .map(|(entity, transform, lin_vel, ang_vel)| {
+                        (entity, transform.clone(), lin_vel.clone(), ang_vel.clone())
+                    })
+                    .collect();
+            TaskExtractedData {
+                data: transforms_to_move,
+            }
+        }
+
+        fn write_back(
+            &self,
+            result: bevy_transform_interpolation::background_fixed_schedule::TaskResult<Self>,
+            mut world: &mut World,
+        ) {
+            let mut q_transforms =
+                world.query_filtered::<(&mut Transform, &mut LinearVelocity), With<ToMove>>();
+            for (entity, new_transform, new_lin_vel, _) in result.result_raw.transforms.iter() {
+                if let Ok((mut transform, mut lin_vel)) = q_transforms.get_mut(&mut world, *entity)
+                {
+                    *transform = *new_transform;
+                    *lin_vel = new_lin_vel.clone();
+                }
+            }
+        }
+    }
+
+    #[derive(Debug, Component, Clone)]
+    pub struct TaskExtractedData {
+        pub data: Vec<(Entity, Transform, LinearVelocity, AngularVelocity)>,
+    }
+
+    /// The linear velocity of an entity indicating its movement speed and direction.
+    #[derive(Component, Debug, Clone, Deref, DerefMut)]
+    pub struct LinearVelocity(pub Vec2);
+
+    /// The angular velocity of an entity indicating its rotation speed.
+    #[derive(Component, Debug, Clone, Deref, DerefMut)]
+    pub struct AngularVelocity(pub f32);
+
+    #[derive(Component, Debug, Clone)]
+    pub struct ToMove;
+
+    /// Flips the movement directions of objects when they reach the left or right side of the screen.
+    fn flip_movement_direction(query: IterMut<(&mut Transform, &mut LinearVelocity)>) {
+        for (transform, lin_vel) in query {
+            if transform.translation.x > 500.0 && lin_vel.0.x > 0.0 {
+                lin_vel.0 = Vec2::new(-lin_vel.x.abs(), 0.0);
+            } else if transform.translation.x < -500.0 && lin_vel.0.x < 0.0 {
+                lin_vel.0 = Vec2::new(lin_vel.x.abs(), 0.0);
+            }
+        }
+    }
+
+    /// Moves entities based on their `LinearVelocity`.
+    fn movement(query: IterMut<(&mut Transform, &mut LinearVelocity)>, delta: Duration) {
+        let delta_secs = delta.as_secs_f32();
+        for (transform, lin_vel) in query {
+            transform.translation += lin_vel.extend(0.0) * delta_secs;
+        }
+    }
+
+    /// Rotates entities based on their `AngularVelocity`.
+    fn rotate(query: IterMut<(&mut Transform, &mut AngularVelocity)>, delta: Duration) {
+        let delta_secs = delta.as_secs_f32();
+        for (transform, ang_vel) in query {
+            transform.rotate_local_z(ang_vel.0 * delta_secs);
+        }
+    }
+}
diff --git a/src/background_fixed_schedule.rs b/src/background_fixed_schedule.rs
index 4eb9209..e47f16c 100644
--- a/src/background_fixed_schedule.rs
+++ b/src/background_fixed_schedule.rs
@@ -1,50 +1,15 @@
 use bevy::ecs::schedule::{LogLevel, ScheduleBuildSettings, ScheduleLabel};
+use bevy::ecs::world;
+use bevy::log::tracing_subscriber::fmt::time;
 use bevy::prelude::*;
 use bevy::tasks::AsyncComputeTaskPool;
 use bevy::{log::trace, prelude::World, time::Time};
 use crossbeam_channel::Receiver;
 use rand::{thread_rng, Rng};
+use std::default;
 use std::slice::IterMut;
 use std::{collections::VecDeque, time::Duration};
 
-/// The linear velocity of an entity indicating its movement speed and direction.
-#[derive(Component, Debug, Clone, Deref, DerefMut)]
-pub struct LinearVelocity(pub Vec2);
-
-/// The angular velocity of an entity indicating its rotation speed.
-#[derive(Component, Debug, Clone, Deref, DerefMut)]
-pub struct AngularVelocity(pub f32);
-
-#[derive(Component, Debug, Clone)]
-pub struct ToMove;
-
-/// Flips the movement directions of objects when they reach the left or right side of the screen.
-fn flip_movement_direction(query: IterMut<(&mut Transform, &mut LinearVelocity)>) {
-    for (transform, lin_vel) in query {
-        if transform.translation.x > 500.0 && lin_vel.0.x > 0.0 {
-            lin_vel.0 = Vec2::new(-lin_vel.x.abs(), 0.0);
-        } else if transform.translation.x < -500.0 && lin_vel.0.x < 0.0 {
-            lin_vel.0 = Vec2::new(lin_vel.x.abs(), 0.0);
-        }
-    }
-}
-
-/// Moves entities based on their `LinearVelocity`.
-fn movement(query: IterMut<(&mut Transform, &mut LinearVelocity)>, delta: Duration) {
-    let delta_secs = delta.as_secs_f32();
-    for (transform, lin_vel) in query {
-        transform.translation += lin_vel.extend(0.0) * delta_secs;
-    }
-}
-
-/// Rotates entities based on their `AngularVelocity`.
-fn rotate(query: IterMut<(&mut Transform, &mut AngularVelocity)>, delta: Duration) {
-    let delta_secs = delta.as_secs_f32();
-    for (transform, ang_vel) in query {
-        transform.rotate_local_z(ang_vel.0 * delta_secs);
-    }
-}
-
 ///
 /// The task inside this component is polled by the system [`handle_tasks`].
 ///
@@ -52,19 +17,19 @@ fn rotate(query: IterMut<(&mut Transform, &mut AngularVelocity)>, delta: Duratio
 ///
 /// This component is removed when the task is done
 #[derive(Component, Debug)]
-pub struct WorkTask {
+pub struct WorkTask<T: TaskWorkerTrait + Send + Sync> {
     /// The time in seconds at which we started the simulation, as reported by the used render time [`Time::elapsed`].
     pub started_at_render_time: Duration,
     /// Amount of frames elapsed since the simulation started.
     pub update_frames_elapsed: u32,
     /// The channel end to receive the simulation result.
-    pub recv: Receiver<TaskResultRaw>,
+    pub recv: Receiver<TaskResultRaw<T>>,
 }
 
 /// The result of a task to be handled.
 #[derive(Debug, Default)]
-pub struct TaskResultRaw {
-    pub transforms: Vec<(Entity, Transform, LinearVelocity, AngularVelocity)>,
+pub struct TaskResultRaw<T: TaskWorkerTrait + Send + Sync> {
+    pub transforms: T::TaskResultPure,
     /// The duration in seconds **simulated** by the simulation.
     ///
     /// This is different from the real time it took to simulate the physics.
@@ -74,9 +39,8 @@ pub struct TaskResultRaw {
 }
 
 /// The result of a task to be handled.
-#[derive(Debug, Default)]
-pub struct TaskResult {
-    pub result: TaskResultRaw,
+pub struct TaskResult<T: TaskWorkerTrait + Send + Sync> {
+    pub result_raw: TaskResultRaw<T>,
     pub render_time_elapsed_during_the_simulation: Duration,
     /// The time at which we started the simulation, as reported by the used render time [`Time::elapsed`].
     pub started_at_render_time: Duration,
@@ -85,23 +49,26 @@ pub struct TaskResult {
 }
 
 /// The result of a task to be handled.
-#[derive(Debug, Default, Component)]
-pub struct TaskResults {
+#[derive(Default, Component)]
+pub struct TaskResults<T: TaskWorkerTrait + Send + Sync> {
     /// The results of the tasks.
     ///
     /// This is a queue because we might be spawning a new task while another has not been processed yet.
     ///
     /// To avoid overwriting the results, we keep them in a queue.
-    pub results: VecDeque<TaskResult>,
+    pub results: VecDeque<TaskResult<T>>,
 }
 
-pub struct BackgroundFixedUpdatePlugin;
+#[derive(Default)]
+pub struct BackgroundFixedUpdatePlugin<T: TaskWorkerTrait> {
+    pub phantom: std::marker::PhantomData<T>,
+}
 
-impl Plugin for BackgroundFixedUpdatePlugin {
+impl<T: TaskWorkerTrait> Plugin for BackgroundFixedUpdatePlugin<T> {
     fn build(&self, app: &mut App) {
         app.add_systems(
             bevy::app::prelude::RunFixedMainLoop, // TODO: use a specific schedule for this, à la bevy's FixedMainLoop
-            FixedMain::run_schedule,
+            FixedMain::run_schedule::<T>,
         );
 
         // this handles checking for task completion, firing writeback schedules and spawning a new task.
@@ -118,7 +85,7 @@ impl Plugin for BackgroundFixedUpdatePlugin {
         app.init_schedule(PreWriteBack);
         app.edit_schedule(WriteBack, |schedule| {
             schedule
-                .add_systems(handle_task)
+                .add_systems(handle_task::<T>)
                 .set_build_settings(ScheduleBuildSettings {
                     ambiguity_detection: LogLevel::Error,
                     ..default()
@@ -126,7 +93,7 @@ impl Plugin for BackgroundFixedUpdatePlugin {
         });
         app.edit_schedule(SpawnTask, |schedule| {
             schedule
-                .add_systems(spawn_task)
+                .add_systems((extract::<T>, spawn_task::<T>).chain())
                 .set_build_settings(ScheduleBuildSettings {
                     ambiguity_detection: LogLevel::Error,
                     ..default()
@@ -158,6 +125,29 @@ pub struct Timestep {
     pub timestep: Duration,
 }
 
+/// Struct to be able to configure what the task should do.
+/// TODO: extract first, then do work.
+#[derive(Clone, Component)]
+pub struct TaskWorker<T: TaskWorkerTrait> {
+    pub worker: T,
+}
+
+pub trait TaskWorkerTrait: Clone + Send + Sync + 'static {
+    type TaskExtractedData: Clone + Send + Sync + 'static + Component;
+    type TaskResultPure: Clone + Send + Sync + 'static;
+
+    fn extract(&self, world: &mut World) -> Self::TaskExtractedData;
+
+    fn work(
+        &self,
+        data: Self::TaskExtractedData,
+        timestep: Duration,
+        substep_count: u32,
+    ) -> Self::TaskResultPure;
+
+    fn write_back(&self, result: TaskResult<Self>, world: &mut World);
+}
+
 #[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
 pub enum FixedMainLoop {
     Before,
@@ -198,14 +188,17 @@ pub struct FixedMain;
 
 impl FixedMain {
     /// A system that runs the [`SingleTaskSchedule`] if the task was done.
-    pub fn run_schedule(world: &mut World, mut has_run_at_least_once: Local<bool>) {
+    pub fn run_schedule<T: TaskWorkerTrait>(
+        world: &mut World,
+        mut has_run_at_least_once: Local<bool>,
+    ) {
         if !*has_run_at_least_once {
-            world.run_system_cached(spawn_task);
+            let _ = world.run_schedule(SpawnTask);
             *has_run_at_least_once = true;
             return;
         }
         world
-            .run_system_cached(finish_task_and_store_result)
+            .run_system_cached(finish_task_and_store_result::<T>)
             .unwrap();
 
         // Compute difference between task and render time.
@@ -215,17 +208,15 @@ impl FixedMain {
         task_to_render_time.diff += clock.delta().as_secs_f64();
         if task_to_render_time.diff < timestep.timestep.as_secs_f64() {
             // Task is too far ahead, we should not read the simulation.
-            //world.run_system_cached(spawn_task);
             info!("Task is too far ahead, we should not read the simulation.");
             return;
         }
         let simulated_time = {
-            let mut query = world.query::<&TaskResults>();
+            let mut query = world.query::<&TaskResults<T>>();
             let task_result = query.single(world).results.front();
-            task_result.map(|task_result| task_result.result.simulated_time)
+            task_result.map(|task_result| task_result.result_raw.simulated_time)
         };
         let Some(simulated_time) = simulated_time else {
-            //world.run_system_cached(spawn_task);
             info!("No task result found.");
             return;
         };
@@ -270,39 +261,52 @@ impl HandleTask {
     }
 }
 
+pub fn extract<T: TaskWorkerTrait>(world: &mut World) {
+    let Ok((entity_ctx, worker)) = world
+        .query_filtered::<(Entity, &TaskWorker<T>), With<Timestep>>()
+        .get_single(&world)
+    else {
+        info!("No correct entity found.");
+        return;
+    };
+
+    let extractor = worker.worker.clone();
+    let extracted_data = extractor.extract(world);
+    world.entity_mut(entity_ctx).insert(extracted_data.clone());
+}
+
 /// This system spawns a [`WorkTask`] is none are ongoing.
 /// The task simulate computationally intensive work that potentially spans multiple frames/ticks.
 ///
 /// A separate system, [`handle_tasks`], will poll the spawned tasks on subsequent
 /// frames/ticks, and use the results to spawn cubes
-pub fn spawn_task(
+pub fn spawn_task<T: TaskWorkerTrait>(
     mut commands: Commands,
-    q_context: Query<(Entity, &TaskToRenderTime, &Timestep, Has<WorkTask>)>,
-    q_transforms: Query<(Entity, &Transform, &LinearVelocity, &AngularVelocity), With<ToMove>>,
+    q_context: Query<(
+        Entity,
+        &TaskToRenderTime,
+        &TaskWorker<T>,
+        &Timestep,
+        &T::TaskExtractedData,
+        Has<WorkTask<T>>,
+    )>,
     virtual_time: Res<Time<Virtual>>,
 ) {
-    let Ok((entity_ctx, task_to_render_time, timestep, has_work)) = q_context.get_single() else {
+    let Ok((entity_ctx, task_to_render_time, worker, timestep, extracted_data, has_work)) =
+        q_context.get_single()
+    else {
         info!("No correct entity found.");
         return;
     };
-    if has_work {
-        info!("A task is ongoing.");
-        return;
-    }
     let timestep = timestep.timestep;
 
     // TODO: tweak this on user side, to allow the simulation to catch up with the render time.
     let mut substep_count = 1;
 
-    let mut transforms_to_move: Vec<(Entity, Transform, LinearVelocity, AngularVelocity)> =
-        q_transforms
-            .iter()
-            .map(|(entity, transform, lin_vel, ang_vel)| {
-                (entity, transform.clone(), lin_vel.clone(), ang_vel.clone())
-            })
-            .collect();
     let (sender, recv) = crossbeam_channel::unbounded();
 
+    let transforms_to_move = extracted_data.clone();
+    let worker = worker.clone();
     let thread_pool = AsyncComputeTaskPool::get();
     thread_pool
         .spawn(async move {
@@ -313,38 +317,14 @@ pub fn spawn_task(
                 simulated_time
             );
             profiling::scope!("Rapier physics simulation");
-            // Simulate an expensive task
-
-            let to_simulate = simulated_time.as_millis() as u64;
-            std::thread::sleep(Duration::from_millis(thread_rng().gen_range(200..201)));
-
-            // Move entities in a fixed amount of time. The movement should appear smooth for interpolated entities.
-            flip_movement_direction(
-                transforms_to_move
-                    .iter_mut()
-                    .map(|(_, transform, lin_vel, _)| (transform, lin_vel))
-                    .collect::<Vec<_>>()
-                    .iter_mut(),
-            );
-            movement(
-                transforms_to_move
-                    .iter_mut()
-                    .map(|(_, transform, lin_vel, _)| (transform, lin_vel))
-                    .collect::<Vec<_>>()
-                    .iter_mut(),
+            let transforms_to_move =
+                worker
+                    .worker
+                    .work(transforms_to_move, timestep, substep_count);
+            let result = TaskResultRaw::<T> {
+                transforms: transforms_to_move,
                 simulated_time,
-            );
-            rotate(
-                transforms_to_move
-                    .iter_mut()
-                    .map(|(_, transform, _, ang_vel)| (transform, ang_vel))
-                    .collect::<Vec<_>>()
-                    .iter_mut(),
-                simulated_time,
-            );
-            let mut result = TaskResultRaw::default();
-            result.transforms = transforms_to_move;
-            result.simulated_time = simulated_time;
+            };
             let _ = sender.send(result);
         })
         .detach();
@@ -361,20 +341,20 @@ pub fn spawn_task(
 /// and adds a [`TaskResult`] component.
 ///
 /// This expects only 1 task at a time.
-pub(crate) fn finish_task_and_store_result(
+pub(crate) fn finish_task_and_store_result<T: TaskWorkerTrait>(
     mut commands: Commands,
     time: Res<Time<Virtual>>,
-    mut q_tasks: Query<(Entity, &mut WorkTask, &mut TaskResults)>,
+    mut q_tasks: Query<(Entity, &mut WorkTask<T>, &mut TaskResults<T>)>,
 ) {
     let Ok((e, mut task, mut results)) = q_tasks.get_single_mut() else {
         return;
     };
     task.update_frames_elapsed += 1;
 
-    let mut handle_result = |task_result: TaskResultRaw| {
-        commands.entity(e).remove::<WorkTask>();
-        results.results.push_back(TaskResult {
-            result: task_result,
+    let mut handle_result = |task_result_raw: TaskResultRaw<T>| {
+        commands.entity(e).remove::<WorkTask<T>>();
+        results.results.push_back(TaskResult::<T> {
+            result_raw: task_result_raw,
             render_time_elapsed_during_the_simulation: dbg!(time.elapsed())
                 - dbg!(task.started_at_render_time),
             started_at_render_time: task.started_at_render_time,
@@ -395,29 +375,25 @@ pub(crate) fn finish_task_and_store_result(
     }
 }
 
-pub(crate) fn handle_task(
-    mut task_results: Query<(&mut TaskResults, &mut TaskToRenderTime)>,
-    mut q_transforms: Query<(&mut Transform, &mut LinearVelocity)>,
-) {
-    for (mut results, mut task_to_render) in task_results.iter_mut() {
+pub(crate) fn handle_task<T: TaskWorkerTrait>(world: &mut World) {
+    let mut task_results =
+        world.query::<(&mut TaskResults<T>, &TaskWorker<T>, &mut TaskToRenderTime)>();
+
+    let mut tasks_to_handle = vec![];
+    for (mut results, worker, mut task_to_render) in task_results.iter_mut(world) {
         let Some(task) = results.results.pop_front() else {
             continue;
         };
+        task_to_render.last_task_frame_count = task.update_frames_elapsed;
         // Apply transform changes.
         info!(
             "handle_task: simulated_time: {:?}",
-            task.result.simulated_time
+            task.result_raw.simulated_time
         );
-        for (entity, new_transform, new_lin_vel, _) in task.result.transforms.iter() {
-            if let Ok((mut transform, mut lin_vel)) = q_transforms.get_mut(*entity) {
-                *transform = *new_transform;
-                *lin_vel = new_lin_vel.clone();
-            }
-        }
-        //let diff_this_frame = dbg!(task.render_time_elapsed_during_the_simulation.as_secs_f64())
-        //    - dbg!(task.result.simulated_time.as_secs_f64());
-        //task_to_render.diff += dbg!(diff_this_frame);
-        //task_to_render.diff += dbg!(diff_this_frame);
-        task_to_render.last_task_frame_count = task.update_frames_elapsed;
+        tasks_to_handle.push((worker.clone(), task));
+    }
+
+    for (worker, task) in tasks_to_handle {
+        worker.worker.write_back(task, world);
     }
 }

From fb4c1542d46ae129b7523d92651d71a3da0f0d64 Mon Sep 17 00:00:00 2001
From: Thierry Berger <contact@thierryberger.com>
Date: Thu, 26 Dec 2024 08:56:01 +0100
Subject: [PATCH 3/8] fix interpolation example

---
 examples/interpolation.rs | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/examples/interpolation.rs b/examples/interpolation.rs
index 4276cf8..b99e672 100644
--- a/examples/interpolation.rs
+++ b/examples/interpolation.rs
@@ -45,6 +45,14 @@ fn main() {
     app.run();
 }
 
+/// The linear velocity of an entity indicating its movement speed and direction.
+#[derive(Component, Deref, DerefMut)]
+struct LinearVelocity(Vec2);
+
+/// The angular velocity of an entity indicating its rotation speed.
+#[derive(Component, Deref, DerefMut)]
+struct AngularVelocity(f32);
+
 fn setup(
     mut commands: Commands,
     mut materials: ResMut<Assets<ColorMaterial>>,

From d41cd27f239ca012f76c2ea9c43c3ad9f1b79839 Mon Sep 17 00:00:00 2001
From: Thierry Berger <contact@thierryberger.com>
Date: Thu, 26 Dec 2024 11:26:51 +0100
Subject: [PATCH 4/8] removed unused file

---
 examples/interpolate_custom_schedule_retry.rs | 722 ------------------
 1 file changed, 722 deletions(-)
 delete mode 100644 examples/interpolate_custom_schedule_retry.rs

diff --git a/examples/interpolate_custom_schedule_retry.rs b/examples/interpolate_custom_schedule_retry.rs
deleted file mode 100644
index b4bd70e..0000000
--- a/examples/interpolate_custom_schedule_retry.rs
+++ /dev/null
@@ -1,722 +0,0 @@
-//! This example showcases how `Transform` interpolation can be used to make movement
-//! appear smooth at fixed timesteps.
-//!
-//! `Transform` interpolation updates `Transform` at every frame in between
-//! fixed ticks to smooth out the visual result. The interpolation is done
-//! from the previous positions to the current positions, which keeps movement smooth,
-//! but has the downside of making movement feel slightly delayed as the rendered
-//! result lags slightly behind the true positions.
-//!
-//! For an example of how transform extrapolation could be implemented instead,
-//! see `examples/extrapolation.rs`.
-
-use bevy::{
-    color::palettes::{
-        css::WHITE,
-        tailwind::{CYAN_400, RED_400},
-    },
-    ecs::schedule::{LogLevel, ScheduleBuildSettings, ScheduleLabel},
-    prelude::*,
-    tasks::AsyncComputeTaskPool,
-};
-use bevy_transform_interpolation::{
-    prelude::*, RotationEasingState, ScaleEasingState, TransformEasingSet, TranslationEasingState,
-};
-use crossbeam_channel::Receiver;
-use rand::{thread_rng, Rng};
-use std::{collections::VecDeque, slice::IterMut, time::Duration};
-
-const MOVEMENT_SPEED: f32 = 250.0;
-const ROTATION_SPEED: f32 = 2.0;
-
-// TODO: update this time to use it correctly.
-// See https://github.com/bevyengine/bevy/blob/d4b07a51149c4cc69899f7424df473ff817fe324/crates/bevy_time/src/fixed.rs#L241
-
-fn main() {
-    let mut app = App::new();
-
-    // Add the `TransformInterpolationPlugin` to the app to enable transform interpolation.
-    app.add_plugins(DefaultPlugins);
-
-    // Set the fixed timestep to just 5 Hz for demonstration purposes.
-
-    // Setup the scene and UI, and update text in `Update`.
-    app.add_systems(Startup, (setup, setup_text)).add_systems(
-        bevy::app::prelude::RunFixedMainLoop,
-        (
-            change_timestep,
-            update_timestep_text,
-            update_diff_to_render_text,
-        ),
-    );
-
-    // This runs every frame to poll if our task was done.
-    app.add_systems(
-        bevy::app::prelude::RunFixedMainLoop, // TODO: use a specific schedule for this, à la bevy's FixedMainLoop
-        task_schedule::FixedMain::run_schedule,
-    );
-
-    // this handles checking for task completion, firing writeback schedules and spawning a new task.
-    app.edit_schedule(task_schedule::FixedMain, |schedule| {
-        schedule
-            .add_systems(task_schedule::HandleTask::run_schedule)
-            .set_build_settings(ScheduleBuildSettings {
-                ambiguity_detection: LogLevel::Error,
-                ..default()
-            });
-    });
-
-    // those schedules are part of FixedMain
-    app.init_schedule(task_schedule::PreWriteBack);
-    app.edit_schedule(task_schedule::WriteBack, |schedule| {
-        schedule
-            .add_systems((handle_task,))
-            .set_build_settings(ScheduleBuildSettings {
-                ambiguity_detection: LogLevel::Error,
-                ..default()
-            });
-    });
-    app.edit_schedule(task_schedule::PostWriteBack, |schedule| {
-        schedule.set_build_settings(ScheduleBuildSettings {
-            ambiguity_detection: LogLevel::Error,
-            ..default()
-        });
-    });
-
-    app.add_systems(
-        bevy::app::prelude::RunFixedMainLoop,
-        (ease_translation_lerp, ease_rotation_slerp, ease_scale_lerp)
-            .in_set(TransformEasingSet::Ease),
-    );
-    // this will spawn a new task if needed.
-    app.edit_schedule(task_schedule::MaybeSpawnTask, |schedule| {
-        schedule
-            .add_systems(spawn_task)
-            .set_build_settings(ScheduleBuildSettings {
-                ambiguity_detection: LogLevel::Error,
-                ..default()
-            });
-    });
-
-    // Run the app.
-    app.run();
-}
-/// Eases the translations of entities with linear interpolation.
-fn ease_translation_lerp(
-    mut query: Query<(&mut Transform, &TranslationEasingState)>,
-    time: Query<(&TaskToRenderTime, &Timestep, &LastTaskTimings)>,
-) {
-    let Ok((time, timestep, last_task_timing)) = time.get_single() else {
-        return;
-    };
-    let overstep = (time.diff.max(0.0)
-        / (timestep.timestep - last_task_timing.render_time_elapsed_during_the_simulation)
-            .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);
-        }
-    });
-}
-
-/// The linear velocity of an entity indicating its movement speed and direction.
-#[derive(Component, Debug, Deref, DerefMut, Clone)]
-pub struct LinearVelocity(Vec2);
-
-/// The angular velocity of an entity indicating its rotation speed.
-#[derive(Component, Debug, Deref, DerefMut, Clone)]
-pub struct AngularVelocity(f32);
-
-#[derive(Component, Debug, Clone)]
-struct ToMove;
-
-fn setup(
-    mut commands: Commands,
-    mut materials: ResMut<Assets<ColorMaterial>>,
-    mut meshes: ResMut<Assets<Mesh>>,
-) {
-    // Spawn a camera.
-    commands.spawn(Camera2d);
-
-    let mesh = meshes.add(Rectangle::from_length(60.0));
-
-    commands.spawn((
-        TaskToRenderTime::default(),
-        Timestep {
-            timestep: Duration::from_secs_f32(0.5),
-        },
-        TaskResults::default(),
-    ));
-
-    // This entity uses transform interpolation.
-    commands.spawn((
-        Name::new("Interpolation"),
-        Mesh2d(mesh.clone()),
-        MeshMaterial2d(materials.add(Color::from(CYAN_400)).clone()),
-        Transform::from_xyz(-500.0, 60.0, 0.0),
-        TransformInterpolation,
-        LinearVelocity(Vec2::new(MOVEMENT_SPEED, 0.0)),
-        AngularVelocity(ROTATION_SPEED),
-        ToMove,
-    ));
-
-    // This entity is simulated in `FixedUpdate` without any smoothing.
-    commands.spawn((
-        Name::new("No Interpolation"),
-        Mesh2d(mesh.clone()),
-        MeshMaterial2d(materials.add(Color::from(RED_400)).clone()),
-        Transform::from_xyz(-500.0, -60.0, 0.0),
-        LinearVelocity(Vec2::new(MOVEMENT_SPEED, 0.0)),
-        AngularVelocity(ROTATION_SPEED),
-        ToMove,
-    ));
-}
-
-/// Changes the timestep of the simulation when the up or down arrow keys are pressed.
-fn change_timestep(mut time: Query<&mut Timestep>, keyboard_input: Res<ButtonInput<KeyCode>>) {
-    let mut time = time.single_mut();
-    if keyboard_input.pressed(KeyCode::ArrowUp) {
-        let new_timestep = (time.timestep.as_secs_f64() * 0.9).max(1.0 / 255.0);
-        time.timestep = Duration::from_secs_f64(new_timestep);
-    }
-    if keyboard_input.pressed(KeyCode::ArrowDown) {
-        let new_timestep = (time.timestep.as_secs_f64() * 1.1)
-            .min(1.0)
-            .max(1.0 / 255.0);
-        time.timestep = Duration::from_secs_f64(new_timestep);
-    }
-}
-
-/// Flips the movement directions of objects when they reach the left or right side of the screen.
-fn flip_movement_direction(query: IterMut<(&mut Transform, &mut LinearVelocity)>) {
-    for (transform, lin_vel) in query {
-        if transform.translation.x > 500.0 && lin_vel.0.x > 0.0 {
-            lin_vel.0 = Vec2::new(-MOVEMENT_SPEED, 0.0);
-        } else if transform.translation.x < -500.0 && lin_vel.0.x < 0.0 {
-            lin_vel.0 = Vec2::new(MOVEMENT_SPEED, 0.0);
-        }
-    }
-}
-
-/// Moves entities based on their `LinearVelocity`.
-fn movement(query: IterMut<(&mut Transform, &mut LinearVelocity)>, delta: Duration) {
-    let delta_secs = delta.as_secs_f32();
-    for (transform, lin_vel) in query {
-        transform.translation += lin_vel.extend(0.0) * delta_secs;
-    }
-}
-
-/// Rotates entities based on their `AngularVelocity`.
-fn rotate(query: IterMut<(&mut Transform, &mut AngularVelocity)>, delta: Duration) {
-    let delta_secs = delta.as_secs_f32();
-    for (transform, ang_vel) in query {
-        transform.rotate_local_z(ang_vel.0 * delta_secs);
-    }
-}
-
-#[derive(Component)]
-struct TimestepText;
-
-#[derive(Component)]
-struct TaskToRenderTimeText;
-
-fn setup_text(mut commands: Commands) {
-    let font = TextFont {
-        font_size: 20.0,
-        ..default()
-    };
-
-    commands
-        .spawn((
-            Text::new("Fixed Hz: "),
-            TextColor::from(WHITE),
-            font.clone(),
-            Node {
-                position_type: PositionType::Absolute,
-                top: Val::Px(10.0),
-                left: Val::Px(10.0),
-                ..default()
-            },
-        ))
-        .with_child((TimestepText, TextSpan::default()));
-
-    commands.spawn((
-        Text::new("Change Timestep With Up/Down Arrow"),
-        TextColor::from(WHITE),
-        font.clone(),
-        Node {
-            position_type: PositionType::Absolute,
-            top: Val::Px(10.0),
-            right: Val::Px(10.0),
-            ..default()
-        },
-    ));
-
-    commands.spawn((
-        Text::new("Interpolation"),
-        TextColor::from(CYAN_400),
-        font.clone(),
-        Node {
-            position_type: PositionType::Absolute,
-            top: Val::Px(50.0),
-            left: Val::Px(10.0),
-            ..default()
-        },
-    ));
-
-    commands.spawn((
-        Text::new("No Interpolation"),
-        TextColor::from(RED_400),
-        font.clone(),
-        Node {
-            position_type: PositionType::Absolute,
-            top: Val::Px(75.0),
-            left: Val::Px(10.0),
-            ..default()
-        },
-    ));
-
-    commands
-        .spawn((
-            Text::new("Diff to render time: "),
-            TextColor::from(WHITE),
-            font.clone(),
-            Node {
-                position_type: PositionType::Absolute,
-                top: Val::Px(100.0),
-                left: Val::Px(10.0),
-                ..default()
-            },
-        ))
-        .with_child((TaskToRenderTimeText, TextSpan::default()));
-}
-
-fn update_timestep_text(
-    mut text: Single<&mut TextSpan, With<TimestepText>>,
-    time: Query<&Timestep>,
-) {
-    let timestep = time.single().timestep.as_secs_f32().recip();
-    text.0 = format!("{timestep:.2}");
-}
-
-fn update_diff_to_render_text(
-    mut text: Single<&mut TextSpan, With<TaskToRenderTimeText>>,
-    task_to_render: Single<&TaskToRenderTime>,
-) {
-    text.0 = format!("{:.2}", task_to_render.diff);
-}
-
-pub mod task_schedule {
-
-    use bevy::{
-        ecs::schedule::ScheduleLabel,
-        log::{info, trace},
-        prelude::{SystemSet, World},
-        time::Time,
-    };
-
-    use crate::TaskToRenderTime;
-
-    #[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
-    pub enum FixedMainLoop {
-        Before,
-        During,
-        After,
-    }
-
-    /// Executes before the task result is propagated to the ECS.
-    #[derive(ScheduleLabel, Clone, Copy, Debug, PartialEq, Eq, Hash)]
-    pub struct PreWriteBack;
-
-    /// Propagates the task result to the ECS.
-    #[derive(ScheduleLabel, Clone, Copy, Debug, PartialEq, Eq, Hash)]
-    pub struct WriteBack;
-
-    /// Called after the propagation of the task result to the ECS.
-    #[derive(ScheduleLabel, Clone, Copy, Debug, PartialEq, Eq, Hash)]
-    pub struct PostWriteBack;
-
-    /// Called once to start a task, then after receiving each task result.
-    #[derive(ScheduleLabel, Clone, Copy, Debug, PartialEq, Eq, Hash)]
-    pub struct MaybeSpawnTask;
-
-    /// Schedule running [`PreWriteBack`], [`WriteBack`] and [`PostWriteBack`]
-    /// only if it received its data from the [`super::WorkTask`] present in the single Entity containing it.
-    ///
-    /// This Schedule overrides [`Res<Time>`][Time] to be the task's time ([`Time<Fixed<MyTaskTime>>`]).
-    ///
-    /// It's also responsible for spawning a new [`super::WorkTask`].
-    ///
-    /// This Schedule does not support multiple Entities with the same `Task` component.
-    // TODO: Schedule as entities might be able to support multiple entities?
-    ///
-    /// This works similarly to [`bevy's FixedMain`][bevy::app::FixedMain],
-    /// but it is not blocked by the render loop.
-    #[derive(Debug, Hash, PartialEq, Eq, Clone, ScheduleLabel)]
-    pub struct FixedMain;
-
-    impl FixedMain {
-        /// A system that runs the [`SingleTaskSchedule`] if the task was done.
-        pub fn run_schedule(world: &mut World) {
-            world
-                .run_system_cached(crate::finish_task_and_store_result)
-                .unwrap();
-
-            // Compute difference between task and render time.
-            let clock = world.resource::<Time>().as_generic();
-            let mut query = world.query::<(&mut TaskToRenderTime, &super::Timestep)>();
-            let (mut task_to_render_time, timestep) = query.single_mut(world);
-            task_to_render_time.diff += clock.delta().as_secs_f64();
-            // should we apply deferred commands?
-            if task_to_render_time.diff <= timestep.timestep.as_secs_f64() {
-                // Task is too far ahead, we should not read the simulation.
-                return;
-            }
-            let simulated_time = {
-                let mut query = world.query::<&crate::TaskResults>();
-                let task_result = query.single(world).results.front();
-                task_result.map(|task_result| task_result.result.simulated_time)
-            };
-            let Some(simulated_time) = simulated_time else {
-                let mut query = world.query::<&crate::LastTaskTimings>();
-                if query.get_single(world).is_err() {
-                    world.run_schedule(MaybeSpawnTask);
-                }
-                return;
-            };
-            let mut query = world.query::<&mut TaskToRenderTime>();
-            let mut task_to_render_time = query.single_mut(world);
-            task_to_render_time.diff -= simulated_time.as_secs_f64();
-            let _ = world.try_schedule_scope(FixedMain, |world, schedule| {
-                // Advance simulation.
-                trace!("Running FixedMain schedule");
-                schedule.run(world);
-
-                // If physics is paused, reset delta time to stop simulation
-                // unless users manually advance `Time<Physics>`.
-                /*if is_paused {
-                    world
-                        .resource_mut::<Time<Physics>>()
-                        .advance_by(Duration::ZERO);
-                }
-                */
-            });
-            // PROBLEM: This is outside of our fixed update, so we're reading the interpolated transforms.
-            // This is unacceptable because that's not our ground truth.
-            //world.run_schedule(MaybeSpawnTask);
-        }
-    }
-
-    /// Schedule handling a single task.
-    #[derive(Debug, Hash, PartialEq, Eq, Clone, ScheduleLabel)]
-    pub struct HandleTask;
-
-    impl HandleTask {
-        pub fn run_schedule(world: &mut World) {
-            let _ = world.try_schedule_scope(PreWriteBack, |world, schedule| {
-                schedule.run(world);
-            });
-            let _ = world.try_schedule_scope(WriteBack, |world, schedule| {
-                schedule.run(world);
-            });
-            let _ = world.try_schedule_scope(MaybeSpawnTask, |world, schedule| {
-                schedule.run(world);
-            });
-            let _ = world.try_schedule_scope(PostWriteBack, |world, schedule| {
-                schedule.run(world);
-            });
-        }
-    }
-}
-
-///
-/// The task inside this component is polled by the system [`handle_tasks`].
-///
-/// Any changes to [`Transform`]s being modified by the task will be overridden when the task finishes.
-///
-/// This component is removed when the task is done
-#[derive(Component, Debug)]
-pub struct WorkTask {
-    /// The time in seconds at which we started the simulation, as reported by the used render time [`Time::elapsed`].
-    pub started_at_render_time: Duration,
-    /// Amount of frames elapsed since the simulation started.
-    pub update_frames_elapsed: u32,
-    /// The channel end to receive the simulation result.
-    pub recv: Receiver<TaskResultRaw>,
-}
-
-/// The result of a task to be handled.
-#[derive(Debug, Default)]
-pub struct TaskResultRaw {
-    pub transforms: Vec<(Entity, Transform, LinearVelocity, AngularVelocity)>,
-    /// The duration in seconds **simulated** by the simulation.
-    ///
-    /// This is different from the real time it took to simulate the physics.
-    ///
-    /// It is needed to synchronize the simulation with the render time.
-    pub simulated_time: Duration,
-}
-
-/// The result of a task to be handled.
-#[derive(Debug, Default)]
-pub struct TaskResult {
-    pub result: TaskResultRaw,
-    pub render_time_elapsed_during_the_simulation: Duration,
-    /// The time at which we started the simulation, as reported by the used render time [`Time::elapsed`].
-    pub started_at_render_time: Duration,
-    /// Amount of frames elapsed since the simulation started.
-    pub update_frames_elapsed: u32,
-}
-/// The result of last task result, helpful for interpolation.
-#[derive(Debug, Default, Component)]
-pub struct LastTaskTimings {
-    pub render_time_elapsed_during_the_simulation: Duration,
-    /// The time at which we started the simulation, as reported by the used render time [`Time::elapsed`].
-    pub started_at_render_time: Duration,
-}
-
-#[derive(Debug, Default, Component)]
-pub struct RealTransform(pub Transform);
-
-/// The result of a task to be handled.
-#[derive(Debug, Default, Component)]
-pub struct TaskResults {
-    /// The results of the tasks.
-    ///
-    /// This is a queue because we might be spawning a new task while another has not been processed yet.
-    ///
-    /// To avoid overwriting the results, we keep them in a queue.
-    pub results: VecDeque<TaskResult>,
-}
-
-/// Difference between tasks and rendering time
-#[derive(Component, Default, Reflect, Clone)]
-pub struct TaskToRenderTime {
-    /// Difference in seconds between tasks and rendering time.
-    ///
-    /// We don't use [`Duration`] because it can be negative.
-    pub diff: f64,
-    /// Amount of rendering frames last task took.
-    pub last_task_frame_count: u32,
-}
-
-/// Difference between tasks and rendering time
-#[derive(Component, Default, Reflect, Clone)]
-pub struct Timestep {
-    pub timestep: Duration,
-}
-
-/// This system spawns a [`WorkTask`] is none are ongoing.
-/// The task simulate computationally intensive work that potentially spans multiple frames/ticks.
-///
-/// A separate system, [`handle_tasks`], will poll the spawned tasks on subsequent
-/// frames/ticks, and use the results to spawn cubes
-pub(crate) fn spawn_task(
-    mut commands: Commands,
-    q_context: Query<(
-        Entity,
-        &TaskToRenderTime,
-        &Timestep,
-        Has<WorkTask>,
-        &TaskResults,
-    )>,
-    q_transforms: Query<(Entity, &mut Transform, &LinearVelocity, &AngularVelocity), With<ToMove>>,
-    virtual_time: Res<Time<Virtual>>,
-) {
-    let Ok((entity_ctx, task_to_render_time, timestep, has_work, results)) = q_context.get_single()
-    else {
-        info!("No correct entity found.");
-        return;
-    };
-    if has_work {
-        info!("A task is ongoing.");
-        return;
-    }
-    let timestep = timestep.timestep;
-
-    // We are not impacting task to render diff yet, because the task has not run yet.
-    // Ideally, this should be driven from user code.
-    let mut sim_to_render_time = task_to_render_time.clone();
-
-    let mut substep_count = 1;
-    /*while sim_to_render_time.diff > timestep.as_secs_f64() {
-        sim_to_render_time.diff -= timestep.as_secs_f64();
-        substep_count += 1;
-    }
-    if substep_count == 0 {
-        info!("No substeps needed.");
-        return;
-    }*/
-
-    let mut transforms_to_move: Vec<(Entity, Transform, LinearVelocity, AngularVelocity)> =
-        q_transforms
-            .iter()
-            .map(|(entity, transform, lin_vel, ang_vel)| {
-                (entity, transform.clone(), lin_vel.clone(), ang_vel.clone())
-            })
-            .collect();
-    let (sender, recv) = crossbeam_channel::unbounded();
-
-    let thread_pool = AsyncComputeTaskPool::get();
-    thread_pool
-        .spawn(async move {
-            let simulated_time = timestep * substep_count;
-
-            info!(
-                "Let's spawn a simulation task for time: {:?}",
-                simulated_time
-            );
-            profiling::scope!("Task ongoing");
-            // Simulate an expensive task
-
-            let to_simulate = simulated_time.as_millis() as u64;
-            std::thread::sleep(Duration::from_millis(thread_rng().gen_range(100..101)));
-
-            // Move entities in a fixed amount of time. The movement should appear smooth for interpolated entities.
-            flip_movement_direction(
-                transforms_to_move
-                    .iter_mut()
-                    .map(|(_, transform, lin_vel, _)| (transform, lin_vel))
-                    .collect::<Vec<_>>()
-                    .iter_mut(),
-            );
-            movement(
-                transforms_to_move
-                    .iter_mut()
-                    .map(|(_, transform, lin_vel, _)| (transform, lin_vel))
-                    .collect::<Vec<_>>()
-                    .iter_mut(),
-                simulated_time,
-            );
-            rotate(
-                transforms_to_move
-                    .iter_mut()
-                    .map(|(_, transform, _, ang_vel)| (transform, ang_vel))
-                    .collect::<Vec<_>>()
-                    .iter_mut(),
-                simulated_time,
-            );
-            let mut result = TaskResultRaw::default();
-            result.transforms = transforms_to_move;
-            result.simulated_time = simulated_time;
-            let _ = sender.send(result);
-        })
-        .detach();
-
-    commands.entity(entity_ctx).insert(WorkTask {
-        recv,
-        started_at_render_time: virtual_time.elapsed(),
-        update_frames_elapsed: 0,
-    });
-}
-
-/// This system queries for `Task<RapierSimulation>` component. It polls the
-/// task, if it has finished, it removes the [`WorkTask`] component from the entity,
-/// and adds a [`TaskResult`] component.
-///
-/// This expects only 1 task at a time.
-pub(crate) fn finish_task_and_store_result(
-    mut commands: Commands,
-    time: Res<Time<Virtual>>,
-    mut q_tasks: Query<(Entity, &mut WorkTask, &mut TaskResults)>,
-) {
-    let Ok((e, mut task, mut results)) = q_tasks.get_single_mut() else {
-        return;
-    };
-    task.update_frames_elapsed += 1;
-
-    let mut handle_result = |task_result: TaskResultRaw| {
-        commands.entity(e).remove::<WorkTask>();
-        results.results.push_back(TaskResult {
-            result: task_result,
-            render_time_elapsed_during_the_simulation: dbg!(time.elapsed())
-                - dbg!(task.started_at_render_time),
-            started_at_render_time: task.started_at_render_time,
-            update_frames_elapsed: task.update_frames_elapsed,
-        });
-        info!("Task finished!");
-    };
-    // TODO: configure this somehow.
-    if task.update_frames_elapsed > 60 {
-        // Do not tolerate more delay over the rendering: block on the result of the simulation.
-        if let Some(result) = task.recv.recv().ok() {
-            handle_result(result);
-        }
-    } else {
-        if let Some(result) = task.recv.try_recv().ok() {
-            handle_result(result);
-        }
-    }
-}
-
-pub(crate) fn handle_task(
-    mut commands: Commands,
-    mut task_results: Query<(Entity, &mut TaskResults, &mut TaskToRenderTime)>,
-    mut q_transforms: Query<(&mut RealTransform, &mut LinearVelocity)>,
-) {
-    for (e, mut results, mut task_to_render) in task_results.iter_mut() {
-        let Some(task) = results.results.pop_front() else {
-            continue;
-        };
-        commands.entity(e).insert(LastTaskTimings {
-            render_time_elapsed_during_the_simulation: task
-                .render_time_elapsed_during_the_simulation,
-            started_at_render_time: task.started_at_render_time,
-        });
-        // Apply transform changes.
-        info!(
-            "handle_task: simulated_time: {:?}",
-            task.result.simulated_time
-        );
-        for (entity, new_transform, new_lin_vel, _) in task.result.transforms.iter() {
-            if let Ok((mut transform, mut lin_vel)) = q_transforms.get_mut(*entity) {
-                transform.0 = *new_transform;
-                *lin_vel = new_lin_vel.clone();
-            }
-        }
-        //let diff_this_frame = dbg!(task.render_time_elapsed_during_the_simulation.as_secs_f64())
-        //    - dbg!(task.result.simulated_time.as_secs_f64());
-        //task_to_render.diff += dbg!(diff_this_frame);
-        //task_to_render.diff += dbg!(diff_this_frame);
-        task_to_render.last_task_frame_count = task.update_frames_elapsed;
-    }
-}

From 88d8cb36f0465b0b47012ad7db674e4d8b12858b Mon Sep 17 00:00:00 2001
From: Thierry Berger <contact@thierryberger.com>
Date: Thu, 26 Dec 2024 15:00:47 +0100
Subject: [PATCH 5/8] removed debug print statements

---
 src/background_fixed_schedule.rs | 41 ++++----------------------------
 src/lib.rs                       |  3 ---
 2 files changed, 4 insertions(+), 40 deletions(-)

diff --git a/src/background_fixed_schedule.rs b/src/background_fixed_schedule.rs
index e47f16c..08c46f5 100644
--- a/src/background_fixed_schedule.rs
+++ b/src/background_fixed_schedule.rs
@@ -208,7 +208,6 @@ impl FixedMain {
         task_to_render_time.diff += clock.delta().as_secs_f64();
         if task_to_render_time.diff < timestep.timestep.as_secs_f64() {
             // Task is too far ahead, we should not read the simulation.
-            info!("Task is too far ahead, we should not read the simulation.");
             return;
         }
         let simulated_time = {
@@ -217,7 +216,6 @@ impl FixedMain {
             task_result.map(|task_result| task_result.result_raw.simulated_time)
         };
         let Some(simulated_time) = simulated_time else {
-            info!("No task result found.");
             return;
         };
         let mut query = world.query::<&mut TaskToRenderTime>();
@@ -225,17 +223,7 @@ impl FixedMain {
         task_to_render_time.diff -= simulated_time.as_secs_f64();
         let _ = world.try_schedule_scope(FixedMain, |world, schedule| {
             // Advance simulation.
-            info!("Running FixedMain schedule");
             schedule.run(world);
-
-            // If physics is paused, reset delta time to stop simulation
-            // unless users manually advance `Time<Physics>`.
-            /*if is_paused {
-                world
-                    .resource_mut::<Time<Physics>>()
-                    .advance_by(Duration::ZERO);
-            }
-            */
         });
     }
 }
@@ -282,26 +270,16 @@ pub fn extract<T: TaskWorkerTrait>(world: &mut World) {
 /// frames/ticks, and use the results to spawn cubes
 pub fn spawn_task<T: TaskWorkerTrait>(
     mut commands: Commands,
-    q_context: Query<(
-        Entity,
-        &TaskToRenderTime,
-        &TaskWorker<T>,
-        &Timestep,
-        &T::TaskExtractedData,
-        Has<WorkTask<T>>,
-    )>,
+    q_context: Query<(Entity, &TaskWorker<T>, &Timestep, &T::TaskExtractedData)>,
     virtual_time: Res<Time<Virtual>>,
 ) {
-    let Ok((entity_ctx, task_to_render_time, worker, timestep, extracted_data, has_work)) =
-        q_context.get_single()
-    else {
-        info!("No correct entity found.");
+    let Ok((entity_ctx, worker, timestep, extracted_data)) = q_context.get_single() else {
         return;
     };
     let timestep = timestep.timestep;
 
     // TODO: tweak this on user side, to allow the simulation to catch up with the render time.
-    let mut substep_count = 1;
+    let substep_count = 1;
 
     let (sender, recv) = crossbeam_channel::unbounded();
 
@@ -311,11 +289,6 @@ pub fn spawn_task<T: TaskWorkerTrait>(
     thread_pool
         .spawn(async move {
             let simulated_time = timestep * substep_count;
-
-            info!(
-                "Let's spawn a simulation task for time: {:?}",
-                simulated_time
-            );
             profiling::scope!("Rapier physics simulation");
             let transforms_to_move =
                 worker
@@ -355,12 +328,10 @@ pub(crate) fn finish_task_and_store_result<T: TaskWorkerTrait>(
         commands.entity(e).remove::<WorkTask<T>>();
         results.results.push_back(TaskResult::<T> {
             result_raw: task_result_raw,
-            render_time_elapsed_during_the_simulation: dbg!(time.elapsed())
-                - dbg!(task.started_at_render_time),
+            render_time_elapsed_during_the_simulation: time.elapsed() - task.started_at_render_time,
             started_at_render_time: task.started_at_render_time,
             update_frames_elapsed: task.update_frames_elapsed,
         });
-        info!("Task finished!");
     };
     // TODO: configure this somehow.
     if task.update_frames_elapsed > 60 {
@@ -386,10 +357,6 @@ pub(crate) fn handle_task<T: TaskWorkerTrait>(world: &mut World) {
         };
         task_to_render.last_task_frame_count = task.update_frames_elapsed;
         // Apply transform changes.
-        info!(
-            "handle_task: simulated_time: {:?}",
-            task.result_raw.simulated_time
-        );
         tasks_to_handle.push((worker.clone(), task));
     }
 
diff --git a/src/lib.rs b/src/lib.rs
index 40511a9..744e457 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -372,7 +372,6 @@ pub fn reset_easing_states_on_transform_change(
 /// Resets the `start` and `end` states for translation interpolation.
 fn reset_translation_easing(mut query: Query<&mut TranslationEasingState>) {
     for mut easing in &mut query {
-        info!("reset_translation_easing");
         easing.start = None;
         easing.end = None;
     }
@@ -400,10 +399,8 @@ fn ease_translation_lerp(
     time: Res<Time<Fixed>>,
 ) {
     let overstep = time.overstep_fraction();
-    info!("ease_translation_lerp; overstep: {:?}", overstep);
     query.iter_mut().for_each(|(mut transform, interpolation)| {
         if let (Some(start), Some(end)) = (interpolation.start, interpolation.end) {
-            info!("{:?} - {:?}", start, end);
             transform.translation = start.lerp(end, overstep.min(1.0));
         }
     });

From 8effd484e323fe4bf99a259887aab9e1d5e82f39 Mon Sep 17 00:00:00 2001
From: Thierry Berger <contact@thierryberger.com>
Date: Thu, 26 Dec 2024 15:33:37 +0100
Subject: [PATCH 6/8] use required component + remove some unused imports or
 obsolete comments

---
 examples/interpolate_custom_schedule.rs | 13 ++++---------
 src/background_fixed_schedule.rs        | 23 ++++++++++++++---------
 src/interpolation.rs                    |  1 -
 3 files changed, 18 insertions(+), 19 deletions(-)

diff --git a/examples/interpolate_custom_schedule.rs b/examples/interpolate_custom_schedule.rs
index 7b3cc5f..f92d229 100644
--- a/examples/interpolate_custom_schedule.rs
+++ b/examples/interpolate_custom_schedule.rs
@@ -12,7 +12,7 @@
 
 use bevy::{
     color::palettes::{
-        css::{ORANGE, RED, WHITE},
+        css::WHITE,
         tailwind::{CYAN_400, RED_400},
     },
     ecs::schedule::ScheduleLabel,
@@ -59,8 +59,6 @@ fn main() {
         interpolation_plugin,
     ));
 
-    // Set the fixed timestep to just 5 Hz for demonstration purposes.
-
     // Setup the scene and UI, and update text in `Update`.
     app.add_systems(Startup, (setup, setup_text)).add_systems(
         bevy::app::prelude::RunFixedMainLoop,
@@ -71,8 +69,6 @@ fn main() {
         ),
     );
 
-    // This runs every frame to poll if our task was done.
-
     app.add_systems(
         bevy::app::prelude::RunFixedMainLoop,
         (ease_translation_lerp, ease_rotation_slerp, ease_scale_lerp)
@@ -82,6 +78,7 @@ fn main() {
     // Run the app.
     app.run();
 }
+
 /// Eases the translations of entities with linear interpolation.
 fn ease_translation_lerp(
     mut query: Query<(&mut Transform, &TranslationEasingState)>,
@@ -147,8 +144,8 @@ fn setup(
     let mesh = meshes.add(Rectangle::from_length(60.0));
 
     commands.spawn((
-        TaskToRenderTime::default(),
         Timestep {
+            // Set the fixed timestep to just 5 Hz for demonstration purposes.
             timestep: Duration::from_secs_f32(0.5),
         },
         TaskResults::<TaskWorkerTraitImpl>::default(),
@@ -293,7 +290,6 @@ pub mod task_user {
 
     use bevy::prelude::*;
     use bevy_transform_interpolation::background_fixed_schedule::TaskWorkerTrait;
-    use rand::{thread_rng, Rng};
 
     #[derive(Debug, Clone, Default)]
     pub struct TaskWorkerTraitImpl;
@@ -309,9 +305,8 @@ pub mod task_user {
             substep_count: u32,
         ) -> Vec<(Entity, Transform, LinearVelocity, AngularVelocity)> {
             let simulated_time = timestep * substep_count;
-            let to_simulate = simulated_time.as_millis() as u64;
             // Simulate an expensive task
-            std::thread::sleep(Duration::from_millis(thread_rng().gen_range(200..201)));
+            std::thread::sleep(Duration::from_millis(200));
 
             // Move entities in a fixed amount of time. The movement should appear smooth for interpolated entities.
             flip_movement_direction(
diff --git a/src/background_fixed_schedule.rs b/src/background_fixed_schedule.rs
index 08c46f5..c711def 100644
--- a/src/background_fixed_schedule.rs
+++ b/src/background_fixed_schedule.rs
@@ -1,13 +1,8 @@
 use bevy::ecs::schedule::{LogLevel, ScheduleBuildSettings, ScheduleLabel};
-use bevy::ecs::world;
-use bevy::log::tracing_subscriber::fmt::time;
 use bevy::prelude::*;
 use bevy::tasks::AsyncComputeTaskPool;
-use bevy::{log::trace, prelude::World, time::Time};
+use bevy::{prelude::World, time::Time};
 use crossbeam_channel::Receiver;
-use rand::{thread_rng, Rng};
-use std::default;
-use std::slice::IterMut;
 use std::{collections::VecDeque, time::Duration};
 
 ///
@@ -67,7 +62,7 @@ pub struct BackgroundFixedUpdatePlugin<T: TaskWorkerTrait> {
 impl<T: TaskWorkerTrait> Plugin for BackgroundFixedUpdatePlugin<T> {
     fn build(&self, app: &mut App) {
         app.add_systems(
-            bevy::app::prelude::RunFixedMainLoop, // TODO: use a specific schedule for this, à la bevy's FixedMainLoop
+            bevy::app::prelude::RunFixedMainLoop,
             FixedMain::run_schedule::<T>,
         );
 
@@ -120,14 +115,24 @@ pub struct TaskToRenderTime {
 }
 
 /// Difference between tasks and rendering time
-#[derive(Component, Default, Reflect, Clone)]
+#[derive(Component, Reflect, Clone)]
 pub struct Timestep {
     pub timestep: Duration,
 }
 
+impl Default for Timestep {
+    fn default() -> Self {
+        Self {
+            timestep: Duration::from_secs_f64(1.0 / 64.0),
+        }
+    }
+}
+
 /// Struct to be able to configure what the task should do.
-/// TODO: extract first, then do work.
+// TODO: This should also require `TaskResults` and `WorkTask`
+// but their type parameter not enforcing `Default`  makes the require macro fail. This should be a bevy issue.
 #[derive(Clone, Component)]
+#[require(TaskToRenderTime, Timestep)]
 pub struct TaskWorker<T: TaskWorkerTrait> {
     pub worker: T,
 }
diff --git a/src/interpolation.rs b/src/interpolation.rs
index fda46ad..553a3dd 100644
--- a/src/interpolation.rs
+++ b/src/interpolation.rs
@@ -6,7 +6,6 @@
 
 use crate::*;
 use bevy::{
-    app::FixedMain,
     ecs::schedule::{InternedScheduleLabel, ScheduleLabel},
     prelude::*,
 };

From 2cbb83e51979612daefd374de92edfaff09008f3 Mon Sep 17 00:00:00 2001
From: Thierry Berger <contact@thierryberger.com>
Date: Fri, 17 Jan 2025 09:58:56 +0100
Subject: [PATCH 7/8] removed unneeded logs + small cleanup

---
 src/interpolation.rs | 2 --
 src/lib.rs           | 1 +
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/interpolation.rs b/src/interpolation.rs
index 5e70144..07cd3d4 100644
--- a/src/interpolation.rs
+++ b/src/interpolation.rs
@@ -331,8 +331,6 @@ fn update_translation_interpolation_end(
 ) {
     for (transform, mut easing) in &mut query {
         easing.end = Some(transform.translation);
-        info!("update_translation_interpolation_end");
-        info!("{easing:?}");
     }
 }
 
diff --git a/src/lib.rs b/src/lib.rs
index ae74b55..2a165f4 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -601,6 +601,7 @@ fn ease_translation_lerp(
     time: Res<Time<Fixed>>,
 ) {
     let overstep = time.overstep_fraction();
+
     query.iter_mut().for_each(|(mut transform, interpolation)| {
         if let (Some(start), Some(end)) = (interpolation.start, interpolation.end) {
             transform.translation = start.lerp(end, overstep.min(1.0));

From eff8cfd5900b19a80e60a3c3165be438490332fc Mon Sep 17 00:00:00 2001
From: Thierry Berger <contact@thierryberger.com>
Date: Fri, 17 Jan 2025 10:13:18 +0100
Subject: [PATCH 8/8] removed unneeded change

---
 src/lib.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/lib.rs b/src/lib.rs
index 2a165f4..6ad9272 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -604,7 +604,7 @@ fn ease_translation_lerp(
 
     query.iter_mut().for_each(|(mut transform, interpolation)| {
         if let (Some(start), Some(end)) = (interpolation.start, interpolation.end) {
-            transform.translation = start.lerp(end, overstep.min(1.0));
+            transform.translation = start.lerp(end, overstep);
         }
     });
 }