Thanks to visit codestin.com
Credit goes to docs.rs

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 tarjan_scc;
17
18pub 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)]
22pub(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.
30pub(crate) struct Dependency {
31    pub(crate) kind: DependencyKind,
32    pub(crate) set: InternedSystemSet,
33    pub(crate) options: TypeIdMap<Box<dyn Any>>,
34}
35
36impl 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)]
52pub(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)]
63pub 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.
72pub(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.
78pub(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.
83pub(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
100impl<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
124pub(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
232pub 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}