From 3016723e0dd0d7c12afaff4425b612114cc6b707 Mon Sep 17 00:00:00 2001 From: Christian Hughes Date: Sat, 15 Nov 2025 17:31:18 +0100 Subject: [PATCH] Schedule::run_system_set --- benches/benches/bevy_ecs/scheduling/mod.rs | 1 + .../benches/bevy_ecs/scheduling/schedule.rs | 120 +++++++++++++++ crates/bevy_ecs/src/schedule/executor/mod.rs | 8 +- .../src/schedule/executor/multi_threaded.rs | 33 +++- .../src/schedule/executor/single_threaded.rs | 14 ++ crates/bevy_ecs/src/schedule/schedule.rs | 143 +++++++++++++++++- .../bevy_ecs/src/system/commands/command.rs | 17 +++ crates/bevy_ecs/src/system/commands/mod.rs | 55 +++++++ crates/bevy_ecs/src/world/mod.rs | 114 +++++++++++++- .../release-notes/run_system_sets.md | 44 ++++++ 10 files changed, 543 insertions(+), 6 deletions(-) create mode 100644 release-content/release-notes/run_system_sets.md diff --git a/benches/benches/bevy_ecs/scheduling/mod.rs b/benches/benches/bevy_ecs/scheduling/mod.rs index 310662e629cad..1c39422c097e8 100644 --- a/benches/benches/bevy_ecs/scheduling/mod.rs +++ b/benches/benches/bevy_ecs/scheduling/mod.rs @@ -19,4 +19,5 @@ criterion_group!( schedule, build_schedule, empty_schedule_run, + run_schedule, ); diff --git a/benches/benches/bevy_ecs/scheduling/schedule.rs b/benches/benches/bevy_ecs/scheduling/schedule.rs index d7e78dd4a6379..8e5738fdc5e67 100644 --- a/benches/benches/bevy_ecs/scheduling/schedule.rs +++ b/benches/benches/bevy_ecs/scheduling/schedule.rs @@ -138,3 +138,123 @@ pub fn empty_schedule_run(criterion: &mut Criterion) { group.finish(); } + +pub fn run_schedule(criterion: &mut Criterion) { + #[derive(Component)] + struct A(f32); + #[derive(Component)] + struct B(f32); + #[derive(Component)] + struct C(f32); + + #[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)] + struct SetA; + #[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)] + struct SetB; + #[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)] + struct SetC; + + fn system_a(mut query: Query<&mut A>) { + query.iter_mut().for_each(|mut a| { + a.0 += 1.0; + }); + } + + fn system_b(mut query: Query<&mut B>) { + query.iter_mut().for_each(|mut b| { + b.0 += 1.0; + }); + } + + fn system_c(mut query: Query<&mut C>) { + query.iter_mut().for_each(|mut c| { + c.0 += 1.0; + }); + } + + let mut group = criterion.benchmark_group("run_schedule"); + group.warm_up_time(core::time::Duration::from_millis(500)); + group.measurement_time(core::time::Duration::from_secs(4)); + + group.bench_function("full/single_threaded", |bencher| { + let mut world = World::default(); + world.spawn_batch((0..10000).map(|_| (A(0.0), B(0.0), C(0.0)))); + + let mut schedule = Schedule::default(); + schedule.set_executor_kind(bevy_ecs::schedule::ExecutorKind::SingleThreaded); + schedule.add_systems(system_a.in_set(SetA)); + schedule.add_systems(system_b.in_set(SetB)); + schedule.add_systems(system_c.in_set(SetC)); + schedule.run(&mut world); + + bencher.iter(|| schedule.run(&mut world)); + }); + + group.bench_function("full/multi_threaded", |bencher| { + let mut world = World::default(); + world.spawn_batch((0..10000).map(|_| (A(0.0), B(0.0), C(0.0)))); + + let mut schedule = Schedule::default(); + schedule.set_executor_kind(bevy_ecs::schedule::ExecutorKind::MultiThreaded); + schedule.add_systems(system_a.in_set(SetA)); + schedule.add_systems(system_b.in_set(SetB)); + schedule.add_systems(system_c.in_set(SetC)); + schedule.run(&mut world); + + bencher.iter(|| schedule.run(&mut world)); + }); + + group.bench_function("single_system_in_set/single_threaded", |bencher| { + let mut world = World::default(); + world.spawn_batch((0..10000).map(|_| (A(0.0), B(0.0), C(0.0)))); + + let mut schedule = Schedule::default(); + schedule.set_executor_kind(bevy_ecs::schedule::ExecutorKind::SingleThreaded); + schedule.add_systems(system_a.in_set(SetA)); + schedule.add_systems(system_b.in_set(SetB)); + schedule.add_systems(system_c.in_set(SetC)); + schedule.run_system_set(&mut world, SetB); + + bencher.iter(|| schedule.run_system_set(&mut world, SetB)); + }); + + group.bench_function("single_system_in_set/multi_threaded", |bencher| { + let mut world = World::default(); + world.spawn_batch((0..10000).map(|_| (A(0.0), B(0.0), C(0.0)))); + + let mut schedule = Schedule::default(); + schedule.set_executor_kind(bevy_ecs::schedule::ExecutorKind::MultiThreaded); + schedule.add_systems(system_a.in_set(SetA)); + schedule.add_systems(system_b.in_set(SetB)); + schedule.add_systems(system_c.in_set(SetC)); + schedule.run_system_set(&mut world, SetB); + + bencher.iter(|| schedule.run_system_set(&mut world, SetB)); + }); + + group.bench_function("multiple_systems_in_set/single_threaded", |bencher| { + let mut world = World::default(); + world.spawn_batch((0..10000).map(|_| (A(0.0), B(0.0), C(0.0)))); + + let mut schedule = Schedule::default(); + schedule.set_executor_kind(bevy_ecs::schedule::ExecutorKind::SingleThreaded); + schedule.add_systems((system_a, system_b, system_c).in_set(SetA)); + schedule.run_system_set(&mut world, SetA); + + bencher.iter(|| schedule.run_system_set(&mut world, SetA)); + }); + + group.bench_function("multiple_systems_in_set/multi_threaded", |bencher| { + let mut world = World::default(); + world.spawn_batch((0..10000).map(|_| (A(0.0), B(0.0), C(0.0)))); + + let mut schedule = Schedule::default(); + schedule.set_executor_kind(bevy_ecs::schedule::ExecutorKind::MultiThreaded); + schedule.add_systems((system_a, system_b, system_c).in_set(SetA)); + schedule.run_system_set(&mut world, SetA); + + bencher.iter(|| schedule.run_system_set(&mut world, SetA)); + }); + + group.finish(); +} diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index 197ba52be51c5..d4fb0b9bb502d 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -3,6 +3,7 @@ mod multi_threaded; mod single_threaded; use alloc::{vec, vec::Vec}; +use bevy_platform::collections::HashMap; use bevy_utils::prelude::DebugName; use core::any::TypeId; @@ -34,6 +35,7 @@ pub(super) trait SystemExecutor: Send + Sync { &mut self, schedule: &mut SystemSchedule, world: &mut World, + subgraph: Option, skip_systems: Option<&FixedBitSet>, error_handler: fn(BevyError, ErrorContext), ); @@ -105,11 +107,14 @@ pub struct SystemSchedule { /// /// If a set doesn't run because of its conditions, this is used to skip all systems in it. pub(super) systems_in_sets_with_conditions: Vec, + /// Sparse mapping of system set node id to systems in the set, used for running + /// subgraphs. This is filled lazily when a subgraph is run for the first time. + pub(super) systems_in_sets: HashMap, } impl SystemSchedule { /// Creates an empty [`SystemSchedule`]. - pub const fn new() -> Self { + pub fn new() -> Self { Self { systems: Vec::new(), system_conditions: Vec::new(), @@ -120,6 +125,7 @@ impl SystemSchedule { system_dependents: Vec::new(), sets_with_conditions_of_systems: Vec::new(), systems_in_sets_with_conditions: Vec::new(), + systems_in_sets: HashMap::default(), } } } diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index 497b937c31f51..4967d65c196ba 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -17,7 +17,7 @@ use crate::{ prelude::Resource, schedule::{ is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, SystemSchedule, - SystemWithAccess, + SystemSetKey, SystemWithAccess, }, system::{RunSystemError, ScheduleSystem}, world::{unsafe_world_cell::UnsafeWorldCell, World}, @@ -239,6 +239,7 @@ impl SystemExecutor for MultiThreadedExecutor { &mut self, schedule: &mut SystemSchedule, world: &mut World, + subgraph: Option, _skip_systems: Option<&FixedBitSet>, error_handler: ErrorHandler, ) { @@ -253,6 +254,23 @@ impl SystemExecutor for MultiThreadedExecutor { .clone_from(&schedule.system_dependencies); state.ready_systems.clone_from(&self.starting_systems); + if let Some(set) = subgraph { + // Get the systems in the set + let systems_in_set = schedule + .systems_in_sets + .get(&set) + .expect("System set not found in schedule."); + + // Mark all systems in the set as completed (waiting to be flipped) + state.completed_systems = systems_in_set.clone(); + // Flip all bits to get the systems not in the set + state.completed_systems.toggle_range(..); + // Signal dependents of systems not in the set, as though they had run + state.signal_all_dependents(); + // Only mark systems in the set as ready to run + state.ready_systems.intersect_with(systems_in_set); + } + // If stepping is enabled, make sure we skip those systems that should // not be run. #[cfg(feature = "bevy_debug_stepping")] @@ -795,6 +813,19 @@ impl ExecutorState { } } } + + fn signal_all_dependents(&mut self) { + for system_index in self.completed_systems.ones() { + for &dep_idx in &self.system_task_metadata[system_index].dependents { + let remaining = &mut self.num_dependencies_remaining[dep_idx]; + debug_assert!(*remaining >= 1); + *remaining -= 1; + if *remaining == 0 && !self.completed_systems.contains(dep_idx) { + self.ready_systems.insert(dep_idx); + } + } + } + } } fn apply_deferred( diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index d1a519a5c1d49..eaf065494d9b2 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -13,6 +13,7 @@ use crate::{ error::{ErrorContext, ErrorHandler}, schedule::{ is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, SystemSchedule, + SystemSetKey, }, system::{RunSystemError, ScheduleSystem}, world::World, @@ -57,9 +58,22 @@ impl SystemExecutor for SingleThreadedExecutor { &mut self, schedule: &mut SystemSchedule, world: &mut World, + subgraph: Option, _skip_systems: Option<&FixedBitSet>, error_handler: ErrorHandler, ) { + if let Some(set) = subgraph { + // Get the systems in the set + let systems_in_set = schedule + .systems_in_sets + .get(&set) + .expect("System set not found in schedule."); + // Mark all systems in the set as completed (waiting to be flipped) + self.completed_systems = systems_in_set.clone(); + // Flip all bits to get the systems not in the set + self.completed_systems.toggle_range(..); + } + // If stepping is enabled, make sure we skip those systems that should // not be run. #[cfg(feature = "bevy_debug_stepping")] diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index ddcd844cb19cf..58356656be2fa 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -10,7 +10,7 @@ use alloc::{ vec, vec::Vec, }; -use bevy_platform::collections::{HashMap, HashSet}; +use bevy_platform::collections::{hash_map::Entry, HashMap, HashSet}; use bevy_utils::{default, TypeIdMap}; use core::{ any::{Any, TypeId}, @@ -537,7 +537,7 @@ impl Schedule { #[cfg(not(feature = "bevy_debug_stepping"))] self.executor - .run(&mut self.executable, world, None, error_handler); + .run(&mut self.executable, world, None, None, error_handler); #[cfg(feature = "bevy_debug_stepping")] { @@ -549,6 +549,73 @@ impl Schedule { self.executor.run( &mut self.executable, world, + None, + skip_systems.as_ref(), + error_handler, + ); + } + } + + /// Runs all systems in the specified system set (including transitively) on + /// the `world`, using its current execution strategy. + pub fn run_system_set(&mut self, world: &mut World, set: impl IntoSystemSet) { + #[cfg(feature = "trace")] + let _span = info_span!("schedule", name = ?self.label).entered(); + + world.check_change_ticks(); + self.initialize(world).unwrap_or_else(|e| { + panic!( + "Error when initializing schedule {:?}: {}", + self.label, + e.to_string(self.graph(), world) + ) + }); + + let set = set.into_system_set().intern(); + let Some(set_key) = self.graph.system_sets.get_key(set) else { + return; + }; + // Lazily initialize `systems_in_set` with the newly requested set. + if let Entry::Vacant(entry) = self.executable.systems_in_sets.entry(set_key) { + let Ok(systems_in_set) = self.graph.systems_in_set(set) else { + // Just log and do nothing if the set does not exist. + warn!( + "Tried to run non-existent system set {:?} in schedule {:?}.", + set, self.label + ); + return; + }; + let mut systems = FixedBitSet::with_capacity(self.executable.systems.len()); + for (index, key) in self.executable.system_ids.iter().enumerate() { + if systems_in_set.contains(key) { + systems.insert(index); + } + } + entry.insert(systems); + } + + let error_handler = world.default_error_handler(); + + #[cfg(not(feature = "bevy_debug_stepping"))] + self.executor.run( + &mut self.executable, + world, + Some(set_key), + None, + error_handler, + ); + + #[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, + world, + Some(set_key), skip_systems.as_ref(), error_handler, ); @@ -905,7 +972,7 @@ impl ScheduleGraph { AnonymousSet::new(id) } - /// Returns a `Vec` containing all [`SystemKey`]s in a [`SystemSet`]. + /// Returns a `HashSet` containing all [`SystemKey`]s in a [`SystemSet`]. /// /// # Errors /// @@ -1301,6 +1368,7 @@ impl ScheduleGraph { system_dependents, sets_with_conditions_of_systems, systems_in_sets_with_conditions, + systems_in_sets: HashMap::default(), } } @@ -2565,4 +2633,73 @@ mod tests { let conflicts = schedule.graph().conflicting_systems(); assert!(conflicts.is_empty()); } + + mod run_system_set { + use bevy_ecs_macros::SystemSet; + + use crate::{ + prelude::{IntoScheduleConfigs, Resource, Schedule}, + schedule::ExecutorKind, + system::ResMut, + world::World, + }; + + #[derive(SystemSet, Clone, Copy, PartialEq, Eq, Debug, Hash)] + enum SystemSets { + Foo, + Bar, + Baz, + } + + #[derive(Resource)] + struct Counter(usize); + + fn increment_counter(mut counter: ResMut) { + counter.0 += 1; + } + + #[test] + fn test_single_threaded() { + let mut schedule = Schedule::default(); + schedule.set_executor_kind(ExecutorKind::SingleThreaded); + schedule.add_systems(( + increment_counter.in_set(SystemSets::Foo), + (increment_counter, increment_counter).in_set(SystemSets::Bar), + )); + + let mut world = World::new(); + world.insert_resource(Counter(0)); + + schedule.run_system_set(&mut world, SystemSets::Foo); + assert_eq!(world.get_resource::().unwrap().0, 1); + + schedule.run_system_set(&mut world, SystemSets::Bar); + assert_eq!(world.get_resource::().unwrap().0, 3); + + schedule.run_system_set(&mut world, SystemSets::Baz); + assert_eq!(world.get_resource::().unwrap().0, 3); + } + + #[test] + fn test_multi_threaded() { + let mut schedule = Schedule::default(); + schedule.set_executor_kind(ExecutorKind::MultiThreaded); + schedule.add_systems(( + increment_counter.in_set(SystemSets::Foo), + (increment_counter, increment_counter).in_set(SystemSets::Bar), + )); + + let mut world = World::new(); + world.insert_resource(Counter(0)); + + schedule.run_system_set(&mut world, SystemSets::Foo); + assert_eq!(world.get_resource::().unwrap().0, 1); + + schedule.run_system_set(&mut world, SystemSets::Bar); + assert_eq!(world.get_resource::().unwrap().0, 3); + + schedule.run_system_set(&mut world, SystemSets::Baz); + assert_eq!(world.get_resource::().unwrap().0, 3); + } + } } diff --git a/crates/bevy_ecs/src/system/commands/command.rs b/crates/bevy_ecs/src/system/commands/command.rs index 70e917471a77a..5e065f34b518c 100644 --- a/crates/bevy_ecs/src/system/commands/command.rs +++ b/crates/bevy_ecs/src/system/commands/command.rs @@ -11,6 +11,7 @@ use crate::{ error::Result, event::Event, message::{Message, Messages}, + prelude::IntoSystemSet, resource::Resource, schedule::ScheduleLabel, system::{IntoSystem, SystemId, SystemInput}, @@ -208,6 +209,22 @@ pub fn run_schedule(label: impl ScheduleLabel) -> impl Command { } } +/// A [`Command`] that runs the given [`SystemSet`] in the schedule corresponding +/// to the given [`ScheduleLabel`]. All systems in the set (including transitively) +/// will be run. +/// +/// [`SystemSet`]: crate::schedule::SystemSet +pub fn run_system_set( + schedule: impl ScheduleLabel, + set: impl IntoSystemSet, +) -> impl Command { + let set = set.into_system_set(); + move |world: &mut World| -> Result { + world.try_run_system_set(schedule, set)?; + Ok(()) + } +} + /// Triggers the given [`Event`], which will run any [`Observer`]s watching for it. /// /// [`Observer`]: crate::observer::Observer diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 8422ceacaa643..364ba2a58a16a 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -27,6 +27,7 @@ use crate::{ event::{EntityEvent, Event}, message::Message, observer::Observer, + prelude::IntoSystemSet, resource::Resource, schedule::ScheduleLabel, system::{ @@ -1237,6 +1238,60 @@ impl<'w, 's> Commands<'w, 's> { pub fn run_schedule(&mut self, label: impl ScheduleLabel) { self.queue(command::run_schedule(label).handle_error_with(warn)); } + + /// Runs the given [`SystemSet`] within the schedule corresponding to the + /// given [`ScheduleLabel`]. All systems in the set (including transitively) + /// will be executed. + /// + /// Calls [`World::try_run_system_set`](World::try_run_system_set). + /// + /// # Fallible + /// + /// This command will fail if the given [`ScheduleLabel`] + /// does not correspond to a [`Schedule`](crate::schedule::Schedule), + /// + /// It will internally return a [`TryRunScheduleError`](crate::world::error::TryRunScheduleError), + /// which will be handled by [logging the error at the `warn` level](warn). + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # use bevy_ecs::schedule::ScheduleLabel; + /// # #[derive(Default, Resource)] + /// # struct Counter(u32); + /// #[derive(ScheduleLabel, Hash, Debug, PartialEq, Eq, Clone, Copy)] + /// struct FooSchedule; + /// + /// #[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone, Copy)] + /// struct FooSet; + /// + /// # fn foo_system(mut counter: ResMut) { + /// # counter.0 += 1; + /// # } + /// # + /// # let mut schedule = Schedule::new(FooSchedule); + /// # schedule.add_systems(foo_system.in_set(FooSet)); + /// # + /// # let mut world = World::default(); + /// # + /// # world.init_resource::(); + /// # world.add_schedule(schedule); + /// # + /// # assert_eq!(world.resource::().0, 0); + /// # + /// # let mut commands = world.commands(); + /// commands.run_system_set(FooSchedule, FooSet); + /// # + /// # world.flush(); + /// # + /// # assert_eq!(world.resource::().0, 1); + /// ``` + /// + /// [`SystemSet`]: crate::schedule::SystemSet + pub fn run_system_set(&mut self, schedule: impl ScheduleLabel, set: impl IntoSystemSet) { + self.queue(command::run_system_set(schedule, set).handle_error_with(warn)); + } } /// A list of commands that will be run to modify an [`Entity`]. diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index a4f49c7460a35..dfa403bc1626d 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -49,7 +49,7 @@ use crate::{ lifecycle::{ComponentHooks, RemovedComponentMessages, ADD, DESPAWN, INSERT, REMOVE, REPLACE}, message::{Message, MessageId, Messages, WriteBatchIds}, observer::Observers, - prelude::{Add, Despawn, Insert, Remove, Replace}, + prelude::{Add, Despawn, Insert, IntoSystemSet, Remove, Replace}, query::{DebugCheckedUnwrap, QueryData, QueryFilter, QueryState}, relationship::RelationshipHookMode, resource::Resource, @@ -3651,6 +3651,40 @@ impl World { /// and system state is cached. /// /// For simple testing use cases, call [`Schedule::run(&mut world)`](Schedule::run) instead. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # use bevy_ecs::schedule::ScheduleLabel; + /// # #[derive(Default, Resource)] + /// # struct Counter(u32); + /// #[derive(ScheduleLabel, Hash, Debug, PartialEq, Eq, Clone, Copy)] + /// struct FooSchedule; + /// + /// # fn foo_system(mut counter: ResMut) { + /// # counter.0 += 1; + /// # } + /// # + /// # let mut schedule = Schedule::new(FooSchedule); + /// # schedule.add_systems(foo_system); + /// # + /// # let mut world = World::default(); + /// # + /// # world.init_resource::(); + /// # world.add_schedule(schedule); + /// # + /// # assert_eq!(world.resource::().0, 0); + /// # + /// // This will succeed because the schedule exists + /// assert!(world.try_run_schedule(FooSchedule).is_ok()); + /// # assert_eq!(world.resource::().0, 1); + /// + /// // This will fail because the schedule doesn't exist + /// #[derive(ScheduleLabel, Hash, Debug, PartialEq, Eq, Clone, Copy)] + /// struct BarSchedule; + /// assert!(world.try_run_schedule(BarSchedule).is_err()); + /// ``` pub fn try_run_schedule( &mut self, label: impl ScheduleLabel, @@ -3666,6 +3700,8 @@ impl World { /// For simple testing use cases, call [`Schedule::run(&mut world)`](Schedule::run) instead. /// This avoids the need to create a unique [`ScheduleLabel`]. /// + /// See [`World::try_run_schedule`] for an example. + /// /// # Panics /// /// If the requested schedule does not exist. @@ -3673,6 +3709,82 @@ impl World { self.schedule_scope(label, |world, sched| sched.run(world)); } + /// Attempts to run the specified [`SystemSet`] within the [`Schedule`] + /// associated with the `label` a single time, and returns a + /// [`TryRunScheduleError`] if the schedule does not exist. All systems in + /// the set (including transitively) will be executed. + /// + /// The [`Schedule`] is fetched from the [`Schedules`] resource of the world + /// by its label, and system state is cached. + /// + /// For simple testing use cases, call [`Schedule::run_system_set`] instead. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # use bevy_ecs::schedule::ScheduleLabel; + /// # #[derive(Default, Resource)] + /// # struct Counter(u32); + /// #[derive(ScheduleLabel, Hash, Debug, PartialEq, Eq, Clone, Copy)] + /// struct FooSchedule; + /// + /// #[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone, Copy)] + /// struct FooSet; + /// + /// # fn foo_system(mut counter: ResMut) { + /// # counter.0 += 1; + /// # } + /// # + /// # let mut schedule = Schedule::new(FooSchedule); + /// # schedule.add_systems(foo_system.in_set(FooSet)); + /// # + /// # let mut world = World::default(); + /// # + /// # world.init_resource::(); + /// # world.add_schedule(schedule); + /// # + /// # assert_eq!(world.resource::().0, 0); + /// # + /// // This will succeed because the schedule exists + /// assert!(world.try_run_system_set(FooSchedule, FooSet).is_ok()); + /// # assert_eq!(world.resource::().0, 1); + /// + /// // This will fail because the schedule doesn't exist + /// #[derive(ScheduleLabel, Hash, Debug, PartialEq, Eq, Clone, Copy)] + /// struct BarSchedule; + /// assert!(world.try_run_system_set(BarSchedule, FooSet).is_err()); + /// ``` + /// + /// [`SystemSet`]: crate::schedule::SystemSet + pub fn try_run_system_set( + &mut self, + schedule: impl ScheduleLabel, + set: impl IntoSystemSet, + ) -> Result<(), TryRunScheduleError> { + self.try_schedule_scope(schedule, |world, sched| sched.run_system_set(world, set)) + } + + /// Runs the specified [`SystemSet`] within the [`Schedule`] associated with + /// the `label` a single time. All systems in the set (including transitively) + /// will be executed. + /// + /// The [`Schedule`] is fetched from the [`Schedules`] resource of the world + /// by its label, and system state is cached. + /// + /// For simple testing use cases, call [`Schedule::run_system_set`] instead. + /// + /// See [`World::try_run_system_set`] for an example. + /// + /// # Panics + /// + /// If the requested schedule does not exist. + /// + /// [`SystemSet`]: crate::schedule::SystemSet + pub fn run_system_set(&mut self, schedule: impl ScheduleLabel, set: impl IntoSystemSet) { + self.schedule_scope(schedule, |world, sched| sched.run_system_set(world, set)); + } + /// Ignore system order ambiguities caused by conflicts on [`Component`]s of type `T`. pub fn allow_ambiguous_component(&mut self) { let mut schedules = self.remove_resource::().unwrap_or_default(); diff --git a/release-content/release-notes/run_system_sets.md b/release-content/release-notes/run_system_sets.md new file mode 100644 index 0000000000000..338f7196dbed6 --- /dev/null +++ b/release-content/release-notes/run_system_sets.md @@ -0,0 +1,44 @@ +--- +title: Run individual System Sets in Schedules +authors: ["@ItsDoot"] +pull_requests: [21893] +--- + +You can now run a specific system set within a schedule without executing the entire +schedule! This is particularly useful for testing, debugging, or selectively running +parts of your game logic, all without needing to factor features out into separate +schedules: + +```rust +use bevy::prelude::*; + +#[derive(SystemSet, Clone, Copy, PartialEq, Eq, Debug, Hash)] +enum GameSystems { + Physics, + Combat, + UI, +} + +fn physics_system() { /* ... */ } +fn combat_system() { /* ... */ } +fn ui_system() { /* ... */ } + +let mut schedule = Schedule::default(); +schedule.add_systems(( + physics_system.in_set(GameSystems::Physics), + combat_system.in_set(GameSystems::Combat), + ui_system.in_set(GameSystems::UI), +)); + +let mut world = World::new(); + +// Run only the physics systems +schedule.run_system_set(&mut world, GameSystems::Physics); + +// Run only the combat systems +schedule.run_system_set(&mut world, GameSystems::Combat); + +// You can also run system sets from the World or via Commands: +world.run_system_set(MySchedule, MySet); +commands.run_system_set(MySchedule, MySet); +```