Skip to main content

pebble/ecs/
events.rs

1use crate::ecs::resources::Resources;
2use crate::ecs::system::{Res, SystemParam};
3use crate::threading::{BackgroundTasks, SpawnableFuture};
4
5/// A double-buffered queue of `T` events, stored as a singleton resource.
6///
7/// Write with an [`EventWriter<T>`] system param, read with an
8/// [`EventReader<T>`] system param. An event sent during tick `N` stays
9/// visible to readers for the rest of tick `N` and all of tick `N + 1`, then
10/// is dropped — so a reader running anywhere in either tick sees it exactly
11/// once, regardless of whether it runs before or after the writer that sent
12/// it.
13///
14/// Not usable until registered with
15/// [`App::add_event`](crate::app::App::add_event), which inserts this
16/// resource and schedules the per-tick aging that gives the two-tick
17/// guarantee above.
18pub struct Events<T> {
19    current: Vec<(usize, T)>,
20    previous: Vec<(usize, T)>,
21    next_id: usize,
22}
23
24impl<T> Default for Events<T> {
25    fn default() -> Self {
26        Self {
27            current: Vec::new(),
28            previous: Vec::new(),
29            next_id: 0,
30        }
31    }
32}
33
34impl<T> Events<T> {
35    /// Push a new event, tagged with the next monotonically increasing id.
36    pub fn send(&mut self, event: T) {
37        self.current.push((self.next_id, event));
38        self.next_id += 1;
39    }
40
41    /// Age the buffers: last tick's `current` becomes `previous` (still
42    /// visible to readers that haven't caught up), and a fresh `current`
43    /// starts collecting this tick's sends.
44    ///
45    /// Called once per tick, before any user systems run, by the closure
46    /// [`App::add_event`](crate::app::App::add_event) registers — not meant
47    /// to be called directly.
48    pub(crate) fn update(&mut self) {
49        self.previous = std::mem::take(&mut self.current);
50    }
51}
52
53/// Read-only handle to an [`Events<T>`] resource, obtained as a system
54/// parameter.
55///
56/// Each system with an `EventReader<T>` parameter has its own private
57/// read cursor (persisted like [`Local`](crate::ecs::system::Local)), so
58/// multiple readers of the same event type don't interfere with each other.
59pub struct EventReader<'a, T: 'static> {
60    events: hecs::Ref<'a, Events<T>>,
61    last_seen: &'a mut usize,
62}
63
64impl<'a, T: 'static> EventReader<'a, T> {
65    /// Iterate events this reader hasn't seen yet, oldest first, then mark
66    /// them as read.
67    pub fn iter(&mut self) -> impl Iterator<Item = &T> + '_ {
68        let seen = *self.last_seen;
69        let unread: Vec<&T> = self
70            .events
71            .previous
72            .iter()
73            .chain(self.events.current.iter())
74            .filter(|(id, _)| *id >= seen)
75            .map(|(_, event)| event)
76            .collect();
77        *self.last_seen = self.events.next_id;
78        unread.into_iter()
79    }
80
81    /// `true` if there are no unread events for this reader.
82    pub fn is_empty(&self) -> bool {
83        let seen = *self.last_seen;
84        !self
85            .events
86            .previous
87            .iter()
88            .chain(self.events.current.iter())
89            .any(|(id, _)| *id >= seen)
90    }
91}
92
93impl<T> SystemParam for EventReader<'static, T>
94where
95    T: Send + Sync + 'static,
96{
97    type Item<'a> = EventReader<'a, T>;
98    type State = usize;
99
100    fn fetch<'a>(
101        state: &'a mut Self::State,
102        world: &'a hecs::World,
103        resources: &'a Resources,
104    ) -> Self::Item<'a> {
105        EventReader {
106            events: resources.get_resource(world),
107            last_seen: state,
108        }
109    }
110
111}
112
113/// Soft-access variant of [`EventReader<T>`]: `None` instead of panicking
114/// while `T` isn't registered (via [`App::add_event`](crate::app::App::add_event)),
115/// so a system that should just skip when an event type it optionally
116/// consumes hasn't been set up yet doesn't have to hard-depend on it. The
117/// read cursor still persists correctly across ticks once the event type
118/// does appear later — it lives in the same per-system `State` slot either way.
119impl<T> SystemParam for Option<EventReader<'static, T>>
120where
121    T: Send + Sync + 'static,
122{
123    type Item<'a> = Option<EventReader<'a, T>>;
124    type State = usize;
125
126    fn fetch<'a>(
127        state: &'a mut Self::State,
128        world: &'a hecs::World,
129        resources: &'a Resources,
130    ) -> Self::Item<'a> {
131        if resources.has_resource::<Events<T>>(world) {
132            return Some(EventReader {
133                events: resources.get_resource(world),
134                last_seen: state,
135            });
136        }
137        None
138    }
139}
140
141/// Write handle to an [`Events<T>`] resource, obtained as a system
142/// parameter.
143pub struct EventWriter<'a, T: 'static> {
144    events: hecs::RefMut<'a, Events<T>>,
145}
146
147impl<'a, T: 'static> EventWriter<'a, T> {
148    /// Queue `event` for delivery to every [`EventReader<T>`] for the rest
149    /// of this tick and all of the next.
150    pub fn send(&mut self, event: T) {
151        self.events.send(event);
152    }
153}
154
155impl<T> SystemParam for EventWriter<'static, T>
156where
157    T: Send + Sync + 'static,
158{
159    type Item<'a> = EventWriter<'a, T>;
160    type State = ();
161
162    fn fetch<'a>(
163        _state: &'a mut Self::State,
164        world: &'a hecs::World,
165        resources: &'a Resources,
166    ) -> Self::Item<'a> {
167        EventWriter {
168            events: resources.get_resource_mut(world),
169        }
170    }
171
172}
173
174/// Soft-access variant of [`EventWriter<T>`]: `None` instead of panicking
175/// while `T` isn't registered (via [`App::add_event`](crate::app::App::add_event)).
176impl<T> SystemParam for Option<EventWriter<'static, T>>
177where
178    T: Send + Sync + 'static,
179{
180    type Item<'a> = Option<EventWriter<'a, T>>;
181    type State = ();
182
183    fn fetch<'a>(
184        _state: &'a mut Self::State,
185        world: &'a hecs::World,
186        resources: &'a Resources,
187    ) -> Self::Item<'a> {
188        if resources.has_resource::<Events<T>>(world) {
189            return Some(EventWriter {
190                events: resources.get_resource_mut(world),
191            });
192        }
193        None
194    }
195}
196
197/// The channel [`AsyncEventWriter::spawn`] sends into and the drain system
198/// (registered by [`App::add_async_event`](crate::app::App::add_async_event))
199/// reads out of, turning finished background tasks into ordinary `T`
200/// events. A resource, but not one to reach for directly — go through
201/// [`AsyncEventWriter<T>`].
202pub(crate) struct AsyncEventChannel<T> {
203    tx: crossbeam_channel::Sender<T>,
204    rx: crossbeam_channel::Receiver<T>,
205}
206
207impl<T> AsyncEventChannel<T> {
208    pub(crate) fn new() -> Self {
209        let (tx, rx) = crossbeam_channel::unbounded();
210        Self { tx, rx }
211    }
212}
213
214/// `PreUpdate` system registered by [`App::add_async_event`](crate::app::App::add_async_event):
215/// drains every task that finished since last checked and re-delivers each
216/// result as a `T` event. Two ticks of latency at most between a task
217/// finishing and a reader seeing it — one to be drained here, one more it
218/// stays visible for like any [`Events<T>`] send.
219pub(crate) fn drain_async_events<T: Send + Sync + 'static>(
220    channel: Res<AsyncEventChannel<T>>,
221    mut writer: EventWriter<T>,
222) {
223    while let Ok(event) = channel.rx.try_recv() {
224        writer.send(event);
225    }
226}
227
228/// System parameter for spawning async work whose result is delivered as a
229/// `T` event once it resolves. The friendly front door for combining
230/// [`BackgroundTasks::spawn_async`] with the [`Events`] system, instead of
231/// hand-rolling a pending-task resource and poll system per async data
232/// source the way [`GraphicsPlugin`](crate::rendering::graphics_plugin::GraphicsPlugin)
233/// does for the GPU backend.
234///
235/// Register the event type with [`App::add_async_event::<T>()`](crate::app::App::add_async_event)
236/// (instead of [`App::add_event`](crate::app::App::add_event)) to make this usable as a system
237/// parameter. Reading the result is completely ordinary — just an
238/// [`EventReader<T>`], same as any other event; nothing about consuming it
239/// differs from a synchronously-sent one. Requires
240/// [`BackgroundTasksPlugin`](crate::threading::BackgroundTasksPlugin) to be
241/// registered, since [`spawn`](Self::spawn) drives its future through
242/// [`BackgroundTasks::spawn_async`].
243///
244/// ```ignore
245/// app.add_async_event::<ReadbackDone>();
246///
247/// fn start_readback(events: AsyncEventWriter<ReadbackDone>, buffer: Res<SomeGpuBuffer>) {
248///     let future = buffer.0.read(); // buffer.0: pebble::wgpu::buffer::Buffer
249///     events.spawn(async move { ReadbackDone(future.await) });
250/// }
251///
252/// fn on_readback(mut reader: EventReader<ReadbackDone>) {
253///     for event in reader.iter() {
254///         // event.0
255///     }
256/// }
257/// ```
258pub struct AsyncEventWriter<'a, T: Send + 'static> {
259    tasks: hecs::Ref<'a, BackgroundTasks>,
260    channel: hecs::Ref<'a, AsyncEventChannel<T>>,
261}
262
263impl<'a, T: Send + 'static> AsyncEventWriter<'a, T> {
264    /// Spawn `future`. Once it resolves, its output is delivered to every
265    /// [`EventReader<T>`] — no matter how many ticks the future takes, and
266    /// with no need to hold onto a handle or poll anything yourself.
267    pub fn spawn(&self, future: impl SpawnableFuture<T>) {
268        let tx = self.channel.tx.clone();
269        let _ = self.tasks.spawn_async(async move {
270            // Ignore send failure: it only happens if the receiving end
271            // (owned by the App) was dropped, meaning the app has shut down
272            // and there's nothing left to deliver this to.
273            let _ = tx.send(future.await);
274        });
275    }
276}
277
278impl<T> SystemParam for AsyncEventWriter<'static, T>
279where
280    T: Send + Sync + 'static,
281{
282    type Item<'a> = AsyncEventWriter<'a, T>;
283    type State = ();
284
285    fn fetch<'a>(
286        _state: &'a mut Self::State,
287        world: &'a hecs::World,
288        resources: &'a Resources,
289    ) -> Self::Item<'a> {
290        AsyncEventWriter {
291            tasks: resources.get_resource(world),
292            channel: resources.get_resource(world),
293        }
294    }
295
296}
297
298/// Soft-access variant of [`AsyncEventWriter<T>`]: `None` instead of panicking
299/// while `T` isn't registered via [`add_async_event`](crate::app::App::add_async_event)
300/// (or [`BackgroundTasksPlugin`](crate::threading::BackgroundTasksPlugin)
301/// isn't installed yet). Useful for systems that only opportunistically
302/// spawn background work and shouldn't hard-fail while the app is still
303/// assembling its plugins.
304impl<T> SystemParam for Option<AsyncEventWriter<'static, T>>
305where
306    T: Send + Sync + 'static,
307{
308    type Item<'a> = Option<AsyncEventWriter<'a, T>>;
309    type State = ();
310
311    fn fetch<'a>(
312        _state: &'a mut Self::State,
313        world: &'a hecs::World,
314        resources: &'a Resources,
315    ) -> Self::Item<'a> {
316        if resources.has_resource::<BackgroundTasks>(world)
317            && resources.has_resource::<AsyncEventChannel<T>>(world)
318        {
319            return Some(AsyncEventWriter {
320                tasks: resources.get_resource(world),
321                channel: resources.get_resource(world),
322            });
323        }
324        None
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331    use crate::app::{App, SystemStage};
332    use crate::ecs::system::{Local, ResMut};
333
334    struct Damage(u32);
335
336    struct Seen(Vec<u32>);
337
338    fn send_once(mut writer: EventWriter<Damage>, mut sent: Local<bool>) {
339        if !*sent {
340            writer.send(Damage(5));
341            *sent = true;
342        }
343    }
344
345    fn record_damage(mut reader: EventReader<Damage>, mut seen: ResMut<Seen>) {
346        for event in reader.iter() {
347            seen.0.push(event.0);
348        }
349    }
350
351    #[test]
352    fn reader_in_an_earlier_stage_still_catches_a_same_tick_send_one_tick_later() {
353        let mut app = App::new();
354        app.add_event::<Damage>();
355        app.add_resource(Seen(Vec::new()));
356
357        // The reader runs in PreUpdate, before the writer's Update stage —
358        // so within the tick the event is sent, this reader has already run
359        // and can't see it yet. It should only pick it up on the *next*
360        // tick, via the aged `previous` buffer, and exactly once.
361        app.add_system(SystemStage::PreUpdate, record_damage);
362        app.add_system(SystemStage::Update, send_once);
363        app.build();
364
365        app.update(); // tick 1: reader runs first (nothing sent yet), then writer sends
366        assert_eq!(app.get_resource::<Seen>().0, Vec::<u32>::new());
367
368        app.update(); // tick 2: reader now sees last tick's send
369        assert_eq!(app.get_resource::<Seen>().0, vec![5]);
370
371        app.update(); // tick 3: event has aged out, nothing new
372        assert_eq!(app.get_resource::<Seen>().0, vec![5]);
373    }
374
375    struct Loaded(u32);
376
377    fn kick_off(events: AsyncEventWriter<Loaded>, mut sent: Local<bool>) {
378        if !*sent {
379            events.spawn(async { Loaded(42) });
380            *sent = true;
381        }
382    }
383
384    fn record_loaded(mut reader: EventReader<Loaded>, mut seen: ResMut<Seen>) {
385        for event in reader.iter() {
386            seen.0.push(event.0);
387        }
388    }
389
390    #[test]
391    fn async_events_deliver_a_background_tasks_result_as_an_event() {
392        use crate::ecs::plugin::Plugin;
393        use crate::threading::BackgroundTasksPlugin;
394
395        let mut app = App::new();
396        BackgroundTasksPlugin::new(1).build(&mut app);
397        app.add_async_event::<Loaded>();
398        app.add_resource(Seen(Vec::new()));
399        app.add_system(SystemStage::Update, kick_off);
400        app.add_system(SystemStage::PostRender, record_loaded);
401        app.build();
402
403        // The task resolves on a worker thread, off the tick loop — poll a
404        // bounded number of ticks (with a short sleep so the worker gets a
405        // chance to run) instead of assuming it lands on a specific tick.
406        for _ in 0..200 {
407            app.update();
408            if !app.get_resource::<Seen>().0.is_empty() {
409                break;
410            }
411            std::thread::sleep(std::time::Duration::from_millis(5));
412        }
413
414        assert_eq!(app.get_resource::<Seen>().0, vec![42]);
415    }
416}