diff --git a/crates/bevy_gilrs/src/gilrs_system.rs b/crates/bevy_gilrs/src/gilrs_system.rs index df1685d783be7..69a608a8b8b3a 100644 --- a/crates/bevy_gilrs/src/gilrs_system.rs +++ b/crates/bevy_gilrs/src/gilrs_system.rs @@ -4,8 +4,6 @@ use crate::{ }; use bevy_ecs::event::EventWriter; use bevy_ecs::prelude::Commands; -#[cfg(target_arch = "wasm32")] -use bevy_ecs::system::NonSendMut; use bevy_ecs::system::ResMut; use bevy_input::gamepad::{ GamepadConnection, GamepadConnectionEvent, RawGamepadAxisChangedEvent, @@ -15,101 +13,103 @@ use gilrs::{ev::filter::axis_dpad_to_button, EventType, Filter}; pub fn gilrs_event_startup_system( mut commands: Commands, - #[cfg(target_arch = "wasm32")] mut gilrs: NonSendMut, - #[cfg(not(target_arch = "wasm32"))] mut gilrs: ResMut, + mut gilrs: ResMut, mut gamepads: ResMut, mut events: EventWriter, ) { - for (id, gamepad) in gilrs.0.get().gamepads() { - // Create entity and add to mapping - let entity = commands.spawn_empty().id(); - gamepads.id_to_entity.insert(id, entity); - gamepads.entity_to_id.insert(entity, id); - - events.write(GamepadConnectionEvent { - gamepad: entity, - connection: GamepadConnection::Connected { - name: gamepad.name().to_string(), - vendor_id: gamepad.vendor_id(), - product_id: gamepad.product_id(), - }, - }); - } + gilrs.with(|gilrs| { + for (id, gamepad) in gilrs.gamepads() { + // Create entity and add to mapping + let entity = commands.spawn_empty().id(); + gamepads.id_to_entity.insert(id, entity); + gamepads.entity_to_id.insert(entity, id); + events.write(GamepadConnectionEvent { + gamepad: entity, + connection: GamepadConnection::Connected { + name: gamepad.name().to_string(), + vendor_id: gamepad.vendor_id(), + product_id: gamepad.product_id(), + }, + }); + } + }); } pub fn gilrs_event_system( mut commands: Commands, - #[cfg(target_arch = "wasm32")] mut gilrs: NonSendMut, - #[cfg(not(target_arch = "wasm32"))] mut gilrs: ResMut, + mut gilrs: ResMut, mut gamepads: ResMut, mut events: EventWriter, mut connection_events: EventWriter, mut button_events: EventWriter, mut axis_event: EventWriter, ) { - let gilrs = gilrs.0.get(); - while let Some(gilrs_event) = gilrs.next_event().filter_ev(&axis_dpad_to_button, gilrs) { - gilrs.update(&gilrs_event); - match gilrs_event.event { - EventType::Connected => { - let pad = gilrs.gamepad(gilrs_event.id); - let entity = gamepads.get_entity(gilrs_event.id).unwrap_or_else(|| { - let entity = commands.spawn_empty().id(); - gamepads.id_to_entity.insert(gilrs_event.id, entity); - gamepads.entity_to_id.insert(entity, gilrs_event.id); - entity - }); - - let event = GamepadConnectionEvent::new( - entity, - GamepadConnection::Connected { - name: pad.name().to_string(), - vendor_id: pad.vendor_id(), - product_id: pad.product_id(), - }, - ); + gilrs.with(|gilrs| { + while let Some(gilrs_event) = gilrs.next_event().filter_ev(&axis_dpad_to_button, gilrs) { + gilrs.update(&gilrs_event); + match gilrs_event.event { + EventType::Connected => { + let pad = gilrs.gamepad(gilrs_event.id); + let entity = gamepads.get_entity(gilrs_event.id).unwrap_or_else(|| { + let entity = commands.spawn_empty().id(); + gamepads.id_to_entity.insert(gilrs_event.id, entity); + gamepads.entity_to_id.insert(entity, gilrs_event.id); + entity + }); - events.write(event.clone().into()); - connection_events.write(event); - } - EventType::Disconnected => { - let gamepad = gamepads - .id_to_entity - .get(&gilrs_event.id) - .copied() - .expect("mapping should exist from connection"); - let event = GamepadConnectionEvent::new(gamepad, GamepadConnection::Disconnected); - events.write(event.clone().into()); - connection_events.write(event); - } - EventType::ButtonChanged(gilrs_button, raw_value, _) => { - let Some(button) = convert_button(gilrs_button) else { - continue; - }; - let gamepad = gamepads - .id_to_entity - .get(&gilrs_event.id) - .copied() - .expect("mapping should exist from connection"); - events.write(RawGamepadButtonChangedEvent::new(gamepad, button, raw_value).into()); - button_events.write(RawGamepadButtonChangedEvent::new( - gamepad, button, raw_value, - )); - } - EventType::AxisChanged(gilrs_axis, raw_value, _) => { - let Some(axis) = convert_axis(gilrs_axis) else { - continue; - }; - let gamepad = gamepads - .id_to_entity - .get(&gilrs_event.id) - .copied() - .expect("mapping should exist from connection"); - events.write(RawGamepadAxisChangedEvent::new(gamepad, axis, raw_value).into()); - axis_event.write(RawGamepadAxisChangedEvent::new(gamepad, axis, raw_value)); - } - _ => (), - }; - } - gilrs.inc(); + let event = GamepadConnectionEvent::new( + entity, + GamepadConnection::Connected { + name: pad.name().to_string(), + vendor_id: pad.vendor_id(), + product_id: pad.product_id(), + }, + ); + events.write(event.clone().into()); + connection_events.write(event); + } + EventType::Disconnected => { + let gamepad = gamepads + .id_to_entity + .get(&gilrs_event.id) + .copied() + .expect("mapping should exist from connection"); + let event = + GamepadConnectionEvent::new(gamepad, GamepadConnection::Disconnected); + events.write(event.clone().into()); + connection_events.write(event); + } + EventType::ButtonChanged(gilrs_button, raw_value, _) => { + let Some(button) = convert_button(gilrs_button) else { + continue; + }; + let gamepad = gamepads + .id_to_entity + .get(&gilrs_event.id) + .copied() + .expect("mapping should exist from connection"); + events.write( + RawGamepadButtonChangedEvent::new(gamepad, button, raw_value).into(), + ); + button_events.write(RawGamepadButtonChangedEvent::new( + gamepad, button, raw_value, + )); + } + EventType::AxisChanged(gilrs_axis, raw_value, _) => { + let Some(axis) = convert_axis(gilrs_axis) else { + continue; + }; + let gamepad = gamepads + .id_to_entity + .get(&gilrs_event.id) + .copied() + .expect("mapping should exist from connection"); + events.write(RawGamepadAxisChangedEvent::new(gamepad, axis, raw_value).into()); + axis_event.write(RawGamepadAxisChangedEvent::new(gamepad, axis, raw_value)); + } + _ => (), + }; + } + gilrs.inc(); + }); } diff --git a/crates/bevy_gilrs/src/lib.rs b/crates/bevy_gilrs/src/lib.rs index b9f1443e5b245..db939cf3f0942 100644 --- a/crates/bevy_gilrs/src/lib.rs +++ b/crates/bevy_gilrs/src/lib.rs @@ -14,19 +14,48 @@ mod converter; mod gilrs_system; mod rumble; +#[cfg(not(target_arch = "wasm32"))] +use bevy_utils::synccell::SyncCell; + +#[cfg(target_arch = "wasm32")] +use core::cell::RefCell; + use bevy_app::{App, Plugin, PostUpdate, PreStartup, PreUpdate}; use bevy_ecs::entity::hash_map::EntityHashMap; use bevy_ecs::prelude::*; use bevy_input::InputSystem; use bevy_platform_support::collections::HashMap; -use bevy_utils::synccell::SyncCell; use gilrs::GilrsBuilder; use gilrs_system::{gilrs_event_startup_system, gilrs_event_system}; use rumble::{play_gilrs_rumble, RunningRumbleEffects}; use tracing::error; -#[cfg_attr(not(target_arch = "wasm32"), derive(Resource))] -pub(crate) struct Gilrs(pub SyncCell); +#[cfg(target_arch = "wasm32")] +thread_local! { + /// Temporary storage of gilrs data to replace usage of `!Send` resources. This will be replaced with proper + /// storage of `!Send` data after issue #17667 is complete. + /// + /// Using a `thread_local!` here relies on the fact that wasm32 can only be single threaded. Previously, we used a + /// `NonSendMut` parameter, which told Bevy that the system was `!Send`, but now with the removal of `!Send` + /// resource/system parameter usage, there is no internal guarantee that the system will run in only one thread, so + /// we need to rely on the platform to make such a guarantee. + static GILRS: RefCell> = const { RefCell::new(None) }; +} + +#[derive(Resource)] +pub(crate) struct Gilrs { + #[cfg(not(target_arch = "wasm32"))] + cell: SyncCell, +} +impl Gilrs { + #[inline] + pub fn with(&mut self, f: impl FnOnce(&mut gilrs::Gilrs)) { + #[cfg(target_arch = "wasm32")] + GILRS.with(|g| f(g.borrow_mut().as_mut().expect("GILRS was not initialized"))); + #[cfg(not(target_arch = "wasm32"))] + f(self.cell.get()); + } +} /// A [`resource`](Resource) with the mapping of connected [`gilrs::GamepadId`] and their [`Entity`]. #[derive(Debug, Default, Resource)] @@ -65,10 +94,15 @@ impl Plugin for GilrsPlugin { .build() { Ok(gilrs) => { + let g = Gilrs { + #[cfg(not(target_arch = "wasm32"))] + cell: SyncCell::new(gilrs), + }; #[cfg(target_arch = "wasm32")] - app.insert_non_send_resource(Gilrs(SyncCell::new(gilrs))); - #[cfg(not(target_arch = "wasm32"))] - app.insert_resource(Gilrs(SyncCell::new(gilrs))); + GILRS.with(|g| { + g.replace(Some(gilrs)); + }); + app.insert_resource(g); app.init_resource::(); app.init_resource::() .add_systems(PreStartup, gilrs_event_startup_system) diff --git a/crates/bevy_gilrs/src/rumble.rs b/crates/bevy_gilrs/src/rumble.rs index 53a0c945cedef..7e49ec77e98c7 100644 --- a/crates/bevy_gilrs/src/rumble.rs +++ b/crates/bevy_gilrs/src/rumble.rs @@ -1,8 +1,6 @@ //! Handle user specified rumble request events. use crate::{Gilrs, GilrsGamepads}; use bevy_ecs::prelude::{EventReader, Res, ResMut, Resource}; -#[cfg(target_arch = "wasm32")] -use bevy_ecs::system::NonSendMut; use bevy_input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest}; use bevy_platform_support::collections::HashMap; use bevy_time::{Real, Time}; @@ -128,42 +126,42 @@ fn handle_rumble_request( } pub(crate) fn play_gilrs_rumble( time: Res>, - #[cfg(target_arch = "wasm32")] mut gilrs: NonSendMut, - #[cfg(not(target_arch = "wasm32"))] mut gilrs: ResMut, + mut gilrs: ResMut, gamepads: Res, mut requests: EventReader, mut running_rumbles: ResMut, ) { - let gilrs = gilrs.0.get(); - let current_time = time.elapsed(); - // Remove outdated rumble effects. - for rumbles in running_rumbles.rumbles.values_mut() { - // `ff::Effect` uses RAII, dropping = deactivating - rumbles.retain(|RunningRumble { deadline, .. }| *deadline >= current_time); - } - running_rumbles - .rumbles - .retain(|_gamepad, rumbles| !rumbles.is_empty()); - - // Add new effects. - for rumble in requests.read().cloned() { - let gamepad = rumble.gamepad(); - match handle_rumble_request(&mut running_rumbles, gilrs, &gamepads, rumble, current_time) { - Ok(()) => {} - Err(RumbleError::GilrsError(err)) => { - if let ff::Error::FfNotSupported(_) = err { - debug!("Tried to rumble {gamepad:?}, but it doesn't support force feedback"); - } else { - warn!( - "Tried to handle rumble request for {gamepad:?} but an error occurred: {err}" - ); + gilrs.with(|gilrs| { + let current_time = time.elapsed(); + // Remove outdated rumble effects. + for rumbles in running_rumbles.rumbles.values_mut() { + // `ff::Effect` uses RAII, dropping = deactivating + rumbles.retain(|RunningRumble { deadline, .. }| *deadline >= current_time); + } + running_rumbles + .rumbles + .retain(|_gamepad, rumbles| !rumbles.is_empty()); + + // Add new effects. + for rumble in requests.read().cloned() { + let gamepad = rumble.gamepad(); + match handle_rumble_request(&mut running_rumbles, gilrs, &gamepads, rumble, current_time) { + Ok(()) => {} + Err(RumbleError::GilrsError(err)) => { + if let ff::Error::FfNotSupported(_) = err { + debug!("Tried to rumble {gamepad:?}, but it doesn't support force feedback"); + } else { + warn!( + "Tried to handle rumble request for {gamepad:?} but an error occurred: {err}" + ); + } } - } - Err(RumbleError::GamepadNotFound) => { - warn!("Tried to handle rumble request {gamepad:?} but it doesn't exist!"); - } - }; - } + Err(RumbleError::GamepadNotFound) => { + warn!("Tried to handle rumble request {gamepad:?} but it doesn't exist!"); + } + }; + } + }); } #[cfg(test)] diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index c2a3eddc52f04..7cac158b959e7 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -120,11 +120,15 @@ impl Plugin for WinitPlugin { event_loop_builder.with_android_app(bevy_window::ANDROID_APP.get().expect(msg).clone()); } + let event_loop = event_loop_builder + .build() + .expect("Failed to build event loop"); + app.init_non_send_resource::() .init_resource::() .init_resource::() .add_event::() - .set_runner(winit_runner::) + .set_runner(|app| winit_runner(app, event_loop)) .add_systems( Last, ( @@ -139,14 +143,6 @@ impl Plugin for WinitPlugin { app.add_plugins(AccessKitPlugin); app.add_plugins(cursor::CursorPlugin); - - let event_loop = event_loop_builder - .build() - .expect("Failed to build event loop"); - - // `winit`'s windows are bound to the event loop that created them, so the event loop must - // be inserted as a resource here to pass it onto the runner. - app.insert_non_send_resource(event_loop); } } diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index bfa7283d1b7a1..83ee3013d7ded 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -865,17 +865,12 @@ impl WinitAppRunnerState { /// /// Overriding the app's [runner](bevy_app::App::runner) while using `WinitPlugin` will bypass the /// `EventLoop`. -pub fn winit_runner(mut app: App) -> AppExit { +pub fn winit_runner(mut app: App, event_loop: EventLoop) -> AppExit { if app.plugins_state() == PluginsState::Ready { app.finish(); app.cleanup(); } - let event_loop = app - .world_mut() - .remove_non_send_resource::>() - .unwrap(); - app.world_mut() .insert_resource(EventLoopProxyWrapper(event_loop.create_proxy()));