Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/bevy_gilrs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ thread_local! {
/// `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<Option<gilrs::Gilrs>> = const { RefCell::new(None) };
pub static GILRS: RefCell<Option<gilrs::Gilrs>> = const { RefCell::new(None) };
}

#[derive(Resource)]
Expand Down
4 changes: 3 additions & 1 deletion crates/bevy_winit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ mod winit_monitors;
mod winit_windows;

thread_local! {
static WINIT_WINDOWS: RefCell<WinitWindows> = const { RefCell::new(WinitWindows::new()) };
/// Temporary storage of WinitWindows data to replace usage of `!Send` resources. This will be replaced with proper
/// storage of `!Send` data after issue #17667 is complete.
pub static WINIT_WINDOWS: RefCell<WinitWindows> = const { RefCell::new(WinitWindows::new()) };
}

/// A [`Plugin`] that uses `winit` to create and manage windows, and receive window and input
Expand Down
60 changes: 60 additions & 0 deletions release-content/migration-guides/replace_non_send_resources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
title: Replace `Gilrs`, `AccessKitAdapters`, and `WinitWindows` resources
pull_requests: [18386, 17730, 19575]
---

## NonSend Resources Replaced

As an effort to remove `!Send` resources in Bevy, we replaced the following resources:

* `Gilrs` - _For wasm32 only, other platforms are unchanged -_ Replaced with `bevy_gilrs::GILRS`
* `WinitWindows` - Replaced with `bevy_winit::WINIT_WINDOWS`
* `AccessKitAdapters` - Replaced with `bevy_winit::ACCESS_KIT_ADAPTERS`

Each of these are now using `thread_local`s to store the data and are temporary solutions to storing `!Send` data. Even though `thread_local`s are thread safe, they should not be accessed from other threads. If they are accessed from other threads, the data will be uninitialized in each non-main thread, which isn't very useful.

Here is an example of how the data can now be accessed. This example will use `WINIT_WINDOWS` as an example, but the same technique can be applied to the others:

### Immutable Access

```rust
use bevy_winit::WINIT_WINDOWS;

...

WINIT_WINDOWS.with_borrow(|winit_windows| {
// do things with `winit_windows`
});
```

### Mutable Access

```rust
use bevy_winit::WINIT_WINDOWS;

...

WINIT_WINDOWS.with_borrow_mut(|winit_windows| {
// do things with `winit_windows`
});
```

If a borrow is attempted while the data is borrowed elsewhere, the method will panic.
Copy link

@TeamDman TeamDman Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any guidance on how to borrow WINIT_WINDOWS without panicking? I have a system in a custom schedule running after Last using NonSendMarker that's getting a panic for RefCell already borrowed

use bevy::app::MainScheduleOrder;
use bevy::ecs::schedule::ExecutorKind;
use bevy::ecs::schedule::ScheduleLabel;
use bevy::ecs::system::NonSendMarker;
use bevy::prelude::*;
use bevy_winit::WINIT_WINDOWS;
use std::any::type_name;

pub struct WindowIconPlugin;

impl Plugin for WindowIconPlugin {
    fn build(&self, app: &mut App) {
        app.register_type::<WindowIcon>();
        app.add_message::<AddWindowIconWithRetry>();
        app.add_systems(Update, emit_window_icon_requests);

        // Set up a custom schedule to run the window icon update as an exclusive system.
        // We want to avoid `RefCell already borrowed` issues.
        let mut custom_update_schedule = Schedule::new(UpdateWindowIconsSchedule);
        custom_update_schedule.set_executor_kind(ExecutorKind::SingleThreaded);
        app.add_schedule(custom_update_schedule);

        let mut main_schedule_order = app.world_mut().resource_mut::<MainScheduleOrder>();
        main_schedule_order.insert_after(Last, UpdateWindowIconsSchedule);

        app.add_systems(UpdateWindowIconsSchedule, set_window_icon);
    }
}

#[derive(ScheduleLabel, Debug, Hash, PartialEq, Eq, Clone)]
struct UpdateWindowIconsSchedule;

#[derive(Debug, Component, Reflect)]
pub struct WindowIcon {
    pub image: Handle<Image>,
}

#[derive(Debug, Clone, Reflect, Message)]
pub struct AddWindowIconWithRetry {
    pub image: Handle<Image>,
    pub window: Entity,
}

impl WindowIcon {
    pub fn new(image: Handle<Image>) -> Self {
        Self { image }
    }
}

// Sends an AddWindowIconWithRetry message for any entity that just received WindowIcon
fn emit_window_icon_requests(
    mut q: Query<(Entity, &WindowIcon), Added<WindowIcon>>,
    mut writer: MessageWriter<AddWindowIconWithRetry>,
) {
    for (entity, icon) in &mut q {
        debug!("Detected new WindowIcon on {:?}", entity);
        writer.write(AddWindowIconWithRetry {
            image: icon.image.clone(),
            window: entity,
        });
    }
}

fn set_window_icon(
    mut events: ParamSet<(
        MessageReader<AddWindowIconWithRetry>,
        MessageWriter<AddWindowIconWithRetry>,
    )>,
    assets: Res<Assets<Image>>,
    _main_thread_marker: NonSendMarker,
) {
    if events.p0().is_empty() {
        return;
    }
    WINIT_WINDOWS.with_borrow_mut(|windows| { info!("Ahoy!") });
    // let outgoing = WINIT_WINDOWS.with_borrow_mut(|windows| {
    //     let mut outgoing = Vec::new();
    //     for event in events.p0().read() {
    //         debug!(?event.window, ?windows, "Handling {}", type_name::<AddWindowIconWithRetry>());

    //         // Identify window
    //         let target_window = windows.get_window(event.window);
    //         let Some(window) = target_window else {
    //             warn!(
    //                 ?windows,
    //                 "Window {:?} does not exist, retrying later...",
    //                 event.window
    //             );
    //             outgoing.push(event.clone());
    //             continue;
    //         };

    //         // Fetch the image asset
    //         let Some(image) = assets.get(&event.image) else {
    //             error!(
    //                 "Image handle {:?} not found in assets, the window will not have our custom icon",
    //                 event.image
    //             );
    //             continue;
    //         };

    //         // Acquire pixel data from the image
    //         let Some(image_data) = image.data.clone() else {
    //             error!(
    //                 "Image handle {:?} has no data, the window will not have our custom icon",
    //                 event.image
    //             );
    //             continue;
    //         };

    //         // Convert between formats
    //         let icon = match winit::window::Icon::from_rgba(
    //             image_data,
    //             image.texture_descriptor.size.width,
    //             image.texture_descriptor.size.height,
    //         ) {
    //             Ok(icon) => icon,
    //             Err(e) => {
    //                 error!("Failed to construct window icon: {:?}", e);
    //                 continue;
    //             }
    //         };

    //         // Set the window icon
    //         info!(image_size = ?image.size(), "Setting window icon");
    //         window.set_window_icon(Some(icon));
    //     }
    //     outgoing
    // });

    // for event in outgoing {
    //     events.p1().write(event);
    // }
}

EDIT: just going to create a discussion post for this question

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created #21250 where I demonstrate my problem and I identified the solution:

The real problem was that I was using with_borrow_mut when with_borrow is sufficient.


## NonSend Systems

Previously, the use of a `!Send` resource in a system would force the system to execute on the main thread. Since `!Send` resources are removed in Bevy, we needed to create a new way to prevent systems from running on non-main threads. To do this, you can now use `bevy_ecs::system::NonSendMarker` as a system parameter:

```rust
use bevy_ecs::system::NonSendMarker;

fn my_system(
_non_send_marker: NonSendMarker,
) {
ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| {
// do things with adapters
});
}
```

To prevent a panic, if any of the `!Send` resource replacements mentioned in this document are used in a system, the system should _always_ be marked as `!Send` with `bevy_ecs::system::NonSendMarker`.
Loading