Skip to main content

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}