diff --git a/Cargo.lock b/Cargo.lock index 49c69c2a..114db27e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2048,9 +2048,9 @@ checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" [[package]] name = "crossbeam-channel" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" dependencies = [ "crossbeam-utils", ] @@ -5031,6 +5031,7 @@ dependencies = [ "bevy_common_assets", "bytes", "color-eyre", + "crossbeam-channel", "derive_more", "egui", "glam", diff --git a/ryot/Cargo.toml b/ryot/Cargo.toml index b9152263..24962700 100644 --- a/ryot/Cargo.toml +++ b/ryot/Cargo.toml @@ -16,6 +16,7 @@ readme = "../README.md" async-std = "1.12.0" derive_more = "0.99.17" serde_repr = "0.1" +crossbeam-channel = "0.5.12" bevy = { workspace = true, optional = true } bevy_asset_loader = { workspace = true, optional = true } @@ -54,10 +55,10 @@ default = ["bevy"] lmdb = ["dep:heed", "dep:postcard"] compression = ["dep:zstd"] bevy = [ - "dep:bevy", - "dep:bevy_common_assets", - "dep:bevy_asset_loader", - "dep:leafwing-input-manager", + "dep:bevy", + "dep:bevy_common_assets", + "dep:bevy_asset_loader", + "dep:leafwing-input-manager", ] egui = ["dep:egui"] diff --git a/ryot/src/bevy_ryot/async_events.rs b/ryot/src/bevy_ryot/async_events.rs index c43165ab..bd004a5d 100644 --- a/ryot/src/bevy_ryot/async_events.rs +++ b/ryot/src/bevy_ryot/async_events.rs @@ -1,21 +1,55 @@ //! This module provides a way to send events between systems asynchronously. //! It's useful to send events between threads that perform asynchronous tasks, such as loading //! content from disk or loading sprites from a sprite sheet before rendering. +use std::{hash::Hasher, marker::PhantomData}; + use bevy::app::App; use bevy::prelude::*; -use std::sync::mpsc::{channel, Receiver, Sender}; -use std::sync::Mutex; +use crossbeam_channel::{Receiver, Sender}; + +// TODO: doc. +#[derive(SystemSet)] +pub struct AsyncEventSet(PhantomData); + +impl std::fmt::Debug for AsyncEventSet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "AsyncEventSet<{}>", &std::any::type_name::()) + } +} + +impl std::hash::Hash for AsyncEventSet { + fn hash(&self, _state: &mut H) { + // all systems of a given type are the same + } +} + +impl Clone for AsyncEventSet { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for AsyncEventSet {} + +impl PartialEq for AsyncEventSet { + #[inline] + fn eq(&self, _other: &Self) -> bool { + // all systems of a given type are the same + true + } +} + +impl Eq for AsyncEventSet {} /// A resource that emits asynchronous events of a given type. -/// It's a bevy friendly wrapper around a `std::sync::mpsc::Sender`. +/// It's a bevy friendly wrapper around a `crossbeam_channel::Sender`. #[derive(Resource, Deref, DerefMut)] -pub struct EventSender(pub Sender); +pub struct AsyncEventSender(pub Sender); /// A resource that receives asynchronous events of a given type. -/// It's a bevy friendly wrapper around a `std::sync::mpsc::Receiver`. -/// It's wrapped in a `Mutex` to allow multiple systems to safely access it. +/// It's a bevy friendly wrapper around a `crossbeam_channel::Receiver`. #[derive(Resource, Deref, DerefMut)] -struct EventReceiver(Mutex>); +struct AsyncEventReceiver(Receiver); /// A trait to add an asynchronous event to an App. pub trait AsyncEventApp { @@ -27,16 +61,19 @@ pub trait AsyncEventApp { /// the system that sends events from the receiver to Bevy's event system. impl AsyncEventApp for App { fn add_async_event(&mut self) -> &mut Self { - if self.world.contains_resource::>() { + if self.world.contains_resource::>() { return self; } - let (sender, receiver) = channel::(); + let (sender, receiver) = crossbeam_channel::unbounded::(); self.add_event::() - .add_systems(Update, channel_to_event::) - .insert_resource(EventSender(sender)) - .insert_resource(EventReceiver(Mutex::new(receiver))); + .add_systems( + PreUpdate, + unbounded_channel_to_event::.in_set(AsyncEventSet::(PhantomData)), + ) + .insert_resource(AsyncEventSender(sender)) + .insert_resource(AsyncEventReceiver(receiver)); self } @@ -44,10 +81,9 @@ impl AsyncEventApp for App { /// A system that sends events from the receiver to Bevy's event system. /// Converts the asynchronous event into a bevy event and sends it to the event system. -fn channel_to_event(receiver: Res>, mut writer: EventWriter) { - // this should be the only system working with the receiver, - // thus we always expect to get this lock - let events = receiver.lock().expect("unable to acquire mutex lock"); - - writer.send_batch(events.try_iter()); +fn unbounded_channel_to_event( + receiver: Res>, + mut writer: EventWriter, +) { + writer.send_batch(receiver.try_iter()); } diff --git a/ryot_compass/src/bevy_compass/gui.rs b/ryot_compass/src/bevy_compass/gui.rs index 01c3afc0..0412e95f 100644 --- a/ryot_compass/src/bevy_compass/gui.rs +++ b/ryot_compass/src/bevy_compass/gui.rs @@ -10,7 +10,7 @@ use crate::{ExportMap, LoadMap}; #[cfg(not(target_arch = "wasm32"))] use bevy::app::AppExit; #[cfg(not(target_arch = "wasm32"))] -use ryot::bevy_ryot::{AsyncEventApp, EventSender}; +use ryot::bevy_ryot::{AsyncEventApp, AsyncEventSender}; use bevy::{prelude::*, render::camera::Viewport, winit::WinitWindows}; use bevy_egui::{EguiContext, EguiContexts, EguiPlugin, EguiUserTextures}; @@ -81,7 +81,7 @@ fn ui_menu_system( #[cfg(not(target_arch = "wasm32"))] content_assets: Res, #[cfg(not(target_arch = "wasm32"))] mut exit: EventWriter, #[cfg(not(target_arch = "wasm32"))] mut map_export_sender: EventWriter, - #[cfg(not(target_arch = "wasm32"))] load_map_sender: Res>, + #[cfg(not(target_arch = "wasm32"))] load_map_sender: Res>, _windows: NonSend, ) { let Ok(mut cursor) = cursor_query.get_single_mut() else {