legion_task/
graph_builder.rs

1use crate::components::*;
2
3use legion::prelude::*;
4
5/// Implemented by all nodes of a `TaskGraph`. Has a blanket impl that should work for most
6/// `TaskComponent`s.
7pub trait TaskFactory {
8    fn create_task(&self, cmd: &mut CommandBuffer) -> Entity;
9}
10
11impl<'a, T: 'static + Clone + TaskComponent<'a>> TaskFactory for T {
12    fn create_task(&self, cmd: &mut CommandBuffer) -> Entity {
13        make_task(cmd, self.clone())
14    }
15}
16
17// PERF: Cons requires a lot of heap allocations, but this choice was made to avoid using recursive
18// types which prevent assigning different graphs to a single variable (e.g. accumulating a graph in
19// a loop).
20
21/// Implementation detail of `TaskGraph`. Note that two trees may be unequal yet equivalent in how
22/// they `assemble`, for example, `Cons::Seq(x, Cons::Seq(y, z)) != Cons::Seq(Cons::Seq(x, y), z)`,
23/// but they both assemble into a sequence `x -> y -> z`.
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub enum Cons<T> {
26    Fork(Box<Cons<T>>, Box<Cons<T>>),
27    Seq(Box<Cons<T>>, Box<Cons<T>>),
28    Task(T),
29    Nil, // currently required to support graph accumulation
30}
31
32impl<T> Cons<T> {
33    fn remove_nil(self) -> Self {
34        match self {
35            Cons::Seq(head, tail) => match (*head, *tail) {
36                (Cons::Nil, Cons::Nil) => Cons::Nil,
37                (Cons::Nil, t) => t.remove_nil(),
38                (h, Cons::Nil) => h.remove_nil(),
39                (h, t) => Cons::Seq(Box::new(h.remove_nil()), Box::new(t.remove_nil())),
40            },
41            Cons::Fork(head, tail) => match (*head, *tail) {
42                (Cons::Nil, Cons::Nil) => Cons::Nil,
43                (Cons::Nil, t) => t.remove_nil(),
44                (h, Cons::Nil) => h.remove_nil(),
45                (h, t) => Cons::Fork(Box::new(h.remove_nil()), Box::new(t.remove_nil())),
46            },
47            Cons::Task(t) => Cons::Task(t),
48            Cons::Nil => Cons::Nil,
49        }
50    }
51}
52
53/// A node of the binary tree grammar that describes a task graph. `Cons::Seq` lists represent
54/// sequential execution of tasks. `Cons::Fork` lists represent concurrent execution of tasks. The
55/// leaves of the tree are `Cons::Task`s.
56pub type TaskGraph = Cons<Box<dyn TaskFactory + Send + Sync>>;
57
58impl Cons<Box<dyn TaskFactory + Send + Sync>> {
59    fn _assemble(self, fork: Option<Entity>, cmd: &mut CommandBuffer) -> (Entity, Entity) {
60        match self {
61            Cons::Seq(head, tail) => {
62                let (head_first_entity, head_last_entity) = head._assemble(None, cmd);
63                let (tail_first_entity, tail_last_entity) = tail._assemble(None, cmd);
64                join(cmd, tail_first_entity, head_last_entity);
65
66                (head_first_entity, tail_last_entity)
67            }
68            Cons::Fork(head, tail) => {
69                let fork_entity = if let Some(e) = fork {
70                    e
71                } else {
72                    make_fork(cmd)
73                };
74
75                let (_, head_last_entity) = head._assemble(Some(fork_entity), cmd);
76                let (_, tail_last_entity) = tail._assemble(Some(fork_entity), cmd);
77
78                // Any decendents reachable only via Cons::Fork are considered prongs. If a
79                // descendent is a Cons::Seq, then the prong only connects at the "last" entity of
80                // the sequence.
81                if head_last_entity != fork_entity {
82                    add_prong(cmd, fork_entity, head_last_entity);
83                }
84                if tail_last_entity != fork_entity {
85                    add_prong(cmd, fork_entity, tail_last_entity);
86                }
87
88                (fork_entity, fork_entity)
89            }
90            Cons::Task(task) => {
91                let task_entity = task.create_task(cmd);
92
93                (task_entity, task_entity)
94            }
95            Cons::Nil => panic!("Tried to assemble Cons::Nil, which should always be removed."),
96        }
97    }
98
99    /// Mark the root of the `TaskGraph` as final, effectively unblocking the first tasks in this
100    /// graph to be run. Panics if `self` contains no tasks.
101    pub fn assemble(self, on_completion: OnCompletion, cmd: &mut CommandBuffer) -> Entity {
102        let s = self.remove_nil();
103        let (_first_entity, last_entity) = s._assemble(None, cmd);
104        finalize(cmd, last_entity, on_completion);
105
106        last_entity
107    }
108}
109
110// TODO: Get rid of the "@" that precedes every task expression. I am bad at macros, please help!
111
112/// Make a task graph without any tasks. This is used as the initial value for accumulating graphs
113/// dynamically.
114#[macro_export]
115macro_rules! empty_graph {
116    () => {
117        Cons::Nil
118    };
119}
120
121/// Make a single-node `TaskGraph`.
122#[macro_export]
123macro_rules! task {
124    (@$task:expr) => {
125        Cons::Task(Box::new($task))
126    };
127}
128
129// TODO: deduplicate these definitions that are mostly the same
130
131/// Returns a `TaskGraph` that executes the argument list of `TaskGraphs` concurrently.
132#[macro_export]
133macro_rules! fork {
134    (@$head:expr, $($tail:tt)*) => (
135        Cons::Fork(Box::new(fork!(@$head)), Box::new(fork!($($tail)*)))
136    );
137    ($head:expr, $($tail:tt)*) => (
138        Cons::Fork(Box::new(fork!($head)), Box::new(fork!($($tail)*)))
139    );
140    (@$task:expr) => (
141        Cons::Task(Box::new($task))
142    );
143    ($head:expr) => ( $head );
144}
145
146/// Returns a `TaskGraph` that executes the argument list of `TaskGraphs` sequentially.
147#[macro_export]
148macro_rules! seq {
149    (@$head:expr, $($tail:tt)*) => (
150        Cons::Seq(Box::new(seq!(@$head)), Box::new(seq!($($tail)*)))
151    );
152    ($head:expr, $($tail:tt)*) => (
153        Cons::Seq(Box::new(seq!($head)), Box::new(seq!($($tail)*)))
154    );
155    (@$task:expr) => (
156        Cons::Task(Box::new($task))
157    );
158    ($head:expr) => ( $head );
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[derive(Clone, Debug, Eq, PartialEq)]
166    struct Foo(u32);
167
168    #[test]
169    fn task_graph_macro_fork_two() {
170        let x = fork!(@Foo(1u32), @Foo(2u32));
171        assert_eq!(
172            x,
173            Cons::Fork(
174                Box::new(Cons::Task(Box::new(Foo(1u32)))),
175                Box::new(Cons::Task(Box::new(Foo(2u32)))),
176            )
177        );
178    }
179
180    #[test]
181    fn task_graph_macro_fork_three() {
182        let x = fork!(@Foo(1u32), @Foo(2u32), @Foo(3u32));
183        assert_eq!(
184            x,
185            Cons::Fork(
186                Box::new(Cons::Task(Box::new(Foo(1u32)))),
187                Box::new(Cons::Fork(
188                    Box::new(Cons::Task(Box::new(Foo(2u32)))),
189                    Box::new(Cons::Task(Box::new(Foo(3u32)))),
190                )),
191            )
192        );
193    }
194
195    #[test]
196    fn task_graph_macro_nested_fork() {
197        let x: Cons<Box<Foo>> = fork!(@Foo(1u32), fork!(@Foo(2u32), @Foo(3u32)));
198        assert_eq!(
199            x,
200            Cons::Fork(
201                Box::new(Cons::Task(Box::new(Foo(1u32)))),
202                Box::new(Cons::Fork(
203                    Box::new(Cons::Task(Box::new(Foo(2u32)))),
204                    Box::new(Cons::Task(Box::new(Foo(3u32)))),
205                )),
206            )
207        );
208    }
209
210    #[test]
211    fn task_graph_macro_many_nested() {
212        let x = seq!(
213            @Foo(1u32),
214            fork!(
215                seq!(@Foo(2u32), @Foo(3u32), @Foo(4u32)),
216                @Foo(5u32),
217                @Foo(6u32)
218            ),
219            @Foo(7u32)
220        );
221        let y: Cons<Box<Foo>> = Cons::Seq(
222            Box::new(Cons::Task(Box::new(Foo(1u32)))),
223            Box::new(Cons::Seq(
224                Box::new(Cons::Fork(
225                    Box::new(Cons::Seq(
226                        Box::new(Cons::Task(Box::new(Foo(2u32)))),
227                        Box::new(Cons::Seq(
228                            Box::new(Cons::Task(Box::new(Foo(3u32)))),
229                            Box::new(Cons::Task(Box::new(Foo(4u32)))),
230                        )),
231                    )),
232                    Box::new(Cons::Fork(
233                        Box::new(Cons::Task(Box::new(Foo(5u32)))),
234                        Box::new(Cons::Task(Box::new(Foo(6u32)))),
235                    )),
236                )),
237                Box::new(Cons::Task(Box::new(Foo(7u32)))),
238            )),
239        );
240        assert_eq!(x, y);
241    }
242
243    #[test]
244    fn remove_nil_from_left_fork() {
245        let x = fork!(Cons::Nil, @Foo(1));
246        assert_eq!(x.remove_nil(), task!(@Foo(1)));
247    }
248
249    #[test]
250    fn remove_nil_from_right_fork() {
251        let x = fork!(@Foo(1), Cons::Nil);
252        assert_eq!(x.remove_nil(), task!(@Foo(1)));
253    }
254
255    #[test]
256    fn remove_all_nils_nested_fork() {
257        let x = fork!(Cons::Nil, fork!(Cons::Nil, @Foo(1)));
258        assert_eq!(x.remove_nil(), task!(@Foo(1)));
259    }
260
261    #[test]
262    fn accumulate_sequence_in_loop() {
263        let mut s = empty_graph!();
264        for i in 0..4 {
265            s = seq!(s, @Foo(i));
266        }
267        // Unfortunately removing nils puts the tree in an equivalent but not equal shape.
268        assert_eq!(
269            s.remove_nil(),
270            seq!(seq!(seq!(@Foo(0), @Foo(1)), @Foo(2)), @Foo(3))
271        );
272    }
273}