Skip to content

Commit df6e136

Browse files
Replace some !Send resources with thread_local! (#17730)
# Objective Work for issue #17682 What's in this PR: * Removal of some `!Send` resources that Bevy uses internally * Replaces `!Send` resources with `thread_local!` static What this PR does not cover: * The ability to create `!Send` resources still exists * Tests that test `!Send` resources are present (and should not be removed until the ability to create `!Send` resources is removed) * The example `log_layers_ecs` still uses a `!Send` resource. In this example, removing the `!Send` resource results in the system that uses it running on a thread other than the main thread, which doesn't work with lazily initialized `thread_local!` static data. Removing this `!Send` resource will need to be deferred until the System API is extended to support configuring which thread the System runs on. Once an issue for this work is created, it will be mentioned in #17667 Once the System API is extended to allow control of which thread the System runs on, the rest of the `!Send` resources can be removed in a different PR.
1 parent 0b5302d commit df6e136

File tree

5 files changed

+162
-139
lines changed

5 files changed

+162
-139
lines changed

crates/bevy_gilrs/src/gilrs_system.rs

Lines changed: 85 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ use crate::{
44
};
55
use bevy_ecs::event::EventWriter;
66
use bevy_ecs::prelude::Commands;
7-
#[cfg(target_arch = "wasm32")]
8-
use bevy_ecs::system::NonSendMut;
97
use bevy_ecs::system::ResMut;
108
use bevy_input::gamepad::{
119
GamepadConnection, GamepadConnectionEvent, RawGamepadAxisChangedEvent,
@@ -15,101 +13,103 @@ use gilrs::{ev::filter::axis_dpad_to_button, EventType, Filter};
1513

1614
pub fn gilrs_event_startup_system(
1715
mut commands: Commands,
18-
#[cfg(target_arch = "wasm32")] mut gilrs: NonSendMut<Gilrs>,
19-
#[cfg(not(target_arch = "wasm32"))] mut gilrs: ResMut<Gilrs>,
16+
mut gilrs: ResMut<Gilrs>,
2017
mut gamepads: ResMut<GilrsGamepads>,
2118
mut events: EventWriter<GamepadConnectionEvent>,
2219
) {
23-
for (id, gamepad) in gilrs.0.get().gamepads() {
24-
// Create entity and add to mapping
25-
let entity = commands.spawn_empty().id();
26-
gamepads.id_to_entity.insert(id, entity);
27-
gamepads.entity_to_id.insert(entity, id);
28-
29-
events.write(GamepadConnectionEvent {
30-
gamepad: entity,
31-
connection: GamepadConnection::Connected {
32-
name: gamepad.name().to_string(),
33-
vendor_id: gamepad.vendor_id(),
34-
product_id: gamepad.product_id(),
35-
},
36-
});
37-
}
20+
gilrs.with(|gilrs| {
21+
for (id, gamepad) in gilrs.gamepads() {
22+
// Create entity and add to mapping
23+
let entity = commands.spawn_empty().id();
24+
gamepads.id_to_entity.insert(id, entity);
25+
gamepads.entity_to_id.insert(entity, id);
26+
events.write(GamepadConnectionEvent {
27+
gamepad: entity,
28+
connection: GamepadConnection::Connected {
29+
name: gamepad.name().to_string(),
30+
vendor_id: gamepad.vendor_id(),
31+
product_id: gamepad.product_id(),
32+
},
33+
});
34+
}
35+
});
3836
}
3937

4038
pub fn gilrs_event_system(
4139
mut commands: Commands,
42-
#[cfg(target_arch = "wasm32")] mut gilrs: NonSendMut<Gilrs>,
43-
#[cfg(not(target_arch = "wasm32"))] mut gilrs: ResMut<Gilrs>,
40+
mut gilrs: ResMut<Gilrs>,
4441
mut gamepads: ResMut<GilrsGamepads>,
4542
mut events: EventWriter<RawGamepadEvent>,
4643
mut connection_events: EventWriter<GamepadConnectionEvent>,
4744
mut button_events: EventWriter<RawGamepadButtonChangedEvent>,
4845
mut axis_event: EventWriter<RawGamepadAxisChangedEvent>,
4946
) {
50-
let gilrs = gilrs.0.get();
51-
while let Some(gilrs_event) = gilrs.next_event().filter_ev(&axis_dpad_to_button, gilrs) {
52-
gilrs.update(&gilrs_event);
53-
match gilrs_event.event {
54-
EventType::Connected => {
55-
let pad = gilrs.gamepad(gilrs_event.id);
56-
let entity = gamepads.get_entity(gilrs_event.id).unwrap_or_else(|| {
57-
let entity = commands.spawn_empty().id();
58-
gamepads.id_to_entity.insert(gilrs_event.id, entity);
59-
gamepads.entity_to_id.insert(entity, gilrs_event.id);
60-
entity
61-
});
62-
63-
let event = GamepadConnectionEvent::new(
64-
entity,
65-
GamepadConnection::Connected {
66-
name: pad.name().to_string(),
67-
vendor_id: pad.vendor_id(),
68-
product_id: pad.product_id(),
69-
},
70-
);
47+
gilrs.with(|gilrs| {
48+
while let Some(gilrs_event) = gilrs.next_event().filter_ev(&axis_dpad_to_button, gilrs) {
49+
gilrs.update(&gilrs_event);
50+
match gilrs_event.event {
51+
EventType::Connected => {
52+
let pad = gilrs.gamepad(gilrs_event.id);
53+
let entity = gamepads.get_entity(gilrs_event.id).unwrap_or_else(|| {
54+
let entity = commands.spawn_empty().id();
55+
gamepads.id_to_entity.insert(gilrs_event.id, entity);
56+
gamepads.entity_to_id.insert(entity, gilrs_event.id);
57+
entity
58+
});
7159

72-
events.write(event.clone().into());
73-
connection_events.write(event);
74-
}
75-
EventType::Disconnected => {
76-
let gamepad = gamepads
77-
.id_to_entity
78-
.get(&gilrs_event.id)
79-
.copied()
80-
.expect("mapping should exist from connection");
81-
let event = GamepadConnectionEvent::new(gamepad, GamepadConnection::Disconnected);
82-
events.write(event.clone().into());
83-
connection_events.write(event);
84-
}
85-
EventType::ButtonChanged(gilrs_button, raw_value, _) => {
86-
let Some(button) = convert_button(gilrs_button) else {
87-
continue;
88-
};
89-
let gamepad = gamepads
90-
.id_to_entity
91-
.get(&gilrs_event.id)
92-
.copied()
93-
.expect("mapping should exist from connection");
94-
events.write(RawGamepadButtonChangedEvent::new(gamepad, button, raw_value).into());
95-
button_events.write(RawGamepadButtonChangedEvent::new(
96-
gamepad, button, raw_value,
97-
));
98-
}
99-
EventType::AxisChanged(gilrs_axis, raw_value, _) => {
100-
let Some(axis) = convert_axis(gilrs_axis) else {
101-
continue;
102-
};
103-
let gamepad = gamepads
104-
.id_to_entity
105-
.get(&gilrs_event.id)
106-
.copied()
107-
.expect("mapping should exist from connection");
108-
events.write(RawGamepadAxisChangedEvent::new(gamepad, axis, raw_value).into());
109-
axis_event.write(RawGamepadAxisChangedEvent::new(gamepad, axis, raw_value));
110-
}
111-
_ => (),
112-
};
113-
}
114-
gilrs.inc();
60+
let event = GamepadConnectionEvent::new(
61+
entity,
62+
GamepadConnection::Connected {
63+
name: pad.name().to_string(),
64+
vendor_id: pad.vendor_id(),
65+
product_id: pad.product_id(),
66+
},
67+
);
68+
events.write(event.clone().into());
69+
connection_events.write(event);
70+
}
71+
EventType::Disconnected => {
72+
let gamepad = gamepads
73+
.id_to_entity
74+
.get(&gilrs_event.id)
75+
.copied()
76+
.expect("mapping should exist from connection");
77+
let event =
78+
GamepadConnectionEvent::new(gamepad, GamepadConnection::Disconnected);
79+
events.write(event.clone().into());
80+
connection_events.write(event);
81+
}
82+
EventType::ButtonChanged(gilrs_button, raw_value, _) => {
83+
let Some(button) = convert_button(gilrs_button) else {
84+
continue;
85+
};
86+
let gamepad = gamepads
87+
.id_to_entity
88+
.get(&gilrs_event.id)
89+
.copied()
90+
.expect("mapping should exist from connection");
91+
events.write(
92+
RawGamepadButtonChangedEvent::new(gamepad, button, raw_value).into(),
93+
);
94+
button_events.write(RawGamepadButtonChangedEvent::new(
95+
gamepad, button, raw_value,
96+
));
97+
}
98+
EventType::AxisChanged(gilrs_axis, raw_value, _) => {
99+
let Some(axis) = convert_axis(gilrs_axis) else {
100+
continue;
101+
};
102+
let gamepad = gamepads
103+
.id_to_entity
104+
.get(&gilrs_event.id)
105+
.copied()
106+
.expect("mapping should exist from connection");
107+
events.write(RawGamepadAxisChangedEvent::new(gamepad, axis, raw_value).into());
108+
axis_event.write(RawGamepadAxisChangedEvent::new(gamepad, axis, raw_value));
109+
}
110+
_ => (),
111+
};
112+
}
113+
gilrs.inc();
114+
});
115115
}

crates/bevy_gilrs/src/lib.rs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,48 @@ mod converter;
1414
mod gilrs_system;
1515
mod rumble;
1616

17+
#[cfg(not(target_arch = "wasm32"))]
18+
use bevy_utils::synccell::SyncCell;
19+
20+
#[cfg(target_arch = "wasm32")]
21+
use core::cell::RefCell;
22+
1723
use bevy_app::{App, Plugin, PostUpdate, PreStartup, PreUpdate};
1824
use bevy_ecs::entity::hash_map::EntityHashMap;
1925
use bevy_ecs::prelude::*;
2026
use bevy_input::InputSystem;
2127
use bevy_platform_support::collections::HashMap;
22-
use bevy_utils::synccell::SyncCell;
2328
use gilrs::GilrsBuilder;
2429
use gilrs_system::{gilrs_event_startup_system, gilrs_event_system};
2530
use rumble::{play_gilrs_rumble, RunningRumbleEffects};
2631
use tracing::error;
2732

28-
#[cfg_attr(not(target_arch = "wasm32"), derive(Resource))]
29-
pub(crate) struct Gilrs(pub SyncCell<gilrs::Gilrs>);
33+
#[cfg(target_arch = "wasm32")]
34+
thread_local! {
35+
/// Temporary storage of gilrs data to replace usage of `!Send` resources. This will be replaced with proper
36+
/// storage of `!Send` data after issue #17667 is complete.
37+
///
38+
/// Using a `thread_local!` here relies on the fact that wasm32 can only be single threaded. Previously, we used a
39+
/// `NonSendMut` parameter, which told Bevy that the system was `!Send`, but now with the removal of `!Send`
40+
/// resource/system parameter usage, there is no internal guarantee that the system will run in only one thread, so
41+
/// we need to rely on the platform to make such a guarantee.
42+
static GILRS: RefCell<Option<gilrs::Gilrs>> = const { RefCell::new(None) };
43+
}
44+
45+
#[derive(Resource)]
46+
pub(crate) struct Gilrs {
47+
#[cfg(not(target_arch = "wasm32"))]
48+
cell: SyncCell<gilrs::Gilrs>,
49+
}
50+
impl Gilrs {
51+
#[inline]
52+
pub fn with(&mut self, f: impl FnOnce(&mut gilrs::Gilrs)) {
53+
#[cfg(target_arch = "wasm32")]
54+
GILRS.with(|g| f(g.borrow_mut().as_mut().expect("GILRS was not initialized")));
55+
#[cfg(not(target_arch = "wasm32"))]
56+
f(self.cell.get());
57+
}
58+
}
3059

3160
/// A [`resource`](Resource) with the mapping of connected [`gilrs::GamepadId`] and their [`Entity`].
3261
#[derive(Debug, Default, Resource)]
@@ -65,10 +94,15 @@ impl Plugin for GilrsPlugin {
6594
.build()
6695
{
6796
Ok(gilrs) => {
97+
let g = Gilrs {
98+
#[cfg(not(target_arch = "wasm32"))]
99+
cell: SyncCell::new(gilrs),
100+
};
68101
#[cfg(target_arch = "wasm32")]
69-
app.insert_non_send_resource(Gilrs(SyncCell::new(gilrs)));
70-
#[cfg(not(target_arch = "wasm32"))]
71-
app.insert_resource(Gilrs(SyncCell::new(gilrs)));
102+
GILRS.with(|g| {
103+
g.replace(Some(gilrs));
104+
});
105+
app.insert_resource(g);
72106
app.init_resource::<GilrsGamepads>();
73107
app.init_resource::<RunningRumbleEffects>()
74108
.add_systems(PreStartup, gilrs_event_startup_system)

crates/bevy_gilrs/src/rumble.rs

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
//! Handle user specified rumble request events.
22
use crate::{Gilrs, GilrsGamepads};
33
use bevy_ecs::prelude::{EventReader, Res, ResMut, Resource};
4-
#[cfg(target_arch = "wasm32")]
5-
use bevy_ecs::system::NonSendMut;
64
use bevy_input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest};
75
use bevy_platform_support::collections::HashMap;
86
use bevy_time::{Real, Time};
@@ -128,42 +126,42 @@ fn handle_rumble_request(
128126
}
129127
pub(crate) fn play_gilrs_rumble(
130128
time: Res<Time<Real>>,
131-
#[cfg(target_arch = "wasm32")] mut gilrs: NonSendMut<Gilrs>,
132-
#[cfg(not(target_arch = "wasm32"))] mut gilrs: ResMut<Gilrs>,
129+
mut gilrs: ResMut<Gilrs>,
133130
gamepads: Res<GilrsGamepads>,
134131
mut requests: EventReader<GamepadRumbleRequest>,
135132
mut running_rumbles: ResMut<RunningRumbleEffects>,
136133
) {
137-
let gilrs = gilrs.0.get();
138-
let current_time = time.elapsed();
139-
// Remove outdated rumble effects.
140-
for rumbles in running_rumbles.rumbles.values_mut() {
141-
// `ff::Effect` uses RAII, dropping = deactivating
142-
rumbles.retain(|RunningRumble { deadline, .. }| *deadline >= current_time);
143-
}
144-
running_rumbles
145-
.rumbles
146-
.retain(|_gamepad, rumbles| !rumbles.is_empty());
147-
148-
// Add new effects.
149-
for rumble in requests.read().cloned() {
150-
let gamepad = rumble.gamepad();
151-
match handle_rumble_request(&mut running_rumbles, gilrs, &gamepads, rumble, current_time) {
152-
Ok(()) => {}
153-
Err(RumbleError::GilrsError(err)) => {
154-
if let ff::Error::FfNotSupported(_) = err {
155-
debug!("Tried to rumble {gamepad:?}, but it doesn't support force feedback");
156-
} else {
157-
warn!(
158-
"Tried to handle rumble request for {gamepad:?} but an error occurred: {err}"
159-
);
134+
gilrs.with(|gilrs| {
135+
let current_time = time.elapsed();
136+
// Remove outdated rumble effects.
137+
for rumbles in running_rumbles.rumbles.values_mut() {
138+
// `ff::Effect` uses RAII, dropping = deactivating
139+
rumbles.retain(|RunningRumble { deadline, .. }| *deadline >= current_time);
140+
}
141+
running_rumbles
142+
.rumbles
143+
.retain(|_gamepad, rumbles| !rumbles.is_empty());
144+
145+
// Add new effects.
146+
for rumble in requests.read().cloned() {
147+
let gamepad = rumble.gamepad();
148+
match handle_rumble_request(&mut running_rumbles, gilrs, &gamepads, rumble, current_time) {
149+
Ok(()) => {}
150+
Err(RumbleError::GilrsError(err)) => {
151+
if let ff::Error::FfNotSupported(_) = err {
152+
debug!("Tried to rumble {gamepad:?}, but it doesn't support force feedback");
153+
} else {
154+
warn!(
155+
"Tried to handle rumble request for {gamepad:?} but an error occurred: {err}"
156+
);
157+
}
160158
}
161-
}
162-
Err(RumbleError::GamepadNotFound) => {
163-
warn!("Tried to handle rumble request {gamepad:?} but it doesn't exist!");
164-
}
165-
};
166-
}
159+
Err(RumbleError::GamepadNotFound) => {
160+
warn!("Tried to handle rumble request {gamepad:?} but it doesn't exist!");
161+
}
162+
};
163+
}
164+
});
167165
}
168166

169167
#[cfg(test)]

crates/bevy_winit/src/lib.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,15 @@ impl<T: Event> Plugin for WinitPlugin<T> {
120120
event_loop_builder.with_android_app(bevy_window::ANDROID_APP.get().expect(msg).clone());
121121
}
122122

123+
let event_loop = event_loop_builder
124+
.build()
125+
.expect("Failed to build event loop");
126+
123127
app.init_non_send_resource::<WinitWindows>()
124128
.init_resource::<WinitMonitors>()
125129
.init_resource::<WinitSettings>()
126130
.add_event::<RawWinitWindowEvent>()
127-
.set_runner(winit_runner::<T>)
131+
.set_runner(|app| winit_runner(app, event_loop))
128132
.add_systems(
129133
Last,
130134
(
@@ -139,14 +143,6 @@ impl<T: Event> Plugin for WinitPlugin<T> {
139143

140144
app.add_plugins(AccessKitPlugin);
141145
app.add_plugins(cursor::CursorPlugin);
142-
143-
let event_loop = event_loop_builder
144-
.build()
145-
.expect("Failed to build event loop");
146-
147-
// `winit`'s windows are bound to the event loop that created them, so the event loop must
148-
// be inserted as a resource here to pass it onto the runner.
149-
app.insert_non_send_resource(event_loop);
150146
}
151147
}
152148

0 commit comments

Comments
 (0)