diff --git a/Cargo.toml b/Cargo.toml index e1395480f8c9df..ad36a872c7fde6 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 88bcfbbba4a908..c7c3c612d71d3d 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 a7b8847cfca2aa..62dd158944c406 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 da5bc549b9cbcf..2239d1648e922c 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 5b1ceaa27e67a4..6c76abbd1a4212 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 ef52430c868d20..0f9faff081dd7b 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 e31134506f175f..28b1fb7141f85b 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 90eb1d1c307aea..2bedc7bd74ad4a 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 d2b32452b7c1b8..8ac9a9d47bec55 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 4e4558b7a31235..512d1490693dac 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 00000000000000..8a91f359713dbb --- /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 1ba3a2b7e51ff2..35cbd8b02a4253 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 08bec2dcf6a19b..c3dfba74fe799f 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 531a418d4f4ec6..e9a6903c81abc9 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 00000000000000..989f4ef23851ba --- /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 fd37f142be4db1..bcc2ff8b2d72ac 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