Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

wip: simulation in task over multiple frames #29

Open
wants to merge 18 commits into
base: interpolation_exploration
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
experiment with a variable configuration on user side
  • Loading branch information
Vrixyz committed Dec 11, 2024
commit 5ec62c927e79ef5512d45e4ed80afa051b4aac1e
63 changes: 55 additions & 8 deletions bevy_rapier2d/examples/background2.rs
Original file line number Diff line number Diff line change
@@ -7,10 +7,12 @@ use bevy::diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin, LogDiagnost
use bevy::prelude::*;
use bevy::{color::palettes, prelude::*};
use bevy_mod_debugdump::{schedule_graph, schedule_graph_dot};
use bevy_rapier2d::plugin::systems::task::SimulationTask;
use bevy_rapier2d::prelude::*;
use bevy_transform_interpolation::prelude::{
RotationInterpolation, TransformInterpolationPlugin, TranslationInterpolation,
};
use configuration::SyncWithRenderMode;

fn main() {
let mut app = App::new();
@@ -19,11 +21,12 @@ fn main() {
0xF9 as f32 / 255.0,
0xFF as f32 / 255.0,
)))
.insert_resource(TimestepMode::Variable {
max_dt: 100f32,
time_scale: 1f32,
.insert_resource(TimestepMode::SyncWithRender(SyncWithRenderMode {
dt: 1.0 / 60.0,
max_total_dt: 1.0 / 60.0 * 3.0,
time_scale: 1.0,
substeps: 10,
})
}))
.insert_resource(Time::<Fixed>::from_hz(60.0))
.add_plugins((
DefaultPlugins,
@@ -36,9 +39,19 @@ fn main() {
.add_systems(Update, fps_text_update_system);
app.add_systems(
PostUpdate,
(sim_to_render_text_update_system, debug_with_transform_info)
.after(TransformSystem::TransformPropagate),
debug_with_transform_info.after(TransformSystem::TransformPropagate),
);
app.add_systems(
FixedUpdate,
(
sim_to_render_text_update_system,
react_before_starting_simulation,
)
.chain()
.after(PhysicsSet::StepSimulation)
.before(PhysicsSet::StartBackgroundSimulation),
);

let mut debugdump_settings = schedule_graph::Settings::default();
// Filter out some less relevant systems.
debugdump_settings.include_system =
@@ -116,7 +129,14 @@ fn sim_to_render_text_update_system(
) {
let sim_to_render_time = sim_to_render.get_single().unwrap();
for (mut color, mut text) in &mut query {
text.0 = format!("sim to render: {:.2}", sim_to_render_time.diff);
text.0 = format!(
"sim to render: {:.2} ({:.1})",
sim_to_render_time.diff, sim_to_render_time.accumulated_diff
);
text.0 = format!(
"accumulated lag: {:.2}",
sim_to_render_time.accumulated_diff
);
*color = if sim_to_render_time.diff < 0.0 {
// simulation is ahead!
palettes::basic::GREEN.into()
@@ -167,7 +187,6 @@ pub fn setup_physics(mut commands: Commands) {
VisualBallDebug,
);
let ball_column_height = 1000;
let ball_column_height = 250;
for i in 0..ball_column_height {
let y_offset = i as f32 * 41f32;
let x_noise_offset = i as f32 / ball_column_height as f32 - 0.5f32;
@@ -215,3 +234,31 @@ pub fn debug_with_transform_info(
);
}
}

pub fn react_before_starting_simulation(
mut time_step_mode: ResMut<TimestepMode>,
mut q_context: Query<&mut SimulationToRenderTime, Without<SimulationTask>>,
) {
for mut sim_to_render_time in q_context.iter_mut() {
profiling::scope!("react_before_starting_simulation");
dbg!("before starting simulation");
if let TimestepMode::SyncWithRender(sync) = &mut *time_step_mode {
if sim_to_render_time.diff > 0.5f32 {
// The simulation is behind the render time. The simulation slows down,
// the strategy to handle this could be to :
// - run more steps per simulation frame ( avoiding bevy overhead, synchronization, etc)
// - reduce the quality of the simulation (reduce substeps, ccd...)
// - allow a time drift: accept the simulation has slowed down, and don't attempt to catch up.
// For now, we just reset the diff to 0, effectively allowing a time drift,
// but ideally, we should report that to the user.
//dbg!(sim_to_render_time.diff = 0f32);

sync.max_total_dt = 1.0 / 60.0 * 5f32;
sync.substeps = 2;
} else {
sync.max_total_dt = 1.0 / 60.0 * 2f32;
sync.substeps = 5;
}
}
}
}
47 changes: 29 additions & 18 deletions src/plugin/configuration.rs
Original file line number Diff line number Diff line change
@@ -50,24 +50,7 @@ pub enum TimestepMode {
},
/// Use a fixed timestep equal to `IntegrationParameters::dt`, but don't step if the
/// physics simulation advanced by a time greater than the real-world elapsed time multiplied by `time_scale`.
SyncWithRender {
/// The physics simulation will be advanced by this total amount at each Bevy tick, unless
/// the physics simulation time is ahead of a the real time.
dt: f32,
/// Multiplier controlling if the physics simulation should advance faster (> 1.0),
/// at the same speed (= 1.0) or slower (< 1.0) than the real time.
time_scale: f32,
/// The number of substeps that will be performed whenever the physics simulation is advanced.
substeps: usize,
// TODO: add a catch back strategy, where if the physics simulation is behind the real time, it should either:
// - run again
// - run with a higher dt or less substeps
// - allow user to customize ?
// TODO: add a waiting strategy, where if the physics simulation is ahead of the real time, it should either:
// - do nothing: wait for the render to catch up
// - run again, then wait for the render to catch up before reading.
// - allow user to customize ?
},
SyncWithRender(SyncWithRenderMode),
}

impl Default for TimestepMode {
@@ -80,6 +63,34 @@ impl Default for TimestepMode {
}
}

/// Use a fixed timestep equal to `IntegrationParameters::dt`, but don't step if the
/// physics simulation advanced by a time greater than the real-world elapsed time multiplied by `time_scale`.
#[derive(Reflect, Copy, Clone, Debug, PartialEq, Resource)]
pub struct SyncWithRenderMode {
/// The physics simulation will be advanced by this total amount at each bevy_rapier simulation tick.
/// the physics simulation time is ahead of a the real time.
pub dt: f32,
/// Multiplier controlling if the physics simulation should advance faster (> 1.0),
/// at the same speed (= 1.0) or slower (< 1.0) than the real time.
pub time_scale: f32,
/// The number of substeps that will be performed whenever the physics simulation is advanced.
pub substeps: usize,
/// Maximum amount of time the physics simulation may be advanced at each bevy_rapier simulation tick.
/// If the simulation is behind the real time, it will be run again until it catches up or the max_dt is reached.
pub max_total_dt: f32,
// TODO: add a catch back strategy, where if the physics simulation is behind the real time, it should either:
// - run again
// - run with a higher dt or less substeps
// - allow user to customize ?
//
// TODO: add a waiting strategy, where if the physics simulation is ahead of the real time, it should either:
// - do nothing: wait for the render to catch up
// - run again, then wait for the render to catch up before reading.
// - allow user to customize ?
// This could be a target_sim_to_render ; where we should starting new when sim_to_render.diff > target_overtime
//
}

#[derive(Component, Copy, Clone, Debug, Reflect)]
/// A component for specifying configuration information for the physics simulation
pub struct RapierConfiguration {
76 changes: 47 additions & 29 deletions src/plugin/context/mod.rs
Original file line number Diff line number Diff line change
@@ -32,6 +32,8 @@ use crate::prelude::{
ImpulseJoint, MultibodyJoint, RevoluteJoint, TypedJoint,
};

use super::configuration::SyncWithRenderMode;

/// Difference between simulation and rendering time
#[derive(Component, Default, Reflect, Clone)]
pub struct SimulationToRenderTime {
@@ -797,9 +799,10 @@ impl Default for RapierContextSimulation {
impl RapierContextSimulation {
/// Advance the simulation, based on the given timestep mode.
///
/// Returns the time in seconds advanced for the simulation.
/// Returns the time in seconds advanced for the simulation, without taking timescale into account.
/// It's useful to accumulate a time difference with another update logic,
/// such as render loop if we're running the simulation in background.
#[allow(clippy::too_many_arguments)]
#[profiling::function]
pub fn step_simulation(
&mut self,
colliders: &mut RapierContextColliders,
@@ -815,7 +818,7 @@ impl RapierContextSimulation {
&mut Query<(&RapierRigidBodyHandle, &mut TransformInterpolation)>,
>,
) -> f32 {
let mut simulated_time = 0f32;
let mut simulated_time_unscaled = 0f32;
let event_queue = if fill_events {
Some(EventQueue {
deleted_colliders: &self.deleted_colliders,
@@ -840,9 +843,11 @@ impl RapierContextSimulation {
substeps,
} => {
self.integration_parameters.dt = dt;
simulated_time = dt * time_scale;
simulated_time_unscaled = 0.0;

while sim_to_render_time.diff > 0.0 {
while sim_to_render_time.diff >= 0.0 {
simulated_time_unscaled += dt;
profiling::scope!("simulation");
// NOTE: in this comparison we do the same computations we
// will do for the next `while` iteration test, to make sure we
// don't get bit by potential float inaccuracy.
@@ -863,6 +868,7 @@ impl RapierContextSimulation {
substep_integration_parameters.dt = dt / (substeps as Real) * time_scale;

for _ in 0..substeps {
profiling::scope!("step");
self.pipeline.step(
&gravity.into(),
&substep_integration_parameters,
@@ -884,42 +890,54 @@ impl RapierContextSimulation {
sim_to_render_time.diff -= dt;
}
}
TimestepMode::SyncWithRender {
TimestepMode::SyncWithRender(SyncWithRenderMode {
dt,
max_total_dt: max_dt,
time_scale,
substeps,
} => {
}) => {
self.integration_parameters.dt = dt;
simulated_time = dt * time_scale;
simulated_time_unscaled = 0.0;

let mut substep_integration_parameters = self.integration_parameters;
substep_integration_parameters.dt = dt / (substeps as Real) * time_scale;
while sim_to_render_time.diff >= 0.0 && simulated_time_unscaled < max_dt {
simulated_time_unscaled += dt;
profiling::scope!("simulation");

for _ in 0..substeps {
self.pipeline.step(
&gravity.into(),
&substep_integration_parameters,
&mut self.islands,
&mut self.broad_phase,
&mut self.narrow_phase,
&mut rigidbody_set.bodies,
&mut colliders.colliders,
&mut joints.impulse_joints,
&mut joints.multibody_joints,
&mut self.ccd_solver,
None,
hooks,
event_handler,
);
executed_steps += 1;
let mut substep_integration_parameters = self.integration_parameters;
substep_integration_parameters.dt = dt / (substeps as Real) * time_scale;

for _ in 0..substeps {
profiling::scope!("simulation step");
self.pipeline.step(
&gravity.into(),
&substep_integration_parameters,
&mut self.islands,
&mut self.broad_phase,
&mut self.narrow_phase,
&mut rigidbody_set.bodies,
&mut colliders.colliders,
&mut joints.impulse_joints,
&mut joints.multibody_joints,
&mut self.ccd_solver,
None,
hooks,
event_handler,
);
executed_steps += 1;
}
sim_to_render_time.diff -= dt;
}
}
TimestepMode::Variable {
max_dt,
time_scale,
substeps,
} => {
simulated_time = (time.delta_secs() * time_scale).min(max_dt);
let simulated_time = (time.delta_secs() * time_scale).min(max_dt);
// It's not exactly correct if `.min(max_dt)`` triggers,
// but computing simulation to render time in a variable time step
// is not very useful.
simulated_time_unscaled = simulated_time / time_scale;
self.integration_parameters.dt = simulated_time;

let mut substep_integration_parameters = self.integration_parameters;
@@ -982,7 +1000,7 @@ impl RapierContextSimulation {
if executed_steps > 0 {
self.deleted_colliders.clear();
}
simulated_time
simulated_time_unscaled
}
/// Generates bevy events for any physics interactions that have happened
/// that are stored in the events list
55 changes: 22 additions & 33 deletions src/plugin/systems/task.rs
Original file line number Diff line number Diff line change
@@ -103,18 +103,13 @@ pub(crate) fn handle_tasks(
*bodies = result.bodies;
*colliders = result.colliders;
*query_pipeline = result.query_pipeline;
sim_to_render_time.diff -= result.simulated_time;
sim_to_render_time.diff += (time.elapsed() - task.started_at_render_time).as_secs_f32();
if (sim_to_render_time.diff < 0f32) {
// The simulation is behind the render time. The simulation slows down,
// the strategy to handle this could be to :
// - run more steps per simulation frame ( avoiding bevy overhead, synchronization, etc)
// - reduce the quality of the simulation (reduce substeps, ccd...)
// - allow a time drift: accept the simulation has slowed down, and don't attempt to catch up.
// For now, we just reset the diff to 0, effectively allowing a time drift,
// but ideally, we should report that to the user.
sim_to_render_time.diff = 0f32;
}
let render_time_elapsed_during_the_simulation =
(time.elapsed() - task.started_at_render_time).as_secs_f32();
let diff_this_frame =
render_time_elapsed_during_the_simulation - dbg!(result.simulated_time);
sim_to_render_time.diff += dbg!(diff_this_frame);
sim_to_render_time.accumulated_diff += diff_this_frame;

context.send_bevy_events(&mut collision_events, &mut contact_force_events);
commands.entity(entity).remove::<SimulationTask>();
};
@@ -171,9 +166,10 @@ pub(crate) fn spawn_simulation_task<Hooks>(
joints,
query_pipeline,
config,
sim_to_render_time,
mut sim_to_render_time,
) in q_context.iter_mut()
{
dbg!("Let's spawn a simulation task");
// FIXME: Clone this properly?
let mut context = RapierContextSimulation::default();
mem::swap(&mut context, &mut *context_ecs);
@@ -187,36 +183,29 @@ pub(crate) fn spawn_simulation_task<Hooks>(
let config = config.clone();
let timestep_mode = timestep_mode.clone();
let time = time.clone();

let mut sim_to_render_time = sim_to_render_time.clone();

let (sender, recv) = crossbeam_channel::unbounded();

let thread_pool = AsyncComputeTaskPool::get();
let task = thread_pool
.spawn(async move {
async_std::task::sleep(Duration::from_millis(1)).await;
let time_to_catch_up = sim_to_render_time.diff;
let mut simulated_time = 0f32;
if config.physics_pipeline_active {
profiling::scope!("Rapier physics simulation");
let max_tries_to_catch_up = 3;
for i in 0..max_tries_to_catch_up {
if time_to_catch_up < simulated_time {
break;
}
simulated_time += context.step_simulation(
&mut colliders,
&mut joints,
&mut bodies,
config.gravity,
timestep_mode,
true,
&(), // FIXME: &hooks_adapter,
&time,
&mut sim_to_render_time,
None,
);
}
simulated_time += context.step_simulation(
&mut colliders,
&mut joints,
&mut bodies,
config.gravity,
timestep_mode,
true,
&(), // FIXME: &hooks_adapter,
&time,
&mut sim_to_render_time,
None,
);
} else {
bodies.propagate_modified_body_positions_to_colliders(&mut colliders);
}
Loading