Path: blob/main/crates/bevy_ecs/src/schedule/stepping.rs
6849 views
use crate::{1resource::Resource,2schedule::{InternedScheduleLabel, NodeId, Schedule, ScheduleLabel, SystemKey},3system::{IntoSystem, ResMut},4};5use alloc::vec::Vec;6use bevy_platform::collections::HashMap;7use bevy_utils::TypeIdMap;8use core::any::TypeId;9use fixedbitset::FixedBitSet;10use log::{info, warn};11use thiserror::Error;1213#[cfg(not(feature = "bevy_debug_stepping"))]14use log::error;1516#[cfg(test)]17use log::debug;1819#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]20enum Action {21/// Stepping is disabled; run all systems22#[default]23RunAll,2425/// Stepping is enabled, but we're only running required systems this frame26Waiting,2728/// Stepping is enabled; run all systems until the end of the frame, or29/// until we encounter a system marked with [`SystemBehavior::Break`] or all30/// systems in the frame have run.31Continue,3233/// stepping is enabled; only run the next system in our step list34Step,35}3637#[derive(Debug, Copy, Clone)]38enum SystemBehavior {39/// System will always run regardless of stepping action40AlwaysRun,4142/// System will never run while stepping is enabled43NeverRun,4445/// When [`Action::Waiting`] this system will not be run46/// When [`Action::Step`] this system will be stepped47/// When [`Action::Continue`] system execution will stop before executing48/// this system unless its the first system run when continuing49Break,5051/// When [`Action::Waiting`] this system will not be run52/// When [`Action::Step`] this system will be stepped53/// When [`Action::Continue`] this system will be run54Continue,55}5657// schedule_order index, and schedule start point58#[derive(Debug, Default, Clone, Copy)]59struct Cursor {60/// index within `Stepping::schedule_order`61pub schedule: usize,62/// index within the schedule's system list63pub system: usize,64}6566// Two methods of referring to Systems, via TypeId, or per-Schedule NodeId67enum SystemIdentifier {68Type(TypeId),69Node(NodeId),70}7172/// Updates to [`Stepping::schedule_states`] that will be applied at the start73/// of the next render frame74enum Update {75/// Set the action stepping will perform for this render frame76SetAction(Action),77/// Enable stepping for this schedule78AddSchedule(InternedScheduleLabel),79/// Disable stepping for this schedule80RemoveSchedule(InternedScheduleLabel),81/// Clear any system-specific behaviors for this schedule82ClearSchedule(InternedScheduleLabel),83/// Set a system-specific behavior for this schedule & system84SetBehavior(InternedScheduleLabel, SystemIdentifier, SystemBehavior),85/// Clear any system-specific behavior for this schedule & system86ClearBehavior(InternedScheduleLabel, SystemIdentifier),87}8889#[derive(Error, Debug)]90#[error("not available until all configured schedules have been run; try again next frame")]91pub struct NotReady;9293#[derive(Resource, Default)]94/// Resource for controlling system stepping behavior95pub struct Stepping {96// [`ScheduleState`] for each [`Schedule`] with stepping enabled97schedule_states: HashMap<InternedScheduleLabel, ScheduleState>,9899// dynamically generated [`Schedule`] order100schedule_order: Vec<InternedScheduleLabel>,101102// current position in the stepping frame103cursor: Cursor,104105// index in [`schedule_order`] of the last schedule to call `skipped_systems()`106previous_schedule: Option<usize>,107108// Action to perform during this render frame109action: Action,110111// Updates apply at the start of the next render frame112updates: Vec<Update>,113}114115impl core::fmt::Debug for Stepping {116fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {117write!(118f,119"Stepping {{ action: {:?}, schedules: {:?}, order: {:?}",120self.action,121self.schedule_states.keys(),122self.schedule_order123)?;124if self.action != Action::RunAll {125let Cursor { schedule, system } = self.cursor;126match self.schedule_order.get(schedule) {127Some(label) => write!(f, "cursor: {label:?}[{system}], ")?,128None => write!(f, "cursor: None, ")?,129};130}131write!(f, "}}")132}133}134135impl Stepping {136/// Create a new instance of the `Stepping` resource.137pub fn new() -> Self {138Stepping::default()139}140141/// System to call denoting that a new render frame has begun142///143/// Note: This system is automatically added to the default `MainSchedule`.144pub fn begin_frame(stepping: Option<ResMut<Self>>) {145if let Some(mut stepping) = stepping {146stepping.next_frame();147}148}149150/// Return the list of schedules with stepping enabled in the order151/// they are executed in.152pub fn schedules(&self) -> Result<&Vec<InternedScheduleLabel>, NotReady> {153if self.schedule_order.len() == self.schedule_states.len() {154Ok(&self.schedule_order)155} else {156Err(NotReady)157}158}159160/// Return our current position within the stepping frame161///162/// NOTE: This function **will** return `None` during normal execution with163/// stepping enabled. This can happen at the end of the stepping frame164/// after the last system has been run, but before the start of the next165/// render frame.166pub fn cursor(&self) -> Option<(InternedScheduleLabel, NodeId)> {167if self.action == Action::RunAll {168return None;169}170let label = self.schedule_order.get(self.cursor.schedule)?;171let state = self.schedule_states.get(label)?;172state173.node_ids174.get(self.cursor.system)175.map(|node_id| (*label, NodeId::System(*node_id)))176}177178/// Enable stepping for the provided schedule179pub fn add_schedule(&mut self, schedule: impl ScheduleLabel) -> &mut Self {180self.updates.push(Update::AddSchedule(schedule.intern()));181self182}183184/// Disable stepping for the provided schedule185///186/// NOTE: This function will also clear any system-specific behaviors that187/// may have been configured.188pub fn remove_schedule(&mut self, schedule: impl ScheduleLabel) -> &mut Self {189self.updates.push(Update::RemoveSchedule(schedule.intern()));190self191}192193/// Clear behavior set for all systems in the provided [`Schedule`]194pub fn clear_schedule(&mut self, schedule: impl ScheduleLabel) -> &mut Self {195self.updates.push(Update::ClearSchedule(schedule.intern()));196self197}198199/// Begin stepping at the start of the next frame200pub fn enable(&mut self) -> &mut Self {201#[cfg(feature = "bevy_debug_stepping")]202self.updates.push(Update::SetAction(Action::Waiting));203#[cfg(not(feature = "bevy_debug_stepping"))]204error!(205"Stepping cannot be enabled; \206bevy was compiled without the bevy_debug_stepping feature"207);208self209}210211/// Disable stepping, resume normal systems execution212pub fn disable(&mut self) -> &mut Self {213self.updates.push(Update::SetAction(Action::RunAll));214self215}216217/// Check if stepping is enabled218pub fn is_enabled(&self) -> bool {219self.action != Action::RunAll220}221222/// Run the next system during the next render frame223///224/// NOTE: This will have no impact unless stepping has been enabled225pub fn step_frame(&mut self) -> &mut Self {226self.updates.push(Update::SetAction(Action::Step));227self228}229230/// Run all remaining systems in the stepping frame during the next render231/// frame232///233/// NOTE: This will have no impact unless stepping has been enabled234pub fn continue_frame(&mut self) -> &mut Self {235self.updates.push(Update::SetAction(Action::Continue));236self237}238239/// Ensure this system always runs when stepping is enabled240///241/// Note: if the system is run multiple times in the [`Schedule`], this242/// will apply for all instances of the system.243pub fn always_run<Marker>(244&mut self,245schedule: impl ScheduleLabel,246system: impl IntoSystem<(), (), Marker>,247) -> &mut Self {248let type_id = system.system_type_id();249self.updates.push(Update::SetBehavior(250schedule.intern(),251SystemIdentifier::Type(type_id),252SystemBehavior::AlwaysRun,253));254255self256}257258/// Ensure this system instance always runs when stepping is enabled259pub fn always_run_node(&mut self, schedule: impl ScheduleLabel, node: NodeId) -> &mut Self {260self.updates.push(Update::SetBehavior(261schedule.intern(),262SystemIdentifier::Node(node),263SystemBehavior::AlwaysRun,264));265self266}267268/// Ensure this system never runs when stepping is enabled269pub fn never_run<Marker>(270&mut self,271schedule: impl ScheduleLabel,272system: impl IntoSystem<(), (), Marker>,273) -> &mut Self {274let type_id = system.system_type_id();275self.updates.push(Update::SetBehavior(276schedule.intern(),277SystemIdentifier::Type(type_id),278SystemBehavior::NeverRun,279));280281self282}283284/// Ensure this system instance never runs when stepping is enabled285pub fn never_run_node(&mut self, schedule: impl ScheduleLabel, node: NodeId) -> &mut Self {286self.updates.push(Update::SetBehavior(287schedule.intern(),288SystemIdentifier::Node(node),289SystemBehavior::NeverRun,290));291self292}293294/// Add a breakpoint for system295pub fn set_breakpoint<Marker>(296&mut self,297schedule: impl ScheduleLabel,298system: impl IntoSystem<(), (), Marker>,299) -> &mut Self {300let type_id = system.system_type_id();301self.updates.push(Update::SetBehavior(302schedule.intern(),303SystemIdentifier::Type(type_id),304SystemBehavior::Break,305));306307self308}309310/// Add a breakpoint for system instance311pub fn set_breakpoint_node(&mut self, schedule: impl ScheduleLabel, node: NodeId) -> &mut Self {312self.updates.push(Update::SetBehavior(313schedule.intern(),314SystemIdentifier::Node(node),315SystemBehavior::Break,316));317self318}319320/// Clear a breakpoint for the system321pub fn clear_breakpoint<Marker>(322&mut self,323schedule: impl ScheduleLabel,324system: impl IntoSystem<(), (), Marker>,325) -> &mut Self {326self.clear_system(schedule, system);327328self329}330331/// clear a breakpoint for system instance332pub fn clear_breakpoint_node(333&mut self,334schedule: impl ScheduleLabel,335node: NodeId,336) -> &mut Self {337self.clear_node(schedule, node);338self339}340341/// Clear any behavior set for the system342pub fn clear_system<Marker>(343&mut self,344schedule: impl ScheduleLabel,345system: impl IntoSystem<(), (), Marker>,346) -> &mut Self {347let type_id = system.system_type_id();348self.updates.push(Update::ClearBehavior(349schedule.intern(),350SystemIdentifier::Type(type_id),351));352353self354}355356/// clear a breakpoint for system instance357pub fn clear_node(&mut self, schedule: impl ScheduleLabel, node: NodeId) -> &mut Self {358self.updates.push(Update::ClearBehavior(359schedule.intern(),360SystemIdentifier::Node(node),361));362self363}364365/// lookup the first system for the supplied schedule index366fn first_system_index_for_schedule(&self, index: usize) -> usize {367let Some(label) = self.schedule_order.get(index) else {368return 0;369};370let Some(state) = self.schedule_states.get(label) else {371return 0;372};373state.first.unwrap_or(0)374}375376/// Move the cursor to the start of the first schedule377fn reset_cursor(&mut self) {378self.cursor = Cursor {379schedule: 0,380system: self.first_system_index_for_schedule(0),381};382}383384/// Advance schedule states for the next render frame385fn next_frame(&mut self) {386// if stepping is enabled; reset our internal state for the start of387// the next frame388if self.action != Action::RunAll {389self.action = Action::Waiting;390self.previous_schedule = None;391392// if the cursor passed the last schedule, reset it393if self.cursor.schedule >= self.schedule_order.len() {394self.reset_cursor();395}396}397398if self.updates.is_empty() {399return;400}401402let mut reset_cursor = false;403for update in self.updates.drain(..) {404match update {405Update::SetAction(Action::RunAll) => {406self.action = Action::RunAll;407reset_cursor = true;408}409Update::SetAction(action) => {410// This match block is really just to filter out invalid411// transitions, and add debugging messages for permitted412// transitions. Any action transition that falls through413// this match block will be performed.414#[expect(415clippy::match_same_arms,416reason = "Readability would be negatively impacted by combining the `(Waiting, RunAll)` and `(Continue, RunAll)` match arms."417)]418match (self.action, action) {419// ignore non-transition updates, and prevent a call to420// enable() from overwriting a step or continue call421(Action::RunAll, Action::RunAll)422| (Action::Waiting, Action::Waiting)423| (Action::Continue, Action::Continue)424| (Action::Step, Action::Step)425| (Action::Continue, Action::Waiting)426| (Action::Step, Action::Waiting) => continue,427428// when stepping is disabled429(Action::RunAll, Action::Waiting) => info!("enabled stepping"),430(Action::RunAll, _) => {431warn!(432"stepping not enabled; call Stepping::enable() \433before step_frame() or continue_frame()"434);435continue;436}437438// stepping enabled; waiting439(Action::Waiting, Action::RunAll) => info!("disabled stepping"),440(Action::Waiting, Action::Continue) => info!("continue frame"),441(Action::Waiting, Action::Step) => info!("step frame"),442443// stepping enabled; continue frame444(Action::Continue, Action::RunAll) => info!("disabled stepping"),445(Action::Continue, Action::Step) => {446warn!("ignoring step_frame(); already continuing next frame");447continue;448}449450// stepping enabled; step frame451(Action::Step, Action::RunAll) => info!("disabled stepping"),452(Action::Step, Action::Continue) => {453warn!("ignoring continue_frame(); already stepping next frame");454continue;455}456}457458// permitted action transition; make the change459self.action = action;460}461Update::AddSchedule(l) => {462self.schedule_states.insert(l, ScheduleState::default());463}464Update::RemoveSchedule(label) => {465self.schedule_states.remove(&label);466if let Some(index) = self.schedule_order.iter().position(|l| l == &label) {467self.schedule_order.remove(index);468}469reset_cursor = true;470}471Update::ClearSchedule(label) => match self.schedule_states.get_mut(&label) {472Some(state) => state.clear_behaviors(),473None => {474warn!(475"stepping is not enabled for schedule {label:?}; \476use `.add_stepping({label:?})` to enable stepping"477);478}479},480Update::SetBehavior(label, system, behavior) => {481match self.schedule_states.get_mut(&label) {482Some(state) => state.set_behavior(system, behavior),483None => {484warn!(485"stepping is not enabled for schedule {label:?}; \486use `.add_stepping({label:?})` to enable stepping"487);488}489}490}491Update::ClearBehavior(label, system) => {492match self.schedule_states.get_mut(&label) {493Some(state) => state.clear_behavior(system),494None => {495warn!(496"stepping is not enabled for schedule {label:?}; \497use `.add_stepping({label:?})` to enable stepping"498);499}500}501}502}503}504505if reset_cursor {506self.reset_cursor();507}508}509510/// get the list of systems this schedule should skip for this render511/// frame512pub fn skipped_systems(&mut self, schedule: &Schedule) -> Option<FixedBitSet> {513if self.action == Action::RunAll {514return None;515}516517// grab the label and state for this schedule518let label = schedule.label();519let state = self.schedule_states.get_mut(&label)?;520521// Stepping is enabled, and this schedule is supposed to be stepped.522//523// We need to maintain a list of schedules in the order that they call524// this function. We'll check the ordered list now to see if this525// schedule is present. If not, we'll add it after the last schedule526// that called this function. Finally we want to save off the index of527// this schedule in the ordered schedule list. This is used to528// determine if this is the schedule the cursor is pointed at.529let index = self.schedule_order.iter().position(|l| *l == label);530let index = match (index, self.previous_schedule) {531(Some(index), _) => index,532(None, None) => {533self.schedule_order.insert(0, label);5340535}536(None, Some(last)) => {537self.schedule_order.insert(last + 1, label);538last + 1539}540};541// Update the index of the previous schedule to be the index of this542// schedule for the next call543self.previous_schedule = Some(index);544545#[cfg(test)]546debug!(547"cursor {:?}, index {}, label {:?}",548self.cursor, index, label549);550551// if the stepping frame cursor is pointing at this schedule, we'll run552// the schedule with the current stepping action. If this is not the553// cursor schedule, we'll run the schedule with the waiting action.554let cursor = self.cursor;555let (skip_list, next_system) = if index == cursor.schedule {556let (skip_list, next_system) =557state.skipped_systems(schedule, cursor.system, self.action);558559// if we just stepped this schedule, then we'll switch the action560// to be waiting561if self.action == Action::Step {562self.action = Action::Waiting;563}564(skip_list, next_system)565} else {566// we're not supposed to run any systems in this schedule, so pull567// the skip list, but ignore any changes it makes to the cursor.568let (skip_list, _) = state.skipped_systems(schedule, 0, Action::Waiting);569(skip_list, Some(cursor.system))570};571572// update the stepping frame cursor based on if there are any systems573// remaining to be run in the schedule574// Note: Don't try to detect the end of the render frame here using the575// schedule index. We don't know all schedules have been added to the576// schedule_order, so only next_frame() knows its safe to reset the577// cursor.578match next_system {579Some(i) => self.cursor.system = i,580None => {581let index = cursor.schedule + 1;582self.cursor = Cursor {583schedule: index,584system: self.first_system_index_for_schedule(index),585};586587#[cfg(test)]588debug!("advanced schedule index: {} -> {}", cursor.schedule, index);589}590}591592Some(skip_list)593}594}595596#[derive(Default)]597struct ScheduleState {598/// per-system [`SystemBehavior`]599behaviors: HashMap<NodeId, SystemBehavior>,600601/// order of [`NodeId`]s in the schedule602///603/// This is a cached copy of `SystemExecutable::system_ids`. We need it604/// available here to be accessed by [`Stepping::cursor()`] so we can return605/// [`NodeId`]s to the caller.606node_ids: Vec<SystemKey>,607608/// changes to system behavior that should be applied the next time609/// [`ScheduleState::skipped_systems()`] is called610behavior_updates: TypeIdMap<Option<SystemBehavior>>,611612/// This field contains the first steppable system in the schedule.613first: Option<usize>,614}615616impl ScheduleState {617// set the stepping behavior for a system in this schedule618fn set_behavior(&mut self, system: SystemIdentifier, behavior: SystemBehavior) {619self.first = None;620match system {621SystemIdentifier::Node(node_id) => {622self.behaviors.insert(node_id, behavior);623}624// Behaviors are indexed by NodeId, but we cannot map a system625// TypeId to a NodeId without the `Schedule`. So queue this update626// to be processed the next time `skipped_systems()` is called.627SystemIdentifier::Type(type_id) => {628self.behavior_updates.insert(type_id, Some(behavior));629}630}631}632633// clear the stepping behavior for a system in this schedule634fn clear_behavior(&mut self, system: SystemIdentifier) {635self.first = None;636match system {637SystemIdentifier::Node(node_id) => {638self.behaviors.remove(&node_id);639}640// queue TypeId updates to be processed later when we have Schedule641SystemIdentifier::Type(type_id) => {642self.behavior_updates.insert(type_id, None);643}644}645}646647// clear all system behaviors648fn clear_behaviors(&mut self) {649self.behaviors.clear();650self.behavior_updates.clear();651self.first = None;652}653654// apply system behavior updates by looking up the node id of the system in655// the schedule, and updating `systems`656fn apply_behavior_updates(&mut self, schedule: &Schedule) {657// Systems may be present multiple times within a schedule, so we658// iterate through all systems in the schedule, and check our behavior659// updates for the system TypeId.660// PERF: If we add a way to efficiently query schedule systems by their TypeId, we could remove the full661// system scan here662for (key, system) in schedule.systems().unwrap() {663let behavior = self.behavior_updates.get(&system.type_id());664match behavior {665None => continue,666Some(None) => {667self.behaviors.remove(&NodeId::System(key));668}669Some(Some(behavior)) => {670self.behaviors.insert(NodeId::System(key), *behavior);671}672}673}674self.behavior_updates.clear();675676#[cfg(test)]677debug!("apply_updates(): {:?}", self.behaviors);678}679680fn skipped_systems(681&mut self,682schedule: &Schedule,683start: usize,684mut action: Action,685) -> (FixedBitSet, Option<usize>) {686use core::cmp::Ordering;687688// if our NodeId list hasn't been populated, copy it over from the689// schedule690if self.node_ids.len() != schedule.systems_len() {691self.node_ids.clone_from(&schedule.executable().system_ids);692}693694// Now that we have the schedule, apply any pending system behavior695// updates. The schedule is required to map from system `TypeId` to696// `NodeId`.697if !self.behavior_updates.is_empty() {698self.apply_behavior_updates(schedule);699}700701// if we don't have a first system set, set it now702if self.first.is_none() {703for (i, (key, _)) in schedule.systems().unwrap().enumerate() {704match self.behaviors.get(&NodeId::System(key)) {705Some(SystemBehavior::AlwaysRun | SystemBehavior::NeverRun) => continue,706Some(_) | None => {707self.first = Some(i);708break;709}710}711}712}713714let mut skip = FixedBitSet::with_capacity(schedule.systems_len());715let mut pos = start;716717for (i, (key, _system)) in schedule.systems().unwrap().enumerate() {718let behavior = self719.behaviors720.get(&NodeId::System(key))721.unwrap_or(&SystemBehavior::Continue);722723#[cfg(test)]724debug!(725"skipped_systems(): systems[{}], pos {}, Action::{:?}, Behavior::{:?}, {}",726i,727pos,728action,729behavior,730_system.name()731);732733match (action, behavior) {734// regardless of which action we're performing, if the system735// is marked as NeverRun, add it to the skip list.736// Also, advance the cursor past this system if it is our737// current position738(_, SystemBehavior::NeverRun) => {739skip.insert(i);740if i == pos {741pos += 1;742}743}744// similarly, ignore any system marked as AlwaysRun; they should745// never be added to the skip list746// Also, advance the cursor past this system if it is our747// current position748(_, SystemBehavior::AlwaysRun) => {749if i == pos {750pos += 1;751}752}753// if we're waiting, no other systems besides AlwaysRun should754// be run, so add systems to the skip list755(Action::Waiting, _) => skip.insert(i),756757// If we're stepping, the remaining behaviors don't matter,758// we're only going to run the system at our cursor. Any system759// prior to the cursor is skipped. Once we encounter the system760// at the cursor, we'll advance the cursor, and set behavior to761// Waiting to skip remaining systems.762(Action::Step, _) => match i.cmp(&pos) {763Ordering::Less => skip.insert(i),764Ordering::Equal => {765pos += 1;766action = Action::Waiting;767}768Ordering::Greater => unreachable!(),769},770// If we're continuing, and the step behavior is continue, we771// want to skip any systems prior to our start position. That's772// where the stepping frame left off last time we ran anything.773(Action::Continue, SystemBehavior::Continue) => {774if i < start {775skip.insert(i);776}777}778// If we're continuing, and we encounter a breakpoint we may779// want to stop before executing the system. To do this we780// skip this system and set the action to Waiting.781//782// Note: if the cursor is pointing at this system, we will run783// it anyway. This allows the user to continue, hit a784// breakpoint, then continue again to run the breakpoint system785// and any following systems.786(Action::Continue, SystemBehavior::Break) => {787if i != start {788skip.insert(i);789790// stop running systems if the breakpoint isn't the791// system under the cursor.792if i > start {793action = Action::Waiting;794}795}796}797// should have never gotten into this method if stepping is798// disabled799(Action::RunAll, _) => unreachable!(),800}801802// If we're at the cursor position, and not waiting, advance the803// cursor.804if i == pos && action != Action::Waiting {805pos += 1;806}807}808809// output is the skip list, and the index of the next system to run in810// this schedule.811if pos >= schedule.systems_len() {812(skip, None)813} else {814(skip, Some(pos))815}816}817}818819#[cfg(all(test, feature = "bevy_debug_stepping"))]820#[expect(clippy::print_stdout, reason = "Allowed in tests.")]821mod tests {822use super::*;823use crate::{prelude::*, schedule::ScheduleLabel};824use alloc::{format, vec};825use slotmap::SlotMap;826use std::println;827828#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]829struct TestSchedule;830831#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]832struct TestScheduleA;833834#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]835struct TestScheduleB;836837#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]838struct TestScheduleC;839840#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]841struct TestScheduleD;842843fn first_system() {}844fn second_system() {}845fn third_system() {}846847fn setup() -> (Schedule, World) {848let mut world = World::new();849let mut schedule = Schedule::new(TestSchedule);850schedule.add_systems((first_system, second_system).chain());851schedule.initialize(&mut world).unwrap();852(schedule, world)853}854855// Helper for verifying skip_lists are equal, and if not, printing a human856// readable message.857macro_rules! assert_skip_list_eq {858($actual:expr, $expected:expr, $system_names:expr) => {859let actual = $actual;860let expected = $expected;861let systems: &Vec<&str> = $system_names;862863if (actual != expected) {864use core::fmt::Write as _;865866// mismatch, let's construct a human-readable message of what867// was returned868let mut msg = format!(869"Schedule:\n {:9} {:16}{:6} {:6} {:6}\n",870"index", "name", "expect", "actual", "result"871);872for (i, name) in systems.iter().enumerate() {873let _ = write!(msg, " system[{:1}] {:16}", i, name);874match (expected.contains(i), actual.contains(i)) {875(true, true) => msg.push_str("skip skip pass\n"),876(true, false) => {877msg.push_str("skip run FAILED; system should not have run\n")878}879(false, true) => {880msg.push_str("run skip FAILED; system should have run\n")881}882(false, false) => msg.push_str("run run pass\n"),883}884}885assert_eq!(actual, expected, "{}", msg);886}887};888}889890// Helper for verifying that a set of systems will be run for a given skip891// list892macro_rules! assert_systems_run {893($schedule:expr, $skipped_systems:expr, $($system:expr),*) => {894// pull an ordered list of systems in the schedule, and save the895// system TypeId, and name.896let systems: Vec<(TypeId, alloc::string::String)> = $schedule.systems().unwrap()897.map(|(_, system)| {898(system.type_id(), system.name().as_string())899})900.collect();901902// construct a list of systems that are expected to run903let mut expected = FixedBitSet::with_capacity(systems.len());904$(905let sys = IntoSystem::into_system($system);906for (i, (type_id, _)) in systems.iter().enumerate() {907if sys.type_id() == *type_id {908expected.insert(i);909}910}911)*912913// flip the run list to get our skip list914expected.toggle_range(..);915916// grab the list of skipped systems917let actual = match $skipped_systems {918None => FixedBitSet::with_capacity(systems.len()),919Some(b) => b,920};921let system_names: Vec<&str> = systems922.iter()923.map(|(_,n)| n.rsplit_once("::").unwrap().1)924.collect();925926assert_skip_list_eq!(actual, expected, &system_names);927};928}929930// Helper for verifying the expected systems will be run by the schedule931//932// This macro will construct an expected FixedBitSet for the systems that933// should be skipped, and compare it with the results from stepping the934// provided schedule. If they don't match, it generates a human-readable935// error message and asserts.936macro_rules! assert_schedule_runs {937($schedule:expr, $stepping:expr, $($system:expr),*) => {938// advance stepping to the next frame, and build the skip list for939// this schedule940$stepping.next_frame();941assert_systems_run!($schedule, $stepping.skipped_systems($schedule), $($system),*);942};943}944945#[test]946fn stepping_disabled() {947let (schedule, _world) = setup();948949let mut stepping = Stepping::new();950stepping.add_schedule(TestSchedule).disable().next_frame();951952assert!(stepping.skipped_systems(&schedule).is_none());953assert!(stepping.cursor().is_none());954}955956#[test]957fn unknown_schedule() {958let (schedule, _world) = setup();959960let mut stepping = Stepping::new();961stepping.enable().next_frame();962963assert!(stepping.skipped_systems(&schedule).is_none());964}965966#[test]967fn disabled_always_run() {968let (schedule, _world) = setup();969970let mut stepping = Stepping::new();971stepping972.add_schedule(TestSchedule)973.disable()974.always_run(TestSchedule, first_system);975976assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system);977}978979#[test]980fn waiting_always_run() {981let (schedule, _world) = setup();982983let mut stepping = Stepping::new();984stepping985.add_schedule(TestSchedule)986.enable()987.always_run(TestSchedule, first_system);988989assert_schedule_runs!(&schedule, &mut stepping, first_system);990}991992#[test]993fn step_always_run() {994let (schedule, _world) = setup();995996let mut stepping = Stepping::new();997stepping998.add_schedule(TestSchedule)999.enable()1000.always_run(TestSchedule, first_system)1001.step_frame();10021003assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system);1004}10051006#[test]1007fn continue_always_run() {1008let (schedule, _world) = setup();10091010let mut stepping = Stepping::new();1011stepping1012.add_schedule(TestSchedule)1013.enable()1014.always_run(TestSchedule, first_system)1015.continue_frame();10161017assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system);1018}10191020#[test]1021fn disabled_never_run() {1022let (schedule, _world) = setup();10231024let mut stepping = Stepping::new();1025stepping1026.add_schedule(TestSchedule)1027.never_run(TestSchedule, first_system)1028.disable();1029assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system);1030}10311032#[test]1033fn waiting_never_run() {1034let (schedule, _world) = setup();10351036let mut stepping = Stepping::new();1037stepping1038.add_schedule(TestSchedule)1039.enable()1040.never_run(TestSchedule, first_system);10411042assert_schedule_runs!(&schedule, &mut stepping,);1043}10441045#[test]1046fn step_never_run() {1047let (schedule, _world) = setup();10481049let mut stepping = Stepping::new();1050stepping1051.add_schedule(TestSchedule)1052.enable()1053.never_run(TestSchedule, first_system)1054.step_frame();10551056assert_schedule_runs!(&schedule, &mut stepping, second_system);1057}10581059#[test]1060fn continue_never_run() {1061let (schedule, _world) = setup();10621063let mut stepping = Stepping::new();1064stepping1065.add_schedule(TestSchedule)1066.enable()1067.never_run(TestSchedule, first_system)1068.continue_frame();10691070assert_schedule_runs!(&schedule, &mut stepping, second_system);1071}10721073#[test]1074fn disabled_breakpoint() {1075let (schedule, _world) = setup();10761077let mut stepping = Stepping::new();1078stepping1079.add_schedule(TestSchedule)1080.disable()1081.set_breakpoint(TestSchedule, second_system);10821083assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system);1084}10851086#[test]1087fn waiting_breakpoint() {1088let (schedule, _world) = setup();10891090let mut stepping = Stepping::new();1091stepping1092.add_schedule(TestSchedule)1093.enable()1094.set_breakpoint(TestSchedule, second_system);10951096assert_schedule_runs!(&schedule, &mut stepping,);1097}10981099#[test]1100fn step_breakpoint() {1101let (schedule, _world) = setup();11021103let mut stepping = Stepping::new();1104stepping1105.add_schedule(TestSchedule)1106.enable()1107.set_breakpoint(TestSchedule, second_system)1108.step_frame();11091110// since stepping stops at every system, breakpoints are ignored during1111// stepping1112assert_schedule_runs!(&schedule, &mut stepping, first_system);1113stepping.step_frame();1114assert_schedule_runs!(&schedule, &mut stepping, second_system);11151116// let's go again to verify that we wrap back around to the start of1117// the frame1118stepping.step_frame();1119assert_schedule_runs!(&schedule, &mut stepping, first_system);11201121// should be back in a waiting state now that it ran first_system1122assert_schedule_runs!(&schedule, &mut stepping,);1123}11241125#[test]1126fn continue_breakpoint() {1127let (schedule, _world) = setup();11281129let mut stepping = Stepping::new();1130stepping1131.add_schedule(TestSchedule)1132.enable()1133.set_breakpoint(TestSchedule, second_system)1134.continue_frame();11351136assert_schedule_runs!(&schedule, &mut stepping, first_system);1137stepping.continue_frame();1138assert_schedule_runs!(&schedule, &mut stepping, second_system);1139stepping.continue_frame();1140assert_schedule_runs!(&schedule, &mut stepping, first_system);1141}11421143/// regression test for issue encountered while writing `system_stepping`1144/// example1145#[test]1146fn continue_step_continue_with_breakpoint() {1147let mut world = World::new();1148let mut schedule = Schedule::new(TestSchedule);1149schedule.add_systems((first_system, second_system, third_system).chain());1150schedule.initialize(&mut world).unwrap();11511152let mut stepping = Stepping::new();1153stepping1154.add_schedule(TestSchedule)1155.enable()1156.set_breakpoint(TestSchedule, second_system);11571158stepping.continue_frame();1159assert_schedule_runs!(&schedule, &mut stepping, first_system);11601161stepping.step_frame();1162assert_schedule_runs!(&schedule, &mut stepping, second_system);11631164stepping.continue_frame();1165assert_schedule_runs!(&schedule, &mut stepping, third_system);1166}11671168#[test]1169fn clear_breakpoint() {1170let (schedule, _world) = setup();11711172let mut stepping = Stepping::new();1173stepping1174.add_schedule(TestSchedule)1175.enable()1176.set_breakpoint(TestSchedule, second_system)1177.continue_frame();11781179assert_schedule_runs!(&schedule, &mut stepping, first_system);1180stepping.continue_frame();1181assert_schedule_runs!(&schedule, &mut stepping, second_system);11821183stepping.clear_breakpoint(TestSchedule, second_system);1184stepping.continue_frame();1185assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system);1186}11871188#[test]1189fn clear_system() {1190let (schedule, _world) = setup();11911192let mut stepping = Stepping::new();1193stepping1194.add_schedule(TestSchedule)1195.enable()1196.never_run(TestSchedule, second_system)1197.continue_frame();1198assert_schedule_runs!(&schedule, &mut stepping, first_system);11991200stepping.clear_system(TestSchedule, second_system);1201stepping.continue_frame();1202assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system);1203}12041205#[test]1206fn clear_schedule() {1207let (schedule, _world) = setup();12081209let mut stepping = Stepping::new();1210stepping1211.add_schedule(TestSchedule)1212.enable()1213.never_run(TestSchedule, first_system)1214.never_run(TestSchedule, second_system)1215.continue_frame();1216assert_schedule_runs!(&schedule, &mut stepping,);12171218stepping.clear_schedule(TestSchedule);1219stepping.continue_frame();1220assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system);1221}12221223/// This was discovered in code-review, ensure that `clear_schedule` also1224/// clears any pending changes too.1225#[test]1226fn set_behavior_then_clear_schedule() {1227let (schedule, _world) = setup();12281229let mut stepping = Stepping::new();1230stepping1231.add_schedule(TestSchedule)1232.enable()1233.continue_frame();1234assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system);12351236stepping.never_run(TestSchedule, first_system);1237stepping.clear_schedule(TestSchedule);1238stepping.continue_frame();1239assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system);1240}12411242/// Ensure that if they `clear_schedule` then make further changes to the1243/// schedule, those changes after the clear are applied.1244#[test]1245fn clear_schedule_then_set_behavior() {1246let (schedule, _world) = setup();12471248let mut stepping = Stepping::new();1249stepping1250.add_schedule(TestSchedule)1251.enable()1252.continue_frame();1253assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system);12541255stepping.clear_schedule(TestSchedule);1256stepping.never_run(TestSchedule, first_system);1257stepping.continue_frame();1258assert_schedule_runs!(&schedule, &mut stepping, second_system);1259}12601261// Schedules such as FixedUpdate can be called multiple times in a single1262// render frame. Ensure we only run steppable systems the first time the1263// schedule is run1264#[test]1265fn multiple_calls_per_frame_continue() {1266let (schedule, _world) = setup();12671268let mut stepping = Stepping::new();1269stepping1270.add_schedule(TestSchedule)1271.enable()1272.always_run(TestSchedule, second_system)1273.continue_frame();12741275// start a new frame, then run the schedule two times; first system1276// should only run on the first one1277stepping.next_frame();1278assert_systems_run!(1279&schedule,1280stepping.skipped_systems(&schedule),1281first_system,1282second_system1283);1284assert_systems_run!(1285&schedule,1286stepping.skipped_systems(&schedule),1287second_system1288);1289}1290#[test]1291fn multiple_calls_per_frame_step() {1292let (schedule, _world) = setup();12931294let mut stepping = Stepping::new();1295stepping.add_schedule(TestSchedule).enable().step_frame();12961297// start a new frame, then run the schedule two times; first system1298// should only run on the first one1299stepping.next_frame();1300assert_systems_run!(&schedule, stepping.skipped_systems(&schedule), first_system);1301assert_systems_run!(&schedule, stepping.skipped_systems(&schedule),);1302}13031304#[test]1305fn step_duplicate_systems() {1306let mut world = World::new();1307let mut schedule = Schedule::new(TestSchedule);1308schedule.add_systems((first_system, first_system, second_system).chain());1309schedule.initialize(&mut world).unwrap();13101311let mut stepping = Stepping::new();1312stepping.add_schedule(TestSchedule).enable();13131314// needed for assert_skip_list_eq!1315let system_names = vec!["first_system", "first_system", "second_system"];1316// we're going to step three times, and each system in order should run1317// only once1318for system_index in 0..3 {1319// build the skip list by setting all bits, then clearing our the1320// one system that should run this step1321let mut expected = FixedBitSet::with_capacity(3);1322expected.set_range(.., true);1323expected.set(system_index, false);13241325// step the frame and get the skip list1326stepping.step_frame();1327stepping.next_frame();1328let skip_list = stepping1329.skipped_systems(&schedule)1330.expect("TestSchedule has been added to Stepping");13311332assert_skip_list_eq!(skip_list, expected, &system_names);1333}1334}13351336#[test]1337fn step_run_if_false() {1338let mut world = World::new();1339let mut schedule = Schedule::new(TestSchedule);13401341// This needs to be a system test to confirm the interaction between1342// the skip list and system conditions in Schedule::run(). That means1343// all of our systems need real bodies that do things.1344//1345// first system will be configured as `run_if(|| false)`, so it can1346// just panic if called1347let first_system: fn() = move || {1348panic!("first_system should not be run");1349};13501351// The second system, we need to know when it has been called, so we'll1352// add a resource for tracking if it has been run. The system will1353// increment the run count.1354#[derive(Resource)]1355struct RunCount(usize);1356world.insert_resource(RunCount(0));1357let second_system = |mut run_count: ResMut<RunCount>| {1358println!("I have run!");1359run_count.0 += 1;1360};13611362// build our schedule; first_system should never run, followed by1363// second_system.1364schedule.add_systems((first_system.run_if(|| false), second_system).chain());1365schedule.initialize(&mut world).unwrap();13661367// set up stepping1368let mut stepping = Stepping::new();1369stepping.add_schedule(TestSchedule).enable();1370world.insert_resource(stepping);13711372// if we step, and the run condition is false, we should not run1373// second_system. The stepping cursor is at first_system, and if1374// first_system wasn't able to run, that's ok.1375let mut stepping = world.resource_mut::<Stepping>();1376stepping.step_frame();1377stepping.next_frame();1378schedule.run(&mut world);1379assert_eq!(1380world.resource::<RunCount>().0,13810,1382"second_system should not have run"1383);13841385// now on the next step, second_system should run1386let mut stepping = world.resource_mut::<Stepping>();1387stepping.step_frame();1388stepping.next_frame();1389schedule.run(&mut world);1390assert_eq!(1391world.resource::<RunCount>().0,13921,1393"second_system should have run"1394);1395}13961397#[test]1398fn remove_schedule() {1399let (schedule, _world) = setup();1400let mut stepping = Stepping::new();1401stepping.add_schedule(TestSchedule).enable();14021403// run the schedule once and verify all systems are skipped1404assert_schedule_runs!(&schedule, &mut stepping,);1405assert!(!stepping.schedules().unwrap().is_empty());14061407// remove the test schedule1408stepping.remove_schedule(TestSchedule);1409assert_schedule_runs!(&schedule, &mut stepping, first_system, second_system);1410assert!(stepping.schedules().unwrap().is_empty());1411}14121413// verify that Stepping can construct an ordered list of schedules1414#[test]1415fn schedules() {1416let mut world = World::new();14171418// build & initialize a few schedules1419let mut schedule_a = Schedule::new(TestScheduleA);1420schedule_a.initialize(&mut world).unwrap();1421let mut schedule_b = Schedule::new(TestScheduleB);1422schedule_b.initialize(&mut world).unwrap();1423let mut schedule_c = Schedule::new(TestScheduleC);1424schedule_c.initialize(&mut world).unwrap();1425let mut schedule_d = Schedule::new(TestScheduleD);1426schedule_d.initialize(&mut world).unwrap();14271428// setup stepping and add all the schedules1429let mut stepping = Stepping::new();1430stepping1431.add_schedule(TestScheduleA)1432.add_schedule(TestScheduleB)1433.add_schedule(TestScheduleC)1434.add_schedule(TestScheduleD)1435.enable()1436.next_frame();14371438assert!(stepping.schedules().is_err());14391440stepping.skipped_systems(&schedule_b);1441assert!(stepping.schedules().is_err());1442stepping.skipped_systems(&schedule_a);1443assert!(stepping.schedules().is_err());1444stepping.skipped_systems(&schedule_c);1445assert!(stepping.schedules().is_err());14461447// when we call the last schedule, Stepping should have enough data to1448// return an ordered list of schedules1449stepping.skipped_systems(&schedule_d);1450assert!(stepping.schedules().is_ok());14511452assert_eq!(1453*stepping.schedules().unwrap(),1454vec![1455TestScheduleB.intern(),1456TestScheduleA.intern(),1457TestScheduleC.intern(),1458TestScheduleD.intern(),1459]1460);1461}14621463#[test]1464fn verify_cursor() {1465// helper to build a cursor tuple for the supplied schedule1466fn cursor(schedule: &Schedule, index: usize) -> (InternedScheduleLabel, NodeId) {1467let node_id = schedule.executable().system_ids[index];1468(schedule.label(), NodeId::System(node_id))1469}14701471let mut world = World::new();1472let mut slotmap = SlotMap::<SystemKey, ()>::with_key();14731474// create two schedules with a number of systems in them1475let mut schedule_a = Schedule::new(TestScheduleA);1476schedule_a.add_systems((|| {}, || {}, || {}, || {}).chain());1477schedule_a.initialize(&mut world).unwrap();1478let mut schedule_b = Schedule::new(TestScheduleB);1479schedule_b.add_systems((|| {}, || {}, || {}, || {}).chain());1480schedule_b.initialize(&mut world).unwrap();14811482// setup stepping and add all schedules1483let mut stepping = Stepping::new();1484stepping1485.add_schedule(TestScheduleA)1486.add_schedule(TestScheduleB)1487.enable();14881489assert!(stepping.cursor().is_none());14901491// step the system nine times, and verify the cursor before & after1492// each step1493let mut cursors = Vec::new();1494for _ in 0..9 {1495stepping.step_frame().next_frame();1496cursors.push(stepping.cursor());1497stepping.skipped_systems(&schedule_a);1498stepping.skipped_systems(&schedule_b);1499cursors.push(stepping.cursor());1500}15011502#[rustfmt::skip]1503assert_eq!(1504cursors,1505vec![1506// before render frame // after render frame1507None, Some(cursor(&schedule_a, 1)),1508Some(cursor(&schedule_a, 1)), Some(cursor(&schedule_a, 2)),1509Some(cursor(&schedule_a, 2)), Some(cursor(&schedule_a, 3)),1510Some(cursor(&schedule_a, 3)), Some(cursor(&schedule_b, 0)),1511Some(cursor(&schedule_b, 0)), Some(cursor(&schedule_b, 1)),1512Some(cursor(&schedule_b, 1)), Some(cursor(&schedule_b, 2)),1513Some(cursor(&schedule_b, 2)), Some(cursor(&schedule_b, 3)),1514Some(cursor(&schedule_b, 3)), None,1515Some(cursor(&schedule_a, 0)), Some(cursor(&schedule_a, 1)),1516]1517);15181519let sys0 = slotmap.insert(());1520let sys1 = slotmap.insert(());1521let _sys2 = slotmap.insert(());1522let sys3 = slotmap.insert(());15231524// reset our cursor (disable/enable), and update stepping to test if the1525// cursor properly skips over AlwaysRun & NeverRun systems. Also set1526// a Break system to ensure that shows properly in the cursor1527stepping1528// disable/enable to reset cursor1529.disable()1530.enable()1531.set_breakpoint_node(TestScheduleA, NodeId::System(sys1))1532.always_run_node(TestScheduleA, NodeId::System(sys3))1533.never_run_node(TestScheduleB, NodeId::System(sys0));15341535let mut cursors = Vec::new();1536for _ in 0..9 {1537stepping.step_frame().next_frame();1538cursors.push(stepping.cursor());1539stepping.skipped_systems(&schedule_a);1540stepping.skipped_systems(&schedule_b);1541cursors.push(stepping.cursor());1542}15431544#[rustfmt::skip]1545assert_eq!(1546cursors,1547vec![1548// before render frame // after render frame1549Some(cursor(&schedule_a, 0)), Some(cursor(&schedule_a, 1)),1550Some(cursor(&schedule_a, 1)), Some(cursor(&schedule_a, 2)),1551Some(cursor(&schedule_a, 2)), Some(cursor(&schedule_b, 1)),1552Some(cursor(&schedule_b, 1)), Some(cursor(&schedule_b, 2)),1553Some(cursor(&schedule_b, 2)), Some(cursor(&schedule_b, 3)),1554Some(cursor(&schedule_b, 3)), None,1555Some(cursor(&schedule_a, 0)), Some(cursor(&schedule_a, 1)),1556Some(cursor(&schedule_a, 1)), Some(cursor(&schedule_a, 2)),1557Some(cursor(&schedule_a, 2)), Some(cursor(&schedule_b, 1)),1558]1559);1560}1561}156215631564