rig_ecs/tasks.rs
1//! Work that runs off the main thread: a [`Running`] task on an entity
2//! becomes a [`Done`] component when it finishes, through one `poll_tasks`
3//! system, so observers of that component carry on; on an effect, such as
4//! an open tool call, it becomes the effect's
5//! [`Outcome`](super::stage::Outcome) in
6//! [`Stage::Arrive`](super::stage::Stage::Arrive). A long-lived source,
7//! such as a socket or a thread's channel, is a [`Running::stream`] whose
8//! every item becomes a [`Done`] on the entity, in order. Each finished
9//! task and each item also calls [`Wake`], so a loop that sleeps while
10//! nothing happens runs a frame for it.
11
12use std::sync::Arc;
13use std::sync::atomic::{AtomicBool, Ordering};
14
15use bevy_ecs::prelude::*;
16use bevy_ecs::query::QueryFilter;
17use bevy_tasks::futures::check_ready;
18use bevy_tasks::{ConditionalSend, ConditionalSendFuture, Task, TaskPool};
19use crossbeam_channel::Receiver;
20use futures::{Stream, StreamExt};
21
22use super::stage::{self, Effect};
23
24/// Frames run right after a woken one (see [`run_settle_frames`]).
25const SETTLE_FRAMES: u32 = 4;
26
27/// Wakes the app's loop from another thread: a finished call, a streamed
28/// fragment, terminal input or a signal. The runner that sleeps between
29/// frames inserts its own; the default does nothing, for a loop that never
30/// sleeps for long. A windowed app inserts its own too (rig-harness's
31/// PLUGINS.md, "A window").
32///
33/// A woken frame often leaves work for the next, such as a command's
34/// notice or a task that finished just after it woke the loop, so the
35/// kernel asks for a few more frames after each wake, in `Last`, whatever
36/// the loop.
37#[derive(Resource, Clone)]
38pub struct Wake {
39 wake: Arc<dyn Fn() + Send + Sync>,
40 woken: Arc<AtomicBool>,
41}
42
43impl Wake {
44 /// A wake that calls `wake`, which asks the loop for a frame from any
45 /// thread.
46 pub fn new(wake: impl Fn() + Send + Sync + 'static) -> Self {
47 Self {
48 wake: Arc::new(wake),
49 woken: Arc::new(AtomicBool::new(false)),
50 }
51 }
52
53 /// Asks the loop for a frame soon.
54 pub fn wake(&self) {
55 self.woken.store(true, Ordering::Release);
56 (self.wake)();
57 }
58}
59
60impl Default for Wake {
61 fn default() -> Self {
62 Self::new(|| {})
63 }
64}
65
66/// Runs [`SETTLE_FRAMES`] more frames after a frame some [`Wake`] woke,
67/// in `Last`, by asking the loop for each without counting it as a wake.
68pub(crate) fn run_settle_frames(wake: Res<Wake>, mut left: Local<u32>) {
69 if wake.woken.swap(false, Ordering::AcqRel) {
70 *left = SETTLE_FRAMES;
71 }
72 if *left > 0 {
73 *left -= 1;
74 (wake.wake)();
75 }
76}
77
78/// What a finished task does to its entity: inserts its output.
79type Finish = Box<dyn FnOnce(EntityWorldMut) + Send>;
80
81/// Work running off the main thread for its entity, such as a tool call's
82/// work: when
83/// it finishes, the kernel replaces it with its output in [`PollTasks`], as a
84/// [`Done<T>`] or the component [`Running::spawn_into`] names, and
85/// observers of that component carry on. Bevy's own pattern
86/// (`examples/async_tasks/async_compute.rs`): one task component for every
87/// kind of output, so no plugin registers a system for its own. Dropping
88/// it, or the entity, cancels the task; so does the app's exit.
89#[derive(Component)]
90pub struct Running(Work);
91
92/// What a [`Running`] waits for.
93enum Work {
94 /// One output, when the task finishes.
95 Task(Task<Finish>),
96 /// An output per item, as they come, until the task ends.
97 Stream {
98 items: Receiver<Finish>,
99 task: Task<()>,
100 },
101}
102
103impl Running {
104 /// Spawns `work` on `pool`; it ends as a [`Done<T>`] on the entity, and
105 /// wakes the loop.
106 pub fn spawn<T: Send + Sync + 'static>(
107 pool: &TaskPool,
108 wake: &Wake,
109 work: impl ConditionalSendFuture<Output = T> + 'static,
110 ) -> Self {
111 Self::spawn_into::<Done<T>, T>(pool, wake, work)
112 }
113
114 /// [`Running::spawn`], ending as the component `C` made from the
115 /// output.
116 pub fn spawn_into<C: Component + From<T>, T: Send + 'static>(
117 pool: &TaskPool,
118 wake: &Wake,
119 work: impl ConditionalSendFuture<Output = T> + 'static,
120 ) -> Self {
121 Self::finishing(pool, wake, work, |mut entity, output| {
122 entity.insert(C::from(output));
123 })
124 }
125
126 /// Spawns `work` on `pool` for the effect it goes on, such as an open
127 /// tool call's `ToolResult`; it ends as the effect's
128 /// [`stage::Outcome`], unless something answered it first.
129 pub fn answering<T: Into<stage::Outcome> + Send + 'static>(
130 pool: &TaskPool,
131 wake: &Wake,
132 work: impl ConditionalSendFuture<Output = T> + 'static,
133 ) -> Self {
134 Self::finishing(pool, wake, work, |mut entity, output| {
135 let outcome: stage::Outcome = output.into();
136 entity.insert_if_new(outcome);
137 })
138 }
139
140 /// Spawns a task on `pool` that runs `items` to their end, such as a
141 /// socket's messages or a thread's channel: each item is inserted on
142 /// the entity as a [`Done<T>`], in order, and wakes the loop; once the
143 /// last one is in, `Running` is removed. Several items may come in one
144 /// frame, so a reader observes `Insert<Done<T>>`, which sees each.
145 pub fn stream<T: Send + Sync + 'static>(
146 pool: &TaskPool,
147 wake: &Wake,
148 items: impl Stream<Item = T> + ConditionalSend + 'static,
149 ) -> Self {
150 let (sender, received) = crossbeam_channel::unbounded();
151 let wake = wake.clone();
152 let task = pool.spawn(async move {
153 let mut items = std::pin::pin!(items);
154 while let Some(item) = items.next().await {
155 let insert = move |mut entity: EntityWorldMut| {
156 entity.insert(Done(item));
157 };
158 // Dropped with its entity: nothing reads the rest.
159 if sender.send(Box::new(insert) as Finish).is_err() {
160 break;
161 }
162 wake.wake();
163 }
164 wake.wake();
165 });
166 Self(Work::Stream {
167 items: received,
168 task,
169 })
170 }
171
172 /// Spawns `work` on `pool`; `finish` puts its output on the entity.
173 pub(crate) fn finishing<T: Send + 'static>(
174 pool: &TaskPool,
175 wake: &Wake,
176 work: impl ConditionalSendFuture<Output = T> + 'static,
177 finish: fn(EntityWorldMut, T),
178 ) -> Self {
179 let wake = wake.clone();
180 Self(Work::Task(pool.spawn(async move {
181 let output = work.await;
182 wake.wake();
183 Box::new(move |entity: EntityWorldMut| finish(entity, output)) as Finish
184 })))
185 }
186
187 /// Whether it is a [`Running::stream`], a source that may never end,
188 /// rather than work that finishes.
189 pub fn is_stream(&self) -> bool {
190 matches!(self.0, Work::Stream { .. })
191 }
192
193 /// Cancels the task, once awaited.
194 pub(crate) async fn cancel(self) {
195 match self.0 {
196 Work::Task(task) => {
197 task.cancel().await;
198 }
199 Work::Stream { task, .. } => {
200 task.cancel().await;
201 }
202 }
203 }
204}
205
206/// A finished task's output, or a [`Running::stream`]'s latest item.
207#[derive(Component)]
208pub struct Done<T: Send + Sync + 'static>(pub T);
209
210impl<T: Send + Sync + 'static> From<T> for Done<T> {
211 fn from(output: T) -> Self {
212 Self(output)
213 }
214}
215
216/// The system polling running work, in `Update`: a system that reads what
217/// finished this frame runs after it.
218#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
219pub struct PollTasks;
220
221/// Replaces each finished [`Running`] task with its output, but an
222/// effect's, which the kernel takes in [`Stage::Arrive`](stage::Stage::Arrive).
223pub(crate) fn poll_tasks(
224 tasks: Query<(Entity, &mut Running), Without<Effect>>,
225 commands: Commands,
226) {
227 finish(tasks, commands);
228}
229
230/// Replaces each finished effect's [`Running`] task with its outcome, in
231/// [`Stage::Arrive`](stage::Stage::Arrive).
232pub(crate) fn arrive(tasks: Query<(Entity, &mut Running), With<Effect>>, commands: Commands) {
233 finish(tasks, commands);
234}
235
236/// Replaces each finished task among `tasks` with its output, and puts a
237/// stream's items on its entity in order, removing it once it ended.
238fn finish<F: QueryFilter>(mut tasks: Query<(Entity, &mut Running), F>, mut commands: Commands) {
239 for (entity, mut running) in &mut tasks {
240 match &mut running.bypass_change_detection().0 {
241 Work::Task(task) => {
242 if let Some(finish) = check_ready(task) {
243 commands.entity(entity).remove::<Running>().queue(finish);
244 }
245 }
246 Work::Stream { items, task } => {
247 // Every item was sent before the task ended.
248 let ended = check_ready(task).is_some();
249 let mut entity = commands.entity(entity);
250 for insert in items.try_iter() {
251 entity.queue(insert);
252 }
253 if ended {
254 entity.remove::<Running>();
255 }
256 }
257 }
258 }
259}