bevy_ecs/schedule/graph/
mod.rs

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