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); } }); }