Skip to content

Commit 2f38464

Browse files
authored
Bubble sync points if ignore_deferred, do not ignore if target system is exclusive (#17880)
# Objective Fixes #17828 This fixes two bugs: 1. Exclusive systems should see the effect of all commands queued to that point. That does not happen when the system is configured with `*_ignore_deferred` which may lead to surprising situations. These configurations should not behave like that. 2. If `*_ignore_deferred` is used, no sync point may be added at all **after** the config. Currently this can happen if the last nodes in that config have no deferred parameters themselves. Instead, sync points should always be added after such a config, so long systems have deferred parameters. ## Solution 1. When adding sync points on edges, do not consider `AutoInsertApplyDeferredPass::no_sync_edges` if the target is an exclusive system. 2. when going through the nodes in a directed way, store the information that `AutoInsertApplyDeferredPass::no_sync_edges` suppressed adding a sync point at the target node. Then, when the target node is evaluated later by the iteration and that prior suppression was the case, the target node will behave like it has deferred parameters even if the system itself does not. ## Testing I added a test for each bug, please let me know if more are wanted and if yes, which cases you would want to see. These tests also can be read as examples how the current code would fail.
1 parent c753107 commit 2f38464

File tree

2 files changed

+125
-45
lines changed

2 files changed

+125
-45
lines changed

crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs

+68-45
Original file line numberDiff line numberDiff line change
@@ -99,71 +99,92 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass {
9999
.any(|(parent, _)| set_has_conditions(graph, parent))
100100
}
101101

102-
let mut system_has_conditions_cache = HashMap::default();
103-
104-
fn is_valid_explicit_sync_point(
105-
graph: &ScheduleGraph,
106-
system: NodeId,
107-
system_has_conditions_cache: &mut HashMap<usize, bool>,
108-
) -> bool {
102+
let mut system_has_conditions_cache = HashMap::<usize, bool>::default();
103+
let mut is_valid_explicit_sync_point = |system: NodeId| {
109104
let index = system.index();
110105
is_apply_deferred(graph.systems[index].get().unwrap())
111106
&& !*system_has_conditions_cache
112107
.entry(index)
113108
.or_insert_with(|| system_has_conditions(graph, system))
114-
}
109+
};
115110

116-
// calculate the number of sync points each sync point is from the beginning of the graph
117-
let mut distances: HashMap<usize, u32> =
111+
// Calculate the distance for each node.
112+
// The "distance" is the number of sync points between a node and the beginning of the graph.
113+
// Also store if a preceding edge would have added a sync point but was ignored to add it at
114+
// a later edge that is not ignored.
115+
let mut distances_and_pending_sync: HashMap<usize, (u32, bool)> =
118116
HashMap::with_capacity_and_hasher(topo.len(), Default::default());
117+
119118
// Keep track of any explicit sync nodes for a specific distance.
120119
let mut distance_to_explicit_sync_node: HashMap<u32, NodeId> = HashMap::default();
120+
121+
// Determine the distance for every node and collect the explicit sync points.
121122
for node in &topo {
122-
let node_system = graph.systems[node.index()].get().unwrap();
123-
124-
let node_needs_sync =
125-
if is_valid_explicit_sync_point(graph, *node, &mut system_has_conditions_cache) {
126-
distance_to_explicit_sync_node.insert(
127-
distances.get(&node.index()).copied().unwrap_or_default(),
128-
*node,
129-
);
130-
131-
// This node just did a sync, so the only reason to do another sync is if one was
132-
// explicitly scheduled afterwards.
133-
false
134-
} else {
135-
node_system.has_deferred()
136-
};
123+
let (node_distance, mut node_needs_sync) = distances_and_pending_sync
124+
.get(&node.index())
125+
.copied()
126+
.unwrap_or_default();
127+
128+
if is_valid_explicit_sync_point(*node) {
129+
// The distance of this sync point does not change anymore as the iteration order
130+
// makes sure that this node is no unvisited target of another node.
131+
// Because of this, the sync point can be stored for this distance to be reused as
132+
// automatically added sync points later.
133+
distance_to_explicit_sync_node.insert(node_distance, *node);
134+
135+
// This node just did a sync, so the only reason to do another sync is if one was
136+
// explicitly scheduled afterwards.
137+
node_needs_sync = false;
138+
} else if !node_needs_sync {
139+
// No previous node has postponed sync points to add so check if the system itself
140+
// has deferred params that require a sync point to apply them.
141+
node_needs_sync = graph.systems[node.index()].get().unwrap().has_deferred();
142+
}
137143

138144
for target in dependency_flattened.neighbors_directed(*node, Direction::Outgoing) {
139-
let edge_needs_sync = node_needs_sync
140-
&& !self.no_sync_edges.contains(&(*node, target))
141-
|| is_valid_explicit_sync_point(
142-
graph,
143-
target,
144-
&mut system_has_conditions_cache,
145-
);
146-
147-
let weight = if edge_needs_sync { 1 } else { 0 };
148-
149-
// Use whichever distance is larger, either the current distance, or the distance to
150-
// the parent plus the weight.
151-
let distance = distances
152-
.get(&target.index())
153-
.copied()
154-
.unwrap_or_default()
155-
.max(distances.get(&node.index()).copied().unwrap_or_default() + weight);
145+
let (target_distance, target_pending_sync) = distances_and_pending_sync
146+
.entry(target.index())
147+
.or_default();
148+
149+
let mut edge_needs_sync = node_needs_sync;
150+
if node_needs_sync
151+
&& !graph.systems[target.index()].get().unwrap().is_exclusive()
152+
&& self.no_sync_edges.contains(&(*node, target))
153+
{
154+
// The node has deferred params to apply, but this edge is ignoring sync points.
155+
// Mark the target as 'delaying' those commands to a future edge and the current
156+
// edge as not needing a sync point.
157+
*target_pending_sync = true;
158+
edge_needs_sync = false;
159+
}
156160

157-
distances.insert(target.index(), distance);
161+
let mut weight = 0;
162+
if edge_needs_sync || is_valid_explicit_sync_point(target) {
163+
// The target distance grows if a sync point is added between it and the node.
164+
// Also raise the distance if the target is a sync point itself so it then again
165+
// raises the distance of following nodes as that is what the distance is about.
166+
weight = 1;
167+
}
168+
169+
// The target cannot have fewer sync points in front of it than the preceding node.
170+
*target_distance = (node_distance + weight).max(*target_distance);
158171
}
159172
}
160173

161174
// Find any edges which have a different number of sync points between them and make sure
162175
// there is a sync point between them.
163176
for node in &topo {
164-
let node_distance = distances.get(&node.index()).copied().unwrap_or_default();
177+
let (node_distance, _) = distances_and_pending_sync
178+
.get(&node.index())
179+
.copied()
180+
.unwrap_or_default();
181+
165182
for target in dependency_flattened.neighbors_directed(*node, Direction::Outgoing) {
166-
let target_distance = distances.get(&target.index()).copied().unwrap_or_default();
183+
let (target_distance, _) = distances_and_pending_sync
184+
.get(&target.index())
185+
.copied()
186+
.unwrap_or_default();
187+
167188
if node_distance == target_distance {
168189
// These nodes are the same distance, so they don't need an edge between them.
169190
continue;
@@ -174,6 +195,7 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass {
174195
// already!
175196
continue;
176197
}
198+
177199
let sync_point = distance_to_explicit_sync_node
178200
.get(&target_distance)
179201
.copied()
@@ -182,6 +204,7 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass {
182204
sync_point_graph.add_edge(*node, sync_point);
183205
sync_point_graph.add_edge(sync_point, target);
184206

207+
// The edge without the sync point is now redundant.
185208
sync_point_graph.remove_edge(*node, target);
186209
}
187210
}

crates/bevy_ecs/src/schedule/schedule.rs

+57
Original file line numberDiff line numberDiff line change
@@ -2262,6 +2262,63 @@ mod tests {
22622262
assert_eq!(schedule.executable.systems.len(), 5);
22632263
}
22642264

2265+
#[test]
2266+
fn do_not_consider_ignore_deferred_before_exclusive_system() {
2267+
let mut schedule = Schedule::default();
2268+
let mut world = World::default();
2269+
// chain_ignore_deferred adds no sync points usually but an exception is made for exclusive systems
2270+
schedule.add_systems(
2271+
(
2272+
|_: Commands| {},
2273+
// <- no sync point is added here because the following system is not exclusive
2274+
|mut commands: Commands| commands.insert_resource(Resource1),
2275+
// <- sync point is added here because the following system is exclusive which expects to see all commands to that point
2276+
|world: &mut World| assert!(world.contains_resource::<Resource1>()),
2277+
// <- no sync point is added here because the previous system has no deferred parameters
2278+
|_: &mut World| {},
2279+
// <- no sync point is added here because the following system is not exclusive
2280+
|_: Commands| {},
2281+
)
2282+
.chain_ignore_deferred(),
2283+
);
2284+
schedule.run(&mut world);
2285+
2286+
assert_eq!(schedule.executable.systems.len(), 6); // 5 systems + 1 sync point
2287+
}
2288+
2289+
#[test]
2290+
fn bubble_sync_point_through_ignore_deferred_node() {
2291+
let mut schedule = Schedule::default();
2292+
let mut world = World::default();
2293+
2294+
let insert_resource_config = (
2295+
// the first system has deferred commands
2296+
|mut commands: Commands| commands.insert_resource(Resource1),
2297+
// the second system has no deferred commands
2298+
|| {},
2299+
)
2300+
// the first two systems are chained without a sync point in between
2301+
.chain_ignore_deferred();
2302+
2303+
schedule.add_systems(
2304+
(
2305+
insert_resource_config,
2306+
// the third system would panic if the command of the first system was not applied
2307+
|_: Res<Resource1>| {},
2308+
)
2309+
// the third system is chained after the first two, possibly with a sync point in between
2310+
.chain(),
2311+
);
2312+
2313+
// To add a sync point between the second and third system despite the second having no commands,
2314+
// the first system has to signal the second system that there are unapplied commands.
2315+
// With that the second system will add a sync point after it so the third system will find the resource.
2316+
2317+
schedule.run(&mut world);
2318+
2319+
assert_eq!(schedule.executable.systems.len(), 4); // 3 systems + 1 sync point
2320+
}
2321+
22652322
#[test]
22662323
fn disable_auto_sync_points() {
22672324
let mut schedule = Schedule::default();

0 commit comments

Comments
 (0)