Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_ecs/src/schedule/graph/mod.rs
6849 views
1
use alloc::{boxed::Box, vec, vec::Vec};
2
use core::{
3
any::{Any, TypeId},
4
fmt::Debug,
5
};
6
use smallvec::SmallVec;
7
8
use bevy_platform::collections::{HashMap, HashSet};
9
use bevy_utils::TypeIdMap;
10
11
use fixedbitset::FixedBitSet;
12
13
use crate::schedule::set::*;
14
15
mod graph_map;
16
mod tarjan_scc;
17
18
pub use graph_map::{DiGraph, Direction, GraphNodeId, UnGraph};
19
20
/// Specifies what kind of edge should be added to the dependency graph.
21
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)]
22
pub(crate) enum DependencyKind {
23
/// A node that should be preceded.
24
Before,
25
/// A node that should be succeeded.
26
After,
27
}
28
29
/// An edge to be added to the dependency graph.
30
pub(crate) struct Dependency {
31
pub(crate) kind: DependencyKind,
32
pub(crate) set: InternedSystemSet,
33
pub(crate) options: TypeIdMap<Box<dyn Any>>,
34
}
35
36
impl Dependency {
37
pub fn new(kind: DependencyKind, set: InternedSystemSet) -> Self {
38
Self {
39
kind,
40
set,
41
options: Default::default(),
42
}
43
}
44
pub fn add_config<T: 'static>(mut self, option: T) -> Self {
45
self.options.insert(TypeId::of::<T>(), Box::new(option));
46
self
47
}
48
}
49
50
/// Configures ambiguity detection for a single system.
51
#[derive(Clone, Debug, Default)]
52
pub(crate) enum Ambiguity {
53
#[default]
54
Check,
55
/// Ignore warnings with systems in any of these system sets. May contain duplicates.
56
IgnoreWithSet(Vec<InternedSystemSet>),
57
/// Ignore all warnings.
58
IgnoreAll,
59
}
60
61
/// Metadata about how the node fits in the schedule graph
62
#[derive(Default)]
63
pub struct GraphInfo {
64
/// the sets that the node belongs to (hierarchy)
65
pub(crate) hierarchy: Vec<InternedSystemSet>,
66
/// the sets that the node depends on (must run before or after)
67
pub(crate) dependencies: Vec<Dependency>,
68
pub(crate) ambiguous_with: Ambiguity,
69
}
70
71
/// Converts 2D row-major pair of indices into a 1D array index.
72
pub(crate) fn index(row: usize, col: usize, num_cols: usize) -> usize {
73
debug_assert!(col < num_cols);
74
(row * num_cols) + col
75
}
76
77
/// Converts a 1D array index into a 2D row-major pair of indices.
78
pub(crate) fn row_col(index: usize, num_cols: usize) -> (usize, usize) {
79
(index / num_cols, index % num_cols)
80
}
81
82
/// Stores the results of the graph analysis.
83
pub(crate) struct CheckGraphResults<N: GraphNodeId> {
84
/// Boolean reachability matrix for the graph.
85
pub(crate) reachable: FixedBitSet,
86
/// Pairs of nodes that have a path connecting them.
87
pub(crate) connected: HashSet<(N, N)>,
88
/// Pairs of nodes that don't have a path connecting them.
89
pub(crate) disconnected: Vec<(N, N)>,
90
/// Edges that are redundant because a longer path exists.
91
pub(crate) transitive_edges: Vec<(N, N)>,
92
/// Variant of the graph with no transitive edges.
93
pub(crate) transitive_reduction: DiGraph<N>,
94
/// Variant of the graph with all possible transitive edges.
95
// TODO: this will very likely be used by "if-needed" ordering
96
#[expect(dead_code, reason = "See the TODO above this attribute.")]
97
pub(crate) transitive_closure: DiGraph<N>,
98
}
99
100
impl<N: GraphNodeId> Default for CheckGraphResults<N> {
101
fn default() -> Self {
102
Self {
103
reachable: FixedBitSet::new(),
104
connected: HashSet::default(),
105
disconnected: Vec::new(),
106
transitive_edges: Vec::new(),
107
transitive_reduction: DiGraph::default(),
108
transitive_closure: DiGraph::default(),
109
}
110
}
111
}
112
113
/// Processes a DAG and computes its:
114
/// - transitive reduction (along with the set of removed edges)
115
/// - transitive closure
116
/// - reachability matrix (as a bitset)
117
/// - pairs of nodes connected by a path
118
/// - pairs of nodes not connected by a path
119
///
120
/// The algorithm implemented comes from
121
/// ["On the calculation of transitive reduction-closure of orders"][1] by Habib, Morvan and Rampon.
122
///
123
/// [1]: https://doi.org/10.1016/0012-365X(93)90164-O
124
pub(crate) fn check_graph<N: GraphNodeId>(
125
graph: &DiGraph<N>,
126
topological_order: &[N],
127
) -> CheckGraphResults<N> {
128
if graph.node_count() == 0 {
129
return CheckGraphResults::default();
130
}
131
132
let n = graph.node_count();
133
134
// build a copy of the graph where the nodes and edges appear in topsorted order
135
let mut map = <HashMap<_, _>>::with_capacity_and_hasher(n, Default::default());
136
let mut topsorted = DiGraph::<N>::default();
137
// iterate nodes in topological order
138
for (i, &node) in topological_order.iter().enumerate() {
139
map.insert(node, i);
140
topsorted.add_node(node);
141
// insert nodes as successors to their predecessors
142
for pred in graph.neighbors_directed(node, Direction::Incoming) {
143
topsorted.add_edge(pred, node);
144
}
145
}
146
147
let mut reachable = FixedBitSet::with_capacity(n * n);
148
let mut connected = <HashSet<_>>::default();
149
let mut disconnected = Vec::new();
150
151
let mut transitive_edges = Vec::new();
152
let mut transitive_reduction = DiGraph::default();
153
let mut transitive_closure = DiGraph::default();
154
155
let mut visited = FixedBitSet::with_capacity(n);
156
157
// iterate nodes in topological order
158
for node in topsorted.nodes() {
159
transitive_reduction.add_node(node);
160
transitive_closure.add_node(node);
161
}
162
163
// iterate nodes in reverse topological order
164
for a in topsorted.nodes().rev() {
165
let index_a = *map.get(&a).unwrap();
166
// iterate their successors in topological order
167
for b in topsorted.neighbors_directed(a, Direction::Outgoing) {
168
let index_b = *map.get(&b).unwrap();
169
debug_assert!(index_a < index_b);
170
if !visited[index_b] {
171
// edge <a, b> is not redundant
172
transitive_reduction.add_edge(a, b);
173
transitive_closure.add_edge(a, b);
174
reachable.insert(index(index_a, index_b, n));
175
176
let successors = transitive_closure
177
.neighbors_directed(b, Direction::Outgoing)
178
.collect::<Vec<_>>();
179
for c in successors {
180
let index_c = *map.get(&c).unwrap();
181
debug_assert!(index_b < index_c);
182
if !visited[index_c] {
183
visited.insert(index_c);
184
transitive_closure.add_edge(a, c);
185
reachable.insert(index(index_a, index_c, n));
186
}
187
}
188
} else {
189
// edge <a, b> is redundant
190
transitive_edges.push((a, b));
191
}
192
}
193
194
visited.clear();
195
}
196
197
// partition pairs of nodes into "connected by path" and "not connected by path"
198
for i in 0..(n - 1) {
199
// reachable is upper triangular because the nodes were topsorted
200
for index in index(i, i + 1, n)..=index(i, n - 1, n) {
201
let (a, b) = row_col(index, n);
202
let pair = (topological_order[a], topological_order[b]);
203
if reachable[index] {
204
connected.insert(pair);
205
} else {
206
disconnected.push(pair);
207
}
208
}
209
}
210
211
// fill diagonal (nodes reach themselves)
212
// for i in 0..n {
213
// reachable.set(index(i, i, n), true);
214
// }
215
216
CheckGraphResults {
217
reachable,
218
connected,
219
disconnected,
220
transitive_edges,
221
transitive_reduction,
222
transitive_closure,
223
}
224
}
225
226
/// Returns the simple cycles in a strongly-connected component of a directed graph.
227
///
228
/// The algorithm implemented comes from
229
/// ["Finding all the elementary circuits of a directed graph"][1] by D. B. Johnson.
230
///
231
/// [1]: https://doi.org/10.1137/0204007
232
pub fn simple_cycles_in_component<N: GraphNodeId>(graph: &DiGraph<N>, scc: &[N]) -> Vec<Vec<N>> {
233
let mut cycles = vec![];
234
let mut sccs = vec![SmallVec::from_slice(scc)];
235
236
while let Some(mut scc) = sccs.pop() {
237
// only look at nodes and edges in this strongly-connected component
238
let mut subgraph = DiGraph::<N>::default();
239
for &node in &scc {
240
subgraph.add_node(node);
241
}
242
243
for &node in &scc {
244
for successor in graph.neighbors(node) {
245
if subgraph.contains_node(successor) {
246
subgraph.add_edge(node, successor);
247
}
248
}
249
}
250
251
// path of nodes that may form a cycle
252
let mut path = Vec::with_capacity(subgraph.node_count());
253
// we mark nodes as "blocked" to avoid finding permutations of the same cycles
254
let mut blocked: HashSet<_> =
255
HashSet::with_capacity_and_hasher(subgraph.node_count(), Default::default());
256
// connects nodes along path segments that can't be part of a cycle (given current root)
257
// those nodes can be unblocked at the same time
258
let mut unblock_together: HashMap<N, HashSet<N>> =
259
HashMap::with_capacity_and_hasher(subgraph.node_count(), Default::default());
260
// stack for unblocking nodes
261
let mut unblock_stack = Vec::with_capacity(subgraph.node_count());
262
// nodes can be involved in multiple cycles
263
let mut maybe_in_more_cycles: HashSet<N> =
264
HashSet::with_capacity_and_hasher(subgraph.node_count(), Default::default());
265
// stack for DFS
266
let mut stack = Vec::with_capacity(subgraph.node_count());
267
268
// we're going to look for all cycles that begin and end at this node
269
let root = scc.pop().unwrap();
270
// start a path at the root
271
path.clear();
272
path.push(root);
273
// mark this node as blocked
274
blocked.insert(root);
275
276
// DFS
277
stack.clear();
278
stack.push((root, subgraph.neighbors(root)));
279
while !stack.is_empty() {
280
let &mut (ref node, ref mut successors) = stack.last_mut().unwrap();
281
if let Some(next) = successors.next() {
282
if next == root {
283
// found a cycle
284
maybe_in_more_cycles.extend(path.iter());
285
cycles.push(path.clone());
286
} else if !blocked.contains(&next) {
287
// first time seeing `next` on this path
288
maybe_in_more_cycles.remove(&next);
289
path.push(next);
290
blocked.insert(next);
291
stack.push((next, subgraph.neighbors(next)));
292
continue;
293
} else {
294
// not first time seeing `next` on this path
295
}
296
}
297
298
if successors.peekable().peek().is_none() {
299
if maybe_in_more_cycles.contains(node) {
300
unblock_stack.push(*node);
301
// unblock this node's ancestors
302
while let Some(n) = unblock_stack.pop() {
303
if blocked.remove(&n) {
304
let unblock_predecessors = unblock_together.entry(n).or_default();
305
unblock_stack.extend(unblock_predecessors.iter());
306
unblock_predecessors.clear();
307
}
308
}
309
} else {
310
// if its descendants can be unblocked later, this node will be too
311
for successor in subgraph.neighbors(*node) {
312
unblock_together.entry(successor).or_default().insert(*node);
313
}
314
}
315
316
// remove node from path and DFS stack
317
path.pop();
318
stack.pop();
319
}
320
}
321
322
drop(stack);
323
324
// remove node from subgraph
325
subgraph.remove_node(root);
326
327
// divide remainder into smaller SCCs
328
sccs.extend(subgraph.iter_sccs().filter(|scc| scc.len() > 1));
329
}
330
331
cycles
332
}
333
334