Skip to content

Commit 300b275

Browse files
geieredgarcart
andauthored
run_if for SystemConfigs via anonymous system sets (#7676)
# Objective - Fixes #7659 ## Solution The idea of anonymous system sets or "implicit hidden organizational sets" was briefly mentioned by @cart here: #7634 (comment). - `Schedule::add_systems` creates an implicit, anonymous system set of all systems in `SystemConfigs`. - All dependencies and conditions from the `SystemConfigs` are now applied to the implicit system set, instead of being applied to each individual system. This should not change the behavior, AFAIU, because `before`, `after`, `run_if` and `ambiguous_with` are transitive properties from a set to its members. - The newly added `AnonymousSystemSet` stores the names of its members to provide better error messages. - The names are stored in a reference counted slice, allowing fast clones of the `AnonymousSystemSet`. - However, only the pointer of the slice is used for hash and equality operations - This ensures that two `AnonymousSystemSet` are not equal, even if they have the same members / member names. - So two identical `add_systems` calls will produce two different `AnonymousSystemSet`s. - Clones of the same `AnonymousSystemSet` will be equal. ## Drawbacks If my assumptions are correct, the observed behavior should stay the same. But the number of system sets in the `Schedule` will increase with each `add_systems` call. If this has negative performance implications, `add_systems` could be changed to only create the implicit system set if necessary / when a run condition was added. --------- Co-authored-by: Carter Anderson <[email protected]>
1 parent a1d771a commit 300b275

File tree

4 files changed

+115
-14
lines changed

4 files changed

+115
-14
lines changed

crates/bevy_app/src/app.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,8 +380,10 @@ impl App {
380380
/// # fn system_a() {}
381381
/// # fn system_b() {}
382382
/// # fn system_c() {}
383+
/// # fn should_run() -> bool { true }
383384
/// #
384385
/// app.add_systems(Update, (system_a, system_b, system_c));
386+
/// app.add_systems(Update, (system_a, system_b).run_if(should_run));
385387
/// ```
386388
pub fn add_systems<M>(
387389
&mut self,

crates/bevy_ecs/src/schedule/config.rs

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pub enum SystemConfigs {
5959
SystemConfig(SystemConfig),
6060
Configs {
6161
configs: Vec<SystemConfigs>,
62+
collective_conditions: Vec<BoxedCondition>,
6263
/// If `true`, adds `before -> after` ordering constraints between the successive elements.
6364
chained: bool,
6465
},
@@ -78,7 +79,7 @@ impl SystemConfigs {
7879
})
7980
}
8081

81-
fn in_set_inner(&mut self, set: BoxedSystemSet) {
82+
pub(crate) fn in_set_inner(&mut self, set: BoxedSystemSet) {
8283
match self {
8384
SystemConfigs::SystemConfig(config) => {
8485
config.graph_info.sets.push(set);
@@ -162,13 +163,16 @@ impl SystemConfigs {
162163
}
163164
}
164165

165-
fn run_if_inner(&mut self, condition: BoxedCondition) {
166+
pub(crate) fn run_if_inner(&mut self, condition: BoxedCondition) {
166167
match self {
167168
SystemConfigs::SystemConfig(config) => {
168169
config.conditions.push(condition);
169170
}
170-
SystemConfigs::Configs { .. } => {
171-
todo!("run_if is not implemented for groups of systems yet")
171+
SystemConfigs::Configs {
172+
collective_conditions,
173+
..
174+
} => {
175+
collective_conditions.push(condition);
172176
}
173177
}
174178
}
@@ -212,12 +216,12 @@ where
212216
///
213217
/// ```
214218
/// # use bevy_ecs::prelude::*;
215-
/// # let mut app = Schedule::new();
219+
/// # let mut schedule = Schedule::new();
216220
/// # fn a() {}
217221
/// # fn b() {}
218222
/// # fn condition() -> bool { true }
219-
/// app.add_systems((a, b).distributive_run_if(condition));
220-
/// app.add_systems((a.run_if(condition), b.run_if(condition)));
223+
/// schedule.add_systems((a, b).distributive_run_if(condition));
224+
/// schedule.add_systems((a.run_if(condition), b.run_if(condition)));
221225
/// ```
222226
///
223227
/// # Note
@@ -237,6 +241,32 @@ where
237241
///
238242
/// The `Condition` will be evaluated at most once (per schedule run),
239243
/// the first time a system in this set prepares to run.
244+
///
245+
/// If this set contains more than one system, calling `run_if` is equivalent to adding each
246+
/// system to a common set and configuring the run condition on that set, as shown below:
247+
///
248+
/// # Examples
249+
///
250+
/// ```
251+
/// # use bevy_ecs::prelude::*;
252+
/// # let mut schedule = Schedule::new();
253+
/// # fn a() {}
254+
/// # fn b() {}
255+
/// # fn condition() -> bool { true }
256+
/// # #[derive(SystemSet, Debug, Eq, PartialEq, Hash, Clone, Copy)]
257+
/// # struct C;
258+
/// schedule.add_systems((a, b).run_if(condition));
259+
/// schedule.add_systems((a, b).in_set(C)).configure_set(C.run_if(condition));
260+
/// ```
261+
///
262+
/// # Note
263+
///
264+
/// Because the condition will only be evaluated once, there is no guarantee that the condition
265+
/// is upheld after the first system has run. You need to make sure that no other systems that
266+
/// could invalidate the condition are scheduled inbetween the first and last run system.
267+
///
268+
/// Use [`distributive_run_if`](IntoSystemConfigs::distributive_run_if) if you want the
269+
/// condition to be evaluated for each individual system, right before one is run.
240270
fn run_if<M>(self, condition: impl Condition<M>) -> SystemConfigs {
241271
self.into_configs().run_if(condition)
242272
}
@@ -364,6 +394,7 @@ macro_rules! impl_system_collection {
364394
let ($($sys,)*) = self;
365395
SystemConfigs::Configs {
366396
configs: vec![$($sys.into_configs(),)*],
397+
collective_conditions: Vec::new(),
367398
chained: false,
368399
}
369400
}

crates/bevy_ecs/src/schedule/schedule.rs

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,10 @@ impl SystemSetNode {
349349
pub fn is_system_type(&self) -> bool {
350350
self.inner.system_type().is_some()
351351
}
352+
353+
pub fn is_anonymous(&self) -> bool {
354+
self.inner.is_anonymous()
355+
}
352356
}
353357

354358
/// A [`BoxedSystem`] with metadata, stored in a [`ScheduleGraph`].
@@ -525,7 +529,27 @@ impl ScheduleGraph {
525529
}
526530
}
527531
}
528-
SystemConfigs::Configs { configs, chained } => {
532+
SystemConfigs::Configs {
533+
mut configs,
534+
collective_conditions,
535+
chained,
536+
} => {
537+
let more_than_one_entry = configs.len() > 1;
538+
if !collective_conditions.is_empty() {
539+
if more_than_one_entry {
540+
let set = AnonymousSet::new();
541+
for config in &mut configs {
542+
config.in_set_inner(set.dyn_clone());
543+
}
544+
let mut set_config = set.into_config();
545+
set_config.conditions.extend(collective_conditions);
546+
self.configure_set(set_config);
547+
} else {
548+
for condition in collective_conditions {
549+
configs[0].run_if_inner(condition);
550+
}
551+
}
552+
}
529553
let mut config_iter = configs.into_iter();
530554
let mut nodes_in_scope = Vec::new();
531555
let mut densely_chained = true;
@@ -607,7 +631,6 @@ impl ScheduleGraph {
607631
nodes_in_scope.append(&mut previous_result.nodes);
608632
}
609633
} else {
610-
let more_than_one_entry = config_iter.len() > 1;
611634
for config in config_iter {
612635
let result = self.add_systems_inner(config, ancestor_chained);
613636
densely_chained = densely_chained && result.densely_chained;
@@ -705,8 +728,7 @@ impl ScheduleGraph {
705728
match self.system_set_ids.get(set) {
706729
Some(set_id) => {
707730
if id == set_id {
708-
let string = format!("{:?}", &set);
709-
return Err(ScheduleBuildError::HierarchyLoop(string));
731+
return Err(ScheduleBuildError::HierarchyLoop(self.get_node_name(id)));
710732
}
711733
}
712734
None => {
@@ -742,8 +764,7 @@ impl ScheduleGraph {
742764
match self.system_set_ids.get(set) {
743765
Some(set_id) => {
744766
if id == set_id {
745-
let string = format!("{:?}", &set);
746-
return Err(ScheduleBuildError::DependencyLoop(string));
767+
return Err(ScheduleBuildError::DependencyLoop(self.get_node_name(id)));
747768
}
748769
}
749770
None => {
@@ -1257,14 +1278,33 @@ impl ScheduleGraph {
12571278
name
12581279
}
12591280
}
1260-
NodeId::Set(_) => self.system_sets[id.index()].name(),
1281+
NodeId::Set(_) => {
1282+
let set = &self.system_sets[id.index()];
1283+
if set.is_anonymous() {
1284+
self.anonymous_set_name(id)
1285+
} else {
1286+
set.name()
1287+
}
1288+
}
12611289
};
12621290
if self.settings.use_shortnames {
12631291
name = bevy_utils::get_short_name(&name);
12641292
}
12651293
name
12661294
}
12671295

1296+
fn anonymous_set_name(&self, id: &NodeId) -> String {
1297+
format!(
1298+
"({})",
1299+
self.hierarchy
1300+
.graph
1301+
.edges_directed(*id, Direction::Outgoing)
1302+
.map(|(_, member_id, _)| self.get_node_name(&member_id))
1303+
.reduce(|a, b| format!("{a}, {b}"))
1304+
.unwrap_or_default()
1305+
)
1306+
}
1307+
12681308
fn get_node_kind(&self, id: &NodeId) -> &'static str {
12691309
match id {
12701310
NodeId::System(_) => "system",

crates/bevy_ecs/src/schedule/set.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::any::TypeId;
22
use std::fmt::Debug;
33
use std::hash::{Hash, Hasher};
44
use std::marker::PhantomData;
5+
use std::sync::atomic::{AtomicUsize, Ordering};
56

67
pub use bevy_ecs_macros::{ScheduleLabel, SystemSet};
78
use bevy_utils::define_boxed_label;
@@ -23,6 +24,10 @@ pub trait SystemSet: DynHash + Debug + Send + Sync + 'static {
2324
None
2425
}
2526

27+
/// Returns `true` if this system set is an [`AnonymousSet`].
28+
fn is_anonymous(&self) -> bool {
29+
false
30+
}
2631
/// Creates a boxed clone of the label corresponding to this system set.
2732
fn dyn_clone(&self) -> Box<dyn SystemSet>;
2833
}
@@ -102,6 +107,29 @@ impl<T> SystemSet for SystemTypeSet<T> {
102107
}
103108
}
104109

110+
/// A [`SystemSet`] implicitly created when using
111+
/// [`Schedule::add_systems`](super::Schedule::add_systems).
112+
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
113+
pub struct AnonymousSet(usize);
114+
115+
static NEXT_ANONYMOUS_SET_ID: AtomicUsize = AtomicUsize::new(0);
116+
117+
impl AnonymousSet {
118+
pub(crate) fn new() -> Self {
119+
Self(NEXT_ANONYMOUS_SET_ID.fetch_add(1, Ordering::Relaxed))
120+
}
121+
}
122+
123+
impl SystemSet for AnonymousSet {
124+
fn is_anonymous(&self) -> bool {
125+
true
126+
}
127+
128+
fn dyn_clone(&self) -> Box<dyn SystemSet> {
129+
Box::new(*self)
130+
}
131+
}
132+
105133
/// Types that can be converted into a [`SystemSet`].
106134
pub trait IntoSystemSet<Marker>: Sized {
107135
type Set: SystemSet;

0 commit comments

Comments
 (0)