From 5c52d0aeee6e49fce6a449128a46e783513d38e5 Mon Sep 17 00:00:00 2001 From: "David M. Lary" Date: Fri, 2 Feb 2024 23:18:38 -0600 Subject: [PATCH] System Stepping implemented as Resource (#8453) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Add interactive system debugging capabilities to bevy, providing step/break/continue style capabilities to running system schedules. * Original implementation: #8063 - `ignore_stepping()` everywhere was too much complexity * Schedule-config & Resource discussion: #8168 - Decided on selective adding of Schedules & Resource-based control ## Solution Created `Stepping` Resource. This resource can be used to enable stepping on a per-schedule basis. Systems within schedules can be individually configured to: * AlwaysRun: Ignore any stepping state and run every frame * NeverRun: Never run while stepping is enabled - this allows for disabling of systems while debugging * Break: If we're running the full frame, stop before this system is run Stepping provides two modes of execution that reflect traditional debuggers: * Step-based: Only execute one system at a time * Continue/Break: Run all systems, but stop before running a system marked as Break ### Demo https://user-images.githubusercontent.com/857742/233630981-99f3bbda-9ca6-4cc4-a00f-171c4946dc47.mov Breakout has been modified to use Stepping. The game runs normally for a couple of seconds, then stepping is enabled and the game appears to pause. A list of Schedules & Systems appears with a cursor at the first System in the list. The demo then steps forward full frames using the spacebar until the ball is about to hit a brick. Then we step system by system as the ball impacts a brick, showing the cursor moving through the individual systems. Finally the demo switches back to frame stepping as the ball changes course. ### Limitations Due to architectural constraints in bevy, there are some cases systems stepping will not function as a user would expect. #### Event-driven systems Stepping does not support systems that are driven by `Event`s as events are flushed after 1-2 frames. Although game systems are not running while stepping, ignored systems are still running every frame, so events will be flushed. This presents to the user as stepping the event-driven system never executes the system. It does execute, but the events have already been flushed. This can be resolved by changing event handling to use a buffer for events, and only dropping an event once all readers have read it. The work-around to allow these systems to properly execute during stepping is to have them ignore stepping: `app.add_systems(event_driven_system.ignore_stepping())`. This was done in the breakout example to ensure sound played even while stepping. #### Conditional Systems When a system is stepped, it is given an opportunity to run. If the conditions of the system say it should not run, it will not. Similar to Event-driven systems, if a system is conditional, and that condition is only true for a very small time window, then stepping the system may not execute the system. This includes depending on any sort of external clock. This exhibits to the user as the system not always running when it is stepped. A solution to this limitation is to ensure any conditions are consistent while stepping is enabled. For example, all systems that modify any state the condition uses should also enable stepping. #### State-transition Systems Stepping is configured on the per-`Schedule` level, requiring the user to have a `ScheduleLabel`. To support state-transition systems, bevy generates needed schedules dynamically. Currently it’s very difficult (if not impossible, I haven’t verified) for the user to get the labels for these schedules. Without ready access to the dynamically generated schedules, and a resolution for the `Event` lifetime, **stepping of the state-transition systems is not supported** --- ## Changelog - `Schedule::run()` updated to consult `Stepping` Resource to determine which Systems to run each frame - Added `Schedule.label` as a `BoxedSystemLabel`, along with supporting `Schedule::set_label()` and `Schedule::label()` methods - `Stepping` needed to know which `Schedule` was running, and prior to this PR, `Schedule` didn't track its own label - Would have preferred to add `Schedule::with_label()` and remove `Schedule::new()`, but this PR touches enough already - Added calls to `Schedule.set_label()` to `App` and `World` as needed - Added `Stepping` resource - Added `Stepping::begin_frame()` system to `MainSchedulePlugin` - Run before `Main::run_main()` - Notifies any `Stepping` Resource a new render frame is starting ## Migration Guide - Add a call to `Schedule::set_label()` for any custom `Schedule` - This is only required if the `Schedule` will be stepped --------- Co-authored-by: Carter Anderson --- Cargo.toml | 15 + crates/bevy_app/Cargo.toml | 3 +- crates/bevy_app/src/main_schedule.rs | 6 + crates/bevy_ecs/Cargo.toml | 3 +- crates/bevy_ecs/src/schedule/executor/mod.rs | 7 +- .../src/schedule/executor/multi_threaded.rs | 32 +- .../bevy_ecs/src/schedule/executor/simple.rs | 15 +- .../src/schedule/executor/single_threaded.rs | 15 +- crates/bevy_ecs/src/schedule/mod.rs | 57 + crates/bevy_ecs/src/schedule/schedule.rs | 60 +- crates/bevy_ecs/src/schedule/stepping.rs | 1562 +++++++++++++++++ crates/bevy_internal/Cargo.toml | 6 + docs/cargo_features.md | 1 + examples/README.md | 1 + examples/ecs/system_stepping.rs | 204 +++ examples/games/breakout.rs | 18 +- examples/games/stepping.rs | 273 +++ 17 files changed, 2268 insertions(+), 10 deletions(-) create mode 100644 crates/bevy_ecs/src/schedule/stepping.rs create mode 100644 examples/ecs/system_stepping.rs create mode 100644 examples/games/stepping.rs diff --git a/Cargo.toml b/Cargo.toml index e1395480f8..ad36a872c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ default = [ "tonemapping_luts", "default_font", "webgl2", + "bevy_debug_stepping", ] # Force dynamic linking, which improves iterative compile times @@ -295,6 +296,9 @@ file_watcher = ["bevy_internal/file_watcher"] # Enables watching in memory asset providers for Bevy Asset hot-reloading embedded_watcher = ["bevy_internal/embedded_watcher"] +# Enable stepping-based debugging of Bevy systems +bevy_debug_stepping = ["bevy_internal/bevy_debug_stepping"] + [dependencies] bevy_dylib = { path = "crates/bevy_dylib", version = "0.12.0", default-features = false, optional = true } bevy_internal = { path = "crates/bevy_internal", version = "0.12.0", default-features = false } @@ -1566,6 +1570,17 @@ description = "Illustrates creating custom system parameters with `SystemParam`" category = "ECS (Entity Component System)" wasm = false +[[example]] +name = "system_stepping" +path = "examples/ecs/system_stepping.rs" +doc-scrape-examples = true + +[package.metadata.example.system_stepping] +name = "System Stepping" +description = "Demonstrate stepping through systems in order of execution" +category = "ECS (Entity Component System)" +wasm = false + # Time [[example]] name = "time" diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index 88bcfbbba4..c7c3c612d7 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -11,7 +11,8 @@ keywords = ["bevy"] [features] trace = [] bevy_ci_testing = ["serde", "ron"] -default = ["bevy_reflect"] +bevy_debug_stepping = [] +default = ["bevy_reflect", "bevy_debug_stepping"] bevy_reflect = ["dep:bevy_reflect", "bevy_ecs/bevy_reflect"] [dependencies] diff --git a/crates/bevy_app/src/main_schedule.rs b/crates/bevy_app/src/main_schedule.rs index a7b8847cfc..62dd158944 100644 --- a/crates/bevy_app/src/main_schedule.rs +++ b/crates/bevy_app/src/main_schedule.rs @@ -256,6 +256,12 @@ impl Plugin for MainSchedulePlugin { .init_resource::() .add_systems(Main, Main::run_main) .add_systems(FixedMain, FixedMain::run_fixed_main); + + #[cfg(feature = "bevy_debug_stepping")] + { + use bevy_ecs::schedule::{IntoSystemConfigs, Stepping}; + app.add_systems(Main, Stepping::begin_frame.before(Main::run_main)); + } } } diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index da5bc549b9..2239d1648e 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -12,7 +12,8 @@ categories = ["game-engines", "data-structures"] [features] trace = [] multi-threaded = ["bevy_tasks/multi-threaded"] -default = ["bevy_reflect"] +bevy_debug_stepping = [] +default = ["bevy_reflect", "bevy_debug_stepping"] [dependencies] bevy_ptr = { path = "../bevy_ptr", version = "0.12.0" } diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index 5b1ceaa27e..6c76abbd1a 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -18,7 +18,12 @@ use crate::{ pub(super) trait SystemExecutor: Send + Sync { fn kind(&self) -> ExecutorKind; fn init(&mut self, schedule: &SystemSchedule); - fn run(&mut self, schedule: &mut SystemSchedule, world: &mut World); + fn run( + &mut self, + schedule: &mut SystemSchedule, + skip_systems: Option, + world: &mut World, + ); fn set_apply_final_deferred(&mut self, value: bool); } diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index ef52430c86..0f9faff081 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -163,7 +163,12 @@ impl SystemExecutor for MultiThreadedExecutor { self.num_dependencies_remaining = Vec::with_capacity(sys_count); } - fn run(&mut self, schedule: &mut SystemSchedule, world: &mut World) { + fn run( + &mut self, + schedule: &mut SystemSchedule, + _skip_systems: Option, + world: &mut World, + ) { // reset counts self.num_systems = schedule.systems.len(); if self.num_systems == 0 { @@ -181,6 +186,31 @@ impl SystemExecutor for MultiThreadedExecutor { } } + // If stepping is enabled, make sure we skip those systems that should + // not be run. + #[cfg(feature = "bevy_debug_stepping")] + if let Some(mut skipped_systems) = _skip_systems { + debug_assert_eq!(skipped_systems.len(), self.completed_systems.len()); + // mark skipped systems as completed + self.completed_systems |= &skipped_systems; + self.num_completed_systems = self.completed_systems.count_ones(..); + + // signal the dependencies for each of the skipped systems, as + // though they had run + for system_index in skipped_systems.ones() { + self.signal_dependents(system_index); + } + + // Finally, we need to clear all skipped systems from the ready + // list. + // + // We invert the skipped system mask to get the list of systems + // that should be run. Then we bitwise AND it with the ready list, + // resulting in a list of ready systems that aren't skipped. + skipped_systems.toggle_range(..); + self.ready_systems &= skipped_systems; + } + let thread_executor = world .get_resource::() .map(|e| e.0.clone()); diff --git a/crates/bevy_ecs/src/schedule/executor/simple.rs b/crates/bevy_ecs/src/schedule/executor/simple.rs index e31134506f..28b1fb7141 100644 --- a/crates/bevy_ecs/src/schedule/executor/simple.rs +++ b/crates/bevy_ecs/src/schedule/executor/simple.rs @@ -30,7 +30,20 @@ impl SystemExecutor for SimpleExecutor { self.completed_systems = FixedBitSet::with_capacity(sys_count); } - fn run(&mut self, schedule: &mut SystemSchedule, world: &mut World) { + fn run( + &mut self, + schedule: &mut SystemSchedule, + _skip_systems: Option, + world: &mut World, + ) { + // If stepping is enabled, make sure we skip those systems that should + // not be run. + #[cfg(feature = "bevy_debug_stepping")] + if let Some(skipped_systems) = _skip_systems { + // mark skipped systems as completed + self.completed_systems |= &skipped_systems; + } + for system_index in 0..schedule.systems.len() { #[cfg(feature = "trace")] let name = schedule.systems[system_index].name(); diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 90eb1d1c30..2bedc7bd74 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -38,7 +38,20 @@ impl SystemExecutor for SingleThreadedExecutor { self.unapplied_systems = FixedBitSet::with_capacity(sys_count); } - fn run(&mut self, schedule: &mut SystemSchedule, world: &mut World) { + fn run( + &mut self, + schedule: &mut SystemSchedule, + _skip_systems: Option, + world: &mut World, + ) { + // If stepping is enabled, make sure we skip those systems that should + // not be run. + #[cfg(feature = "bevy_debug_stepping")] + if let Some(skipped_systems) = _skip_systems { + // mark skipped systems as completed + self.completed_systems |= &skipped_systems; + } + for system_index in 0..schedule.systems.len() { #[cfg(feature = "trace")] let name = schedule.systems[system_index].name(); diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index d2b32452b7..8ac9a9d47b 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -8,6 +8,7 @@ mod graph_utils; mod schedule; mod set; mod state; +mod stepping; pub use self::condition::*; pub use self::config::*; @@ -1098,4 +1099,60 @@ mod tests { assert!(schedule.graph().conflicting_systems().is_empty()); } } + + #[cfg(feature = "bevy_debug_stepping")] + mod stepping { + use super::*; + use bevy_ecs::system::SystemState; + + #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] + pub struct TestSchedule; + + macro_rules! assert_executor_supports_stepping { + ($executor:expr) => { + // create a test schedule + let mut schedule = Schedule::new(TestSchedule); + schedule + .set_executor_kind($executor) + .add_systems(|| panic!("Executor ignored Stepping")); + + // Add our schedule to stepping & and enable stepping; this should + // prevent any systems in the schedule from running + let mut stepping = Stepping::default(); + stepping.add_schedule(TestSchedule).enable(); + + // create a world, and add the stepping resource + let mut world = World::default(); + world.insert_resource(stepping); + + // start a new frame by running ihe begin_frame() system + let mut system_state: SystemState>> = + SystemState::new(&mut world); + let res = system_state.get_mut(&mut world); + Stepping::begin_frame(res); + + // now run the schedule; this will panic if the executor doesn't + // handle stepping + schedule.run(&mut world); + }; + } + + /// verify the [`SimpleExecutor`] supports stepping + #[test] + fn simple_executor() { + assert_executor_supports_stepping!(ExecutorKind::Simple); + } + + /// verify the [`SingleThreadedExecutor`] supports stepping + #[test] + fn single_threaded_executor() { + assert_executor_supports_stepping!(ExecutorKind::SingleThreaded); + } + + /// verify the [`MultiThreadedExecutor`] supports stepping + #[test] + fn multi_threaded_executor() { + assert_executor_supports_stepping!(ExecutorKind::MultiThreaded); + } + } } diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 4e4558b7a3..512d149069 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -25,6 +25,8 @@ use crate::{ world::World, }; +pub use stepping::Stepping; + /// Resource that stores [`Schedule`]s mapped to [`ScheduleLabel`]s excluding the current running [`Schedule`]. #[derive(Default, Resource)] pub struct Schedules { @@ -238,6 +240,11 @@ impl Schedule { } } + /// Get the `InternedScheduleLabel` for this `Schedule`. + pub fn label(&self) -> InternedScheduleLabel { + self.label + } + /// Add a collection of systems to the schedule. pub fn add_systems(&mut self, systems: impl IntoSystemConfigs) -> &mut Self { self.graph.process_configs(systems.into_configs(), false); @@ -324,7 +331,17 @@ impl Schedule { world.check_change_ticks(); self.initialize(world) .unwrap_or_else(|e| panic!("Error when initializing schedule {:?}: {e}", self.label)); - self.executor.run(&mut self.executable, world); + + #[cfg(not(feature = "bevy_debug_stepping"))] + let skip_systems = None; + + #[cfg(feature = "bevy_debug_stepping")] + let skip_systems = match world.get_resource_mut::() { + None => None, + Some(mut stepping) => stepping.skipped_systems(self), + }; + + self.executor.run(&mut self.executable, skip_systems, world); } /// Initializes any newly-added systems and conditions, rebuilds the executable schedule, @@ -366,6 +383,11 @@ impl Schedule { &mut self.graph } + /// Returns the [`SystemSchedule`]. + pub(crate) fn executable(&self) -> &SystemSchedule { + &self.executable + } + /// Iterates the change ticks of all systems in the schedule and clamps any older than /// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). /// This prevents overflow and thus prevents false positives. @@ -402,6 +424,36 @@ impl Schedule { system.apply_deferred(world); } } + + /// Returns an iterator over all systems in this schedule. + /// + /// Note: this method will return [`ScheduleNotInitialized`] if the + /// schedule has never been initialized or run. + pub fn systems( + &self, + ) -> Result + Sized, ScheduleNotInitialized> { + if !self.executor_initialized { + return Err(ScheduleNotInitialized); + } + + let iter = self + .executable + .system_ids + .iter() + .zip(&self.executable.systems) + .map(|(node_id, system)| (*node_id, system)); + + Ok(iter) + } + + /// Returns the number of systems in this schedule. + pub fn systems_len(&self) -> usize { + if !self.executor_initialized { + self.graph.systems.len() + } else { + self.executable.systems.len() + } + } } /// A directed acyclic graph structure. @@ -1939,6 +1991,12 @@ impl ScheduleBuildSettings { } } +/// Error to denote that [`Schedule::initialize`] or [`Schedule::run`] has not yet been called for +/// this schedule. +#[derive(Error, Debug)] +#[error("executable schedule has not been built")] +pub struct ScheduleNotInitialized; + #[cfg(test)] mod tests { use crate::{ diff --git a/crates/bevy_ecs/src/schedule/stepping.rs b/crates/bevy_ecs/src/schedule/stepping.rs new file mode 100644 index 0000000000..8a91f35971 --- /dev/null +++ b/crates/bevy_ecs/src/schedule/stepping.rs @@ -0,0 +1,1562 @@ +use fixedbitset::FixedBitSet; +use std::any::TypeId; +use std::collections::HashMap; + +use crate::{ + schedule::{InternedScheduleLabel, NodeId, Schedule, ScheduleLabel}, + system::{IntoSystem, ResMut, Resource, System}, +}; +use bevy_utils::{ + thiserror::Error, + tracing::{error, info, warn}, +}; + +#[cfg(test)] +use bevy_utils::tracing::debug; + +use crate as bevy_ecs; + +#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] +enum Action { + /// Stepping is disabled; run all systems + #[default] + RunAll, + + /// Stepping is enabled, but we're only running required systems this frame + Waiting, + + /// Stepping is enabled; run all systems until the end of the frame, or + /// until we encounter a system marked with [`SystemBehavior::Break`] or all + /// systems in the frame have run. + Continue, + + /// stepping is enabled; only run the next system in our step list + Step, +} + +#[derive(Debug, Copy, Clone)] +enum SystemBehavior { + /// System will always run regardless of stepping action + AlwaysRun, + + /// System will never run while stepping is enabled + NeverRun, + + /// When [`Action::Waiting`] this system will not be run + /// When [`Action::Step`] this system will be stepped + /// When [`Action::Continue`] system execution will stop before executing + /// this system unless its the first system run when continuing + Break, + + /// When [`Action::Waiting`] this system will not be run + /// When [`Action::Step`] this system will be stepped + /// When [`Action::Continue`] this system will be run + Continue, +} + +// schedule_order index, and schedule start point +#[derive(Debug, Default, Clone, Copy)] +struct Cursor { + /// index within Stepping.schedule_order + pub schedule: usize, + /// index within the schedule's system list + pub system: usize, +} + +// Two methods of referring to Systems, via TypeId, or per-Schedule NodeId +enum SystemIdentifier { + Type(TypeId), + Node(NodeId), +} + +/// Updates to [`Stepping.schedule_states`] that will be applied at the start +/// of the next render frame +enum Update { + /// Set the action stepping will perform for this render frame + SetAction(Action), + /// Enable stepping for this schedule + AddSchedule(InternedScheduleLabel), + /// Disable stepping for this schedule + RemoveSchedule(InternedScheduleLabel), + /// Clear any system-specific behaviors for this schedule + ClearSchedule(InternedScheduleLabel), + /// Set a system-specific behavior for this schedule & system + SetBehavior(InternedScheduleLabel, SystemIdentifier, SystemBehavior), + /// Clear any system-specific behavior for this schedule & system + ClearBehavior(InternedScheduleLabel, SystemIdentifier), +} + +#[derive(Error, Debug)] +#[error("not available until all configured schedules have been run; try again next frame")] +pub struct NotReady; + +#[derive(Resource, Default)] +/// Resource for controlling system stepping behavior +pub struct Stepping { + // [`ScheduleState`] for each [`Schedule`] with stepping enabled + schedule_states: HashMap, + + // dynamically generated [`Schedule`] order + schedule_order: Vec, + + // current position in the stepping frame + cursor: Cursor, + + // index in [`schedule_order`] of the last schedule to call `skipped_systems()` + previous_schedule: Option, + + // Action to perform during this render frame + action: Action, + + // Updates apply at the start of the next render frame + updates: Vec, +} + +impl std::fmt::Debug for Stepping { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Stepping {{ action: {:?}, schedules: {:?}, order: {:?}", + self.action, + self.schedule_states.keys(), + self.schedule_order + )?; + if self.action != Action::RunAll { + let Cursor { schedule, system } = self.cursor; + match self.schedule_order.get(schedule) { + Some(label) => write!(f, "cursor: {:?}[{}], ", label, system)?, + None => write!(f, "cursor: None, ")?, + }; + } + write!(f, "}}") + } +} + +impl Stepping { + /// Create a new instance of the `Stepping` resource. + pub fn new() -> Self { + Stepping::default() + } + + /// System to call denoting that a new render frame has begun + /// + /// Note: This system is automatically added to the default `MainSchedule`. + pub fn begin_frame(stepping: Option>) { + if let Some(mut stepping) = stepping { + stepping.next_frame(); + } + } + + /// Return the list of schedules with stepping enabled in the order + /// they are executed in. + pub fn schedules(&self) -> Result<&Vec, NotReady> { + if self.schedule_order.len() == self.schedule_states.len() { + Ok(&self.schedule_order) + } else { + Err(NotReady) + } + } + + /// Return our current position within the stepping frame + /// + /// NOTE: This function **will** return `None` during normal execution with + /// stepping enabled. This can happen at the end of the stepping frame + /// after the last system has been run, but before the start of the next + /// render frame. + pub fn cursor(&self) -> Option<(InternedScheduleLabel, NodeId)> { + if self.action == Action::RunAll { + return None; + } + let label = match self.schedule_order.get(self.cursor.schedule) { + None => return None, + Some(label) => label, + }; + let state = match self.schedule_states.get(label) { + None => return None, + Some(state) => state, + }; + state + .node_ids + .get(self.cursor.system) + .map(|node_id| (*label, *node_id)) + } + + /// Enable stepping for the provided schedule + pub fn add_schedule(&mut self, schedule: impl ScheduleLabel) -> &mut Self { + self.updates.push(Update::AddSchedule(schedule.intern())); + self + } + + /// Disable stepping for the provided schedule + /// + /// NOTE: This function will also clear any system-specific behaviors that + /// may have been configured. + pub fn remove_schedule(&mut self, schedule: impl ScheduleLabel) -> &mut Self { + self.updates.push(Update::RemoveSchedule(schedule.intern())); + self + } + + /// Clear behavior set for all systems in the provided [`Schedule`] + pub fn clear_schedule(&mut self, schedule: impl ScheduleLabel) -> &mut Self { + self.updates.push(Update::ClearSchedule(schedule.intern())); + self + } + + /// Begin stepping at the start of the next frame + pub fn enable(&mut self) -> &mut Self { + #[cfg(feature = "bevy_debug_stepping")] + self.updates.push(Update::SetAction(Action::Waiting)); + #[cfg(not(feature = "bevy_debug_stepping"))] + error!( + "Stepping cannot be enabled; \ + bevy was compiled without the bevy_debug_stepping feature" + ); + self + } + + /// Disable stepping, resume normal systems execution + pub fn disable(&mut self) -> &mut Self { + self.updates.push(Update::SetAction(Action::RunAll)); + self + } + + /// Check if stepping is enabled + pub fn is_enabled(&self) -> bool { + self.action != Action::RunAll + } + + /// Run the next system during the next render frame + /// + /// NOTE: This will have no impact unless stepping has been enabled + pub fn step_frame(&mut self) -> &mut Self { + self.updates.push(Update::SetAction(Action::Step)); + self + } + + /// Run all remaining systems in the stepping frame during the next render + /// frame + /// + /// NOTE: This will have no impact unless stepping has been enabled + pub fn continue_frame(&mut self) -> &mut Self { + self.updates.push(Update::SetAction(Action::Continue)); + self + } + + /// Ensure this system always runs when stepping is enabled + /// + /// Note: if the system is run multiple times in the [`Schedule`], this + /// will apply for all instances of the system. + pub fn always_run( + &mut self, + schedule: impl ScheduleLabel, + system: impl IntoSystem<(), (), Marker>, + ) -> &mut Self { + // PERF: ideally we don't actually need to construct the system to retrieve the TypeId. + // Unfortunately currently IntoSystem::into_system(system).type_id() != TypeId::of::() + // If these are aligned, we can use TypeId::of::() here + let type_id = IntoSystem::into_system(system).type_id(); + self.updates.push(Update::SetBehavior( + schedule.intern(), + SystemIdentifier::Type(type_id), + SystemBehavior::AlwaysRun, + )); + + self + } + + /// Ensure this system instance always runs when stepping is enabled + pub fn always_run_node(&mut self, schedule: impl ScheduleLabel, node: NodeId) -> &mut Self { + self.updates.push(Update::SetBehavior( + schedule.intern(), + SystemIdentifier::Node(node), + SystemBehavior::AlwaysRun, + )); + self + } + + /// Ensure this system never runs when stepping is enabled + pub fn never_run( + &mut self, + schedule: impl ScheduleLabel, + system: impl IntoSystem<(), (), Marker>, + ) -> &mut Self { + let type_id = IntoSystem::into_system(system).type_id(); + self.updates.push(Update::SetBehavior( + schedule.intern(), + SystemIdentifier::Type(type_id), + SystemBehavior::NeverRun, + )); + + self + } + + /// Ensure this system instance never runs when stepping is enabled + pub fn never_run_node(&mut self, schedule: impl ScheduleLabel, node: NodeId) -> &mut Self { + self.updates.push(Update::SetBehavior( + schedule.intern(), + SystemIdentifier::Node(node), + SystemBehavior::NeverRun, + )); + self + } + + /// Add a breakpoint for system + pub fn set_breakpoint( + &mut self, + schedule: impl ScheduleLabel, + system: impl IntoSystem<(), (), Marker>, + ) -> &mut Self { + let type_id = IntoSystem::into_system(system).type_id(); + self.updates.push(Update::SetBehavior( + schedule.intern(), + SystemIdentifier::Type(type_id), + SystemBehavior::Break, + )); + + self + } + + /// Add a breakpoint for system instance + pub fn set_breakpoint_node(&mut self, schedule: impl ScheduleLabel, node: NodeId) -> &mut Self { + self.updates.push(Update::SetBehavior( + schedule.intern(), + SystemIdentifier::Node(node), + SystemBehavior::Break, + )); + self + } + + /// Clear a breakpoint for the system + pub fn clear_breakpoint( + &mut self, + schedule: impl ScheduleLabel, + system: impl IntoSystem<(), (), Marker>, + ) -> &mut Self { + self.clear_system(schedule, system); + + self + } + + /// clear a breakpoint for system instance + pub fn clear_breakpoint_node( + &mut self, + schedule: impl ScheduleLabel, + node: NodeId, + ) -> &mut Self { + self.clear_node(schedule, node); + self + } + + /// Clear any behavior set for the system + pub fn clear_system( + &mut self, + schedule: impl ScheduleLabel, + system: impl IntoSystem<(), (), Marker>, + ) -> &mut Self { + let type_id = IntoSystem::into_system(system).type_id(); + self.updates.push(Update::ClearBehavior( + schedule.intern(), + SystemIdentifier::Type(type_id), + )); + + self + } + + /// clear a breakpoint for system instance + pub fn clear_node(&mut self, schedule: impl ScheduleLabel, node: NodeId) -> &mut Self { + self.updates.push(Update::ClearBehavior( + schedule.intern(), + SystemIdentifier::Node(node), + )); + self + } + + /// lookup the first system for the supplied schedule index + fn first_system_index_for_schedule(&self, index: usize) -> usize { + let label = match self.schedule_order.get(index) { + None => return 0, + Some(label) => label, + }; + let state = match self.schedule_states.get(label) { + None => return 0, + Some(state) => state, + }; + state.first.unwrap_or(0) + } + + /// Move the cursor to the start of the first schedule + fn reset_cursor(&mut self) { + self.cursor = Cursor { + schedule: 0, + system: self.first_system_index_for_schedule(0), + }; + } + + /// Advance schedule states for the next render frame + fn next_frame(&mut self) { + // if stepping is enabled; reset our internal state for the start of + // the next frame + if self.action != Action::RunAll { + self.action = Action::Waiting; + self.previous_schedule = None; + + // if the cursor passed the last schedule, reset it + if self.cursor.schedule >= self.schedule_order.len() { + self.reset_cursor(); + } + } + + if self.updates.is_empty() { + return; + } + + let mut reset_cursor = false; + for update in self.updates.drain(..) { + match update { + Update::SetAction(Action::RunAll) => { + self.action = Action::RunAll; + reset_cursor = true; + } + Update::SetAction(action) => { + // This match block is really just to filter out invalid + // transitions, and add debugging messages for permitted + // transitions. Any action transition that falls through + // this match block will be performed. + match (self.action, action) { + // ignore non-transition updates, and prevent a call to + // enable() from overwriting a step or continue call + (Action::RunAll, Action::RunAll) + | (Action::Waiting, Action::Waiting) + | (Action::Continue, Action::Continue) + | (Action::Step, Action::Step) + | (Action::Continue, Action::Waiting) + | (Action::Step, Action::Waiting) => continue, + + // when stepping is disabled + (Action::RunAll, Action::Waiting) => info!("enabled stepping"), + (Action::RunAll, _) => { + warn!( + "stepping not enabled; call Stepping::enable() \ + before step_frame() or continue_frame()" + ); + continue; + } + + // stepping enabled; waiting + (Action::Waiting, Action::RunAll) => info!("disabled stepping"), + (Action::Waiting, Action::Continue) => info!("continue frame"), + (Action::Waiting, Action::Step) => info!("step frame"), + + // stepping enabled; continue frame + (Action::Continue, Action::RunAll) => info!("disabled stepping"), + (Action::Continue, Action::Step) => { + warn!("ignoring step_frame(); already continuing next frame"); + continue; + } + + // stepping enabled; step frame + (Action::Step, Action::RunAll) => info!("disabled stepping"), + (Action::Step, Action::Continue) => { + warn!("ignoring continue_frame(); already stepping next frame"); + continue; + } + } + + // permitted action transition; make the change + self.action = action; + } + Update::AddSchedule(l) => { + self.schedule_states.insert(l, ScheduleState::default()); + } + Update::RemoveSchedule(label) => { + self.schedule_states.remove(&label); + if let Some(index) = self.schedule_order.iter().position(|l| l == &label) { + self.schedule_order.remove(index); + } + reset_cursor = true; + } + Update::ClearSchedule(label) => match self.schedule_states.get_mut(&label) { + Some(state) => state.clear_behaviors(), + None => { + warn!( + "stepping is not enabled for schedule {:?}; \ + use `.add_stepping({:?})` to enable stepping", + label, label + ); + } + }, + Update::SetBehavior(label, system, behavior) => { + match self.schedule_states.get_mut(&label) { + Some(state) => state.set_behavior(system, behavior), + None => { + warn!( + "stepping is not enabled for schedule {:?}; \ + use `.add_stepping({:?})` to enable stepping", + label, label + ); + } + } + } + Update::ClearBehavior(label, system) => { + match self.schedule_states.get_mut(&label) { + Some(state) => state.clear_behavior(system), + None => { + warn!( + "stepping is not enabled for schedule {:?}; \ + use `.add_stepping({:?})` to enable stepping", + label, label + ); + } + } + } + } + } + + if reset_cursor { + self.reset_cursor(); + } + } + + /// get the list of systems this schedule should skip for this render + /// frame + pub fn skipped_systems(&mut self, schedule: &Schedule) -> Option { + if self.action == Action::RunAll { + return None; + } + + // grab the label and state for this schedule + let label = schedule.label(); + let state = self.schedule_states.get_mut(&label)?; + + // Stepping is enabled, and this schedule is supposed to be stepped. + // + // We need to maintain a list of schedules in the order that they call + // this function. We'll check the ordered list now to see if this + // schedule is present. If not, we'll add it after the last schedule + // that called this function. Finally we want to save off the index of + // this schedule in the ordered schedule list. This is used to + // determine if this is the schedule the cursor is pointed at. + let index = self.schedule_order.iter().position(|l| *l == label); + let index = match (index, self.previous_schedule) { + (Some(index), _) => index, + (None, None) => { + self.schedule_order.insert(0, label); + 0 + } + (None, Some(last)) => { + self.schedule_order.insert(last + 1, label); + last + 1 + } + }; + // Update the index of the previous schedule to be the index of this + // schedule for the next call + self.previous_schedule = Some(index); + + #[cfg(test)] + debug!( + "cursor {:?}, index {}, label {:?}", + self.cursor, index, label + ); + + // if the stepping frame cursor is pointing at this schedule, we'll run + // the schedule with the current stepping action. If this is not the + // cursor schedule, we'll run the schedule with the waiting action. + let cursor = self.cursor; + let (skip_list, next_system) = if index == cursor.schedule { + let (skip_list, next_system) = + state.skipped_systems(schedule, cursor.system, self.action); + + // if we just stepped this schedule, then we'll switch the action + // to be waiting + if self.action == Action::Step { + self.action = Action::Waiting; + } + (skip_list, next_system) + } else { + // we're not supposed to run any systems in this schedule, so pull + // the skip list, but ignore any changes it makes to the cursor. + let (skip_list, _) = state.skipped_systems(schedule, 0, Action::Waiting); + (skip_list, Some(cursor.system)) + }; + + // update the stepping frame cursor based on if there are any systems + // remaining to be run in the schedule + // Note: Don't try to detect the end of the render frame here using the + // schedule index. We don't know all schedules have been added to the + // schedule_order, so only next_frame() knows its safe to reset the + // cursor. + match next_system { + Some(i) => self.cursor.system = i, + None => { + let index = cursor.schedule + 1; + self.cursor = Cursor { + schedule: index, + system: self.first_system_index_for_schedule(index), + }; + + #[cfg(test)] + debug!("advanced schedule index: {} -> {}", cursor.schedule, index); + } + } + + Some(skip_list) + } +} + +#[derive(Default)] +struct ScheduleState { + /// per-system [`SystemBehavior`] + behaviors: HashMap, + + /// order of NodeIds in the schedule + /// + /// This is a cached copy of SystemExecutable.system_ids. We need it + /// available here to be accessed by Stepping::cursor() so we can return + /// NodeIds to the caller. + node_ids: Vec, + + /// changes to system behavior that should be applied the next time + /// [`ScheduleState::skipped_systems()`] is called + behavior_updates: HashMap>, + + /// This field contains the first steppable system in the schedule. + first: Option, +} + +impl ScheduleState { + // set the stepping behavior for a system in this schedule + fn set_behavior(&mut self, system: SystemIdentifier, behavior: SystemBehavior) { + self.first = None; + match system { + SystemIdentifier::Node(node_id) => { + self.behaviors.insert(node_id, behavior); + } + // Behaviors are indexed by NodeId, but we cannot map a system + // TypeId to a NodeId without the `Schedule`. So queue this update + // to be processed the next time `skipped_systems()` is called. + SystemIdentifier::Type(type_id) => { + self.behavior_updates.insert(type_id, Some(behavior)); + } + } + } + + // clear the stepping behavior for a system in this schedule + fn clear_behavior(&mut self, system: SystemIdentifier) { + self.first = None; + match system { + SystemIdentifier::Node(node_id) => { + self.behaviors.remove(&node_id); + } + // queue TypeId updates to be processed later when we have Schedule + SystemIdentifier::Type(type_id) => { + self.behavior_updates.insert(type_id, None); + } + } + } + + // clear all system behaviors + fn clear_behaviors(&mut self) { + self.behaviors.clear(); + self.behavior_updates.clear(); + self.first = None; + } + + // apply system behavior updates by looking up the node id of the system in + // the schedule, and updating `systems` + fn apply_behavior_updates(&mut self, schedule: &Schedule) { + // Systems may be present multiple times within a schedule, so we + // iterate through all systems in the schedule, and check our behavior + // updates for the system TypeId. + // PERF: If we add a way to efficiently query schedule systems by their TypeId, we could remove the full + // system scan here + for (node_id, system) in schedule.systems().unwrap() { + let behavior = self.behavior_updates.get(&system.type_id()); + match behavior { + None => continue, + Some(None) => { + self.behaviors.remove(&node_id); + } + Some(Some(behavior)) => { + self.behaviors.insert(node_id, *behavior); + } + } + } + self.behavior_updates.clear(); + + #[cfg(test)] + debug!("apply_updates(): {:?}", self.behaviors); + } + + fn skipped_systems( + &mut self, + schedule: &Schedule, + start: usize, + mut action: Action, + ) -> (FixedBitSet, Option) { + use std::cmp::Ordering; + + // if our NodeId list hasn't been populated, copy it over from the + // schedule + if self.node_ids.len() != schedule.systems_len() { + self.node_ids = schedule.executable().system_ids.clone(); + } + + // Now that we have the schedule, apply any pending system behavior + // updates. The schedule is required to map from system `TypeId` to + // `NodeId`. + if !self.behavior_updates.is_empty() { + self.apply_behavior_updates(schedule); + } + + // if we don't have a first system set, set it now + if self.first.is_none() { + for (i, (node_id, _)) in schedule.systems().unwrap().enumerate() { + match self.behaviors.get(&node_id) { + Some(SystemBehavior::AlwaysRun | SystemBehavior::NeverRun) => continue, + Some(_) | None => { + self.first = Some(i); + break; + } + } + } + } + + let mut skip = FixedBitSet::with_capacity(schedule.systems_len()); + let mut pos = start; + + for (i, (node_id, _system)) in schedule.systems().unwrap().enumerate() { + let behavior = self + .behaviors + .get(&node_id) + .unwrap_or(&SystemBehavior::Continue); + + #[cfg(test)] + debug!( + "skipped_systems(): systems[{}], pos {}, Action::{:?}, Behavior::{:?}, {}", + i, + pos, + action, + behavior, + _system.name() + ); + + match (action, behavior) { + // regardless of which action we're performing, if the system + // is marked as NeverRun, add it to the skip list. + // Also, advance the cursor past this system if it is our + // current position + (_, SystemBehavior::NeverRun) => { + skip.insert(i); + if i == pos { + pos += 1; + } + } + // similarly, ignore any system marked as AlwaysRun; they should + // never be added to the skip list + // Also, advance the cursor past this system if it is our + // current position + (_, SystemBehavior::AlwaysRun) => { + if i == pos { + pos += 1; + } + } + // if we're waiting, no other systems besides AlwaysRun should + // be run, so add systems to the skip list + (Action::Waiting, _) => skip.insert(i), + + // If we're stepping, the remaining behaviors don't matter, + // we're only going to run the system at our cursor. Any system + // prior to the cursor is skipped. Once we encounter the system + // at the cursor, we'll advance the cursor, and set behavior to + // Waiting to skip remaining systems. + (Action::Step, _) => match i.cmp(&pos) { + Ordering::Less => skip.insert(i), + Ordering::Equal => { + pos += 1; + action = Action::Waiting; + } + Ordering::Greater => unreachable!(), + }, + // If we're continuing, and the step behavior is continue, we + // want to skip any systems prior to our start position. That's + // where the stepping frame left off last time we ran anything. + (Action::Continue, SystemBehavior::Continue) => { + if i < start { + skip.insert(i); + } + } + // If we're continuing, and we encounter a breakpoint we may + // want to stop before executing the system. To do this we + // skip this system and set the action to Waiting. + // + // Note: if the cursor is pointing at this system, we will run + // it anyway. This allows the user to continue, hit a + // breakpoint, then continue again to run the breakpoint system + // and any following systems. + (Action::Continue, SystemBehavior::Break) => { + if i != start { + skip.insert(i); + + // stop running systems if the breakpoint isn't the + // system under the cursor. + if i > start { + action = Action::Waiting; + } + } + } + // should have never gotten into this method if stepping is + // disabled + (Action::RunAll, _) => unreachable!(), + } + + // If we're at the cursor position, and not waiting, advance the + // cursor. + if i == pos && action != Action::Waiting { + pos += 1; + } + } + + // output is the skip list, and the index of the next system to run in + // this schedule. + if pos >= schedule.systems_len() { + (skip, None) + } else { + (skip, Some(pos)) + } + } +} + +#[cfg(all(test, feature = "bevy_debug_stepping"))] +mod tests { + use super::*; + use crate::prelude::*; + use crate::{schedule::ScheduleLabel, world::World}; + + pub use crate as bevy_ecs; + + #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] + struct TestSchedule; + + #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] + struct TestScheduleA; + + #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] + struct TestScheduleB; + + #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] + struct TestScheduleC; + + #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] + struct TestScheduleD; + + fn first_system() {} + fn second_system() {} + fn third_system() {} + + fn setup() -> (Schedule, World) { + let mut world = World::new(); + let mut schedule = Schedule::new(TestSchedule); + schedule.add_systems((first_system, second_system).chain()); + schedule.initialize(&mut world).unwrap(); + (schedule, world) + } + + // Helper for verifying skip_lists are equal, and if not, printing a human + // readable message. + macro_rules! assert_skip_list_eq { + ($actual:expr, $expected:expr, $system_names:expr) => { + let actual = $actual; + let expected = $expected; + let systems: &Vec<&str> = $system_names; + + if (actual != expected) { + use std::fmt::Write as _; + + // mismatch, let's construct a human-readable message of what + // was returned + let mut msg = format!( + "Schedule:\n {:9} {:16}{:6} {:6} {:6}\n", + "index", "name", "expect", "actual", "result" + ); + for (i, name) in systems.iter().enumerate() { + let _ = write!(msg, " system[{:1}] {:16}", i, name); + match (expected.contains(i), actual.contains(i)) { + (true, true) => msg.push_str("skip skip pass\n"), + (true, false) => { + msg.push_str("skip run FAILED; system should not have run\n") + } + (false, true) => { + msg.push_str("run skip FAILED; system should have run\n") + } + (false, false) => msg.push_str("run run pass\n"), + } + } + assert_eq!(actual, expected, "{}", msg); + } + }; + } + + // Helper for verifying that a set of systems will be run for a given skip + // list + macro_rules! assert_systems_run { + ($schedule:expr, $skipped_systems:expr, $($system:expr),*) => { + // pull an ordered list of systems in the schedule, and save the + // system TypeId, and name. + let systems: Vec<(TypeId, std::borrow::Cow<'static, str>)> = $schedule.systems().unwrap() + .map(|(_, system)| { + (system.type_id(), system.name()) + }) + .collect(); + + // construct a list of systems that are expected to run + let mut expected = FixedBitSet::with_capacity(systems.len()); + $( + let sys = IntoSystem::into_system($system); + for (i, (type_id, _)) in systems.iter().enumerate() { + if sys.type_id() == *type_id { + expected.insert(i); + } + } + )* + + // flip the run list to get our skip list + expected.toggle_range(..); + + // grab the list of skipped systems + let actual = match $skipped_systems { + None => FixedBitSet::with_capacity(systems.len()), + Some(b) => b, + }; + let system_names: Vec<&str> = systems + .iter() + .map(|(_,n)| n.rsplit_once("::").unwrap().1) + .collect(); + + assert_skip_list_eq!(actual, expected, &system_names); + }; + } + + // Helper for verifying the expected systems will be run by the schedule + // + // This macro will construct an expected FixedBitSet for the systems that + // should be skipped, and compare it with the results from stepping the + // provided schedule. If they don't match, it generates a human-readable + // error message and asserts. + macro_rules! assert_schedule_runs { + ($schedule:expr, $stepping:expr, $($system:expr),*) => { + // advance stepping to the next frame, and build the skip list for + // this schedule + $stepping.next_frame(); + assert_systems_run!($schedule, $stepping.skipped_systems($schedule), $($system),*); + }; + } + + #[test] + fn stepping_disabled() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping.add_schedule(TestSchedule).disable().next_frame(); + + assert!(stepping.skipped_systems(&schedule).is_none()); + assert!(stepping.cursor().is_none()); + } + + #[test] + fn unknown_schedule() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping.enable().next_frame(); + + assert!(stepping.skipped_systems(&schedule).is_none()); + } + + #[test] + fn disabled_always_run() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .disable() + .always_run(TestSchedule, first_system); + + assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system); + } + + #[test] + fn waiting_always_run() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .enable() + .always_run(TestSchedule, first_system); + + assert_schedule_runs!(&schedule, &mut stepping, first_system); + } + + #[test] + fn step_always_run() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .enable() + .always_run(TestSchedule, first_system) + .step_frame(); + + assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system); + } + + #[test] + fn continue_always_run() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .enable() + .always_run(TestSchedule, first_system) + .continue_frame(); + + assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system); + } + + #[test] + fn disabled_never_run() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .never_run(TestSchedule, first_system) + .disable(); + assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system); + } + + #[test] + fn waiting_never_run() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .enable() + .never_run(TestSchedule, first_system); + + assert_schedule_runs!(&schedule, &mut stepping,); + } + + #[test] + fn step_never_run() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .enable() + .never_run(TestSchedule, first_system) + .step_frame(); + + assert_schedule_runs!(&schedule, &mut stepping, second_system); + } + + #[test] + fn continue_never_run() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .enable() + .never_run(TestSchedule, first_system) + .continue_frame(); + + assert_schedule_runs!(&schedule, &mut stepping, second_system); + } + + #[test] + fn disabled_breakpoint() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .disable() + .set_breakpoint(TestSchedule, second_system); + + assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system); + } + + #[test] + fn waiting_breakpoint() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .enable() + .set_breakpoint(TestSchedule, second_system); + + assert_schedule_runs!(&schedule, &mut stepping,); + } + + #[test] + fn step_breakpoint() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .enable() + .set_breakpoint(TestSchedule, second_system) + .step_frame(); + + // since stepping stops at every system, breakpoints are ignored during + // stepping + assert_schedule_runs!(&schedule, &mut stepping, first_system); + stepping.step_frame(); + assert_schedule_runs!(&schedule, &mut stepping, second_system); + + // let's go again to verify that we wrap back around to the start of + // the frame + stepping.step_frame(); + assert_schedule_runs!(&schedule, &mut stepping, first_system); + + // should be back in a waiting state now that it ran first_system + assert_schedule_runs!(&schedule, &mut stepping,); + } + + #[test] + fn continue_breakpoint() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .enable() + .set_breakpoint(TestSchedule, second_system) + .continue_frame(); + + assert_schedule_runs!(&schedule, &mut stepping, first_system); + stepping.continue_frame(); + assert_schedule_runs!(&schedule, &mut stepping, second_system); + stepping.continue_frame(); + assert_schedule_runs!(&schedule, &mut stepping, first_system); + } + + /// regression test for issue encountered while writing `system_stepping` + /// example + #[test] + fn continue_step_continue_with_breakpoint() { + let mut world = World::new(); + let mut schedule = Schedule::new(TestSchedule); + schedule.add_systems((first_system, second_system, third_system).chain()); + schedule.initialize(&mut world).unwrap(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .enable() + .set_breakpoint(TestSchedule, second_system); + + stepping.continue_frame(); + assert_schedule_runs!(&schedule, &mut stepping, first_system); + + stepping.step_frame(); + assert_schedule_runs!(&schedule, &mut stepping, second_system); + + stepping.continue_frame(); + assert_schedule_runs!(&schedule, &mut stepping, third_system); + } + + #[test] + fn clear_breakpoint() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .enable() + .set_breakpoint(TestSchedule, second_system) + .continue_frame(); + + assert_schedule_runs!(&schedule, &mut stepping, first_system); + stepping.continue_frame(); + assert_schedule_runs!(&schedule, &mut stepping, second_system); + + stepping.clear_breakpoint(TestSchedule, second_system); + stepping.continue_frame(); + assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system); + } + + #[test] + fn clear_system() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .enable() + .never_run(TestSchedule, second_system) + .continue_frame(); + assert_schedule_runs!(&schedule, &mut stepping, first_system); + + stepping.clear_system(TestSchedule, second_system); + stepping.continue_frame(); + assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system); + } + + #[test] + fn clear_schedule() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .enable() + .never_run(TestSchedule, first_system) + .never_run(TestSchedule, second_system) + .continue_frame(); + assert_schedule_runs!(&schedule, &mut stepping,); + + stepping.clear_schedule(TestSchedule); + stepping.continue_frame(); + assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system); + } + + /// This was discovered in code-review, ensure that `clear_schedule` also + /// clears any pending changes too. + #[test] + fn set_behavior_then_clear_schedule() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .enable() + .continue_frame(); + assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system); + + stepping.never_run(TestSchedule, first_system); + stepping.clear_schedule(TestSchedule); + stepping.continue_frame(); + assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system); + } + + /// Ensure that if they `clear_schedule` then make further changes to the + /// schedule, those changes after the clear are applied. + #[test] + fn clear_schedule_then_set_behavior() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .enable() + .continue_frame(); + assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system); + + stepping.clear_schedule(TestSchedule); + stepping.never_run(TestSchedule, first_system); + stepping.continue_frame(); + assert_schedule_runs!(&schedule, &mut stepping, second_system); + } + + // Schedules such as FixedUpdate can be called multiple times in a single + // render frame. Ensure we only run steppable systems the first time the + // schedule is run + #[test] + fn multiple_calls_per_frame_continue() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestSchedule) + .enable() + .always_run(TestSchedule, second_system) + .continue_frame(); + + // start a new frame, then run the schedule two times; first system + // should only run on the first one + stepping.next_frame(); + assert_systems_run!( + &schedule, + stepping.skipped_systems(&schedule), + first_system, + second_system + ); + assert_systems_run!( + &schedule, + stepping.skipped_systems(&schedule), + second_system + ); + } + #[test] + fn multiple_calls_per_frame_step() { + let (schedule, _world) = setup(); + + let mut stepping = Stepping::new(); + stepping.add_schedule(TestSchedule).enable().step_frame(); + + // start a new frame, then run the schedule two times; first system + // should only run on the first one + stepping.next_frame(); + assert_systems_run!(&schedule, stepping.skipped_systems(&schedule), first_system); + assert_systems_run!(&schedule, stepping.skipped_systems(&schedule),); + } + + #[test] + fn step_duplicate_systems() { + let mut world = World::new(); + let mut schedule = Schedule::new(TestSchedule); + schedule.add_systems((first_system, first_system, second_system).chain()); + schedule.initialize(&mut world).unwrap(); + + let mut stepping = Stepping::new(); + stepping.add_schedule(TestSchedule).enable(); + + // needed for assert_skip_list_eq! + let system_names = vec!["first_system", "first_system", "second_system"]; + // we're going to step three times, and each system in order should run + // only once + for system_index in 0..3 { + // build the skip list by setting all bits, then clearing our the + // one system that should run this step + let mut expected = FixedBitSet::with_capacity(3); + expected.set_range(.., true); + expected.set(system_index, false); + + // step the frame and get the skip list + stepping.step_frame(); + stepping.next_frame(); + let skip_list = stepping + .skipped_systems(&schedule) + .expect("TestSchedule has been added to Stepping"); + + assert_skip_list_eq!(skip_list, expected, &system_names); + } + } + + #[test] + fn step_run_if_false() { + let mut world = World::new(); + let mut schedule = Schedule::new(TestSchedule); + + // This needs to be a system test to confirm the interaction between + // the skip list and system conditions in Schedule::run(). That means + // all of our systems need real bodies that do things. + // + // first system will be configured as `run_if(|| false)`, so it can + // just panic if called + let first_system = move || panic!("first_system should not be run"); + + // The second system, we need to know when it has been called, so we'll + // add a resource for tracking if it has been run. The system will + // increment the run count. + #[derive(Resource)] + struct RunCount(usize); + world.insert_resource(RunCount(0)); + let second_system = |mut run_count: ResMut| { + println!("I have run!"); + run_count.0 += 1; + }; + + // build our schedule; first_system should never run, followed by + // second_system. + schedule.add_systems((first_system.run_if(|| false), second_system).chain()); + schedule.initialize(&mut world).unwrap(); + + // set up stepping + let mut stepping = Stepping::new(); + stepping.add_schedule(TestSchedule).enable(); + world.insert_resource(stepping); + + // if we step, and the run condition is false, we should not run + // second_system. The stepping cursor is at first_system, and if + // first_system wasn't able to run, that's ok. + let mut stepping = world.resource_mut::(); + stepping.step_frame(); + stepping.next_frame(); + schedule.run(&mut world); + assert_eq!( + world.resource::().0, + 0, + "second_system should not have run" + ); + + // now on the next step, second_system should run + let mut stepping = world.resource_mut::(); + stepping.step_frame(); + stepping.next_frame(); + schedule.run(&mut world); + assert_eq!( + world.resource::().0, + 1, + "second_system should have run" + ); + } + + #[test] + fn remove_schedule() { + let (schedule, _world) = setup(); + let mut stepping = Stepping::new(); + stepping.add_schedule(TestSchedule).enable(); + + // run the schedule once and verify all systems are skipped + assert_schedule_runs!(&schedule, &mut stepping,); + assert!(!stepping.schedules().unwrap().is_empty()); + + // remove the test schedule + stepping.remove_schedule(TestSchedule); + assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system); + assert!(stepping.schedules().unwrap().is_empty()); + } + + // verify that Stepping can construct an ordered list of schedules + #[test] + fn schedules() { + let mut world = World::new(); + + // build & initialize a few schedules + let mut schedule_a = Schedule::new(TestScheduleA); + schedule_a.initialize(&mut world).unwrap(); + let mut schedule_b = Schedule::new(TestScheduleB); + schedule_b.initialize(&mut world).unwrap(); + let mut schedule_c = Schedule::new(TestScheduleC); + schedule_c.initialize(&mut world).unwrap(); + let mut schedule_d = Schedule::new(TestScheduleD); + schedule_d.initialize(&mut world).unwrap(); + + // setup stepping and add all the schedules + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestScheduleA) + .add_schedule(TestScheduleB) + .add_schedule(TestScheduleC) + .add_schedule(TestScheduleD) + .enable() + .next_frame(); + + assert!(stepping.schedules().is_err()); + + stepping.skipped_systems(&schedule_b); + assert!(stepping.schedules().is_err()); + stepping.skipped_systems(&schedule_a); + assert!(stepping.schedules().is_err()); + stepping.skipped_systems(&schedule_c); + assert!(stepping.schedules().is_err()); + + // when we call the last schedule, Stepping should have enough data to + // return an ordered list of schedules + stepping.skipped_systems(&schedule_d); + assert!(stepping.schedules().is_ok()); + + assert_eq!( + *stepping.schedules().unwrap(), + vec![ + TestScheduleB.intern(), + TestScheduleA.intern(), + TestScheduleC.intern(), + TestScheduleD.intern(), + ] + ); + } + + #[test] + fn verify_cursor() { + // helper to build a cursor tuple for the supplied schedule + fn cursor(schedule: &Schedule, index: usize) -> (InternedScheduleLabel, NodeId) { + let node_id = schedule.executable().system_ids[index]; + (schedule.label(), node_id) + } + + let mut world = World::new(); + + // create two schedules with a number of systems in them + let mut schedule_a = Schedule::new(TestScheduleA); + schedule_a.add_systems((|| {}, || {}, || {}, || {}).chain()); + schedule_a.initialize(&mut world).unwrap(); + let mut schedule_b = Schedule::new(TestScheduleB); + schedule_b.add_systems((|| {}, || {}, || {}, || {}).chain()); + schedule_b.initialize(&mut world).unwrap(); + + // setup stepping and add all schedules + let mut stepping = Stepping::new(); + stepping + .add_schedule(TestScheduleA) + .add_schedule(TestScheduleB) + .enable(); + + assert!(stepping.cursor().is_none()); + + // step the system nine times, and verify the cursor before & after + // each step + let mut cursors = Vec::new(); + for _ in 0..9 { + stepping.step_frame().next_frame(); + cursors.push(stepping.cursor()); + stepping.skipped_systems(&schedule_a); + stepping.skipped_systems(&schedule_b); + cursors.push(stepping.cursor()); + } + + #[rustfmt::skip] + assert_eq!( + cursors, + vec![ + // before render frame // after render frame + None, Some(cursor(&schedule_a, 1)), + Some(cursor(&schedule_a, 1)), Some(cursor(&schedule_a, 2)), + Some(cursor(&schedule_a, 2)), Some(cursor(&schedule_a, 3)), + Some(cursor(&schedule_a, 3)), Some(cursor(&schedule_b, 0)), + Some(cursor(&schedule_b, 0)), Some(cursor(&schedule_b, 1)), + Some(cursor(&schedule_b, 1)), Some(cursor(&schedule_b, 2)), + Some(cursor(&schedule_b, 2)), Some(cursor(&schedule_b, 3)), + Some(cursor(&schedule_b, 3)), None, + Some(cursor(&schedule_a, 0)), Some(cursor(&schedule_a, 1)), + ] + ); + + // reset our cursor (disable/enable), and update stepping to test if the + // cursor properly skips over AlwaysRun & NeverRun systems. Also set + // a Break system to ensure that shows properly in the cursor + stepping + // disable/enable to reset cursor + .disable() + .enable() + .set_breakpoint_node(TestScheduleA, NodeId::System(1)) + .always_run_node(TestScheduleA, NodeId::System(3)) + .never_run_node(TestScheduleB, NodeId::System(0)); + + let mut cursors = Vec::new(); + for _ in 0..9 { + stepping.step_frame().next_frame(); + cursors.push(stepping.cursor()); + stepping.skipped_systems(&schedule_a); + stepping.skipped_systems(&schedule_b); + cursors.push(stepping.cursor()); + } + + #[rustfmt::skip] + assert_eq!( + cursors, + vec![ + // before render frame // after render frame + Some(cursor(&schedule_a, 0)), Some(cursor(&schedule_a, 1)), + Some(cursor(&schedule_a, 1)), Some(cursor(&schedule_a, 2)), + Some(cursor(&schedule_a, 2)), Some(cursor(&schedule_b, 1)), + Some(cursor(&schedule_b, 1)), Some(cursor(&schedule_b, 2)), + Some(cursor(&schedule_b, 2)), Some(cursor(&schedule_b, 3)), + Some(cursor(&schedule_b, 3)), None, + Some(cursor(&schedule_a, 0)), Some(cursor(&schedule_a, 1)), + Some(cursor(&schedule_a, 1)), Some(cursor(&schedule_a, 2)), + Some(cursor(&schedule_a, 2)), Some(cursor(&schedule_b, 1)), + ] + ); + } +} diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 1ba3a2b7e5..35cbd8b02a 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -156,6 +156,12 @@ file_watcher = ["bevy_asset?/file_watcher"] # Enables watching embedded files for Bevy Asset hot-reloading embedded_watcher = ["bevy_asset?/embedded_watcher"] +# Enable system stepping support +bevy_debug_stepping = [ + "bevy_ecs/bevy_debug_stepping", + "bevy_app/bevy_debug_stepping", +] + [dependencies] # bevy bevy_a11y = { path = "../bevy_a11y", version = "0.12.0" } diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 08bec2dcf6..c3dfba74fe 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -17,6 +17,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_asset|Provides asset functionality| |bevy_audio|Provides audio functionality| |bevy_core_pipeline|Provides cameras and other basic render pipeline features| +|bevy_debug_stepping|Enable stepping-based debugging of Bevy systems| |bevy_gilrs|Adds gamepad support| |bevy_gizmos|Adds support for rendering gizmos| |bevy_gltf|[glTF](https://www.khronos.org/gltf/) support| diff --git a/examples/README.md b/examples/README.md index 531a418d4f..e9a6903c81 100644 --- a/examples/README.md +++ b/examples/README.md @@ -246,6 +246,7 @@ Example | Description [System Closure](../examples/ecs/system_closure.rs) | Show how to use closures as systems, and how to configure `Local` variables by capturing external state [System Parameter](../examples/ecs/system_param.rs) | Illustrates creating custom system parameters with `SystemParam` [System Piping](../examples/ecs/system_piping.rs) | Pipe the output of one system into a second, allowing you to handle any errors gracefully +[System Stepping](../examples/ecs/system_stepping.rs) | Demonstrate stepping through systems in order of execution ## Games diff --git a/examples/ecs/system_stepping.rs b/examples/ecs/system_stepping.rs new file mode 100644 index 0000000000..989f4ef238 --- /dev/null +++ b/examples/ecs/system_stepping.rs @@ -0,0 +1,204 @@ +use bevy::{ecs::schedule::Stepping, log::LogPlugin, prelude::*}; + +fn main() { + let mut app = App::new(); + + app + // to display log messages from Stepping resource + .add_plugins(LogPlugin::default()) + .add_systems( + Update, + ( + update_system_one, + // establish a dependency here to simplify descriptions below + update_system_two.after(update_system_one), + update_system_three.after(update_system_two), + update_system_four, + ), + ) + .add_systems(PreUpdate, pre_update_system); + + // For the simplicity of this example, we directly modify the `Stepping` + // resource here and run the systems with `App::update()`. Each call to + // `App::update()` is the equivalent of a single frame render when using + // `App::run()`. + // + // In a real-world situation, the `Stepping` resource would be modified by + // a system based on input from the user. A full demonstration of this can + // be found in the breakout example. + println!( + r#" + Actions: call app.update() + Result: All systems run normally"# + ); + app.update(); + + println!( + r#" + Actions: Add the Stepping resource then call app.update() + Result: All systems run normally. Stepping has no effect unless explicitly + configured for a Schedule, and Stepping has been enabled."# + ); + app.insert_resource(Stepping::new()); + app.update(); + + println!( + r#" + Actions: Add the Update Schedule to Stepping; enable Stepping; call + app.update() + Result: Only the systems in PreUpdate run. When Stepping is enabled, + systems in the configured schedules will not run unless: + * Stepping::step_frame() is called + * Stepping::continue_frame() is called + * System has been configured to always run"# + ); + let mut stepping = app.world.resource_mut::(); + stepping.add_schedule(Update).enable(); + app.update(); + + println!( + r#" + Actions: call Stepping.step_frame(); call app.update() + Result: The PreUpdate systems run, and one Update system will run. In + Stepping, step means run the next system across all the schedules + that have been added to the Stepping resource."# + ); + let mut stepping = app.world.resource_mut::(); + stepping.step_frame(); + app.update(); + + println!( + r#" + Actions: call app.update() + Result: Only the PreUpdate systems run. The previous call to + Stepping::step_frame() only applies for the next call to + app.update()/the next frame rendered. + "# + ); + app.update(); + + println!( + r#" + Actions: call Stepping::continue_frame(); call app.update() + Result: PreUpdate system will run, and all remaining Update systems will + run. Stepping::continue_frame() tells stepping to run all systems + starting after the last run system until it hits the end of the + frame, or it encounters a system with a breakpoint set. In this + case, we previously performed a step, running one system in Update. + This continue will cause all remaining systems in Update to run."# + ); + let mut stepping = app.world.resource_mut::(); + stepping.continue_frame(); + app.update(); + + println!( + r#" + Actions: call Stepping::step_frame() & app.update() four times in a row + Result: PreUpdate system runs every time we call app.update(), along with + one system from the Update schedule each time. This shows what + execution would look like to step through an entire frame of + systems."# + ); + for _ in 0..4 { + let mut stepping = app.world.resource_mut::(); + stepping.step_frame(); + app.update(); + } + + println!( + r#" + Actions: Stepping::always_run(Update, update_system_two); step through all + systems + Result: PreUpdate system and update_system_two() will run every time we + call app.update(). We'll also only need to step three times to + execute all systems in the frame. Stepping::always_run() allows + us to granularly allow systems to run when stepping is enabled."# + ); + let mut stepping = app.world.resource_mut::(); + stepping.always_run(Update, update_system_two); + for _ in 0..3 { + let mut stepping = app.world.resource_mut::(); + stepping.step_frame(); + app.update(); + } + + println!( + r#" + Actions: Stepping::never_run(Update, update_system_two); continue through + all systems + Result: All systems except update_system_two() will execute. + Stepping::never_run() allows us to disable systems while Stepping + is enabled."# + ); + let mut stepping = app.world.resource_mut::(); + stepping.never_run(Update, update_system_two); + stepping.continue_frame(); + app.update(); + + println!( + r#" + Actions: Stepping::set_breakpoint(Update, update_system_two); continue, + step, continue + Result: During the first continue, pre_update_system() and + update_system_one() will run. update_system_four() may also run + as it has no dependency on update_system_two() or + update_system_three(). Nether update_system_two() nor + update_system_three() will run in the first app.update() call as + they form a chained dependency on update_system_one() and run + in order of one, two, three. Stepping stops system execution in + the Update schedule when it encounters the breakpoint for + update_system_three(). + During the step we run update_system_two() along with the + pre_update_system(). + During the final continue pre_update_system() and + update_system_three() run."# + ); + let mut stepping = app.world.resource_mut::(); + stepping.set_breakpoint(Update, update_system_two); + stepping.continue_frame(); + app.update(); + let mut stepping = app.world.resource_mut::(); + stepping.step_frame(); + app.update(); + let mut stepping = app.world.resource_mut::(); + stepping.continue_frame(); + app.update(); + + println!( + r#" + Actions: Stepping::clear_breakpoint(Update, update_system_two); continue + through all systems + Result: All systems will run"# + ); + let mut stepping = app.world.resource_mut::(); + stepping.clear_breakpoint(Update, update_system_two); + stepping.continue_frame(); + app.update(); + + println!( + r#" + Actions: Stepping::disable(); app.update() + Result: All systems will run. With Stepping disabled, there's no need to + call Stepping::step_frame() or Stepping::continue_frame() to run + systems in the Update schedule."# + ); + let mut stepping = app.world.resource_mut::(); + stepping.disable(); + app.update(); +} + +fn pre_update_system() { + println!("▶ pre_update_system"); +} +fn update_system_one() { + println!("▶ update_system_one"); +} +fn update_system_two() { + println!("▶ update_system_two"); +} +fn update_system_three() { + println!("▶ update_system_three"); +} +fn update_system_four() { + println!("▶ update_system_four"); +} diff --git a/examples/games/breakout.rs b/examples/games/breakout.rs index fd37f142be..bcc2ff8b2d 100644 --- a/examples/games/breakout.rs +++ b/examples/games/breakout.rs @@ -6,6 +6,8 @@ use bevy::{ sprite::MaterialMesh2dBundle, }; +mod stepping; + // These constants are defined in `Transform` units. // Using the default 2D camera they correspond 1:1 with screen pixels. const PADDLE_SIZE: Vec3 = Vec3::new(120.0, 20.0, 0.0); @@ -50,6 +52,12 @@ const SCORE_COLOR: Color = Color::rgb(1.0, 0.5, 0.5); fn main() { App::new() .add_plugins(DefaultPlugins) + .add_plugins( + stepping::SteppingPlugin::default() + .add_schedule(Update) + .add_schedule(FixedUpdate) + .at(Val::Percent(35.0), Val::Percent(50.0)), + ) .insert_resource(Scoreboard { score: 0 }) .insert_resource(ClearColor(BACKGROUND_COLOR)) .add_event::() @@ -170,6 +178,9 @@ struct Scoreboard { score: usize, } +#[derive(Component)] +struct ScoreboardUi; + // Add the game's entities to our world fn setup( mut commands: Commands, @@ -218,7 +229,8 @@ fn setup( )); // Scoreboard - commands.spawn( + commands.spawn(( + ScoreboardUi, TextBundle::from_sections([ TextSection::new( "Score: ", @@ -240,7 +252,7 @@ fn setup( left: SCOREBOARD_TEXT_PADDING, ..default() }), - ); + )); // Walls commands.spawn(WallBundle::new(WallLocation::Left)); @@ -338,7 +350,7 @@ fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>, time: Res