From 7d3e794649ae0d84dccd2715786a2896a3d924f6 Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Tue, 26 Nov 2024 16:59:22 +0100 Subject: [PATCH 01/17] wip early working POC background simulation --- Cargo.toml | 2 +- bevy_rapier2d/Cargo.toml | 12 ++- bevy_rapier2d/examples/background2.rs | 146 ++++++++++++++++++++++++++ bevy_rapier3d/Cargo.toml | 3 + src/plugin/configuration.rs | 2 +- src/plugin/context/mod.rs | 7 +- src/plugin/plugin.rs | 50 ++++++--- src/plugin/systems/mod.rs | 5 +- src/plugin/systems/rigid_body.rs | 11 +- src/plugin/systems/task.rs | 145 +++++++++++++++++++++++++ 10 files changed, 359 insertions(+), 24 deletions(-) create mode 100644 bevy_rapier2d/examples/background2.rs create mode 100644 src/plugin/systems/task.rs diff --git a/Cargo.toml b/Cargo.toml index cd1b8bc9..d2ac1fa1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ rust.unexpected_cfgs = { level = "warn", check-cfg = [ [profile.dev] # Use slightly better optimization by default, as examples otherwise seem laggy. -opt-level = 1 +# opt-level = 1 [profile.release] codegen-units = 1 diff --git a/bevy_rapier2d/Cargo.toml b/bevy_rapier2d/Cargo.toml index 983e9b33..0b47379b 100644 --- a/bevy_rapier2d/Cargo.toml +++ b/bevy_rapier2d/Cargo.toml @@ -47,14 +47,20 @@ serde-serialize = ["rapier2d/serde-serialize", "bevy/serialize", "serde"] enhanced-determinism = ["rapier2d/enhanced-determinism"] headless = [] async-collider = ["bevy/bevy_asset", "bevy/bevy_scene"] +background_simulation = [] +profiling = ["rapier2d/profiler"] [dependencies] bevy = { version = "0.15.0-rc.2", default-features = false } nalgebra = { version = "0.33", features = ["convert-glam029"] } -rapier2d = "0.22" +#rapier2d = { git = "http://github.com/dimforge/rapier", branch = "master" } +rapier2d = "*" +profiling = "*" bitflags = "2.4" log = "0.4" serde = { version = "1", features = ["derive"], optional = true } +crossbeam-channel = "0.5" +async-std = "*" [dev-dependencies] bevy = { version = "0.15.0-rc.2", default-features = false, features = [ @@ -74,3 +80,7 @@ bevy_transform_interpolation = { git = "https://github.com/Jondolf/bevy_transfor [package.metadata.docs.rs] # Enable all the features when building the docs on docs.rs features = ["debug-render-2d", "serde-serialize"] + +[[example]] +name = "background2" +required-features = ["background_simulation", "bevy/multi_threaded"] diff --git a/bevy_rapier2d/examples/background2.rs b/bevy_rapier2d/examples/background2.rs new file mode 100644 index 00000000..c1689d53 --- /dev/null +++ b/bevy_rapier2d/examples/background2.rs @@ -0,0 +1,146 @@ +//! This example should be run with features bevy/multi_threaded and bevy_rapier2d/background_simulation + +use std::{fs::File, io::Write}; + +use bevy::diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}; +use bevy::{color::palettes, prelude::*}; +use bevy_mod_debugdump::{schedule_graph, schedule_graph_dot}; +use bevy_rapier2d::prelude::*; +use bevy_transform_interpolation::prelude::{ + RotationInterpolation, TransformInterpolationPlugin, TranslationInterpolation, +}; + +fn main() { + let mut app = App::new(); + app.insert_resource(ClearColor(Color::srgb( + 0xF9 as f32 / 255.0, + 0xF9 as f32 / 255.0, + 0xFF as f32 / 255.0, + ))) + .insert_resource(TimestepMode::Variable { + max_dt: 100f32, + time_scale: 1f32, + substeps: 10, + }) + .insert_resource(Time::::from_hz(60.0)) + .add_plugins(( + DefaultPlugins, + FrameTimeDiagnosticsPlugin, + LogDiagnosticsPlugin::default(), + TransformInterpolationPlugin::default(), + RapierPhysicsPlugin::::pixels_per_meter(100.0).in_fixed_schedule(), + RapierDebugRenderPlugin::default(), + )) + .add_systems(Startup, (setup_graphics, setup_physics)); + app.add_systems( + PostUpdate, + debug_with_transform_info.after(TransformSystem::TransformPropagate), + ); + let mut debugdump_settings = schedule_graph::Settings::default(); + // Filter out some less relevant systems. + debugdump_settings.include_system = + Some(Box::new(|system: &(dyn System)| { + if system.name().starts_with("bevy_pbr") + || system.name().starts_with("bevy_render") + || system.name().starts_with("bevy_gizmos") + || system.name().starts_with("bevy_winit") + || system.name().starts_with("bevy_sprite") + { + return false; + } + true + })); + let dot = schedule_graph_dot(&mut app, PostUpdate, &debugdump_settings); + + let mut file = File::create("interpolation2.dot").expect("could not create file."); + file.set_len(0).unwrap(); + file.write_all(&dot.as_bytes()) + .expect("Could not write to file"); + + app.run(); +} + +#[derive(Component, Clone)] +pub struct VisualBallDebug; + +pub fn setup_graphics(mut commands: Commands) { + commands.spawn(( + Camera2d::default(), + OrthographicProjection { + scale: 15.0, + ..OrthographicProjection::default_2d() + }, + Transform::from_xyz(0.0, 50.0, 0.0), + )); +} + +pub fn setup_physics(mut commands: Commands) { + /* + * Ground + */ + let ground_size = 5000.0; + let ground_height = 100.0; + + commands.spawn(( + Transform::from_xyz(0.0, 0.0 * -ground_height, 0.0), + Collider::cuboid(ground_size, ground_height), + )); + + let ball = ( + Transform::from_xyz(0.0, 200.0, 0.0), + RigidBody::Dynamic, + Collider::ball(20.0), + Restitution { + coefficient: 0.99, + combine_rule: CoefficientCombineRule::Max, + }, + VisualBallDebug, + ); + let ball_column_height = 1000; + 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; + commands.spawn(ball.clone()).insert(Transform::from_xyz( + 80.0 + x_noise_offset, + 200.0 + y_offset, + 0.0, + )); + commands.spawn(ball.clone()).insert(( + Transform::from_xyz(0.0 + x_noise_offset, 200.0 + y_offset, 0.0), + TranslationInterpolation, + )); + commands.spawn(ball.clone()).insert(( + Transform::from_xyz(-80.0 + x_noise_offset, 200.0 + y_offset, 0.0), + TranslationInterpolation, + ColliderDebug::NeverRender, + )); + + for i in 0..4 { + let x_offset = 80.0 * i as f32; + commands.spawn(ball.clone()).insert(( + Transform::from_xyz(-x_offset + x_noise_offset, 200.0 + y_offset, 0.0), + TranslationInterpolation, + RotationInterpolation, + ColliderDebug::NeverRender, + )); + commands.spawn(ball.clone()).insert(( + Transform::from_xyz(x_offset + x_noise_offset, 200.0 + y_offset, 0.0), + TranslationInterpolation, + ColliderDebug::NeverRender, + )); + } + } +} + +pub fn debug_with_transform_info( + mut gizmos: Gizmos, + entities: Query<(&Transform, &Collider), With>, +) { + for (transform, collider) in entities.iter() { + gizmos.circle( + transform.translation, + collider.as_ball().unwrap().radius(), + palettes::basic::RED, + ); + } +} diff --git a/bevy_rapier3d/Cargo.toml b/bevy_rapier3d/Cargo.toml index c17f1ccf..c22321ad 100644 --- a/bevy_rapier3d/Cargo.toml +++ b/bevy_rapier3d/Cargo.toml @@ -48,6 +48,7 @@ serde-serialize = ["rapier3d/serde-serialize", "bevy/serialize", "serde"] enhanced-determinism = ["rapier3d/enhanced-determinism"] headless = [] async-collider = ["bevy/bevy_asset", "bevy/bevy_scene"] +background_simulation = [] [dependencies] bevy = { version = "0.15.0-rc.2", default-features = false } @@ -56,6 +57,7 @@ rapier3d = "0.22" bitflags = "2.4" log = "0.4" serde = { version = "1", features = ["derive"], optional = true } +crossbeam-channel = "0.5" [dev-dependencies] bevy = { version = "0.15.0-rc.3", default-features = false, features = [ @@ -71,6 +73,7 @@ bevy_egui = "0.30.0" divan = "0.1" bevy_rapier_benches3d = { version = "0.1", path = "../bevy_rapier_benches3d" } + [package.metadata.docs.rs] # Enable all the features when building the docs on docs.rs features = ["debug-render-3d", "serde-serialize"] diff --git a/src/plugin/configuration.rs b/src/plugin/configuration.rs index edb3529c..ecfe3550 100644 --- a/src/plugin/configuration.rs +++ b/src/plugin/configuration.rs @@ -9,7 +9,7 @@ use crate::math::{Real, Vect}; use {crate::prelude::TransformInterpolation, rapier::dynamics::IntegrationParameters}; /// Difference between simulation and rendering time -#[derive(Component, Default, Reflect)] +#[derive(Component, Clone, Default, Reflect)] pub struct SimulationToRenderTime { /// Difference between simulation and rendering time pub diff: f32, diff --git a/src/plugin/context/mod.rs b/src/plugin/context/mod.rs index b04e5848..5650a699 100644 --- a/src/plugin/context/mod.rs +++ b/src/plugin/context/mod.rs @@ -230,10 +230,7 @@ impl RapierContext { &mut self, gravity: Vect, timestep_mode: TimestepMode, - events: Option<( - &EventWriter, - &EventWriter, - )>, + fill_events: bool, hooks: &dyn PhysicsHooks, time: &Time, sim_to_render_time: &mut SimulationToRenderTime, @@ -241,7 +238,7 @@ impl RapierContext { &mut Query<(&RapierRigidBodyHandle, &mut TransformInterpolation)>, >, ) { - let event_queue = if events.is_some() { + let event_queue = if fill_events { Some(EventQueue { deleted_colliders: &self.deleted_colliders, collision_events: RwLock::new(Vec::new()), diff --git a/src/plugin/plugin.rs b/src/plugin/plugin.rs index 7d6907c8..707ec944 100644 --- a/src/plugin/plugin.rs +++ b/src/plugin/plugin.rs @@ -129,9 +129,14 @@ where ) .chain() .into_configs(), - PhysicsSet::StepSimulation => (systems::step_simulation::) - .in_set(PhysicsSet::StepSimulation) - .into_configs(), + PhysicsSet::StepSimulation => { + #[cfg(feature = "background_simulation")] + let systems = (systems::task::handle_tasks); + + #[cfg(not(feature = "background_simulation"))] + let systems = systems::step_simulation::; + systems.in_set(PhysicsSet::StepSimulation).into_configs() + } PhysicsSet::Writeback => ( systems::update_colliding_entities, systems::writeback_rigid_bodies, @@ -140,6 +145,12 @@ where ) .in_set(PhysicsSet::Writeback) .into_configs(), + #[cfg(feature = "background_simulation")] + PhysicsSet::StartBackgroundSimulation => { + (systems::task::spawn_simulation_task::,) + .in_set(PhysicsSet::StartBackgroundSimulation) + .into_configs() + } } } } @@ -178,6 +189,9 @@ pub enum PhysicsSet { /// components and the [`GlobalTransform`] component. /// These systems typically run immediately after [`PhysicsSet::StepSimulation`]. Writeback, + /// The systems responsible for starting the background simulation task. + #[cfg(feature = "background_simulation")] + StartBackgroundSimulation, } impl Plugin for RapierPhysicsPlugin @@ -242,16 +256,24 @@ where // Add each set as necessary if self.default_system_setup { - app.configure_sets( - self.schedule, - ( - PhysicsSet::SyncBackend, - PhysicsSet::StepSimulation, - PhysicsSet::Writeback, - ) - .chain() - .before(TransformSystem::TransformPropagate), - ); + #[cfg(feature = "background_simulation")] + let sets = ( + PhysicsSet::StepSimulation, + PhysicsSet::Writeback, + PhysicsSet::SyncBackend, + PhysicsSet::StartBackgroundSimulation, + ) + .chain() + .before(TransformSystem::TransformPropagate); + #[cfg(not(feature = "background_simulation"))] + let sets = ( + PhysicsSet::SyncBackend, + PhysicsSet::StepSimulation, + PhysicsSet::Writeback, + ) + .chain() + .before(TransformSystem::TransformPropagate); + app.configure_sets(self.schedule, sets); app.configure_sets( self.schedule, RapierTransformPropagateSet.in_set(PhysicsSet::SyncBackend), @@ -263,6 +285,8 @@ where Self::get_systems(PhysicsSet::SyncBackend), Self::get_systems(PhysicsSet::StepSimulation), Self::get_systems(PhysicsSet::Writeback), + #[cfg(feature = "background_simulation")] + Self::get_systems(PhysicsSet::StartBackgroundSimulation), ), ); app.init_resource::(); diff --git a/src/plugin/systems/mod.rs b/src/plugin/systems/mod.rs index fdcaa945..e5c192dd 100644 --- a/src/plugin/systems/mod.rs +++ b/src/plugin/systems/mod.rs @@ -8,6 +8,9 @@ mod remove; mod rigid_body; mod writeback; +#[cfg(feature = "background_simulation")] +pub mod task; + pub use character_controller::*; pub use collider::*; pub use joint::*; @@ -51,7 +54,7 @@ pub fn step_simulation( context.step_simulation( config.gravity, *timestep_mode, - Some((&collision_events, &contact_force_events)), + true, &hooks_adapter, &time, &mut sim_to_render_time, diff --git a/src/plugin/systems/rigid_body.rs b/src/plugin/systems/rigid_body.rs index 67a91886..199da935 100644 --- a/src/plugin/systems/rigid_body.rs +++ b/src/plugin/systems/rigid_body.rs @@ -353,7 +353,11 @@ pub fn apply_rigid_body_user_changes( /// System responsible for writing the result of the last simulation step into our `bevy_rapier` /// components and the [`GlobalTransform`] component. pub fn writeback_rigid_bodies( - mut context: WriteRapierContext, + #[cfg(feature = "background_simulation")] mut context: Query< + &mut RapierContext, + Without, + >, + #[cfg(not(feature = "background_simulation"))] mut context: Query<&mut RapierContext>, timestep_mode: Res, config: Query<&RapierConfiguration>, sim_to_render_time: Query<&SimulationToRenderTime>, @@ -374,7 +378,10 @@ pub fn writeback_rigid_bodies( } let handle = handle.0; - let context = context.context(link).into_inner(); + let Ok(context) = context.get_mut(link.0) else { + continue; + }; + let context = context.into_inner(); let sim_to_render_time = sim_to_render_time .get(link.0) .expect("Could not get `SimulationToRenderTime`"); diff --git a/src/plugin/systems/task.rs b/src/plugin/systems/task.rs new file mode 100644 index 00000000..b82647de --- /dev/null +++ b/src/plugin/systems/task.rs @@ -0,0 +1,145 @@ +//! ```plantuml +//! +//! flowchart TD +//! +//! A[Set to allow user to change physics data] --> COPY(copy physics into background) +//! COPY --> FORK@{ shape: fork, label: "Start bevy task" } +//! FORK --> Loop{elapsed time} +//! FORK --> SIM["rapier physics simulation (50ms ; 20fps)"] +//! Loop -->|"<50ms"| BU["bevy update (16.6ms ; 60FPS)"] +//! BU --> Loop +//! Loop -->|"\>=50ms"| J(join) +//! SIM --> J@{ shape: fork, label: "Join bevy task" } +//! J --> WRITE(Write physics from background task into bevy) +//! WRITE --> A +//! +//! ``` + +use std::mem; + +use bevy::{ + ecs::system::{StaticSystemParam, SystemParamItem}, + prelude::*, + tasks::{block_on, futures_lite::future, AsyncComputeTaskPool, Task}, +}; +use rapier::prelude::*; +use std::time::Duration; + +use crate::{ + pipeline::{CollisionEvent, ContactForceEvent}, + plugin::context, +}; +use crate::{ + plugin::{RapierConfiguration, RapierContext, SimulationToRenderTime, TimestepMode}, + prelude::{BevyPhysicsHooks, RapierRigidBodyHandle, TransformInterpolation}, +}; +use crossbeam_channel::{Receiver, Sender, TryRecvError, TrySendError}; + +use super::BevyPhysicsHooksAdapter; + +/// A component that holds a Rapier simulation task. +/// +/// The task inside this component is polled by the system [`handle_tasks`]. +/// +/// It is unsafe to access the [`RapierContext`] from the same entity. +/// +/// This component is removed when it's safe to access the [`RapierContext`] again. +#[derive(Component)] +pub struct SimulationTask { + pub recv: Receiver, +} + +/// This system queries for [`RapierContext`] that have our `Task` component. It polls the +/// tasks to see if they're complete. If the task is complete it sends rapier'sbevy events and +/// removes the [`SimulationTask`] component from the entity. +pub(crate) fn handle_tasks( + mut commands: Commands, + mut q_context: Query<( + &mut RapierContext, + &RapierConfiguration, + &mut SimulationToRenderTime, + )>, + mut transform_tasks: Query<(Entity, &mut SimulationTask)>, + mut collision_events: EventWriter, + mut contact_force_events: EventWriter, +) { + for (entity, mut task) in &mut transform_tasks { + //if let Some(mut result) = block_on(future::poll_once(&mut task.0)) { + if let Some(mut result) = task.recv.try_recv().ok() { + let (mut context, config, mut sim_to_render_time) = q_context.get_mut(entity).unwrap(); + // mem::forget(mem::replace(&mut *context, result)); + mem::swap(&mut *context, &mut result); + context.send_bevy_events(&mut collision_events, &mut contact_force_events); + commands.entity(entity).remove::(); + } + } +} + +/// This system generates tasks simulating 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_simulation_task( + mut commands: Commands, + mut q_context: Query< + ( + Entity, + &mut RapierContext, + &RapierConfiguration, + &mut SimulationToRenderTime, + ), + Without, + >, + timestep_mode: Res, + hooks: StaticSystemParam, + time: Res