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}