Skip to content

Commit f2a65c2

Browse files
authored
Schedule build pass (#11094)
# Objective This is a follow up to #9822, which automatically adds sync points during the Schedule build process. However, the implementation in #9822 feels very "special case" to me. As the number of things we want to do with the `Schedule` grows, we need a modularized way to manage those behaviors. For example, in one of my current experiments I want to automatically add systems to apply GPU pipeline barriers between systems accessing GPU resources. For dynamic modifications of the schedule, we mostly need these capabilities: - Storing custom data on schedule edges - Storing custom data on schedule nodes - Modify the schedule graph whenever it builds These should be enough to allows us to add "hooks" to the schedule build process for various reasons. cc @hymm ## Solution This PR abstracts the process of schedule modification and created a new trait, `ScheduleBuildPass`. Most of the logics in #9822 were moved to an implementation of `ScheduleBuildPass`, `AutoInsertApplyDeferredPass`. Whether a dependency edge should "ignore deferred" is now indicated by the presence of a marker struct, `IgnoreDeferred`. This PR has no externally visible effects. However, in a future PR I propose to change the `before_ignore_deferred` and `after_ignore_deferred` API into a more general form, `before_with_options` and `after_with_options`. ```rs schedule.add_systems( system.before_with_options(another_system, IgnoreDeferred) ); schedule.add_systems( system.before_with_options(another_system, ( IgnoreDeferred, AnyOtherOption { key: value } )) ); schedule.add_systems( system.before_with_options(another_system, ()) ); ```
1 parent 9ea9c5d commit f2a65c2

File tree

8 files changed

+383
-167
lines changed

8 files changed

+383
-167
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
use alloc::{boxed::Box, collections::BTreeSet, vec::Vec};
2+
3+
use bevy_platform_support::collections::HashMap;
4+
5+
use crate::system::IntoSystem;
6+
use crate::world::World;
7+
8+
use super::{
9+
is_apply_deferred, ApplyDeferred, DiGraph, Direction, NodeId, ReportCycles, ScheduleBuildError,
10+
ScheduleBuildPass, ScheduleGraph, SystemNode,
11+
};
12+
13+
/// A [`ScheduleBuildPass`] that inserts [`ApplyDeferred`] systems into the schedule graph
14+
/// when there are [`Deferred`](crate::prelude::Deferred)
15+
/// in one system and there are ordering dependencies on that system. [`Commands`](crate::system::Commands) is one
16+
/// such deferred buffer.
17+
///
18+
/// This pass is typically automatically added to the schedule. You can disable this by setting
19+
/// [`ScheduleBuildSettings::auto_insert_apply_deferred`](crate::schedule::ScheduleBuildSettings::auto_insert_apply_deferred)
20+
/// to `false`. You may want to disable this if you only want to sync deferred params at the end of the schedule,
21+
/// or want to manually insert all your sync points.
22+
#[derive(Debug, Default)]
23+
pub struct AutoInsertApplyDeferredPass {
24+
/// Dependency edges that will **not** automatically insert an instance of `ApplyDeferred` on the edge.
25+
no_sync_edges: BTreeSet<(NodeId, NodeId)>,
26+
auto_sync_node_ids: HashMap<u32, NodeId>,
27+
}
28+
29+
/// If added to a dependency edge, the edge will not be considered for auto sync point insertions.
30+
pub struct IgnoreDeferred;
31+
32+
impl AutoInsertApplyDeferredPass {
33+
/// Returns the `NodeId` of the cached auto sync point. Will create
34+
/// a new one if needed.
35+
fn get_sync_point(&mut self, graph: &mut ScheduleGraph, distance: u32) -> NodeId {
36+
self.auto_sync_node_ids
37+
.get(&distance)
38+
.copied()
39+
.or_else(|| {
40+
let node_id = self.add_auto_sync(graph);
41+
self.auto_sync_node_ids.insert(distance, node_id);
42+
Some(node_id)
43+
})
44+
.unwrap()
45+
}
46+
/// add an [`ApplyDeferred`] system with no config
47+
fn add_auto_sync(&mut self, graph: &mut ScheduleGraph) -> NodeId {
48+
let id = NodeId::System(graph.systems.len());
49+
50+
graph
51+
.systems
52+
.push(SystemNode::new(Box::new(IntoSystem::into_system(
53+
ApplyDeferred,
54+
))));
55+
graph.system_conditions.push(Vec::new());
56+
57+
// ignore ambiguities with auto sync points
58+
// They aren't under user control, so no one should know or care.
59+
graph.ambiguous_with_all.insert(id);
60+
61+
id
62+
}
63+
}
64+
65+
impl ScheduleBuildPass for AutoInsertApplyDeferredPass {
66+
type EdgeOptions = IgnoreDeferred;
67+
68+
fn add_dependency(&mut self, from: NodeId, to: NodeId, options: Option<&Self::EdgeOptions>) {
69+
if options.is_some() {
70+
self.no_sync_edges.insert((from, to));
71+
}
72+
}
73+
74+
fn build(
75+
&mut self,
76+
_world: &mut World,
77+
graph: &mut ScheduleGraph,
78+
dependency_flattened: &mut DiGraph,
79+
) -> Result<(), ScheduleBuildError> {
80+
let mut sync_point_graph = dependency_flattened.clone();
81+
let topo = graph.topsort_graph(dependency_flattened, ReportCycles::Dependency)?;
82+
83+
// calculate the number of sync points each sync point is from the beginning of the graph
84+
// use the same sync point if the distance is the same
85+
let mut distances: HashMap<usize, Option<u32>> =
86+
HashMap::with_capacity_and_hasher(topo.len(), Default::default());
87+
for node in &topo {
88+
let add_sync_after = graph.systems[node.index()].get().unwrap().has_deferred();
89+
90+
for target in dependency_flattened.neighbors_directed(*node, Direction::Outgoing) {
91+
let add_sync_on_edge = add_sync_after
92+
&& !is_apply_deferred(graph.systems[target.index()].get().unwrap())
93+
&& !self.no_sync_edges.contains(&(*node, target));
94+
95+
let weight = if add_sync_on_edge { 1 } else { 0 };
96+
97+
let distance = distances
98+
.get(&target.index())
99+
.unwrap_or(&None)
100+
.or(Some(0))
101+
.map(|distance| {
102+
distance.max(
103+
distances.get(&node.index()).unwrap_or(&None).unwrap_or(0) + weight,
104+
)
105+
});
106+
107+
distances.insert(target.index(), distance);
108+
109+
if add_sync_on_edge {
110+
let sync_point =
111+
self.get_sync_point(graph, distances[&target.index()].unwrap());
112+
sync_point_graph.add_edge(*node, sync_point);
113+
sync_point_graph.add_edge(sync_point, target);
114+
115+
// edge is now redundant
116+
sync_point_graph.remove_edge(*node, target);
117+
}
118+
}
119+
}
120+
121+
*dependency_flattened = sync_point_graph;
122+
Ok(())
123+
}
124+
125+
fn collapse_set(
126+
&mut self,
127+
set: NodeId,
128+
systems: &[NodeId],
129+
dependency_flattened: &DiGraph,
130+
) -> impl Iterator<Item = (NodeId, NodeId)> {
131+
if systems.is_empty() {
132+
// collapse dependencies for empty sets
133+
for a in dependency_flattened.neighbors_directed(set, Direction::Incoming) {
134+
for b in dependency_flattened.neighbors_directed(set, Direction::Outgoing) {
135+
if self.no_sync_edges.contains(&(a, set))
136+
&& self.no_sync_edges.contains(&(set, b))
137+
{
138+
self.no_sync_edges.insert((a, b));
139+
}
140+
}
141+
}
142+
} else {
143+
for a in dependency_flattened.neighbors_directed(set, Direction::Incoming) {
144+
for &sys in systems {
145+
if self.no_sync_edges.contains(&(a, set)) {
146+
self.no_sync_edges.insert((a, sys));
147+
}
148+
}
149+
}
150+
151+
for b in dependency_flattened.neighbors_directed(set, Direction::Outgoing) {
152+
for &sys in systems {
153+
if self.no_sync_edges.contains(&(set, b)) {
154+
self.no_sync_edges.insert((sys, b));
155+
}
156+
}
157+
}
158+
}
159+
core::iter::empty()
160+
}
161+
}

crates/bevy_ecs/src/schedule/config.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use variadics_please::all_tuples;
44
use crate::{
55
result::Result,
66
schedule::{
7+
auto_insert_apply_deferred::IgnoreDeferred,
78
condition::{BoxedCondition, Condition},
89
graph::{Ambiguity, Dependency, DependencyKind, GraphInfo},
910
set::{InternedSystemSet, IntoSystemSet, SystemSet},
@@ -137,7 +138,7 @@ impl<T> NodeConfigs<T> {
137138
config
138139
.graph_info
139140
.dependencies
140-
.push(Dependency::new(DependencyKind::BeforeNoSync, set));
141+
.push(Dependency::new(DependencyKind::Before, set).add_config(IgnoreDeferred));
141142
}
142143
Self::Configs { configs, .. } => {
143144
for config in configs {
@@ -153,7 +154,7 @@ impl<T> NodeConfigs<T> {
153154
config
154155
.graph_info
155156
.dependencies
156-
.push(Dependency::new(DependencyKind::AfterNoSync, set));
157+
.push(Dependency::new(DependencyKind::After, set).add_config(IgnoreDeferred));
157158
}
158159
Self::Configs { configs, .. } => {
159160
for config in configs {
@@ -224,17 +225,17 @@ impl<T> NodeConfigs<T> {
224225
match &mut self {
225226
Self::NodeConfig(_) => { /* no op */ }
226227
Self::Configs { chained, .. } => {
227-
*chained = Chain::Yes;
228+
chained.set_chained();
228229
}
229-
}
230+
};
230231
self
231232
}
232233

233234
fn chain_ignore_deferred_inner(mut self) -> Self {
234235
match &mut self {
235236
Self::NodeConfig(_) => { /* no op */ }
236237
Self::Configs { chained, .. } => {
237-
*chained = Chain::YesIgnoreDeferred;
238+
chained.set_chained_with_config(IgnoreDeferred);
238239
}
239240
}
240241
self
@@ -582,7 +583,7 @@ macro_rules! impl_system_collection {
582583
SystemConfigs::Configs {
583584
configs: vec![$($sys.into_configs(),)*],
584585
collective_conditions: Vec::new(),
585-
chained: Chain::No,
586+
chained: Default::default(),
586587
}
587588
}
588589
}
@@ -820,7 +821,7 @@ macro_rules! impl_system_set_collection {
820821
SystemSetConfigs::Configs {
821822
configs: vec![$($set.into_configs(),)*],
822823
collective_conditions: Vec::new(),
823-
chained: Chain::No,
824+
chained: Default::default(),
824825
}
825826
}
826827
}

crates/bevy_ecs/src/schedule/graph/graph_map.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ where
6565
S: BuildHasher,
6666
{
6767
/// Create a new `Graph` with estimated capacity.
68-
pub(crate) fn with_capacity(nodes: usize, edges: usize) -> Self
68+
pub fn with_capacity(nodes: usize, edges: usize) -> Self
6969
where
7070
S: Default,
7171
{
@@ -89,14 +89,14 @@ where
8989
}
9090

9191
/// Add node `n` to the graph.
92-
pub(crate) fn add_node(&mut self, n: NodeId) {
92+
pub fn add_node(&mut self, n: NodeId) {
9393
self.nodes.entry(n).or_default();
9494
}
9595

9696
/// Remove a node `n` from the graph.
9797
///
9898
/// Computes in **O(N)** time, due to the removal of edges with other nodes.
99-
pub(crate) fn remove_node(&mut self, n: NodeId) {
99+
pub fn remove_node(&mut self, n: NodeId) {
100100
let Some(links) = self.nodes.swap_remove(&n) else {
101101
return;
102102
};
@@ -166,7 +166,7 @@ where
166166
/// Remove edge from `a` to `b` from the graph.
167167
///
168168
/// Return `false` if the edge didn't exist.
169-
pub(crate) fn remove_edge(&mut self, a: NodeId, b: NodeId) -> bool {
169+
pub fn remove_edge(&mut self, a: NodeId, b: NodeId) -> bool {
170170
let exist1 = self.remove_single_edge(a, b, Outgoing);
171171
let exist2 = if a != b {
172172
self.remove_single_edge(b, a, Incoming)

crates/bevy_ecs/src/schedule/graph/mod.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
use alloc::{vec, vec::Vec};
2-
use core::fmt::Debug;
1+
use alloc::{boxed::Box, vec, vec::Vec};
2+
use core::{
3+
any::{Any, TypeId},
4+
fmt::Debug,
5+
};
36
use smallvec::SmallVec;
47

58
use bevy_platform_support::collections::{HashMap, HashSet};
9+
use bevy_utils::TypeIdMap;
10+
611
use fixedbitset::FixedBitSet;
712

813
use crate::schedule::set::*;
@@ -21,22 +26,26 @@ pub(crate) enum DependencyKind {
2126
Before,
2227
/// A node that should be succeeded.
2328
After,
24-
/// A node that should be preceded and will **not** automatically insert an instance of `ApplyDeferred` on the edge.
25-
BeforeNoSync,
26-
/// A node that should be succeeded and will **not** automatically insert an instance of `ApplyDeferred` on the edge.
27-
AfterNoSync,
2829
}
2930

3031
/// An edge to be added to the dependency graph.
31-
#[derive(Clone)]
3232
pub(crate) struct Dependency {
3333
pub(crate) kind: DependencyKind,
3434
pub(crate) set: InternedSystemSet,
35+
pub(crate) options: TypeIdMap<Box<dyn Any>>,
3536
}
3637

3738
impl Dependency {
3839
pub fn new(kind: DependencyKind, set: InternedSystemSet) -> Self {
39-
Self { kind, set }
40+
Self {
41+
kind,
42+
set,
43+
options: Default::default(),
44+
}
45+
}
46+
pub fn add_config<T: 'static>(mut self, option: T) -> Self {
47+
self.options.insert(TypeId::of::<T>(), Box::new(option));
48+
self
4049
}
4150
}
4251

@@ -52,7 +61,7 @@ pub(crate) enum Ambiguity {
5261
}
5362

5463
/// Metadata about how the node fits in the schedule graph
55-
#[derive(Clone, Default)]
64+
#[derive(Default)]
5665
pub(crate) struct GraphInfo {
5766
/// the sets that the node belongs to (hierarchy)
5867
pub(crate) hierarchy: Vec<InternedSystemSet>,

crates/bevy_ecs/src/schedule/graph/node.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub enum NodeId {
1313

1414
impl NodeId {
1515
/// Returns the internal integer value.
16-
pub(crate) const fn index(&self) -> usize {
16+
pub const fn index(&self) -> usize {
1717
match self {
1818
NodeId::System(index) | NodeId::Set(index) => *index,
1919
}

crates/bevy_ecs/src/schedule/mod.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
//! Contains APIs for ordering systems and executing them on a [`World`](crate::world::World)
22
3+
mod auto_insert_apply_deferred;
34
mod condition;
45
mod config;
56
mod executor;
6-
mod graph;
7+
mod pass;
78
mod schedule;
89
mod set;
910
mod stepping;
1011

1112
use self::graph::*;
1213
pub use self::{condition::*, config::*, executor::*, schedule::*, set::*};
14+
pub use pass::ScheduleBuildPass;
1315

1416
pub use self::graph::NodeId;
1517

18+
/// An implementation of a graph data structure.
19+
pub mod graph;
20+
21+
/// Included optional schedule build passes.
22+
pub mod passes {
23+
pub use crate::schedule::auto_insert_apply_deferred::*;
24+
}
25+
1626
#[cfg(test)]
1727
mod tests {
1828
use super::*;
@@ -1082,7 +1092,7 @@ mod tests {
10821092

10831093
schedule.graph_mut().initialize(&mut world);
10841094
let _ = schedule.graph_mut().build_schedule(
1085-
world.components(),
1095+
&mut world,
10861096
TestSchedule.intern(),
10871097
&BTreeSet::new(),
10881098
);
@@ -1131,7 +1141,7 @@ mod tests {
11311141
let mut world = World::new();
11321142
schedule.graph_mut().initialize(&mut world);
11331143
let _ = schedule.graph_mut().build_schedule(
1134-
world.components(),
1144+
&mut world,
11351145
TestSchedule.intern(),
11361146
&BTreeSet::new(),
11371147
);

0 commit comments

Comments
 (0)