unlit3dx/executor.rs
1//! The async tasks a world runs, and the executor that drives them.
2//!
3//! Some work a world starts is asynchronous — a GPU request that resolves
4//! once a device is ready, for instance — and [`crate::executor::LocalTasks`]
5//! is the pool that carries it. A world owns its tasks, so a task lasts as
6//! long as the world does, whatever call started it.
7//!
8//! The executor is the ecosystem's own, re-exported so a caller needs no
9//! futures dependency of their own. It is the one that fits a frame loop: it
10//! polls every task it holds until none can make progress and never blocks,
11//! and whoever drives the world pumps it — a thread's tick loop before the
12//! tick, a host's frame before the dispatch.
13//!
14//! A spawned task is `'static`, so what it lands has to be shared with the
15//! code that reads it: an `Rc<RefCell<..>>` slot that the tick drains, as a
16//! GPU request does.
17//!
18//! # A world owns its tasks
19//!
20//! The tasks live in the world they belong to, as [a `LocalTasks`](crate::executor::LocalTasks)
21//! component, rather than in [a `LocalPool`](crate::executor::LocalPool) the caller keeps
22//! beside it.
23//! A task therefore lasts as long as the world does, whatever call started
24//! it, and no caller has to thread an executor through to the code that
25//! spawns. Every world the framework builds is given one at creation, before
26//! its setup runs; [`LocalTasks::pump`](crate::executor::LocalTasks::pump)
27//! gives one to a world that lacks it,
28//! which is how the host world, the one the caller made itself, gets its
29//! own.
30
31use std::future::Future;
32
33use unlit_ecs::World;
34use unlit_ecs::bundle;
35
36pub use futures_executor::LocalPool;
37pub use futures_util::task::LocalSpawnExt;
38
39/// The async tasks a world runs, held as a component of that world.
40///
41/// Whoever drives the world pumps them, which is what [`LocalTasks::pump`]
42/// does, so a task lasts as long as the world rather than as long as the call
43/// that started it. A world holds at most one set.
44pub struct LocalTasks(LocalPool);
45
46impl LocalTasks {
47 /// An empty task set.
48 #[must_use]
49 pub fn new() -> Self {
50 Self(LocalPool::new())
51 }
52
53 /// Give `world` a task set, unless it already has one.
54 ///
55 /// Every world the framework builds is given one this way at creation,
56 /// before its setup runs, so the setup can spawn a task of its own.
57 pub fn install(world: &mut World) {
58 if world
59 .root_scope()
60 .query::<&LocalTasks>()
61 .iter()
62 .next()
63 .is_none()
64 {
65 world.root_scope_mut().spawn(bundle![Self::new()]);
66 }
67 }
68
69 /// Drive `world`'s tasks until none can make progress.
70 ///
71 /// Call this once per turn of whatever drives the world: a thread's tick
72 /// loop before the tick, a host's frame before the dispatch. A world the
73 /// caller made itself is given its task set here, so its tasks have an
74 /// owner from its first pumped turn on.
75 ///
76 /// A task that panics unwinds out of the pump and takes the caller's
77 /// turn with it, exactly as a panicking job or tick would.
78 pub fn pump(world: &mut World) {
79 Self::install(world);
80 let mut local_tasks_rows = world.root_scope().query::<&mut LocalTasks>();
81
82 let Some(local_tasks) = local_tasks_rows.iter().next() else {
83 return;
84 };
85 local_tasks.0.run_until_stalled();
86 }
87
88 /// Hand `future` to the tasks `world` runs, answering whether it took it.
89 ///
90 /// The task is polled from the world's next pump on. A world with no task
91 /// set answers `false`: the framework gives one to every world it builds,
92 /// and [`LocalTasks::pump`] gives one to a world that lacks it.
93 pub fn spawn<F>(world: &World, future: F) -> bool
94 where
95 F: Future<Output = ()> + 'static,
96 {
97 let mut local_tasks_rows_2 = world.root_scope().query::<&LocalTasks>();
98
99 let Some(local_tasks) = local_tasks_rows_2.iter().next() else {
100 return false;
101 };
102 local_tasks.0.spawner().spawn_local(future).is_ok()
103 }
104}
105
106impl Default for LocalTasks {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use std::cell::RefCell;
115 use std::rc::Rc;
116
117 use unlit_ecs::World;
118
119 use super::*;
120
121 /// A world's first pump gives it a task set, and later ones drive it.
122 #[test]
123 fn a_worlds_tasks_are_installed_by_the_first_pump() {
124 let mut world = World::new();
125 let seen: Rc<RefCell<bool>> = Rc::default();
126 // A world with no task set refuses the task rather than dropping it.
127 let slot = Rc::clone(&seen);
128 assert!(!LocalTasks::spawn(&world, async move {
129 *slot.borrow_mut() = true;
130 }));
131 LocalTasks::pump(&mut world);
132 let slot = Rc::clone(&seen);
133 assert!(LocalTasks::spawn(&world, async move {
134 *slot.borrow_mut() = true;
135 }));
136 assert!(!*seen.borrow(), "spawning does not poll");
137 LocalTasks::pump(&mut world);
138 assert!(*seen.borrow());
139 }
140
141 /// A spawned future waits for the pool and runs on its next poll.
142 #[test]
143 fn a_pool_runs_a_spawned_future() {
144 let mut pool = LocalPool::new();
145 // A task is `'static`, so what it writes has to be shared with the
146 // test rather than borrowed from it.
147 let seen: Rc<RefCell<bool>> = Rc::default();
148 let slot = Rc::clone(&seen);
149 pool.spawner()
150 .spawn_local(async move { *slot.borrow_mut() = true })
151 .expect("a live pool takes its tasks");
152 assert!(!*seen.borrow(), "spawning does not poll");
153 pool.run_until_stalled();
154 assert!(*seen.borrow());
155 }
156}