diff --git a/Cargo.toml b/Cargo.toml index 70e9a4ca7cb15..54ac183c830b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["game", "engine", "gamedev", "graphics", "bevy"] license = "MIT OR Apache-2.0" readme = "README.md" repository = "https://github.com/bevyengine/bevy" -rust-version = "1.70.0" +rust-version = "1.73.0" [workspace] exclude = [ diff --git a/clippy.toml b/clippy.toml index 0da69b7359423..b4a1bc5974a29 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -doc-valid-idents = ["GilRs", "glTF", "sRGB", "VSync", "WebGL2", "WebGPU", ".."] +doc-valid-idents = ["DirectX", "iOS", "GilRs", "glTF", "macOS", "OpenGL", "sRGB", "VSync", "WebGL2", "WebGPU", ".."] diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 821612a23214e..6c5c05c6d7788 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -1,21 +1,19 @@ -use crate::{First, Main, MainSchedulePlugin, Plugin, Plugins, StateTransition}; +use crate::{ + app_thread_channel, AppEvent, AppEventReceiver, AppEventSender, Main, MainSchedulePlugin, + PlaceholderPlugin, Plugin, Plugins, PluginsState, SubApp, SubApps, +}; pub use bevy_derive::AppLabel; use bevy_ecs::{ prelude::*, - schedule::{ - apply_state_transition, common_conditions::run_once as run_once_condition, - run_enter_schedule, InternedScheduleLabel, IntoSystemConfigs, IntoSystemSetConfigs, - ScheduleBuildSettings, ScheduleLabel, - }, -}; -use bevy_utils::{intern::Interned, thiserror::Error, tracing::debug, HashMap, HashSet}; -use std::{ - fmt::Debug, - panic::{catch_unwind, resume_unwind, AssertUnwindSafe}, + schedule::{IntoSystemConfigs, IntoSystemSetConfigs, ScheduleBuildSettings, ScheduleLabel}, + storage::ThreadLocalStorage, }; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; +use bevy_utils::{intern::Interned, thiserror::Error, tracing::debug}; +use std::fmt::Debug; +use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe}; bevy_utils::define_label!( /// A strongly-typed class of labels used to identify an [`App`]. @@ -35,12 +33,13 @@ pub(crate) enum AppError { } #[allow(clippy::needless_doctest_main)] -/// A container of app logic and data. +/// [`App`] is the primary API for writing user applications. It automates the setup of a +/// [standard lifecycle](Main) and provides interface glue for [plugins](`Plugin`). +/// +/// A single [`App`] can contain multiple [`SubApp`] instances, but [`App`] methods only affect +/// the "main" one. To access a particular [`SubApp`], use [`get_sub_app`](App::get_sub_app) +/// or [`get_sub_app_mut`](App::get_sub_app_mut). /// -/// Bundles together the necessary elements like [`World`] and [`Schedule`] to create -/// an ECS-based application. It also stores a pointer to a [runner function](Self::set_runner). -/// The runner is responsible for managing the application's event loop and applying the -/// [`Schedule`] to the [`World`] to drive application logic. /// /// # Examples /// @@ -61,123 +60,27 @@ pub(crate) enum AppError { /// } /// ``` pub struct App { - /// The main ECS [`World`] of the [`App`]. - /// This stores and provides access to all the main data of the application. - /// The systems of the [`App`] will run using this [`World`]. - /// If additional separate [`World`]-[`Schedule`] pairs are needed, you can use [`sub_app`](App::insert_sub_app)s. - pub world: World, - /// The [runner function](Self::set_runner) is primarily responsible for managing - /// the application's event loop and advancing the [`Schedule`]. - /// Typically, it is not configured manually, but set by one of Bevy's built-in plugins. - /// See `bevy::winit::WinitPlugin` and [`ScheduleRunnerPlugin`](crate::schedule_runner::ScheduleRunnerPlugin). - pub runner: Box, // Send bound is required to make App Send - /// The schedule that systems are added to by default. - /// - /// The schedule that runs the main loop of schedule execution. - /// - /// This is initially set to [`Main`]. - pub main_schedule_label: InternedScheduleLabel, - sub_apps: HashMap, - plugin_registry: Vec>, - plugin_name_added: HashSet, - /// A private counter to prevent incorrect calls to `App::run()` from `Plugin::build()` - building_plugin_depth: usize, - plugins_state: PluginsState, + #[doc(hidden)] + pub sub_apps: SubApps, + #[doc(hidden)] + pub tls: ThreadLocalStorage, + send: AppEventSender, + recv: AppEventReceiver, + /// The function that will manage the app's lifecycle. + /// + /// Bevy provides the [`WinitPlugin`] and [`ScheduleRunnerPlugin`] for windowed and headless + /// applications, respectively. + /// + /// [`WinitPlugin`]: https://docs.rs/bevy/latest/bevy/winit/struct.WinitPlugin.html + /// [`ScheduleRunnerPlugin`]: https://docs.rs/bevy/latest/bevy/app/struct.ScheduleRunnerPlugin.html + pub(crate) runner: Option, } impl Debug for App { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "App {{ sub_apps: ")?; f.debug_map() - .entries(self.sub_apps.iter().map(|(k, v)| (k, v))) - .finish()?; - write!(f, "}}") - } -} - -/// A [`SubApp`] contains its own [`Schedule`] and [`World`] separate from the main [`App`]. -/// This is useful for situations where data and data processing should be kept completely separate -/// from the main application. The primary use of this feature in bevy is to enable pipelined rendering. -/// -/// # Example -/// -/// ```rust -/// # use bevy_app::{App, AppLabel, SubApp, Main}; -/// # use bevy_ecs::prelude::*; -/// # use bevy_ecs::schedule::ScheduleLabel; -/// -/// #[derive(Resource, Default)] -/// struct Val(pub i32); -/// -/// #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)] -/// struct ExampleApp; -/// -/// let mut app = App::new(); -/// -/// // initialize the main app with a value of 0; -/// app.insert_resource(Val(10)); -/// -/// // create a app with a resource and a single schedule -/// let mut sub_app = App::empty(); -/// // add an outer schedule that runs the main schedule -/// sub_app.insert_resource(Val(100)); -/// -/// // initialize main schedule -/// sub_app.add_systems(Main, |counter: Res| { -/// // since we assigned the value from the main world in extract -/// // we see that value instead of 100 -/// assert_eq!(counter.0, 10); -/// }); -/// -/// // add the sub_app to the app -/// app.insert_sub_app(ExampleApp, SubApp::new(sub_app, |main_world, sub_app| { -/// // extract the value from the main app to the sub app -/// sub_app.world.resource_mut::().0 = main_world.resource::().0; -/// })); -/// -/// // This will run the schedules once, since we're using the default runner -/// app.run(); -/// ``` -pub struct SubApp { - /// The [`SubApp`]'s instance of [`App`] - pub app: App, - - /// A function that allows access to both the main [`App`] [`World`] and the [`SubApp`]. This is - /// useful for moving data between the sub app and the main app. - extract: Box, -} - -impl SubApp { - /// Creates a new [`SubApp`]. - /// - /// The provided function `extract` is normally called by the [`update`](App::update) method. - /// After extract is called, the [`Schedule`] of the sub app is run. The [`World`] - /// parameter represents the main app world, while the [`App`] parameter is just a mutable - /// reference to the `SubApp` itself. - pub fn new(app: App, extract: impl Fn(&mut World, &mut App) + Send + 'static) -> Self { - Self { - app, - extract: Box::new(extract), - } - } - - /// Runs the [`SubApp`]'s default schedule. - pub fn run(&mut self) { - self.app.world.run_schedule(self.app.main_schedule_label); - self.app.world.clear_trackers(); - } - - /// Extracts data from main world to this sub-app. - pub fn extract(&mut self, main_world: &mut World) { - (self.extract)(main_world, &mut self.app); - } -} - -impl Debug for SubApp { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "SubApp {{ app: ")?; - f.debug_map() - .entries(self.app.sub_apps.iter().map(|(k, v)| (k, v))) + .entries(self.sub_apps.sub_apps.iter().map(|(k, v)| (k, v))) .finish()?; write!(f, "}}") } @@ -186,9 +89,10 @@ impl Debug for SubApp { impl Default for App { fn default() -> Self { let mut app = App::empty(); + app.sub_apps.main.update_schedule = Some(Main.intern()); + #[cfg(feature = "bevy_reflect")] app.init_resource::(); - app.add_plugins(MainSchedulePlugin); app.add_event::(); @@ -201,25 +105,6 @@ impl Default for App { } } -/// Plugins state in the application -#[derive(PartialEq, Eq, Debug, Clone, Copy)] -pub enum PluginsState { - /// Plugins are being added. - Adding, - /// All plugins already added are ready. - Ready, - /// Finish has been executed for all plugins added. - Finished, - /// Cleanup has been executed for all plugins added. - Cleaned, -} - -// Dummy plugin used to temporary hold the place in the plugin registry -struct PlaceholderPlugin; -impl Plugin for PlaceholderPlugin { - fn build(&self, _app: &mut App) {} -} - impl App { /// Creates a new [`App`] with some default structure to enable core engine features. /// This is the preferred constructor for most use cases. @@ -229,171 +114,290 @@ impl App { /// Creates a new empty [`App`] with minimal default configuration. /// - /// This constructor should be used if you wish to provide custom scheduling, exit handling, cleanup, etc. + /// Use this constructor if you want to customize scheduling, exit handling, cleanup, etc. pub fn empty() -> App { - let mut world = World::new(); - world.init_resource::(); + let (send, recv) = app_thread_channel(); Self { - world, - runner: Box::new(run_once), - sub_apps: HashMap::default(), - plugin_registry: Vec::default(), - plugin_name_added: Default::default(), - main_schedule_label: Main.intern(), - building_plugin_depth: 0, - plugins_state: PluginsState::Adding, + sub_apps: SubApps::new(), + tls: ThreadLocalStorage::new(), + send, + recv, + runner: Some(Box::new(run_once)), } } - /// Advances the execution of the [`Schedule`] by one cycle. - /// - /// This method also updates sub apps. - /// See [`insert_sub_app`](Self::insert_sub_app) for more details. - /// - /// The schedule run by this method is determined by the [`main_schedule_label`](App) field. - /// By default this is [`Main`]. - /// - /// # Panics - /// - /// The active schedule of the app must be set before this method is called. + /// Disassembles the [`App`] and returns its individual parts. + #[doc(hidden)] + pub fn into_parts( + self, + ) -> ( + SubApps, + ThreadLocalStorage, + AppEventSender, + AppEventReceiver, + Option, + ) { + let Self { + sub_apps, + tls, + send, + recv, + runner, + } = self; + + (sub_apps, tls, send, recv, runner) + } + + /// Returns an [`App`] assembled from the given individual parts. + #[doc(hidden)] + pub fn from_parts( + sub_apps: SubApps, + tls: ThreadLocalStorage, + send: AppEventSender, + recv: AppEventReceiver, + runner: Option, + ) -> Self { + App { + sub_apps, + tls, + send, + recv, + runner, + } + } + + /// Inserts the channel to [`ThreadLocals`] into all sub-apps. + #[doc(hidden)] + pub fn insert_tls_channel(&mut self) { + self.sub_apps.iter_mut().for_each(|sub_app| { + self.tls + .insert_channel(sub_app.world_mut(), self.send.clone()); + }); + } + + /// Removes the channel to [`ThreadLocals`] from all sub-apps. + #[doc(hidden)] + pub fn remove_tls_channel(&mut self) { + self.sub_apps + .iter_mut() + .for_each(|sub_app| self.tls.remove_channel(sub_app.world_mut())); + } + + /// Runs the default schedules of all sub-apps (starting with the "main" app) once. pub fn update(&mut self) { - #[cfg(feature = "trace")] - let _bevy_update_span = info_span!("update").entered(); + if self.is_building_plugins() { + panic!("App::update() was called while a plugin was building."); + } + + self.insert_tls_channel(); + + // disassemble + let (mut sub_apps, tls, send, recv, runner) = std::mem::take(self).into_parts(); + + #[cfg(not(target_arch = "wasm32"))] { - #[cfg(feature = "trace")] - let _bevy_main_update_span = info_span!("main app").entered(); - self.world.run_schedule(self.main_schedule_label); + // Move sub-apps to another thread and run an event loop in this thread. + let thread_send = send.clone(); + let thread = std::thread::spawn(move || { + let result = catch_unwind(AssertUnwindSafe(|| { + sub_apps.update(); + thread_send.send(AppEvent::Exit(sub_apps)).unwrap(); + })); + + if let Some(payload) = result.err() { + thread_send.send(AppEvent::Error(payload)).unwrap(); + } + }); + + loop { + // this loop never exits because multiple copies of sender exist + let event = recv.recv().unwrap(); + match event { + AppEvent::Task(task) => { + task(); + } + AppEvent::Exit(x) => { + sub_apps = x; + thread.join().unwrap(); + break; + } + AppEvent::Error(payload) => { + resume_unwind(payload); + } + } + } } - for (_label, sub_app) in &mut self.sub_apps { - #[cfg(feature = "trace")] - let _sub_app_span = info_span!("sub app", name = ?_label).entered(); - sub_app.extract(&mut self.world); - sub_app.run(); + + #[cfg(target_arch = "wasm32")] + { + sub_apps.update(); } - self.world.clear_trackers(); + // reassemble + *self = App::from_parts(sub_apps, tls, send, recv, runner); } - /// Starts the application by calling the app's [runner function](Self::set_runner). + /// Runs the [`App`] by calling its [runner](Self::set_runner). /// - /// Finalizes the [`App`] configuration. For general usage, see the example on the item + /// This will (re)build the [`App`] first. For general usage, see the example on the item /// level documentation. /// - /// # `run()` might not return + /// # Caveats /// - /// Calls to [`App::run()`] might never return. + /// **This method is not required to return.** /// - /// In simple and *headless* applications, one can expect that execution will - /// proceed, normally, after calling [`run()`](App::run()) but this is not the case for - /// windowed applications. + /// Headless apps can generally expect this method to return control to the caller when + /// it completes, but that is not the case for windowed apps. Windowed apps are typically + /// driven by an event loop and some platforms expect the program to terminate when the + /// event loop ends. /// - /// Windowed apps are typically driven by an *event loop* or *message loop* and - /// some window-manager APIs expect programs to terminate when their primary - /// window is closed and that event loop terminates – behavior of processes that - /// do not is often platform dependent or undocumented. - /// - /// By default, *Bevy* uses the `winit` crate for window creation. See - /// [`WinitSettings::return_from_run`](https://docs.rs/bevy/latest/bevy/winit/struct.WinitSettings.html#structfield.return_from_run) - /// for further discussion of this topic and for a mechanism to require that [`App::run()`] - /// *does* return – albeit one that carries its own caveats and disclaimers. + /// Bevy uses `winit` as its default window manager. See [`WinitSettings::return_from_run`] + /// for a mechanism (with many additional caveats) that will guarantee [`App::run()`] returns + /// to the caller. /// /// # Panics /// - /// Panics if called from `Plugin::build()`, because it would prevent other plugins to properly build. + /// Panics if not all plugins have been built. + /// + /// [`WinitSettings::return_from_run`]: https://docs.rs/bevy/latest/bevy/winit/struct.WinitSettings.html#structfield.return_from_run pub fn run(&mut self) { #[cfg(feature = "trace")] - let _bevy_app_run_span = info_span!("bevy_app").entered(); - - let mut app = std::mem::replace(self, App::empty()); - if app.building_plugin_depth > 0 { - panic!("App::run() was called from within Plugin::build(), which is not allowed."); + let _bevy_app_run_span = info_span!("App::run").entered(); + if self.is_building_plugins() { + panic!("App::run() was called while a plugin was building."); } - if app.plugins_state() == PluginsState::Ready { + // Insert channel here because some sub-apps are moved to a different thread during + // plugin build. + self.insert_tls_channel(); + + if self.plugins_state() == PluginsState::Ready { // If we're already ready, we finish up now and advance one frame. // This prevents black frames during the launch transition on iOS. - app.finish(); - app.cleanup(); - app.update(); + self.finish(); + self.cleanup(); + self.update(); } - let runner = std::mem::replace(&mut app.runner, Box::new(run_once)); + let mut app = std::mem::replace(self, App::empty()); + let runner = app.runner.take().expect("an app runner"); (runner)(app); } - /// Check the state of all plugins already added to this app. This is usually called by the - /// event loop, but can be useful for situations where you want to use [`App::update`] + /// Sets the function that will be called when the app is run. + /// + /// The runner function `f` is called only once by [`App::run`]. If the + /// presence of a main loop in the app is desired, it is the responsibility of the runner + /// function to provide it. + /// + /// The runner function is usually not set manually, but by Bevy integrated plugins + /// (e.g. `WinitPlugin`). + /// + /// # Examples + /// + /// ``` + /// # use bevy_app::prelude::*; + /// # + /// fn my_runner(mut app: App) { + /// loop { + /// println!("In main loop"); + /// app.update(); + /// } + /// } + /// + /// App::new() + /// .set_runner(my_runner); + /// ``` + pub fn set_runner(&mut self, f: impl FnOnce(App) + 'static) -> &mut Self { + self.runner = Some(Box::new(f)); + self + } + + /// Returns the state of all plugins. This is usually called by the event loop, but can be + /// useful for situations where you want to use [`App::update`]. + // TODO: &mut self -> &self #[inline] - pub fn plugins_state(&self) -> PluginsState { - match self.plugins_state { + pub fn plugins_state(&mut self) -> PluginsState { + let mut overall_plugins_state = match self.main_mut().plugins_state { PluginsState::Adding => { - for plugin in &self.plugin_registry { + let mut state = PluginsState::Ready; + let plugins = std::mem::take(&mut self.main_mut().plugins); + for plugin in &plugins.registry { + // plugins installed to main need to see all sub-apps if !plugin.ready(self) { - return PluginsState::Adding; + state = PluginsState::Adding; + break; } } - PluginsState::Ready + self.main_mut().plugins = plugins; + state } state => state, - } + }; + + // overall state is the earliest state of any sub-app + self.sub_apps.iter_mut().skip(1).for_each(|s| { + overall_plugins_state = overall_plugins_state.min(s.plugins_state()); + }); + + overall_plugins_state } - /// Run [`Plugin::finish`] for each plugin. This is usually called by the event loop once all + /// Runs [`Plugin::finish`] for each plugin. This is usually called by the event loop once all /// plugins are ready, but can be useful for situations where you want to use [`App::update`]. pub fn finish(&mut self) { - // temporarily remove the plugin registry to run each plugin's setup function on app. - let plugin_registry = std::mem::take(&mut self.plugin_registry); - for plugin in &plugin_registry { + // plugins installed to main should see all sub-apps + let plugins = std::mem::take(&mut self.main_mut().plugins); + for plugin in &plugins.registry { plugin.finish(self); } - self.plugin_registry = plugin_registry; - self.plugins_state = PluginsState::Finished; + let main = self.main_mut(); + main.plugins = plugins; + main.plugins_state = PluginsState::Finished; + self.sub_apps.iter_mut().skip(1).for_each(|s| s.finish()); } - /// Run [`Plugin::cleanup`] for each plugin. This is usually called by the event loop after + /// Runs [`Plugin::cleanup`] for each plugin. This is usually called by the event loop after /// [`App::finish`], but can be useful for situations where you want to use [`App::update`]. pub fn cleanup(&mut self) { - // temporarily remove the plugin registry to run each plugin's setup function on app. - let plugin_registry = std::mem::take(&mut self.plugin_registry); - for plugin in &plugin_registry { + // plugins installed to main should see all sub-apps + let plugins = std::mem::take(&mut self.main_mut().plugins); + for plugin in &plugins.registry { plugin.cleanup(self); } - self.plugin_registry = plugin_registry; - self.plugins_state = PluginsState::Cleaned; + let main = self.main_mut(); + main.plugins = plugins; + main.plugins_state = PluginsState::Cleaned; + self.sub_apps.iter_mut().skip(1).for_each(|s| s.cleanup()); + } + + /// Returns `true` if any of the sub-apps are building plugins. + pub(crate) fn is_building_plugins(&self) -> bool { + self.sub_apps.iter().any(|s| s.is_building_plugins()) } /// Adds [`State`] and [`NextState`] resources, [`OnEnter`] and [`OnExit`] schedules - /// for each state variant (if they don't already exist), an instance of [`apply_state_transition::`] in - /// [`StateTransition`] so that transitions happen before [`Update`](crate::Update) and - /// a instance of [`run_enter_schedule::`] in [`StateTransition`] with a - /// [`run_once`](`run_once_condition`) condition to run the on enter schedule of the - /// initial state. + /// for each state variant (if they don't already exist), an instance of [`apply_state_transition::`] + /// in [`StateTransition`] so that transitions happen before [`Update`] and an instance of + /// [`run_enter_schedule::`] in [`StateTransition`] with a [`run_once`] condition to + /// run the on enter schedule of the initial state. /// /// If you would like to control how other systems run based on the current state, /// you can emulate this behavior using the [`in_state`] [`Condition`]. /// /// Note that you can also apply state transitions at other points in the schedule /// by adding the [`apply_state_transition`] system manually. + /// + /// [`StateTransition`]: crate::StateTransition + /// [`Update`]: crate::Update + /// [`run_once`]: bevy_ecs::schedule::common_conditions::run_once + /// [`run_enter_schedule::`]: bevy_ecs::schedule::run_enter_schedule pub fn add_state(&mut self) -> &mut Self { - self.init_resource::>() - .init_resource::>() - .add_systems( - StateTransition, - ( - run_enter_schedule::.run_if(run_once_condition()), - apply_state_transition::, - ) - .chain(), - ); - - // The OnEnter, OnExit, and OnTransition schedules are lazily initialized - // (i.e. when the first system is added to them), and World::try_run_schedule is used to fail - // gracefully if they aren't present. - + self.main_mut().add_state::(); self } - /// Adds a system to the given schedule in this app's [`Schedules`]. + /// Adds a collection of systems to `schedule` (stored in the main world's [`Schedules`]). /// /// # Examples /// @@ -415,17 +419,7 @@ impl App { schedule: impl ScheduleLabel, systems: impl IntoSystemConfigs, ) -> &mut Self { - let schedule = schedule.intern(); - let mut schedules = self.world.resource_mut::(); - - if let Some(schedule) = schedules.get_mut(schedule) { - schedule.add_systems(systems); - } else { - let mut new_schedule = Schedule::new(schedule); - new_schedule.add_systems(systems); - schedules.insert(new_schedule); - } - + self.main_mut().add_systems(schedule, systems); self } @@ -447,24 +441,14 @@ impl App { schedule: impl ScheduleLabel, sets: impl IntoSystemSetConfigs, ) -> &mut Self { - let schedule = schedule.intern(); - let mut schedules = self.world.resource_mut::(); - if let Some(schedule) = schedules.get_mut(schedule) { - schedule.configure_sets(sets); - } else { - let mut new_schedule = Schedule::new(schedule); - new_schedule.configure_sets(sets); - schedules.insert(new_schedule); - } + self.main_mut().configure_sets(schedule, sets); self } - /// Setup the application to manage events of type `T`. + /// Initializes `T` event handling by inserting an event queue resource ([`Events::`]) + /// and scheduling an [`event_update_system`] in [`First`](crate::First). /// - /// This is done by adding a [`Resource`] of type [`Events::`], - /// and inserting an [`event_update_system`] into [`First`]. - /// - /// See [`Events`] for defining events. + /// See [`Events`] for information on how to define events. /// /// # Examples /// @@ -484,22 +468,14 @@ impl App { where T: Event, { - if !self.world.contains_resource::>() { - self.init_resource::>().add_systems( - First, - bevy_ecs::event::event_update_system:: - .run_if(bevy_ecs::event::event_update_condition::), - ); - } + self.main_mut().add_event::(); self } - /// Inserts a [`Resource`] to the current [`App`] and overwrites any [`Resource`] previously added of the same type. - /// - /// A [`Resource`] in Bevy represents globally unique data. [`Resource`]s must be added to Bevy apps - /// before using them. This happens with [`insert_resource`](Self::insert_resource). + /// Inserts the [`Resource`] into the app, overwriting any existing resource of the same type. /// - /// See [`init_resource`](Self::init_resource) for [`Resource`]s that implement [`Default`] or [`FromWorld`]. + /// There is also an [`init_resource`](Self::init_resource) for resources that have + /// [`Default`] or [`FromWorld`] implementations. /// /// # Examples /// @@ -516,40 +492,16 @@ impl App { /// .insert_resource(MyCounter { counter: 0 }); /// ``` pub fn insert_resource(&mut self, resource: R) -> &mut Self { - self.world.insert_resource(resource); + self.main_mut().insert_resource(resource); self } - /// Inserts a non-send resource to the app. + /// Inserts the [`Resource`], initialized with its default value, into the app, + /// if there is no existing instance of `R`. /// - /// You usually want to use [`insert_resource`](Self::insert_resource), - /// but there are some special cases when a resource cannot be sent across threads. - /// - /// # Examples - /// - /// ``` - /// # use bevy_app::prelude::*; - /// # use bevy_ecs::prelude::*; - /// # - /// struct MyCounter { - /// counter: usize, - /// } - /// - /// App::new() - /// .insert_non_send_resource(MyCounter { counter: 0 }); - /// ``` - pub fn insert_non_send_resource(&mut self, resource: R) -> &mut Self { - self.world.insert_non_send_resource(resource); - self - } - - /// Initialize a [`Resource`] with standard starting values by adding it to the [`World`]. - /// - /// If the [`Resource`] already exists, nothing happens. - /// - /// The [`Resource`] must implement the [`FromWorld`] trait. - /// If the [`Default`] trait is implemented, the [`FromWorld`] trait will use - /// the [`Default::default`] method to initialize the [`Resource`]. + /// `R` must implement [`FromWorld`]. + /// If `R` implements [`Default`], [`FromWorld`] will be automatically implemented and + /// initialize the [`Resource`] with [`Default::default`]. /// /// # Examples /// @@ -574,93 +526,91 @@ impl App { /// .init_resource::(); /// ``` pub fn init_resource(&mut self) -> &mut Self { - self.world.init_resource::(); + self.main_mut().init_resource::(); self } - /// Initialize a non-send [`Resource`] with standard starting values by adding it to the [`World`]. - /// - /// The [`Resource`] must implement the [`FromWorld`] trait. - /// If the [`Default`] trait is implemented, the [`FromWorld`] trait will use - /// the [`Default::default`] method to initialize the [`Resource`]. - pub fn init_non_send_resource(&mut self) -> &mut Self { - self.world.init_non_send_resource::(); - self - } - - /// Sets the function that will be called when the app is run. - /// - /// The runner function `run_fn` is called only once by [`App::run`]. If the - /// presence of a main loop in the app is desired, it is the responsibility of the runner - /// function to provide it. + /// Inserts the [`!Send`](Send) resource into the app, overwriting any existing resource + /// of the same type. /// - /// The runner function is usually not set manually, but by Bevy integrated plugins - /// (e.g. `WinitPlugin`). + /// There is also an [`init_non_send_resource`](Self::init_non_send_resource) for + /// resources that implement [`Default`]. /// /// # Examples /// /// ``` /// # use bevy_app::prelude::*; + /// # use bevy_ecs::prelude::*; /// # - /// fn my_runner(mut app: App) { - /// loop { - /// println!("In main loop"); - /// app.update(); - /// } + /// #[derive(ThreadLocalResource)] + /// struct MyCounter { + /// counter: usize, /// } /// /// App::new() - /// .set_runner(my_runner); + /// .insert_non_send_resource(MyCounter { counter: 0 }); /// ``` - pub fn set_runner(&mut self, run_fn: impl FnOnce(App) + 'static + Send) -> &mut Self { - self.runner = Box::new(run_fn); + pub fn insert_non_send_resource(&mut self, resource: R) -> &mut Self { + self.tls.insert_resource(resource); + self + } + + /// Inserts the [`!Send`](Send) resource into the app, initialized with its default value, + /// if there is no existing instance of `R`. + pub fn init_non_send_resource(&mut self) -> &mut Self { + self.tls.init_resource::(); self } - /// Boxed variant of [`add_plugins`](App::add_plugins) that can be used from a - /// [`PluginGroup`](super::PluginGroup) pub(crate) fn add_boxed_plugin( &mut self, plugin: Box, ) -> Result<&mut Self, AppError> { debug!("added plugin: {}", plugin.name()); - if plugin.is_unique() && !self.plugin_name_added.insert(plugin.name().to_string()) { + if plugin.is_unique() + && !self + .main_mut() + .plugins + .names + .insert(plugin.name().to_string()) + { Err(AppError::DuplicatePlugin { plugin_name: plugin.name().to_string(), })?; } - // Reserve that position in the plugin registry. if a plugin adds plugins, they will be correctly ordered - let plugin_position_in_registry = self.plugin_registry.len(); - self.plugin_registry.push(Box::new(PlaceholderPlugin)); + // Reserve position in the plugin registry. If the plugin adds more plugins, + // they'll all end up in insertion order. + let index = self.main().plugins.registry.len(); + self.main_mut() + .plugins + .registry + .push(Box::new(PlaceholderPlugin)); - self.building_plugin_depth += 1; + self.main_mut().plugin_build_depth += 1; let result = catch_unwind(AssertUnwindSafe(|| plugin.build(self))); - self.building_plugin_depth -= 1; + self.main_mut().plugin_build_depth -= 1; + if let Err(payload) = result { resume_unwind(payload); } - self.plugin_registry[plugin_position_in_registry] = plugin; + + self.main_mut().plugins.registry[index] = plugin; Ok(self) } - /// Checks if a [`Plugin`] has already been added. - /// - /// This can be used by plugins to check if a plugin they depend upon has already been - /// added. + /// Returns `true` if the [`Plugin`] has already been added. pub fn is_plugin_added(&self) -> bool where T: Plugin, { - self.plugin_registry - .iter() - .any(|p| p.downcast_ref::().is_some()) + self.main().is_plugin_added::() } - /// Returns a vector of references to any plugins of type `T` that have been added. + /// Returns a vector of references to all plugins of type `T` that have been added. /// - /// This can be used to read the settings of any already added plugins. - /// This vector will be length zero if no plugins of that type have been added. + /// This can be used to read the settings of any existing plugins. + /// This vector will be empty if no plugins of that type have been added. /// If multiple copies of the same plugin are added to the [`App`], they will be listed in insertion order in this vector. /// /// ```rust @@ -680,16 +630,13 @@ impl App { where T: Plugin, { - self.plugin_registry - .iter() - .filter_map(|p| p.downcast_ref()) - .collect() + self.main().get_added_plugins::() } - /// Adds one or more [`Plugin`]s. + /// Installs a [`Plugin`] collection. /// - /// One of Bevy's core principles is modularity. All Bevy engine features are implemented - /// as [`Plugin`]s. This includes internal features like the renderer. + /// Bevy prioritizes modularity as a core principle. **All** engine features are implemented + /// as plugins, even the complex ones like rendering. /// /// [`Plugin`]s can be grouped into a set by using a [`PluginGroup`]. /// @@ -721,7 +668,7 @@ impl App { /// /// # Panics /// - /// Panics if one of the plugins was already added to the application. + /// Panics if one of the plugins had already been added to the application. /// /// [`PluginGroup`]:super::PluginGroup #[track_caller] @@ -740,25 +687,27 @@ impl App { /// Registers the type `T` in the [`TypeRegistry`](bevy_reflect::TypeRegistry) resource, /// adding reflect data as specified in the [`Reflect`](bevy_reflect::Reflect) derive: + /// /// ```rust,ignore /// #[derive(Reflect)] /// #[reflect(Component, Serialize, Deserialize)] // will register ReflectComponent, ReflectSerialize, ReflectDeserialize /// ``` /// - /// See [`bevy_reflect::TypeRegistry::register`]. + /// See [`bevy_reflect::TypeRegistry::register`] for more information. #[cfg(feature = "bevy_reflect")] pub fn register_type(&mut self) -> &mut Self { - let registry = self.world.resource_mut::(); - registry.write().register::(); + self.main_mut().register_type::(); self } - /// Adds the type data `D` to type `T` in the [`TypeRegistry`](bevy_reflect::TypeRegistry) resource. + /// Associates type data `D` with type `T` in the [`TypeRegistry`](bevy_reflect::TypeRegistry) resource. /// - /// Most of the time [`App::register_type`] can be used instead to register a type you derived [`Reflect`](bevy_reflect::Reflect) for. - /// However, in cases where you want to add a piece of type data that was not included in the list of `#[reflect(...)]` type data in the derive, - /// or where the type is generic and cannot register e.g. `ReflectSerialize` unconditionally without knowing the specific type parameters, - /// this method can be used to insert additional type data. + /// Most of the time [`register_type`](Self::register_type) can be used instead to register a + /// type you derived [`Reflect`](bevy_reflect::Reflect) for. However, in cases where you want to + /// add a piece of type data that was not included in the list of `#[reflect(...)]` type data in + /// the derive, or where the type is generic and cannot register e.g. `ReflectSerialize` + /// unconditionally without knowing the specific type parameters, this method can be used to + /// insert additional type data. /// /// # Example /// ```rust @@ -779,120 +728,108 @@ impl App { >( &mut self, ) -> &mut Self { - let registry = self.world.resource_mut::(); - registry.write().register_type_data::(); + self.main_mut().register_type_data::(); self } - /// Retrieves a `SubApp` stored inside this [`App`]. + /// Returns a reference to the [`World`]. + pub fn world(&self) -> &World { + self.main().world() + } + + /// Returns a mutable reference to the [`World`]. + pub fn world_mut(&mut self) -> &mut World { + self.main_mut().world_mut() + } + + /// Returns a reference to the main [`SubApp`]. + pub fn main(&self) -> &SubApp { + &self.sub_apps.main + } + + /// Returns a mutable reference to the main [`SubApp`]. + pub fn main_mut(&mut self) -> &mut SubApp { + &mut self.sub_apps.main + } + + /// Returns a reference to the [`SubApp`] with the given label. /// /// # Panics /// - /// Panics if the `SubApp` doesn't exist. - pub fn sub_app_mut(&mut self, label: impl AppLabel) -> &mut App { - match self.get_sub_app_mut(label) { - Ok(app) => app, - Err(label) => panic!("Sub-App with label '{:?}' does not exist", label), - } + /// Panics if the sub-app doesn't exist. + pub fn sub_app(&self, label: impl AppLabel) -> &SubApp { + let str = label.intern(); + self.get_sub_app(label).unwrap_or_else(|| { + panic!("No sub-app with label '{:?}' exists.", str); + }) } - /// Retrieves a `SubApp` inside this [`App`] with the given label, if it exists. Otherwise returns - /// an [`Err`] containing the given label. - pub fn get_sub_app_mut(&mut self, label: impl AppLabel) -> Result<&mut App, impl AppLabel> { - self.sub_apps - .get_mut(&label.intern()) - .map(|sub_app| &mut sub_app.app) - .ok_or(label) - } - - /// Retrieves a `SubApp` stored inside this [`App`]. + /// Returns a reference to the [`SubApp`] with the given label. /// /// # Panics /// - /// Panics if the `SubApp` doesn't exist. - pub fn sub_app(&self, label: impl AppLabel) -> &App { - match self.get_sub_app(label) { - Ok(app) => app, - Err(label) => panic!("Sub-App with label '{:?}' does not exist", label), - } + /// Panics if the a sub-app doesn't exist. + pub fn sub_app_mut(&mut self, label: impl AppLabel) -> &mut SubApp { + let str = label.intern(); + self.get_sub_app_mut(label).unwrap_or_else(|| { + panic!("No sub-app with label '{:?}' exists.", str); + }) } - /// Inserts an existing sub app into the app - pub fn insert_sub_app(&mut self, label: impl AppLabel, sub_app: SubApp) { - self.sub_apps.insert(label.intern(), sub_app); + /// Returns a reference to the [`SubApp`] with the given label, if it exists. + pub fn get_sub_app(&self, label: impl AppLabel) -> Option<&SubApp> { + self.sub_apps.sub_apps.get(&label.intern()) } - /// Removes a sub app from the app. Returns [`None`] if the label doesn't exist. - pub fn remove_sub_app(&mut self, label: impl AppLabel) -> Option { - self.sub_apps.remove(&label.intern()) + /// Returns a mutable reference to the [`SubApp`] with the given label, if it exists. + pub fn get_sub_app_mut(&mut self, label: impl AppLabel) -> Option<&mut SubApp> { + self.sub_apps.sub_apps.get_mut(&label.intern()) } - /// Retrieves a `SubApp` inside this [`App`] with the given label, if it exists. Otherwise returns - /// an [`Err`] containing the given label. - pub fn get_sub_app(&self, label: impl AppLabel) -> Result<&App, impl AppLabel> { - self.sub_apps - .get(&label.intern()) - .map(|sub_app| &sub_app.app) - .ok_or(label) + /// Inserts a [`SubApp`] with the given label. + pub fn insert_sub_app(&mut self, label: impl AppLabel, sub_app: SubApp) { + self.sub_apps.sub_apps.insert(label.intern(), sub_app); } - /// Adds a new `schedule` to the [`App`] under the provided `label`. - /// - /// # Warning - /// This method will overwrite any existing schedule at that label. - /// To avoid this behavior, use the `init_schedule` method instead. - pub fn add_schedule(&mut self, schedule: Schedule) -> &mut Self { - let mut schedules = self.world.resource_mut::(); - schedules.insert(schedule); + /// Removes the [`SubApp`] with the given label, if it exists. + pub fn remove_sub_app(&mut self, label: impl AppLabel) -> Option { + self.sub_apps.sub_apps.remove(&label.intern()) + } + /// Inserts a new `schedule` under the provided `label`, overwriting any existing + /// schedule with the same label. + pub fn add_schedule(&mut self, schedule: Schedule) -> &mut Self { + self.main_mut().add_schedule(schedule); self } - /// Initializes a new empty `schedule` to the [`App`] under the provided `label` if it does not exists. + /// Initializes an empty `schedule` under the provided `label`, if it does not exist. /// - /// See [`App::add_schedule`] to pass in a pre-constructed schedule. + /// See [`add_schedule`](Self::add_schedule) to insert an existing schedule. pub fn init_schedule(&mut self, label: impl ScheduleLabel) -> &mut Self { - let label = label.intern(); - let mut schedules = self.world.resource_mut::(); - if !schedules.contains(label) { - schedules.insert(Schedule::new(label)); - } + self.main_mut().init_schedule(label); self } - /// Gets read-only access to the [`Schedule`] with the provided `label` if it exists. + /// Returns a reference to the [`Schedule`] with the provided `label` if it exists. pub fn get_schedule(&self, label: impl ScheduleLabel) -> Option<&Schedule> { - let schedules = self.world.get_resource::()?; - schedules.get(label) + self.main().get_schedule(label) } - /// Gets read-write access to a [`Schedule`] with the provided `label` if it exists. + /// Returns a mutable reference to the [`Schedule`] with the provided `label` if it exists. pub fn get_schedule_mut(&mut self, label: impl ScheduleLabel) -> Option<&mut Schedule> { - let schedules = self.world.get_resource_mut::()?; - // We need to call .into_inner here to satisfy the borrow checker: - // it can reason about reborrows using ordinary references but not the `Mut` smart pointer. - schedules.into_inner().get_mut(label) + self.main_mut().get_schedule_mut(label) } - /// Applies the function to the [`Schedule`] associated with `label`. + /// Runs function `f` with the [`Schedule`] associated with `label`. /// /// **Note:** This will create the schedule if it does not already exist. pub fn edit_schedule( &mut self, label: impl ScheduleLabel, - f: impl FnOnce(&mut Schedule), + f: impl FnMut(&mut Schedule), ) -> &mut Self { - let label = label.intern(); - let mut schedules = self.world.resource_mut::(); - - if schedules.get(label).is_none() { - schedules.insert(Schedule::new(label)); - } - - let schedule = schedules.get_mut(label).unwrap(); - // Call the function f, passing in the schedule retrieved - f(schedule); - + self.main_mut().edit_schedule(label, f); self } @@ -901,7 +838,7 @@ impl App { &mut self, schedule_build_settings: ScheduleBuildSettings, ) -> &mut Self { - self.world + self.world_mut() .resource_mut::() .configure_schedules(schedule_build_settings); self @@ -941,7 +878,7 @@ impl App { /// app.update(); /// ``` pub fn allow_ambiguous_component(&mut self) -> &mut Self { - self.world.allow_ambiguous_component::(); + self.world_mut().allow_ambiguous_component::(); self } @@ -980,14 +917,18 @@ impl App { /// app.update(); /// ``` pub fn allow_ambiguous_resource(&mut self) -> &mut Self { - self.world.allow_ambiguous_resource::(); + self.world_mut().allow_ambiguous_resource::(); self } } +type RunnerFn = Box; + fn run_once(mut app: App) { - let plugins_state = app.plugins_state(); - if plugins_state != PluginsState::Cleaned { + let initial_plugins_state = app.plugins_state(); + + // wait for plugins to finish setting up + if app.plugins_state() != PluginsState::Cleaned { while app.plugins_state() == PluginsState::Adding { #[cfg(not(target_arch = "wasm32"))] bevy_tasks::tick_global_task_pools_on_main_thread(); @@ -996,22 +937,59 @@ fn run_once(mut app: App) { app.cleanup(); } - // if plugins where cleaned before the runner start, an update already ran - if plugins_state != PluginsState::Cleaned { - app.update(); + // If plugins where cleaned before the runner start, an update already ran + if initial_plugins_state == PluginsState::Cleaned { + return; + } + + // disassemble + let (mut sub_apps, mut tls, send, recv, _) = app.into_parts(); + + #[cfg(not(target_arch = "wasm32"))] + { + // Move sub-apps to another thread and run an event loop in this thread. + let thread = std::thread::spawn(move || { + let result = catch_unwind(AssertUnwindSafe(|| { + sub_apps.update(); + send.send(AppEvent::Exit(sub_apps)).unwrap(); + })); + + if let Some(payload) = result.err() { + send.send(AppEvent::Error(payload)).unwrap(); + } + }); + + loop { + let event = recv.recv().unwrap(); + match event { + AppEvent::Task(task) => { + task(); + } + AppEvent::Exit(_) => { + thread.join().unwrap(); + break; + } + AppEvent::Error(payload) => { + resume_unwind(payload); + } + } + } + } + + #[cfg(target_arch = "wasm32")] + { + sub_apps.update(); } + + tls.clear(); } -/// An event that indicates the [`App`] should exit. This will fully exit the app process at the -/// start of the next tick of the schedule. -/// -/// You can also use this event to detect that an exit was requested. In order to receive it, systems -/// subscribing to this event should run after it was emitted and before the schedule of the same -/// frame is over. This is important since [`App::run()`] might never return. +/// An event that indicates the [`App`] should exit. If one or more of these are present at the +/// end of an update, the [runner](App::set_runner) will end and ([maybe](App::run)) return +/// control to the caller. /// -/// If you don't require access to other components or resources, consider implementing the [`Drop`] -/// trait on components/resources for code that runs on exit. That saves you from worrying about -/// system schedule ordering, and is idiomatic Rust. +/// This event can be used to detect when an exit is requested. Make sure that systems listening +/// for this event run before the current update ends. #[derive(Event, Debug, Clone, Default)] pub struct AppExit; @@ -1102,8 +1080,8 @@ mod tests { app.add_state::() .add_systems(OnEnter(AppState::MainMenu), (foo, bar)); - app.world.run_schedule(OnEnter(AppState::MainMenu)); - assert_eq!(app.world.entities().len(), 2); + app.world_mut().run_schedule(OnEnter(AppState::MainMenu)); + assert_eq!(app.world().entities().len(), 2); } #[test] @@ -1112,8 +1090,8 @@ mod tests { app.add_systems(OnEnter(AppState::MainMenu), (foo, bar)) .add_state::(); - app.world.run_schedule(OnEnter(AppState::MainMenu)); - assert_eq!(app.world.entities().len(), 2); + app.world_mut().run_schedule(OnEnter(AppState::MainMenu)); + assert_eq!(app.world().entities().len(), 2); } #[test] diff --git a/crates/bevy_app/src/events.rs b/crates/bevy_app/src/events.rs new file mode 100644 index 0000000000000..a8b5227835b3f --- /dev/null +++ b/crates/bevy_app/src/events.rs @@ -0,0 +1,76 @@ +use bevy_ecs::storage::{ThreadLocalTask, ThreadLocalTaskSendError, ThreadLocalTaskSender}; +use std::any::Any; +use std::sync::mpsc::{channel, Receiver, Sender}; + +/// Events an [`App`](crate::App) can send to another thread when using a multi-threaded runner. +pub enum AppEvent { + /// The app has sent a task with access to [`ThreadLocals`](bevy_ecs::prelude::ThreadLocals). + Task(ThreadLocalTask), + /// The app has exited. + Exit(crate::SubApps), + /// The app has errored. + Error(Box), +} + +/// The sender half of an [`app_thread_channel`]. +#[derive(Clone)] +pub struct AppEventSender(Sender); + +/// Constructs a new asynchronous channel for passing [`AppEvent`] instances +/// to an event loop and returns the sender and receiver halves. +pub fn app_thread_channel() -> (AppEventSender, AppEventReceiver) { + let (send, recv) = channel(); + (AppEventSender(send), AppEventReceiver(recv)) +} + +impl std::ops::Deref for AppEventSender { + type Target = Sender; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for AppEventSender { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl ThreadLocalTaskSender for AppEventSender { + fn send_task( + &mut self, + task: ThreadLocalTask, + ) -> Result<(), ThreadLocalTaskSendError> { + #[cfg(not(target_arch = "wasm32"))] + { + self.send(AppEvent::Task(task)).map_err(|error| { + let AppEvent::Task(task) = error.0 else { + unreachable!() + }; + ThreadLocalTaskSendError(task) + }) + } + + #[cfg(target_arch = "wasm32")] + { + // wasm builds should always access TLS directly + unreachable!("currently, only single-threaded wasm is supported") + } + } +} + +/// The receiver-half of an [`app_thread_channel`]. +pub struct AppEventReceiver(Receiver); + +impl std::ops::Deref for AppEventReceiver { + type Target = Receiver; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for AppEventReceiver { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index 3439620a46633..967e9de50ae86 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -4,20 +4,24 @@ #![allow(clippy::type_complexity)] mod app; +mod events; mod main_schedule; mod plugin; mod plugin_group; mod schedule_runner; +mod sub_app; #[cfg(feature = "bevy_ci_testing")] pub mod ci_testing; pub use app::*; pub use bevy_derive::DynamicPlugin; +pub use events::*; pub use main_schedule::*; pub use plugin::*; pub use plugin_group::*; pub use schedule_runner::*; +pub use sub_app::*; #[allow(missing_docs)] pub mod prelude { @@ -28,6 +32,7 @@ pub mod prelude { First, FixedUpdate, Last, Main, PostStartup, PostUpdate, PreStartup, PreUpdate, SpawnScene, Startup, StateTransition, Update, }, + sub_app::SubApp, DynamicPlugin, Plugin, PluginGroup, }; } diff --git a/crates/bevy_app/src/plugin.rs b/crates/bevy_app/src/plugin.rs index b722737ccb56a..55c0af4944fe5 100644 --- a/crates/bevy_app/src/plugin.rs +++ b/crates/bevy_app/src/plugin.rs @@ -25,7 +25,7 @@ pub trait Plugin: Downcast + Any + Send + Sync { /// Configures the [`App`] to which this plugin is added. fn build(&self, app: &mut App); - /// Has the plugin finished it's setup? This can be useful for plugins that needs something + /// Has the plugin finished its setup? This can be useful for plugins that needs something /// asynchronous to happen before they can finish their setup, like renderer initialization. /// Once the plugin is ready, [`finish`](Plugin::finish) should be called. fn ready(&self, _app: &App) -> bool { @@ -60,6 +60,26 @@ pub trait Plugin: Downcast + Any + Send + Sync { impl_downcast!(Plugin); +/// Plugins state in the application +#[derive(PartialEq, Eq, Debug, Clone, Copy, PartialOrd, Ord)] +pub enum PluginsState { + /// Plugins are being added. + Adding, + /// All plugins already added are ready. + Ready, + /// Finish has been executed for all plugins added. + Finished, + /// Cleanup has been executed for all plugins added. + Cleaned, +} + +/// A dummy plugin that's to temporarily occupy an entry in an app's plugin registry. +pub(crate) struct PlaceholderPlugin; + +impl Plugin for PlaceholderPlugin { + fn build(&self, _app: &mut App) {} +} + /// A type representing an unsafe function that returns a mutable pointer to a [`Plugin`]. /// It is used for dynamically loading plugins. /// diff --git a/crates/bevy_app/src/schedule_runner.rs b/crates/bevy_app/src/schedule_runner.rs index 18b2f0b61fb55..40dc97078251e 100644 --- a/crates/bevy_app/src/schedule_runner.rs +++ b/crates/bevy_app/src/schedule_runner.rs @@ -1,69 +1,63 @@ -use crate::{ - app::{App, AppExit}, - plugin::Plugin, - PluginsState, -}; +use crate::{App, AppEvent, AppExit, Plugin, PluginsState, SubApps}; use bevy_ecs::event::{Events, ManualEventReader}; use bevy_utils::{Duration, Instant}; +#[cfg(not(target_arch = "wasm32"))] +use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe}; #[cfg(target_arch = "wasm32")] use std::{cell::RefCell, rc::Rc}; #[cfg(target_arch = "wasm32")] use wasm_bindgen::{prelude::*, JsCast}; -/// Determines the method used to run an [`App`]'s [`Schedule`](bevy_ecs::schedule::Schedule). -/// -/// It is used in the [`ScheduleRunnerPlugin`]. +/// Determines how frequently the [`App`] should be updated by the [`ScheduleRunnerPlugin`]. #[derive(Copy, Clone, Debug)] pub enum RunMode { - /// Indicates that the [`App`]'s schedule should run repeatedly. + /// The [`App`] will update once. + Once, + /// The [`App`] will update over and over, until an [`AppExit`] event appears. Loop { - /// The minimum [`Duration`] to wait after a [`Schedule`](bevy_ecs::schedule::Schedule) - /// has completed before repeating. A value of [`None`] will not wait. - wait: Option, + /// The minimum time from the start of one update to the next. + /// + /// **Note:** This has no upper limit, but the [`App`] will hang if you set this too high. + wait: Duration, }, - /// Indicates that the [`App`]'s schedule should run only once. - Once, } impl Default for RunMode { fn default() -> Self { - RunMode::Loop { wait: None } + RunMode::Loop { + wait: Duration::ZERO, + } } } -/// Configures an [`App`] to run its [`Schedule`](bevy_ecs::schedule::Schedule) according to a given -/// [`RunMode`]. +/// Runs an [`App`] according to the selected [`RunMode`]. /// -/// [`ScheduleRunnerPlugin`] is included in the -/// [`MinimalPlugins`](https://docs.rs/bevy/latest/bevy/struct.MinimalPlugins.html) plugin group. +/// This plugin is included in the [`MinimalPlugins`] group, but **not** included in the +/// [`DefaultPlugins`] group. [`DefaultPlugins`] assumes the [`App`] will render to a window, +/// so it comes with the [`WinitPlugin`] instead. /// -/// [`ScheduleRunnerPlugin`] is *not* included in the -/// [`DefaultPlugins`](https://docs.rs/bevy/latest/bevy/struct.DefaultPlugins.html) plugin group -/// which assumes that the [`Schedule`](bevy_ecs::schedule::Schedule) will be executed by other means: -/// typically, the `winit` event loop -/// (see [`WinitPlugin`](https://docs.rs/bevy/latest/bevy/winit/struct.WinitPlugin.html)) -/// executes the schedule making [`ScheduleRunnerPlugin`] unnecessary. +/// [`DefaultPlugins`]: https://docs.rs/bevy/latest/bevy/struct.DefaultPlugins.html +/// [`MinimalPlugins`]: https://docs.rs/bevy/latest/bevy/struct.MinimalPlugins.html +/// [`WinitPlugin`]: https://docs.rs/bevy/latest/bevy/winit/struct.WinitPlugin.html #[derive(Default)] pub struct ScheduleRunnerPlugin { - /// Determines whether the [`Schedule`](bevy_ecs::schedule::Schedule) is run once or repeatedly. + /// Determines how frequently the [`App`] should update. pub run_mode: RunMode, } impl ScheduleRunnerPlugin { /// See [`RunMode::Once`]. pub fn run_once() -> Self { - ScheduleRunnerPlugin { + Self { run_mode: RunMode::Once, } } /// See [`RunMode::Loop`]. - pub fn run_loop(wait_duration: Duration) -> Self { - ScheduleRunnerPlugin { - run_mode: RunMode::Loop { - wait: Some(wait_duration), - }, + pub fn run_loop(wait: Duration) -> Self { + Self { + run_mode: RunMode::Loop { wait }, } } } @@ -72,8 +66,8 @@ impl Plugin for ScheduleRunnerPlugin { fn build(&self, app: &mut App) { let run_mode = self.run_mode; app.set_runner(move |mut app: App| { - let plugins_state = app.plugins_state(); - if plugins_state != PluginsState::Cleaned { + // wait for plugins to finish setting up + if app.plugins_state() != PluginsState::Cleaned { while app.plugins_state() == PluginsState::Adding { #[cfg(not(target_arch = "wasm32"))] bevy_tasks::tick_global_task_pools_on_main_thread(); @@ -82,50 +76,75 @@ impl Plugin for ScheduleRunnerPlugin { app.cleanup(); } - let mut app_exit_event_reader = ManualEventReader::::default(); + let mut exit_event_reader = ManualEventReader::::default(); match run_mode { RunMode::Once => { // if plugins where cleaned before the runner start, an update already ran - if plugins_state != PluginsState::Cleaned { + if app.plugins_state() != PluginsState::Cleaned { app.update(); } } RunMode::Loop { wait } => { - let mut tick = move |app: &mut App, - wait: Option| - -> Result, AppExit> { + let mut update = move |sub_apps: &mut SubApps| -> Result { let start_time = Instant::now(); + sub_apps.update(); + let end_time = Instant::now(); - app.update(); - - if let Some(app_exit_events) = - app.world.get_resource_mut::>() + if let Some(exit_events) = + sub_apps.main.world().get_resource::>() { - if let Some(exit) = app_exit_event_reader.read(&app_exit_events).last() - { + if let Some(exit) = exit_event_reader.read(exit_events).last() { return Err(exit.clone()); } } - let end_time = Instant::now(); - - if let Some(wait) = wait { - let exe_time = end_time - start_time; - if exe_time < wait { - return Ok(Some(wait - exe_time)); - } + let elapsed = end_time - start_time; + if elapsed < wait { + return Ok(wait - elapsed); } - Ok(None) + Ok(Duration::ZERO) }; + // disassemble + let (mut sub_apps, mut tls, send, recv, _) = app.into_parts(); + #[cfg(not(target_arch = "wasm32"))] { - while let Ok(delay) = tick(&mut app, wait) { - if let Some(delay) = delay { - std::thread::sleep(delay); + // Move sub-apps to another thread and run an event loop in this thread. + let thread = std::thread::spawn(move || { + let result = catch_unwind(AssertUnwindSafe(|| { + while let Ok(sleep) = update(&mut sub_apps) { + if !sleep.is_zero() { + std::thread::sleep(sleep); + } + } + + send.send(AppEvent::Exit(sub_apps)).unwrap(); + })); + + if let Some(payload) = result.err() { + send.send(AppEvent::Error(payload)).unwrap(); + } + }); + + loop { + let event = recv.recv().unwrap(); + match event { + AppEvent::Task(task) => { + task(); + } + AppEvent::Exit(_) => { + thread.join().unwrap(); + break; + } + AppEvent::Error(payload) => { + resume_unwind(payload); + } } } + + tls.clear(); } #[cfg(target_arch = "wasm32")] @@ -139,24 +158,27 @@ impl Plugin for ScheduleRunnerPlugin { ) .expect("Should register `setTimeout`."); } - let asap = Duration::from_millis(1); - let mut rc = Rc::new(app); + let min_sleep = Duration::from_millis(1); + + let mut rc = Rc::new(sub_apps); let f = Rc::new(RefCell::new(None)); let g = f.clone(); - let c = move || { - let mut app = Rc::get_mut(&mut rc).unwrap(); - let delay = tick(&mut app, wait); - match delay { - Ok(delay) => { - set_timeout(f.borrow().as_ref().unwrap(), delay.unwrap_or(asap)) + let closure = move || { + let mut sub_apps = Rc::get_mut(&mut rc).unwrap(); + match update(&mut sub_apps) { + Ok(sleep) => { + set_timeout(f.borrow().as_ref().unwrap(), sleep.max(min_sleep)) } Err(_) => {} } }; - *g.borrow_mut() = Some(Closure::wrap(Box::new(c) as Box)); - set_timeout(g.borrow().as_ref().unwrap(), asap); + + *g.borrow_mut() = + Some(Closure::wrap(Box::new(closure) as Box)); + + set_timeout(g.borrow().as_ref().unwrap(), min_sleep); }; } } diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs new file mode 100644 index 0000000000000..b018910547e45 --- /dev/null +++ b/crates/bevy_app/src/sub_app.rs @@ -0,0 +1,405 @@ +use crate::{App, First, InternedAppLabel, Plugin, Plugins, PluginsState, StateTransition}; +use bevy_ecs::{ + prelude::*, + schedule::{ + common_conditions::run_once as run_once_condition, run_enter_schedule, + InternedScheduleLabel, IntoSystemConfigs, IntoSystemSetConfigs, ScheduleLabel, + }, +}; +#[cfg(feature = "trace")] +use bevy_utils::tracing::info_span; +use bevy_utils::{default, HashMap, HashSet}; +use std::fmt::Debug; + +type ExtractFn = Box; + +#[derive(Default)] +pub(crate) struct PluginStore { + pub(crate) registry: Vec>, + pub(crate) names: HashSet, +} + +/// A secondary application with its own [`World`]. These can run independently of each other. +/// +/// These are useful for situations where certain processes (e.g. a render thread) need to be kept +/// separate from the main application. +pub struct SubApp { + /// The data of this application. + world: World, + /// Metadata for installed plugins. + pub(crate) plugins: PluginStore, + /// Panics if an update is attempted while plugins are building. + pub(crate) plugin_build_depth: usize, + pub(crate) plugins_state: PluginsState, + /// The schedule that will be run by [`update`](Self::update). + pub update_schedule: Option, + /// A function that gives mutable access to two app worlds. This is primarily + /// intended for copying data from the main world to secondary worlds. + extract: Option, +} + +impl Debug for SubApp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SubApp") + } +} + +impl Default for SubApp { + fn default() -> Self { + let mut world = World::new(); + world.init_resource::(); + Self { + world, + plugins: default(), + plugin_build_depth: 0, + plugins_state: PluginsState::Adding, + update_schedule: None, + extract: None, + } + } +} + +impl SubApp { + /// Returns a default, empty [`SubApp`]. + pub fn new() -> Self { + Self::default() + } + + /// This method is a workaround. Each [`SubApp`] can have its own plugins, but [`Plugin`] + /// works on an [`App`] as a whole. + fn run_as_app(&mut self, f: F) + where + F: FnOnce(&mut App), + { + let mut app = App::empty(); + std::mem::swap(self, &mut app.sub_apps.main); + f(&mut app); + std::mem::swap(self, &mut app.sub_apps.main); + } + + /// Returns a reference to the [`World`]. + pub fn world(&self) -> &World { + &self.world + } + + /// Returns a mutable reference to the [`World`]. + pub fn world_mut(&mut self) -> &mut World { + &mut self.world + } + + /// Runs the default schedule. + pub fn update(&mut self) { + if self.is_building_plugins() { + panic!("SubApp::update() was called while a plugin was building."); + } + + if let Some(label) = self.update_schedule { + self.world.run_schedule(label); + } + self.world.clear_trackers(); + } + + /// Extracts data from `world` into the app's world using the registered extract method. + /// + /// **Note:** There is no default extract method. Calling `extract` does nothing if + /// [`set_extract`](Self::set_extract) has not been called. + pub fn extract(&mut self, world: &mut World) { + if let Some(f) = self.extract.as_mut() { + f(world, &mut self.world); + } + } + + /// Sets the method that will be called by [`extract`](Self::extract). + /// + /// The first argument is the `World` to extract data from, the second argument is the app `World`. + pub fn set_extract(&mut self, extract: F) -> &mut Self + where + F: Fn(&mut World, &mut World) + Send + 'static, + { + self.extract = Some(Box::new(extract)); + self + } + + /// See [`App::insert_resource`]. + pub fn insert_resource(&mut self, resource: R) -> &mut Self { + self.world.insert_resource(resource); + self + } + + /// See [`App::init_resource`]. + pub fn init_resource(&mut self) -> &mut Self { + self.world.init_resource::(); + self + } + + /// See [`App::add_systems`]. + pub fn add_systems( + &mut self, + schedule: impl ScheduleLabel, + systems: impl IntoSystemConfigs, + ) -> &mut Self { + let label = schedule.intern(); + let mut schedules = self.world.resource_mut::(); + if let Some(schedule) = schedules.get_mut(label) { + schedule.add_systems(systems); + } else { + let mut new_schedule = Schedule::new(label); + new_schedule.add_systems(systems); + schedules.insert(new_schedule); + } + + self + } + + /// See [`App::configure_sets`]. + #[track_caller] + pub fn configure_sets( + &mut self, + schedule: impl ScheduleLabel, + sets: impl IntoSystemSetConfigs, + ) -> &mut Self { + let label = schedule.intern(); + let mut schedules = self.world.resource_mut::(); + if let Some(schedule) = schedules.get_mut(label) { + schedule.configure_sets(sets); + } else { + let mut new_schedule = Schedule::new(label); + new_schedule.configure_sets(sets); + schedules.insert(new_schedule); + } + self + } + + /// See [`App::add_schedule`]. + pub fn add_schedule(&mut self, schedule: Schedule) -> &mut Self { + let mut schedules = self.world.resource_mut::(); + schedules.insert(schedule); + self + } + + /// See [`App::init_schedule`]. + pub fn init_schedule(&mut self, label: impl ScheduleLabel) -> &mut Self { + let label = label.intern(); + let mut schedules = self.world.resource_mut::(); + if !schedules.contains(label) { + schedules.insert(Schedule::new(label)); + } + self + } + + /// See [`App::get_schedule`]. + pub fn get_schedule(&self, label: impl ScheduleLabel) -> Option<&Schedule> { + let schedules = self.world.get_resource::()?; + schedules.get(label) + } + + /// See [`App::get_schedule_mut`]. + pub fn get_schedule_mut(&mut self, label: impl ScheduleLabel) -> Option<&mut Schedule> { + let schedules = self.world.get_resource_mut::()?; + // We must call `.into_inner` here because the borrow checker only understands reborrows + // using ordinary references, not our `Mut` smart pointers. + schedules.into_inner().get_mut(label) + } + + /// See [`App::edit_schedule`]. + pub fn edit_schedule( + &mut self, + label: impl ScheduleLabel, + mut f: impl FnMut(&mut Schedule), + ) -> &mut Self { + let label = label.intern(); + let mut schedules = self.world.resource_mut::(); + if !schedules.contains(label) { + schedules.insert(Schedule::new(label)); + } + + let schedule = schedules.get_mut(label).unwrap(); + f(schedule); + + self + } + + /// See [`App::add_state`]. + pub fn add_state(&mut self) -> &mut Self { + self.init_resource::>() + .init_resource::>() + .add_systems( + StateTransition, + ( + run_enter_schedule::.run_if(run_once_condition()), + apply_state_transition::, + ) + .chain(), + ); + + // The OnEnter, OnExit, and OnTransition schedules are lazily initialized + // (i.e. when the first system is added to them), so World::try_run_schedule + // is used to fail gracefully if they aren't present. + self + } + + /// See [`App::add_event`]. + pub fn add_event(&mut self) -> &mut Self + where + T: Event, + { + if !self.world.contains_resource::>() { + self.init_resource::>().add_systems( + First, + bevy_ecs::event::event_update_system:: + .run_if(bevy_ecs::event::event_update_condition::), + ); + } + + self + } + + /// See [`App::add_plugins`]. + pub fn add_plugins(&mut self, plugins: impl Plugins) -> &mut Self { + self.run_as_app(|app| plugins.add_to_app(app)); + self + } + + /// See [`App::is_plugin_added`]. + pub fn is_plugin_added(&self) -> bool + where + T: Plugin, + { + self.plugins + .registry + .iter() + .any(|p| p.downcast_ref::().is_some()) + } + + /// See [`App::get_added_plugins`]. + pub fn get_added_plugins(&self) -> Vec<&T> + where + T: Plugin, + { + self.plugins + .registry + .iter() + .filter_map(|p| p.downcast_ref()) + .collect() + } + + /// Returns `true` if there is no plugin in the middle of being built. + pub(crate) fn is_building_plugins(&self) -> bool { + self.plugin_build_depth > 0 + } + + /// Return the state of plugins. + #[inline] + pub fn plugins_state(&mut self) -> PluginsState { + match self.plugins_state { + PluginsState::Adding => { + let mut state = PluginsState::Ready; + let plugins = std::mem::take(&mut self.plugins); + self.run_as_app(|app| { + for plugin in &plugins.registry { + if !plugin.ready(app) { + state = PluginsState::Adding; + return; + } + } + }); + self.plugins = plugins; + state + } + state => state, + } + } + + /// Runs [`Plugin::finish`] for each plugin. + pub fn finish(&mut self) { + let plugins = std::mem::take(&mut self.plugins); + self.run_as_app(|app| { + for plugin in &plugins.registry { + plugin.finish(app); + } + }); + self.plugins = plugins; + self.plugins_state = PluginsState::Finished; + } + + /// Runs [`Plugin::cleanup`] for each plugin. + pub fn cleanup(&mut self) { + let plugins = std::mem::take(&mut self.plugins); + self.run_as_app(|app| { + for plugin in &plugins.registry { + plugin.cleanup(app); + } + }); + self.plugins = plugins; + self.plugins_state = PluginsState::Cleaned; + } + + /// See [`App::register_type`]. + #[cfg(feature = "bevy_reflect")] + pub fn register_type(&mut self) -> &mut Self { + let registry = self.world.resource_mut::(); + registry.write().register::(); + self + } + + /// See [`App::register_type_data`]. + #[cfg(feature = "bevy_reflect")] + pub fn register_type_data< + T: bevy_reflect::Reflect + bevy_reflect::TypePath, + D: bevy_reflect::TypeData + bevy_reflect::FromType, + >( + &mut self, + ) -> &mut Self { + let registry = self.world.resource_mut::(); + registry.write().register_type_data::(); + self + } +} + +/// The collection of sub-apps that belong to an [`App`]. +#[derive(Default)] +pub struct SubApps { + /// The primary sub-app that contains the "main" world. + pub main: Box, + /// Other, labeled sub-apps. + pub sub_apps: HashMap, +} + +impl SubApps { + pub(crate) fn new() -> Self { + Self { + main: Box::new(SubApp::new()), + sub_apps: HashMap::new(), + } + } + + /// Calls [`update`](SubApp::update) for the main sub-app, and then calls + /// [`extract`](SubApp::extract) and [`update`](SubApp::update) for the rest. + pub fn update(&mut self) { + #[cfg(feature = "trace")] + let _bevy_update_span = info_span!("SubApps::update").entered(); + { + #[cfg(feature = "trace")] + let _bevy_frame_update_span = info_span!("sub app", name = "Main").entered(); + self.main.update(); + } + for (_label, sub_app) in self.sub_apps.iter_mut() { + #[cfg(feature = "trace")] + let _sub_app_span = info_span!("sub app", name = ?_label).entered(); + sub_app.extract(&mut self.main.world); + sub_app.update(); + } + + self.main.world.clear_trackers(); + } + + /// Returns an iterator over the sub-apps (starting with the main one). + pub fn iter(&self) -> impl Iterator + '_ { + std::iter::once(self.main.as_ref()).chain(self.sub_apps.values()) + } + + /// Returns a mutable iterator over the sub-apps (starting with the main one). + pub fn iter_mut(&mut self) -> impl Iterator + '_ { + std::iter::once(self.main.as_mut()).chain(self.sub_apps.values_mut()) + } +} diff --git a/crates/bevy_asset/src/io/embedded/mod.rs b/crates/bevy_asset/src/io/embedded/mod.rs index e5470cd3d5c3f..b7301a17852a4 100644 --- a/crates/bevy_asset/src/io/embedded/mod.rs +++ b/crates/bevy_asset/src/io/embedded/mod.rs @@ -191,7 +191,7 @@ macro_rules! embedded_asset { ($app: ident, $source_path: expr, $path: expr) => {{ let mut embedded = $app - .world + .world_mut() .resource_mut::<$crate::io::embedded::EmbeddedAssetRegistry>(); let path = $crate::embedded_path!($source_path, $path); #[cfg(feature = "embedded_watcher")] @@ -206,7 +206,7 @@ macro_rules! embedded_asset { #[macro_export] macro_rules! load_internal_asset { ($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{ - let mut assets = $app.world.resource_mut::<$crate::Assets<_>>(); + let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>(); assets.insert($handle, ($loader)( include_str!($path_str), std::path::Path::new(file!()) @@ -218,7 +218,7 @@ macro_rules! load_internal_asset { }}; // we can't support params without variadic arguments, so internal assets with additional params can't be hot-reloaded ($app: ident, $handle: ident, $path_str: expr, $loader: expr $(, $param:expr)+) => {{ - let mut assets = $app.world.resource_mut::<$crate::Assets<_>>(); + let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>(); assets.insert($handle, ($loader)( include_str!($path_str), std::path::Path::new(file!()) @@ -235,7 +235,7 @@ macro_rules! load_internal_asset { #[macro_export] macro_rules! load_internal_binary_asset { ($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{ - let mut assets = $app.world.resource_mut::<$crate::Assets<_>>(); + let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>(); assets.insert( $handle, ($loader)( diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 9f9cd44c77b0d..d3e572f4f8141 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -121,7 +121,7 @@ impl Plugin for AssetPlugin { let embedded = EmbeddedAssetRegistry::default(); { let mut sources = app - .world + .world_mut() .get_resource_or_insert_with::(Default::default); sources.init_default_source( &self.file_path, @@ -137,7 +137,7 @@ impl Plugin for AssetPlugin { } match self.mode { AssetMode::Unprocessed => { - let mut builders = app.world.resource_mut::(); + let mut builders = app.world_mut().resource_mut::(); let sources = builders.build_sources(watch, false); app.insert_resource(AssetServer::new( sources, @@ -148,7 +148,7 @@ impl Plugin for AssetPlugin { AssetMode::Processed => { #[cfg(feature = "asset_processor")] { - let mut builders = app.world.resource_mut::(); + let mut builders = app.world_mut().resource_mut::(); let processor = AssetProcessor::new(&mut builders); let mut sources = builders.build_sources(false, watch); sources.gate_on_processor(processor.data.clone()); @@ -164,7 +164,7 @@ impl Plugin for AssetPlugin { } #[cfg(not(feature = "asset_processor"))] { - let mut builders = app.world.resource_mut::(); + let mut builders = app.world_mut().resource_mut::(); let sources = builders.build_sources(false, watch); app.insert_resource(AssetServer::new( sources, @@ -185,7 +185,7 @@ impl Plugin for AssetPlugin { ) .add_systems(UpdateAssets, server::handle_internal_asset_events); - let mut order = app.world.resource_mut::(); + let mut order = app.world_mut().resource_mut::(); order.insert_after(First, UpdateAssets); order.insert_after(PostUpdate, AssetEvents); } @@ -282,20 +282,24 @@ pub trait AssetApp { impl AssetApp for App { fn register_asset_loader(&mut self, loader: L) -> &mut Self { - self.world.resource::().register_loader(loader); + self.world() + .resource::() + .register_loader(loader); self } fn init_asset_loader(&mut self) -> &mut Self { - let loader = L::from_world(&mut self.world); + let loader = L::from_world(self.world_mut()); self.register_asset_loader(loader) } fn init_asset(&mut self) -> &mut Self { let assets = Assets::::default(); - self.world.resource::().register_asset(&assets); - if self.world.contains_resource::() { - let processor = self.world.resource::(); + self.world() + .resource::() + .register_asset(&assets); + if self.world().contains_resource::() { + let processor = self.world().resource::(); // The processor should have its own handle provider separate from the Asset storage // to ensure the id spaces are entirely separate. Not _strictly_ necessary, but // desirable. @@ -319,7 +323,7 @@ impl AssetApp for App { where A: Asset + Reflect + FromReflect + GetTypeRegistration, { - let type_registry = self.world.resource::(); + let type_registry = self.world().resource::(); { let mut type_registry = type_registry.write(); @@ -333,21 +337,21 @@ impl AssetApp for App { } fn preregister_asset_loader(&mut self, extensions: &[&str]) -> &mut Self { - self.world + self.world_mut() .resource_mut::() .preregister_loader::(extensions); self } fn register_asset_processor(&mut self, processor: P) -> &mut Self { - if let Some(asset_processor) = self.world.get_resource::() { + if let Some(asset_processor) = self.world().get_resource::() { asset_processor.register_processor(processor); } self } fn set_default_asset_processor(&mut self, extension: &str) -> &mut Self { - if let Some(asset_processor) = self.world.get_resource::() { + if let Some(asset_processor) = self.world().get_resource::() { asset_processor.set_default_processor::

(extension); } self @@ -359,13 +363,13 @@ impl AssetApp for App { source: AssetSourceBuilder, ) -> &mut Self { let id = id.into(); - if self.world.get_resource::().is_some() { + if self.world().get_resource::().is_some() { error!("{} must be registered before `AssetPlugin` (typically added as part of `DefaultPlugins`)", id); } { let mut sources = self - .world + .world_mut() .get_resource_or_insert_with(AssetSourceBuilders::default); sources.insert(id, source); } @@ -521,7 +525,7 @@ mod tests { fn run_app_until(app: &mut App, mut predicate: impl FnMut(&mut World) -> Option<()>) { for _ in 0..LARGE_ITERATION_COUNT { app.update(); - if (predicate)(&mut app.world).is_some() { + if (predicate)(app.world_mut()).is_some() { return; } } @@ -607,13 +611,13 @@ mod tests { .init_resource::() .register_asset_loader(CoolTextLoader) .add_systems(Update, store_asset_events); - let asset_server = app.world.resource::().clone(); + let asset_server = app.world().resource::().clone(); let handle: Handle = asset_server.load(a_path); let a_id = handle.id(); - let entity = app.world.spawn(handle).id(); + let entity = app.world_mut().spawn(handle).id(); app.update(); { - let a_text = get::(&app.world, a_id); + let a_text = get::(app.world(), a_id); let (a_load, a_deps, a_rec_deps) = asset_server.get_load_states(a_id).unwrap(); assert!(a_text.is_none(), "a's asset should not exist yet"); assert_eq!(a_load, LoadState::Loading, "a should still be loading"); @@ -795,27 +799,27 @@ mod tests { }); { - let mut texts = app.world.resource_mut::>(); + let mut texts = app.world_mut().resource_mut::>(); let a = texts.get_mut(a_id).unwrap(); a.text = "Changed".to_string(); } - app.world.despawn(entity); + app.world_mut().despawn(entity); app.update(); assert_eq!( - app.world.resource::>().len(), + app.world().resource::>().len(), 0, "CoolText asset entities should be despawned when no more handles exist" ); app.update(); // this requires a second update because the parent asset was freed in the previous app.update() assert_eq!( - app.world.resource::>().len(), + app.world().resource::>().len(), 0, "SubText asset entities should be despawned when no more handles exist" ); - let events = app.world.remove_resource::().unwrap(); - let id_results = app.world.remove_resource::().unwrap(); + let events = app.world_mut().remove_resource::().unwrap(); + let id_results = app.world_mut().remove_resource::().unwrap(); let expected_events = vec![ AssetEvent::Added { id: a_id }, AssetEvent::LoadedWithDependencies { @@ -905,7 +909,7 @@ mod tests { let (mut app, gate_opener) = test_app(dir); app.init_asset::() .register_asset_loader(CoolTextLoader); - let asset_server = app.world.resource::().clone(); + let asset_server = app.world().resource::().clone(); let handle: Handle = asset_server.load(a_path); let a_id = handle.id(); { @@ -921,7 +925,7 @@ mod tests { ); } - app.world.spawn(handle); + app.world_mut().spawn(handle); gate_opener.open(a_path); gate_opener.open(b_path); gate_opener.open(c_path); @@ -998,7 +1002,7 @@ mod tests { let id = { let handle = { - let mut texts = app.world.resource_mut::>(); + let mut texts = app.world_mut().resource_mut::>(); texts.add(CoolText { text: hello.clone(), embedded: empty.clone(), @@ -1011,7 +1015,7 @@ mod tests { { let text = app - .world + .world() .resource::>() .get(&handle) .unwrap(); @@ -1022,16 +1026,16 @@ mod tests { // handle is dropped app.update(); assert!( - app.world.resource::>().get(id).is_none(), + app.world().resource::>().get(id).is_none(), "asset has no handles, so it should have been dropped last update" ); // remove event is emitted app.update(); - let events = std::mem::take(&mut app.world.resource_mut::().0); + let events = std::mem::take(&mut app.world_mut().resource_mut::().0); let expected_events = vec![AssetEvent::Added { id }, AssetEvent::Removed { id }]; assert_eq!(events, expected_events); - let dep_handle = app.world.resource::().load(dep_path); + let dep_handle = app.world().resource::().load(dep_path); let a = CoolText { text: "a".to_string(), embedded: empty, @@ -1039,19 +1043,19 @@ mod tests { dependencies: vec![dep_handle.clone()], sub_texts: Vec::new(), }; - let a_handle = app.world.resource::().load_asset(a); + let a_handle = app.world().resource::().load_asset(a); app.update(); // TODO: ideally it doesn't take two updates for the added event to emit app.update(); - let events = std::mem::take(&mut app.world.resource_mut::().0); + let events = std::mem::take(&mut app.world_mut().resource_mut::().0); let expected_events = vec![AssetEvent::Added { id: a_handle.id() }]; assert_eq!(events, expected_events); gate_opener.open(dep_path); loop { app.update(); - let events = std::mem::take(&mut app.world.resource_mut::().0); + let events = std::mem::take(&mut app.world_mut().resource_mut::().0); if events.is_empty() { continue; } @@ -1065,7 +1069,7 @@ mod tests { break; } app.update(); - let events = std::mem::take(&mut app.world.resource_mut::().0); + let events = std::mem::take(&mut app.world_mut().resource_mut::().0); let expected_events = vec![AssetEvent::Added { id: dep_handle.id(), }]; @@ -1112,7 +1116,7 @@ mod tests { app.init_asset::() .init_asset::() .register_asset_loader(CoolTextLoader); - let asset_server = app.world.resource::().clone(); + let asset_server = app.world().resource::().clone(); let handle: Handle = asset_server.load_folder("text"); gate_opener.open(a_path); gate_opener.open(b_path); @@ -1178,6 +1182,6 @@ mod tests { }); // running schedule does not error on ambiguity between the 2 uses_assets systems - app.world.run_schedule(Update); + app.world_mut().run_schedule(Update); } } diff --git a/crates/bevy_asset/src/reflect.rs b/crates/bevy_asset/src/reflect.rs index 6e00323826de1..694bd32233469 100644 --- a/crates/bevy_asset/src/reflect.rs +++ b/crates/bevy_asset/src/reflect.rs @@ -254,7 +254,7 @@ mod tests { .register_asset_reflect::(); let reflect_asset = { - let type_registry = app.world.resource::(); + let type_registry = app.world().resource::(); let type_registry = type_registry.read(); type_registry @@ -267,9 +267,9 @@ mod tests { field: "test".into(), }; - let handle = reflect_asset.add(&mut app.world, &value); + let handle = reflect_asset.add(app.world_mut(), &value); let strukt = match reflect_asset - .get_mut(&mut app.world, handle) + .get_mut(app.world_mut(), handle) .unwrap() .reflect_mut() { @@ -281,19 +281,19 @@ mod tests { .unwrap() .apply(&String::from("edited")); - assert_eq!(reflect_asset.len(&app.world), 1); - let ids: Vec<_> = reflect_asset.ids(&app.world).collect(); + assert_eq!(reflect_asset.len(app.world()), 1); + let ids: Vec<_> = reflect_asset.ids(app.world()).collect(); assert_eq!(ids.len(), 1); let fetched_handle = UntypedHandle::Weak(ids[0]); let asset = reflect_asset - .get(&app.world, fetched_handle.clone_weak()) + .get(app.world(), fetched_handle.clone_weak()) .unwrap(); assert_eq!(asset.downcast_ref::().unwrap().field, "edited"); reflect_asset - .remove(&mut app.world, fetched_handle) + .remove(app.world_mut(), fetched_handle) .unwrap(); - assert_eq!(reflect_asset.len(&app.world), 0); + assert_eq!(reflect_asset.len(app.world()), 0); } } diff --git a/crates/bevy_core/src/lib.rs b/crates/bevy_core/src/lib.rs index c222af6a9d9fe..39db0a040bea9 100644 --- a/crates/bevy_core/src/lib.rs +++ b/crates/bevy_core/src/lib.rs @@ -26,11 +26,9 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use bevy_utils::{Duration, HashSet, Instant, Uuid}; use std::borrow::Cow; use std::ffi::OsString; -use std::marker::PhantomData; use std::ops::Range; use std::path::{Path, PathBuf}; -#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))] use bevy_tasks::tick_global_task_pools_on_main_thread; @@ -118,16 +116,15 @@ impl Plugin for TaskPoolPlugin { _app.add_systems(Last, tick_global_task_pools); } } -/// A dummy type that is [`!Send`](Send), to force systems to run on the main thread. -pub struct NonSendMarker(PhantomData<*mut ()>); /// A system used to check and advanced our task pools. /// -/// Calls [`tick_global_task_pools_on_main_thread`], -/// and uses [`NonSendMarker`] to ensure that this system runs on the main thread +/// Calls [`tick_global_task_pools_on_main_thread`] on the main thread. #[cfg(not(target_arch = "wasm32"))] -fn tick_global_task_pools(_main_thread_marker: Option>) { - tick_global_task_pools_on_main_thread(); +fn tick_global_task_pools(mut main_thread: ThreadLocal) { + main_thread.run(|_| { + tick_global_task_pools_on_main_thread(); + }); } /// Maintains a count of frames rendered since the start of the application. @@ -209,7 +206,7 @@ mod tests { )); app.update(); - let frame_count = app.world.resource::(); + let frame_count = app.world().resource::(); assert_eq!(1, frame_count.0); } } diff --git a/crates/bevy_core_pipeline/src/blit/mod.rs b/crates/bevy_core_pipeline/src/blit/mod.rs index b6698838f4a54..838f3afab4ad4 100644 --- a/crates/bevy_core_pipeline/src/blit/mod.rs +++ b/crates/bevy_core_pipeline/src/blit/mod.rs @@ -16,10 +16,9 @@ impl Plugin for BlitPlugin { } fn finish(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; - render_app .init_resource::() .init_resource::>(); diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index 4c0d8ebf8f9cd..25e5898269e16 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -55,11 +55,9 @@ impl Plugin for BloomPlugin { UniformComponentPlugin::::default(), )); - let render_app = match app.get_sub_app_mut(RenderApp) { - Ok(render_app) => render_app, - Err(_) => return, + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; }; - render_app .init_resource::>() .init_resource::>() @@ -101,11 +99,9 @@ impl Plugin for BloomPlugin { } fn finish(&self, app: &mut App) { - let render_app = match app.get_sub_app_mut(RenderApp) { - Ok(render_app) => render_app, - Err(_) => return, + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; }; - render_app .init_resource::() .init_resource::(); diff --git a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs index 89879938e8a8b..b832f0a69270c 100644 --- a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs +++ b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs @@ -113,9 +113,8 @@ impl Plugin for CASPlugin { UniformComponentPlugin::::default(), )); - let render_app = match app.get_sub_app_mut(RenderApp) { - Ok(render_app) => render_app, - Err(_) => return, + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; }; render_app .init_resource::>() @@ -152,9 +151,8 @@ impl Plugin for CASPlugin { } fn finish(&self, app: &mut App) { - let render_app = match app.get_sub_app_mut(RenderApp) { - Ok(render_app) => render_app, - Err(_) => return, + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; }; render_app.init_resource::(); } diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 530d48cde38a5..dae3c52432013 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -48,11 +48,9 @@ impl Plugin for Core2dPlugin { app.register_type::() .add_plugins(ExtractComponentPlugin::::default()); - let render_app = match app.get_sub_app_mut(RenderApp) { - Ok(render_app) => render_app, - Err(_) => return, + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; }; - render_app .init_resource::>() .add_systems(ExtractSchedule, extract_core_2d_camera_phases) diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index f003ed6191f2b..18de12992c523 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -86,11 +86,9 @@ impl Plugin for Core3dPlugin { .add_plugins((SkyboxPlugin, ExtractComponentPlugin::::default())) .add_systems(PostUpdate, check_msaa); - let render_app = match app.get_sub_app_mut(RenderApp) { - Ok(render_app) => render_app, - Err(_) => return, + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; }; - render_app .init_resource::>() .init_resource::>() diff --git a/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs b/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs index c60306286900a..a273faceb253a 100644 --- a/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs +++ b/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs @@ -36,7 +36,7 @@ impl Plugin for CopyDeferredLightingIdPlugin { "copy_deferred_lighting_id.wgsl", Shader::from_wgsl ); - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; render_app.add_systems( @@ -46,7 +46,7 @@ impl Plugin for CopyDeferredLightingIdPlugin { } fn finish(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; diff --git a/crates/bevy_core_pipeline/src/fxaa/mod.rs b/crates/bevy_core_pipeline/src/fxaa/mod.rs index df37eaa1a8e76..e07c43170b038 100644 --- a/crates/bevy_core_pipeline/src/fxaa/mod.rs +++ b/crates/bevy_core_pipeline/src/fxaa/mod.rs @@ -86,9 +86,8 @@ impl Plugin for FxaaPlugin { app.register_type::(); app.add_plugins(ExtractComponentPlugin::::default()); - let render_app = match app.get_sub_app_mut(RenderApp) { - Ok(render_app) => render_app, - Err(_) => return, + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; }; render_app .init_resource::>() @@ -114,9 +113,8 @@ impl Plugin for FxaaPlugin { } fn finish(&self, app: &mut App) { - let render_app = match app.get_sub_app_mut(RenderApp) { - Ok(render_app) => render_app, - Err(_) => return, + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; }; render_app.init_resource::(); } diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs index d80bc0fce7bc9..a196ecc4fcd94 100644 --- a/crates/bevy_core_pipeline/src/msaa_writeback.rs +++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs @@ -21,10 +21,9 @@ pub struct MsaaWritebackPlugin; impl Plugin for MsaaWritebackPlugin { fn build(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; - render_app.add_systems( Render, prepare_msaa_writeback_pipelines.in_set(RenderSet::Prepare), diff --git a/crates/bevy_core_pipeline/src/skybox/mod.rs b/crates/bevy_core_pipeline/src/skybox/mod.rs index 11caa03afd8aa..acdd8443e2661 100644 --- a/crates/bevy_core_pipeline/src/skybox/mod.rs +++ b/crates/bevy_core_pipeline/src/skybox/mod.rs @@ -36,11 +36,9 @@ impl Plugin for SkyboxPlugin { app.add_plugins(ExtractComponentPlugin::::default()); - let render_app = match app.get_sub_app_mut(RenderApp) { - Ok(render_app) => render_app, - Err(_) => return, + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; }; - render_app .init_resource::>() .add_systems( @@ -53,13 +51,10 @@ impl Plugin for SkyboxPlugin { } fn finish(&self, app: &mut App) { - let render_app = match app.get_sub_app_mut(RenderApp) { - Ok(render_app) => render_app, - Err(_) => return, + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; }; - - let render_device = render_app.world.resource::().clone(); - + let render_device = render_app.world().resource::().clone(); render_app.insert_resource(SkyboxPipeline::new(&render_device)); } } diff --git a/crates/bevy_core_pipeline/src/taa/mod.rs b/crates/bevy_core_pipeline/src/taa/mod.rs index de4069c9abe7d..f0165df7be64f 100644 --- a/crates/bevy_core_pipeline/src/taa/mod.rs +++ b/crates/bevy_core_pipeline/src/taa/mod.rs @@ -56,10 +56,9 @@ impl Plugin for TemporalAntiAliasPlugin { app.insert_resource(Msaa::Off) .register_type::(); - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; - render_app .init_resource::>() .add_systems(ExtractSchedule, extract_taa_settings) @@ -87,7 +86,7 @@ impl Plugin for TemporalAntiAliasPlugin { } fn finish(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index bb4dec7f3bc49..403bea98f649e 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -47,8 +47,8 @@ impl Plugin for TonemappingPlugin { Shader::from_wgsl ); - if !app.world.is_resource_added::() { - let mut images = app.world.resource_mut::>(); + if !app.world().is_resource_added::() { + let mut images = app.world_mut().resource_mut::>(); #[cfg(feature = "tonemapping_luts")] let tonemapping_luts = { @@ -91,20 +91,22 @@ impl Plugin for TonemappingPlugin { ExtractComponentPlugin::::default(), )); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { - render_app - .init_resource::>() - .add_systems( - Render, - prepare_view_tonemapping_pipelines.in_set(RenderSet::Prepare), - ); - } + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app + .init_resource::>() + .add_systems( + Render, + prepare_view_tonemapping_pipelines.in_set(RenderSet::Prepare), + ); } fn finish(&self, app: &mut App) { - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.init_resource::(); - } + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app.init_resource::(); } } diff --git a/crates/bevy_core_pipeline/src/upscaling/mod.rs b/crates/bevy_core_pipeline/src/upscaling/mod.rs index 76475c8692bf5..671287d14cce0 100644 --- a/crates/bevy_core_pipeline/src/upscaling/mod.rs +++ b/crates/bevy_core_pipeline/src/upscaling/mod.rs @@ -13,7 +13,7 @@ pub struct UpscalingPlugin; impl Plugin for UpscalingPlugin { fn build(&self, app: &mut App) { - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.add_systems( Render, prepare_view_upscaling_pipelines.in_set(RenderSet::Prepare), diff --git a/crates/bevy_diagnostic/src/diagnostic.rs b/crates/bevy_diagnostic/src/diagnostic.rs index 70b8362b152f5..e6d5705951000 100644 --- a/crates/bevy_diagnostic/src/diagnostic.rs +++ b/crates/bevy_diagnostic/src/diagnostic.rs @@ -1,4 +1,4 @@ -use bevy_app::App; +use bevy_app::{App, SubApp}; use bevy_ecs::system::{Deferred, Res, Resource, SystemBuffer, SystemParam}; use bevy_log::warn; use bevy_utils::{Duration, Instant, StableHashMap, Uuid}; @@ -288,10 +288,6 @@ impl SystemBuffer for DiagnosticsBuffer { /// Extend [`App`] with new `register_diagnostic` function. pub trait RegisterDiagnostic { - fn register_diagnostic(&mut self, diagnostic: Diagnostic) -> &mut Self; -} - -impl RegisterDiagnostic for App { /// Register a new [`Diagnostic`] with an [`App`]. /// /// Will initialize a [`DiagnosticsStore`] if it doesn't exist. @@ -307,11 +303,22 @@ impl RegisterDiagnostic for App { /// .add_plugins(DiagnosticsPlugin) /// .run(); /// ``` + fn register_diagnostic(&mut self, diagnostic: Diagnostic) -> &mut Self; +} + +impl RegisterDiagnostic for SubApp { fn register_diagnostic(&mut self, diagnostic: Diagnostic) -> &mut Self { self.init_resource::(); - let mut diagnostics = self.world.resource_mut::(); + let mut diagnostics = self.world_mut().resource_mut::(); diagnostics.add(diagnostic); self } } + +impl RegisterDiagnostic for App { + fn register_diagnostic(&mut self, diagnostic: Diagnostic) -> &mut Self { + SubApp::register_diagnostic(self.main_mut(), diagnostic); + self + } +} diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index db10910e0c508..993cc1ec8ac3f 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -39,6 +39,24 @@ pub fn derive_resource(input: TokenStream) -> TokenStream { }) } +pub fn derive_thread_local_resource(input: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(input as DeriveInput); + let bevy_ecs_path: Path = crate::bevy_ecs_path(); + + ast.generics + .make_where_clause() + .predicates + .push(parse_quote! { Self: 'static }); + + let struct_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #bevy_ecs_path::storage::ThreadLocalResource for #struct_name #type_generics #where_clause { + } + }) +} + pub fn derive_component(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); let bevy_ecs_path: Path = crate::bevy_ecs_path(); diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 7ee660f9987d7..d2428a2ac4fc5 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -198,10 +198,6 @@ pub fn impl_param_set(_input: TokenStream) -> TokenStream { #param::init_state(world, &mut #meta); let #param = #param::init_state(world, &mut system_meta.clone()); )* - // Make the ParamSet non-send if any of its parameters are non-send. - if false #(|| !#meta.is_send())* { - system_meta.set_non_send(); - } #( system_meta .component_access_set @@ -469,21 +465,31 @@ pub(crate) fn bevy_ecs_path() -> syn::Path { } #[proc_macro_derive(Event)] +/// Derive macro generating an impl of the trait `Event`. pub fn derive_event(input: TokenStream) -> TokenStream { component::derive_event(input) } #[proc_macro_derive(Resource)] +/// Derive macro generating an impl of the trait `Resource`. pub fn derive_resource(input: TokenStream) -> TokenStream { component::derive_resource(input) } +#[proc_macro_derive(ThreadLocalResource)] +/// Derive macro generating an impl of the trait `ThreadLocalResource`. +pub fn derive_thread_local_resource(input: TokenStream) -> TokenStream { + component::derive_thread_local_resource(input) +} + #[proc_macro_derive(Component, attributes(component))] +/// Derive macro generating an impl of the trait `Component`. pub fn derive_component(input: TokenStream) -> TokenStream { component::derive_component(input) } #[proc_macro_derive(States)] +/// Derive macro generating an impl of the trait `States`. pub fn derive_states(input: TokenStream) -> TokenStream { states::derive_states(input) } diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index 0e6483600bdd6..49557129d462d 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -588,39 +588,6 @@ impl<'a, T: Resource> From> for Mut<'a, T> { } } -/// Unique borrow of a non-[`Send`] resource. -/// -/// Only [`Send`] resources may be accessed with the [`ResMut`] [`SystemParam`](crate::system::SystemParam). In case that the -/// resource does not implement `Send`, this `SystemParam` wrapper can be used. This will instruct -/// the scheduler to instead run the system on the main thread so that it doesn't send the resource -/// over to another thread. -/// -/// # Panics -/// -/// Panics when used as a `SystemParameter` if the resource does not exist. -/// -/// Use `Option>` instead if the resource might not always exist. -pub struct NonSendMut<'a, T: ?Sized + 'static> { - pub(crate) value: &'a mut T, - pub(crate) ticks: TicksMut<'a>, -} - -change_detection_impl!(NonSendMut<'a, T>, T,); -change_detection_mut_impl!(NonSendMut<'a, T>, T,); -impl_methods!(NonSendMut<'a, T>, T,); -impl_debug!(NonSendMut<'a, T>,); - -impl<'a, T: 'static> From> for Mut<'a, T> { - /// Convert this `NonSendMut` into a `Mut`. This allows keeping the change-detection feature of `Mut` - /// while losing the specificity of `NonSendMut`. - fn from(other: NonSendMut<'a, T>) -> Mut<'a, T> { - Mut { - value: other.value, - ticks: other.ticks, - } - } -} - /// Shared borrow of an entity's component with access to change detection. /// Similar to [`Mut`] but is immutable and so doesn't require unique access. pub struct Ref<'a, T: ?Sized> { @@ -920,9 +887,7 @@ mod tests { use crate::{ self as bevy_ecs, - change_detection::{ - Mut, NonSendMut, Ref, ResMut, TicksMut, CHECK_TICK_THRESHOLD, MAX_CHANGE_AGE, - }, + change_detection::{Mut, Ref, ResMut, TicksMut, CHECK_TICK_THRESHOLD, MAX_CHANGE_AGE}, component::{Component, ComponentTicks, Tick}, system::{IntoSystem, Query, System}, world::World, @@ -1079,31 +1044,6 @@ mod tests { assert!(val.is_changed()); } - #[test] - fn mut_from_non_send_mut() { - let mut component_ticks = ComponentTicks { - added: Tick::new(1), - changed: Tick::new(2), - }; - let ticks = TicksMut { - added: &mut component_ticks.added, - changed: &mut component_ticks.changed, - last_run: Tick::new(3), - this_run: Tick::new(4), - }; - let mut res = R {}; - let non_send_mut = NonSendMut { - value: &mut res, - ticks, - }; - - let into_mut: Mut = non_send_mut.into(); - assert_eq!(1, into_mut.ticks.added.get()); - assert_eq!(2, into_mut.ticks.changed.get()); - assert_eq!(3, into_mut.ticks.last_run.get()); - assert_eq!(4, into_mut.ticks.this_run.get()); - } - #[test] fn map_mut() { use super::*; diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index bd6cf102cc651..6e6ce05a99903 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -1,5 +1,6 @@ //! Types for declaring and storing [`Component`]s. +use crate::storage::ThreadLocalResource; use crate::{ self as bevy_ecs, change_detection::MAX_CHANGE_AGE, @@ -634,11 +635,11 @@ impl Components { } } - /// Initializes a [non-send resource](crate::system::NonSend) of type `T` with this instance. + /// Initializes a [`ThreadLocalResource`] of type `T` with this instance. /// If a resource of this type has already been initialized, this will return /// the ID of the pre-existing resource. #[inline] - pub fn init_non_send(&mut self) -> ComponentId { + pub fn init_non_send(&mut self) -> ComponentId { // SAFETY: The [`ComponentDescriptor`] matches the [`TypeId`] unsafe { self.get_or_insert_resource_with(TypeId::of::(), || { @@ -759,6 +760,7 @@ impl<'a> TickCells<'a> { /// # Safety /// All cells contained within must uphold the safety invariants of [`UnsafeCellDeref::read`]. #[inline] + #[allow(dead_code)] pub(crate) unsafe fn read(&self) -> ComponentTicks { ComponentTicks { added: self.added.read(), diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index f971697beb539..6e85028de5d78 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -48,9 +48,10 @@ pub mod prelude { IntoSystemConfigs, IntoSystemSet, IntoSystemSetConfigs, NextState, OnEnter, OnExit, OnTransition, Schedule, Schedules, State, States, SystemSet, }, + storage::{ThreadLocal, ThreadLocalResource, ThreadLocals}, system::{ - Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands, - ParamSet, Query, ReadOnlySystem, Res, ResMut, Resource, System, SystemParamFunction, + Commands, Deferred, In, IntoSystem, Local, ParallelCommands, ParamSet, Query, + ReadOnlySystem, Res, ResMut, Resource, System, SystemParamFunction, }, world::{EntityMut, EntityRef, EntityWorldMut, FromWorld, World}, }; @@ -77,7 +78,6 @@ mod tests { use bevy_tasks::{ComputeTaskPool, TaskPool}; use std::{ any::TypeId, - marker::PhantomData, sync::{ atomic::{AtomicUsize, Ordering}, Arc, Mutex, @@ -91,9 +91,6 @@ mod tests { #[derive(Component, Debug, PartialEq, Eq, Clone, Copy)] struct C; - #[derive(Default)] - struct NonSendA(usize, PhantomData<*mut ()>); - #[derive(Component, Clone, Debug)] struct DropCk(Arc); impl DropCk { @@ -1269,36 +1266,6 @@ mod tests { ); } - #[test] - fn non_send_resource() { - let mut world = World::default(); - world.insert_non_send_resource(123i32); - world.insert_non_send_resource(456i64); - assert_eq!(*world.non_send_resource::(), 123); - assert_eq!(*world.non_send_resource_mut::(), 456); - } - - #[test] - fn non_send_resource_points_to_distinct_data() { - let mut world = World::default(); - world.insert_resource(A(123)); - world.insert_non_send_resource(A(456)); - assert_eq!(*world.resource::(), A(123)); - assert_eq!(*world.non_send_resource::(), A(456)); - } - - #[test] - #[should_panic] - fn non_send_resource_panic() { - let mut world = World::default(); - world.insert_non_send_resource(0i32); - std::thread::spawn(move || { - let _ = world.non_send_resource_mut::(); - }) - .join() - .unwrap(); - } - #[test] fn exact_size_query() { let mut world = World::default(); @@ -1416,32 +1383,6 @@ mod tests { assert_eq!(world.resource::().0, 1); } - #[test] - #[should_panic( - expected = "Attempted to access or drop non-send resource bevy_ecs::tests::NonSendA from thread" - )] - fn non_send_resource_drop_from_different_thread() { - let mut world = World::default(); - world.insert_non_send_resource(NonSendA::default()); - - let thread = std::thread::spawn(move || { - // Dropping the non-send resource on a different thread - // Should result in a panic - drop(world); - }); - - if let Err(err) = thread.join() { - std::panic::resume_unwind(err); - } - } - - #[test] - fn non_send_resource_drop_from_same_thread() { - let mut world = World::default(); - world.insert_non_send_resource(NonSendA::default()); - drop(world); - } - #[test] fn insert_overwrite_drop() { let (dropck1, dropped1) = DropCk::new_pair(); diff --git a/crates/bevy_ecs/src/schedule/config.rs b/crates/bevy_ecs/src/schedule/config.rs index 1c60f5b3da700..798309965441c 100644 --- a/crates/bevy_ecs/src/schedule/config.rs +++ b/crates/bevy_ecs/src/schedule/config.rs @@ -6,17 +6,11 @@ use crate::{ graph_utils::{Ambiguity, Dependency, DependencyKind, GraphInfo}, set::{InternedSystemSet, IntoSystemSet, SystemSet}, }, - system::{BoxedSystem, IntoSystem, System}, + system::{BoxedSystem, IntoSystem}, }; fn new_condition(condition: impl Condition) -> BoxedCondition { let condition_system = IntoSystem::into_system(condition); - assert!( - condition_system.is_send(), - "Condition `{}` accesses `NonSend` resources. This is not currently supported.", - condition_system.name() - ); - Box::new(condition_system) } diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index 36eb76a94abca..1cac2515c17b0 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -1,5 +1,6 @@ use std::{ any::Any, + panic::AssertUnwindSafe, sync::{Arc, Mutex}, }; @@ -8,7 +9,6 @@ use bevy_utils::default; use bevy_utils::syncunsafecell::SyncUnsafeCell; #[cfg(feature = "trace")] use bevy_utils::tracing::{info_span, Instrument, Span}; -use std::panic::AssertUnwindSafe; use async_channel::{Receiver, Sender}; use fixedbitset::FixedBitSet; @@ -58,8 +58,6 @@ struct SystemTaskMetadata { archetype_component_access: Access, /// Indices of the systems that directly depend on the system. dependents: Vec, - /// Is `true` if the system does not access `!Send` data. - is_send: bool, /// Is `true` if the system is exclusive. is_exclusive: bool, /// Cached tracing span for system task @@ -83,8 +81,6 @@ pub struct MultiThreadedExecutor { system_task_metadata: Vec, /// Union of the accesses of all currently running systems. active_access: Access, - /// Returns `true` if a system with non-`Send` access is running. - local_thread_running: bool, /// Returns `true` if an exclusive system is running. exclusive_running: bool, /// The number of systems expected to run. @@ -154,7 +150,6 @@ impl SystemExecutor for MultiThreadedExecutor { self.system_task_metadata.push(SystemTaskMetadata { archetype_component_access: default(), dependents: schedule.system_dependents[index].clone(), - is_send: schedule.systems[index].is_send(), is_exclusive: schedule.systems[index].is_exclusive(), #[cfg(feature = "trace")] system_task_span: info_span!( @@ -278,7 +273,6 @@ impl MultiThreadedExecutor { num_completed_systems: 0, num_dependencies_remaining: Vec::new(), active_access: default(), - local_thread_running: false, exclusive_running: false, evaluated_sets: FixedBitSet::new(), ready_systems: FixedBitSet::new(), @@ -378,10 +372,6 @@ impl MultiThreadedExecutor { return false; } - if !system_meta.is_send && self.local_thread_running { - return false; - } - // TODO: an earlier out if world's archetypes did not change for set_idx in conditions.sets_with_conditions_of_systems[system_index] .difference(&self.evaluated_sets) @@ -532,12 +522,7 @@ impl MultiThreadedExecutor { self.active_access .extend(&system_meta.archetype_component_access); - if system_meta.is_send { - scope.spawn(task); - } else { - self.local_thread_running = true; - scope.spawn_on_external(task); - } + scope.spawn(task); } /// # Safety @@ -580,7 +565,7 @@ impl MultiThreadedExecutor { .system_task_span .clone(), ); - scope.spawn_on_scope(task); + scope.spawn(task); } else { let task = async move { let res = std::panic::catch_unwind(AssertUnwindSafe(|| { @@ -610,11 +595,10 @@ impl MultiThreadedExecutor { .system_task_span .clone(), ); - scope.spawn_on_scope(task); + scope.spawn(task); } self.exclusive_running = true; - self.local_thread_running = true; } fn finish_system_and_handle_dependents(&mut self, result: SystemResult) { @@ -627,10 +611,6 @@ impl MultiThreadedExecutor { self.exclusive_running = false; } - if !self.system_task_metadata[system_index].is_send { - self.local_thread_running = false; - } - debug_assert!(self.num_running_systems >= 1); self.num_running_systems -= 1; self.num_completed_systems += 1; diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 7c7876186613c..c42196362bd3e 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -742,8 +742,6 @@ mod tests { fn empty_system() {} fn res_system(_res: Res) {} fn resmut_system(_res: ResMut) {} - fn nonsend_system(_ns: NonSend) {} - fn nonsendmut_system(_ns: NonSendMut) {} fn read_component_system(_query: Query<&A>) {} fn write_component_system(_query: Query<&mut A>) {} fn with_filtered_component_system(_query: Query<&mut A, With>) {} @@ -786,8 +784,6 @@ mod tests { empty_system, res_system, res_system, - nonsend_system, - nonsend_system, read_component_system, read_component_system, event_reader_system, @@ -834,19 +830,6 @@ mod tests { assert_eq!(schedule.graph().conflicting_systems().len(), 1); } - #[test] - fn nonsend() { - let mut world = World::new(); - world.insert_resource(R); - - let mut schedule = Schedule::default(); - schedule.add_systems((nonsendmut_system, nonsend_system)); - - let _ = schedule.initialize(&mut world); - - assert_eq!(schedule.graph().conflicting_systems().len(), 1); - } - #[test] fn components() { let mut world = World::new(); @@ -939,11 +922,7 @@ mod tests { world.insert_resource(R); let mut schedule = Schedule::default(); - schedule.add_systems(( - resmut_system.ambiguous_with_all(), - res_system, - nonsend_system, - )); + schedule.add_systems((resmut_system.ambiguous_with_all(), res_system)); let _ = schedule.initialize(&mut world); @@ -962,7 +941,6 @@ mod tests { schedule.add_systems(( resmut_system.ambiguous_with(IgnoreMe), res_system.in_set(IgnoreMe), - nonsend_system.in_set(IgnoreMe), )); let _ = schedule.initialize(&mut world); diff --git a/crates/bevy_ecs/src/storage/mod.rs b/crates/bevy_ecs/src/storage/mod.rs index 72b1dced7daf8..b8c3d893cd7e5 100644 --- a/crates/bevy_ecs/src/storage/mod.rs +++ b/crates/bevy_ecs/src/storage/mod.rs @@ -22,14 +22,18 @@ mod blob_vec; mod resource; +mod resource_non_send; mod sparse_set; mod table; pub use resource::*; +pub use resource_non_send::*; pub use sparse_set::*; pub use table::*; -/// The raw data stores of a [`World`](crate::world::World) +pub use bevy_ecs_macros::ThreadLocalResource; + +/// The raw data stores of a [`World`](crate::world::World). #[derive(Default)] pub struct Storages { /// Backing storage for [`SparseSet`] components. @@ -38,6 +42,4 @@ pub struct Storages { pub tables: Tables, /// Backing storage for resources. pub resources: Resources, - /// Backing storage for `!Send` resources. - pub non_send_resources: Resources, } diff --git a/crates/bevy_ecs/src/storage/resource_non_send.rs b/crates/bevy_ecs/src/storage/resource_non_send.rs new file mode 100644 index 0000000000000..80b5649a7fc5a --- /dev/null +++ b/crates/bevy_ecs/src/storage/resource_non_send.rs @@ -0,0 +1,593 @@ +use std::any::TypeId; +use std::cell::RefCell; +use std::marker::PhantomData; +use std::mem; +use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe}; +use std::sync::mpsc::sync_channel; + +use bevy_ptr::{OwningPtr, Ptr}; + +use crate as bevy_ecs; +use crate::archetype::ArchetypeComponentId; +use crate::change_detection::{Mut, MutUntyped, TicksMut}; +use crate::component::{ComponentId, Components, Tick, TickCells}; +use crate::storage::{ResourceData, Resources}; +use crate::system::{Resource, SystemParam}; +use crate::world::{unsafe_world_cell::UnsafeWorldCell, World}; + +thread_local! { + static TLS: RefCell = RefCell::new(ThreadLocals::new()); +} + +/// A type that can be inserted into [`ThreadLocals`]. Unlike [`Resource`], this does not require +/// [`Send`] or [`Sync`]. +pub trait ThreadLocalResource: 'static {} + +/// Storage for registered [`ThreadLocalResource`] values. +pub struct ThreadLocals { + info: Components, + storage: Resources, + curr_tick: Tick, + last_tick: Tick, + // !Send + !Sync + _marker: PhantomData<*const ()>, +} + +impl ThreadLocals { + /// Constructs a new instance of [`ThreadLocals`]. + pub(crate) fn new() -> Self { + Self { + info: Components::default(), + storage: Resources::default(), + curr_tick: Tick::new(0), + last_tick: Tick::new(0), + _marker: PhantomData, + } + } + + /// Initializes a new [`ThreadLocalResource`] type and returns the [`ComponentId`] created for + /// it. + pub fn init_resource_type(&mut self) -> ComponentId { + self.info.init_non_send::() + } + + /// Returns the [`ComponentId`] of the [`ThreadLocalResource`], if it exists. + /// + /// **Note:** The returned `ComponentId` is specific to this `ThreadLocals` instance. You should + /// not use it with another `ThreadLocals` instance. + #[inline] + pub fn resource_id(&self) -> Option { + self.info.get_resource_id(TypeId::of::()) + } + + /// Inserts a new resource with its default value. + /// + /// If the resource already exists, nothing happens. + #[inline] + pub fn init_resource(&mut self) { + if !self.contains_resource::() { + self.insert_resource::(Default::default()); + } + } + + /// Inserts a new resource with the given `value`. + /// + /// Resources are "unique" data of a given type. If you insert a resource of a type that already + /// exists, you will overwrite any existing data. + #[inline] + pub fn insert_resource(&mut self, value: R) { + let id = self.init_resource_type::(); + OwningPtr::make(value, |ptr| { + // SAFETY: id was just initialized and corresponds to resource of type R + unsafe { + self.insert_resource_by_id(id, ptr); + } + }); + } + + /// Removes the resource of a given type and returns it, if it exists. + #[inline] + pub fn remove_resource(&mut self) -> Option { + let id = self.info.get_resource_id(TypeId::of::())?; + let (ptr, _) = self.storage.get_mut(id)?.remove()?; + // SAFETY: `id` came directly from `R`, so this has to be the `R` data + unsafe { Some(ptr.read::()) } + } + + /// Returns `true` if a resource of type `R` exists. + #[inline] + pub fn contains_resource(&self) -> bool { + self.info + .get_resource_id(TypeId::of::()) + .and_then(|id| self.storage.get(id)) + .map(|info| info.is_present()) + .unwrap_or(false) + } + + /// Return's `true` if a resource of type `R` has been added since the storage was last changed. + pub fn is_resource_added(&self) -> bool { + self.info + .get_resource_id(TypeId::of::()) + .and_then(|id| self.storage.get(id)?.get_ticks()) + .map(|ticks| ticks.is_added(self.last_tick, self.curr_tick)) + .unwrap_or(false) + } + + /// Return's `true` if a resource of type `R` has been added or mutably accessed since the + /// storage was last changed. + pub fn is_resource_changed(&self) -> bool { + self.info + .get_resource_id(TypeId::of::()) + .and_then(|id| self.storage.get(id)?.get_ticks()) + .map(|ticks| ticks.is_changed(self.last_tick, self.curr_tick)) + .unwrap_or(false) + } + + /// Returns a reference to the resource. + /// + /// # Panics + /// + /// Panics if the resource does not exist. Use [`get_resource`](ThreadLocals::get_resource) + /// instead if you want to handle this case. + /// + /// If you want to instead insert a value if the resource does not exist, use + /// [`get_resource_or_insert_with`](ThreadLocals::get_resource_or_insert_with). + #[inline] + #[track_caller] + pub fn resource(&self) -> &R { + match self.get_resource() { + Some(x) => x, + None => panic!( + "Requested resource {} does not exist. Did you insert it?", + std::any::type_name::() + ), + } + } + + /// Returns a mutable reference to the resource. + /// + /// # Panics + /// + /// Panics if the resource does not exist. Use [`get_resource_mut`](ThreadLocals::get_resource_mut) + /// instead if you want to handle this case. + /// + /// If you want to instead insert a value if the resource does not exist, use + /// [`get_resource_or_insert_with`](ThreadLocals::get_resource_or_insert_with). + #[inline] + #[track_caller] + pub fn resource_mut(&mut self) -> Mut<'_, R> { + match self.get_resource_mut() { + Some(x) => x, + None => panic!( + "Requested resource {} does not exist. Did you insert it?", + std::any::type_name::() + ), + } + } + + /// Returns a reference to the resource, if it exists. + #[inline] + pub fn get_resource(&self) -> Option<&R> { + let id = self.info.get_resource_id(TypeId::of::())?; + // SAFETY: `id` was derived from `R` directly + unsafe { self.get_resource_by_id(id).map(|ptr| ptr.deref()) } + } + + /// Returns a mutable reference to the resource, if it exists. + #[inline] + pub fn get_resource_mut(&mut self) -> Option> { + // SAFETY: exclusive access is enforced + unsafe { self.get_resource_unchecked_mut() } + } + + /// Returns a mutable reference to the resource. If the resource does not exist, calls `f` and + /// inserts its result first. + #[inline] + pub fn get_resource_or_insert_with( + &mut self, + f: impl FnOnce() -> R, + ) -> Mut<'_, R> { + if !self.contains_resource::() { + self.insert_resource(f()); + } + self.resource_mut() + } + + /// Returns mutable reference to the resource of the given type, if it exists. + /// + /// # Safety + /// + /// The caller must ensure that this reference is unique. + #[inline] + pub unsafe fn get_resource_unchecked_mut(&self) -> Option> { + let id = self.info.get_resource_id(TypeId::of::())?; + // SAFETY: `id` was derived from `R` directly + unsafe { + self.get_resource_mut_by_id(id) + .map(|ptr| ptr.with_type::()) + } + } + + /// # Safety + /// + /// The caller must ensure that `id` is assigned to type `R`. + #[inline] + pub(crate) unsafe fn get_resource_by_id(&self, id: ComponentId) -> Option { + self.storage.get(id)?.get_data() + } + + /// # Safety + /// + /// The caller must ensure that `id` is assigned to type `R` and that this reference is unique. + #[inline] + pub(crate) unsafe fn get_resource_mut_by_id(&self, id: ComponentId) -> Option> { + // SAFETY: caller ensures unaliased access + let (ptr, ticks) = unsafe { self.get_resource_with_ticks(id)? }; + + // SAFETY: caller ensures unaliased access + let ticks = unsafe { TicksMut::from_tick_cells(ticks, self.last_tick, self.curr_tick) }; + + Some(MutUntyped { + // SAFETY: caller ensures unaliased access + value: unsafe { ptr.assert_unique() }, + ticks, + }) + } + + /// Returns untyped references to the data and change ticks of a resource. + /// + /// # Safety + /// + /// The caller must ensure that mutable references have no aliases. + #[inline] + pub(crate) unsafe fn get_resource_with_ticks( + &self, + id: ComponentId, + ) -> Option<(Ptr<'_>, TickCells)> { + self.storage.get(id)?.get_with_ticks() + } + + /// Inserts a new resource with the given `value`. Will replace the value if it already existed. + /// + /// **Prefer the typed API [`ThreadLocals::insert_resource`] when possible. Only use this if + /// the actual types are not known at compile time.** + /// + /// # Safety + /// - `id` must already exist in [`ThreadLocals`] + /// - `value` must be a valid value of the type represented by `id` + #[inline] + pub(crate) unsafe fn insert_resource_by_id(&mut self, id: ComponentId, value: OwningPtr<'_>) { + let curr_tick = self.curr_tick; + // SAFETY: caller ensures the value is a valid value of the type given by `id` + unsafe { + self.initialize_resource_internal(id) + .insert(value, curr_tick); + } + } + + /// # Safety + /// `id` must be valid for this world + #[inline] + unsafe fn initialize_resource_internal(&mut self, id: ComponentId) -> &mut ResourceData { + self.storage + .initialize_with(id, &self.info, || ArchetypeComponentId::new(id.index())) + } + + /// Temporarily removes `R` from the [`ThreadLocals`], then re-inserts it before returning. + pub fn resource_scope( + &mut self, + f: impl FnOnce(&mut ThreadLocals, Mut) -> T, + ) -> T { + let id = self + .info + .get_resource_id(TypeId::of::()) + .unwrap_or_else(|| panic!("resource does not exist: {}", std::any::type_name::())); + + let (ptr, mut ticks) = self + .storage + .get_mut(id) + .and_then(|info| info.remove()) + .unwrap_or_else(|| panic!("resource does not exist: {}", std::any::type_name::())); + + // Read the value onto the stack to avoid potential &mut aliasing. + // SAFETY: pointer is of type R + let mut value = unsafe { ptr.read::() }; + let value_mut = Mut { + value: &mut value, + ticks: TicksMut { + added: &mut ticks.added, + changed: &mut ticks.changed, + last_run: self.last_tick, + this_run: self.curr_tick, + }, + }; + + let result = f(self, value_mut); + assert!( + !self.contains_resource::(), + "Resource `{}` was inserted during a call to `resource_scope`.\n\ + This is not allowed as the original resource is re-inserted after `f` is invoked.", + std::any::type_name::() + ); + + OwningPtr::make(value, |ptr| { + // SAFETY: pointer is of type R + unsafe { + self.storage + .get_mut(id) + .map(|info| info.insert_with_ticks(ptr, ticks)) + .unwrap(); + } + }); + + result + } + + pub(crate) fn update_change_tick(&mut self) { + let tick = self.curr_tick.get(); + self.curr_tick.set(tick.wrapping_add(1)); + } +} + +/// Type alias for tasks that access thread-local data. +pub type ThreadLocalTask = Box; + +/// An error returned from the [`ThreadLocalTaskSender::send_task`] function. +/// +/// A send operation can only fail if the receiving end of a channel is disconnected, +/// implying that the data could never be received. The error contains the data that +/// was sent as a payload so it can be recovered. +#[derive(Debug)] +pub struct ThreadLocalTaskSendError(pub T); + +/// Sends [`ThreadLocalTask`]. +// TODO: can remove this trait if `bevy_app` changes from its own crate to a `bevy_ecs` feature +pub trait ThreadLocalTaskSender: Send + 'static { + /// Attempts to send a task over this channel, returning it back if it could not be sent. + fn send_task( + &mut self, + task: ThreadLocalTask, + ) -> Result<(), ThreadLocalTaskSendError>; +} + +/// A [`Resource`] that enables the use of the [`ThreadLocal`] system parameter. +#[derive(Resource)] +struct ThreadLocalChannel { + thread: std::thread::ThreadId, + sender: Box, +} + +// SAFETY: All operations require an exclusive reference, so there can be no races. +unsafe impl Sync for ThreadLocalChannel {} + +/// A guard to access [`ThreadLocals`]. +pub struct ThreadLocalStorage { + thread: std::thread::ThreadId, + // !Send + !Sync + _marker: PhantomData<*const ()>, +} + +impl Default for ThreadLocalStorage { + fn default() -> Self { + Self { + thread: std::thread::current().id(), + _marker: PhantomData, + } + } +} + +impl ThreadLocalStorage { + /// Constructs a new instance of [`ThreadLocalStorage`]. + pub fn new() -> Self { + Self::default() + } + + /// Removes all resources. + pub fn clear(&mut self) { + // Drop resources normally to avoid the caveats described in + // https://doc.rust-lang.org/std/thread/struct.LocalKey.html + TLS.replace(ThreadLocals::new()); + } + + /// Inserts a new resource with its default value. + /// + /// If the resource already exists, nothing happens. + #[inline] + pub fn init_resource(&mut self) { + TLS.with_borrow_mut(|tls| { + tls.init_resource::(); + }); + } + + /// Inserts a new resource with the given `value`. + /// + /// Resources are "unique" data of a given type. If you insert a resource of a type that already + /// exists, you will overwrite any existing data. + #[inline] + pub fn insert_resource(&mut self, value: R) { + TLS.with_borrow_mut(|tls| { + tls.insert_resource(value); + }); + } + + /// Removes the resource of a given type and returns it, if it exists. + #[inline] + pub fn remove_resource(&mut self) -> Option { + TLS.with_borrow_mut(|tls| tls.remove_resource::()) + } + + /// Temporarily removes `R` from the [`ThreadLocals`], then re-inserts it before returning. + pub fn resource_scope( + &mut self, + f: impl FnOnce(&mut ThreadLocals, Mut) -> T, + ) -> T { + TLS.with_borrow_mut(|tls| tls.resource_scope(f)) + } + + /// Runs `f` in a scope that has access to the thread-local resources. + pub fn run(&mut self, f: F) -> T + where + F: FnOnce(&mut ThreadLocals) -> T, + T: 'static, + { + TLS.with_borrow_mut(|tls| f(tls)) + } + + /// Inserts a channel into `world` that systems in `world` (via [`ThreadLocal`]) can use to + /// access the underlying [`ThreadLocals`]. + pub fn insert_channel(&self, world: &mut World, sender: S) + where + S: ThreadLocalTaskSender, + { + let channel = ThreadLocalChannel { + thread: self.thread, + sender: Box::new(sender), + }; + + world.insert_resource(channel); + } + + /// Removes the channel previously added by [`insert_channel`](ThreadLocalStorage::insert_channel) + /// from `world`, if it exists. + pub fn remove_channel(&self, world: &mut World) { + world.remove_resource::(); + } +} + +enum ThreadLocalAccess<'a> { + Direct, + Indirect(&'a mut dyn ThreadLocalTaskSender), +} + +#[doc(hidden)] +pub struct ThreadLocalState { + component_id: ComponentId, + last_run: Tick, +} + +/// A [`SystemParam`] that grants scoped access to the thread-local data of the main thread. +pub struct ThreadLocal<'w, 's> { + access: ThreadLocalAccess<'w>, + last_run: &'s mut Tick, +} + +impl ThreadLocal<'_, '_> { + /// Runs `f` in a scope that has access to the thread-local resources. + pub fn run(&mut self, f: F) -> T + where + F: FnOnce(&mut ThreadLocals) -> T + Send, + T: Send + 'static, + { + match self.access { + ThreadLocalAccess::Direct => self.run_direct(f), + ThreadLocalAccess::Indirect(_) => self.run_indirect(f), + } + } + + fn run_direct(&mut self, f: F) -> T + where + F: FnOnce(&mut ThreadLocals) -> T + Send, + T: Send + 'static, + { + debug_assert!(matches!(self.access, ThreadLocalAccess::Direct)); + + TLS.with_borrow_mut(|tls| { + tls.update_change_tick(); + let saved = mem::replace(&mut tls.last_tick, *self.last_run); + let result = f(tls); + tls.last_tick = saved; + *self.last_run = tls.curr_tick; + result + }) + } + + fn run_indirect(&mut self, f: F) -> T + where + F: FnOnce(&mut ThreadLocals) -> T + Send, + T: Send + 'static, + { + let ThreadLocalAccess::Indirect(ref mut sender) = self.access else { + unreachable!() + }; + + let system_tick = *self.last_run; + let (result_tx, result_rx) = sync_channel(1); + let task = move || { + TLS.with_borrow_mut(|tls| { + tls.update_change_tick(); + let saved = mem::replace(&mut tls.last_tick, system_tick); + // we want to propagate to caller instead of panicking in the main thread + let result = catch_unwind(AssertUnwindSafe(|| (f(tls), tls.curr_tick))); + tls.last_tick = saved; + result_tx.send(result).unwrap(); + }); + }; + + let task: Box = Box::new(task); + // SAFETY: This function will block the calling thread until `f` completes, + // so any captured references in `f` will remain valid until then. + let task: Box = unsafe { mem::transmute(task) }; + + // Send task to the main thread. + sender + .send_task(task) + .unwrap_or_else(|_| panic!("receiver missing")); + + // Wait to receive result back from the main thread. + match result_rx.recv().unwrap() { + Ok((result, tls_tick)) => { + *self.last_run = tls_tick; + result + } + Err(payload) => { + resume_unwind(payload); + } + } + } +} + +// SAFETY: This system param does not borrow any data from the world. +unsafe impl SystemParam for ThreadLocal<'_, '_> { + type State = ThreadLocalState; + type Item<'w, 's> = ThreadLocal<'w, 's>; + + fn init_state( + world: &mut crate::prelude::World, + system_meta: &mut crate::system::SystemMeta, + ) -> Self::State { + // `ThreadLocalTaskSender` can't require `Sync` because `winit`'s `EventLoopProxy` is not + // `Sync`, so we need exclusive access to `ThreadLocalChannel` here. + let component_id = + crate::system::ResMut::::init_state(world, system_meta); + + ThreadLocalState { + component_id, + last_run: Tick::new(0), + } + } + + unsafe fn get_param<'world, 'state>( + state: &'state mut Self::State, + system_meta: &crate::system::SystemMeta, + world: UnsafeWorldCell<'world>, + curr_tick: Tick, + ) -> Self::Item<'world, 'state> { + let accessor = crate::system::ResMut::::get_param( + &mut state.component_id, + system_meta, + world, + curr_tick, + ); + + let access = if std::thread::current().id() == accessor.thread { + ThreadLocalAccess::Direct + } else { + ThreadLocalAccess::Indirect(&mut *accessor.into_inner().sender) + }; + + ThreadLocal { + access, + last_run: &mut state.last_run, + } + } +} diff --git a/crates/bevy_ecs/src/system/adapter_system.rs b/crates/bevy_ecs/src/system/adapter_system.rs index 228b5ddca2700..ebea1f770b92b 100644 --- a/crates/bevy_ecs/src/system/adapter_system.rs +++ b/crates/bevy_ecs/src/system/adapter_system.rs @@ -96,10 +96,6 @@ where self.system.archetype_component_access() } - fn is_send(&self) -> bool { - self.system.is_send() - } - fn is_exclusive(&self) -> bool { self.system.is_exclusive() } diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index f70712711cc95..bb9a3d8d3b4de 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -154,10 +154,6 @@ where &self.archetype_component_access } - fn is_send(&self) -> bool { - self.a.is_send() && self.b.is_send() - } - fn is_exclusive(&self) -> bool { self.a.is_exclusive() || self.b.is_exclusive() } diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 1a06f1eb03d71..dddfb89c05e3a 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -80,14 +80,6 @@ where &self.system_meta.archetype_component_access } - #[inline] - fn is_send(&self) -> bool { - // exclusive systems should have access to non-send resources - // the executor runs exclusive systems on the main thread, so this - // field reflects that constraint - false - } - #[inline] unsafe fn run_unsafe(&mut self, _input: Self::In, _world: UnsafeWorldCell) -> Self::Out { panic!("Cannot run exclusive systems with a shared World reference"); diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 5b6b68d7859e2..942446c87fb4a 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -22,9 +22,6 @@ pub struct SystemMeta { pub(crate) name: Cow<'static, str>, pub(crate) component_access_set: FilteredAccessSet, pub(crate) archetype_component_access: Access, - // NOTE: this must be kept private. making a SystemMeta non-send is irreversible to prevent - // SystemParams from overriding each other - is_send: bool, pub(crate) last_run: Tick, #[cfg(feature = "trace")] pub(crate) system_span: Span, @@ -39,7 +36,6 @@ impl SystemMeta { name: name.into(), archetype_component_access: Access::default(), component_access_set: FilteredAccessSet::default(), - is_send: true, last_run: Tick::new(0), #[cfg(feature = "trace")] system_span: info_span!("system", name = name), @@ -53,20 +49,6 @@ impl SystemMeta { pub fn name(&self) -> &str { &self.name } - - /// Returns true if the system is [`Send`]. - #[inline] - pub fn is_send(&self) -> bool { - self.is_send - } - - /// Sets the system to be not [`Send`]. - /// - /// This is irreversible. - #[inline] - pub fn set_non_send(&mut self) { - self.is_send = false; - } } // TODO: Actually use this in FunctionSystem. We should probably only do this once Systems are constructed using a World reference @@ -454,11 +436,6 @@ where &self.system_meta.archetype_component_access } - #[inline] - fn is_send(&self) -> bool { - self.system_meta.is_send - } - #[inline] fn is_exclusive(&self) -> bool { false diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 8750e44d26334..09a40ea5421e0 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -89,8 +89,6 @@ //! - [`Local`] //! - [`EventReader`](crate::event::EventReader) //! - [`EventWriter`](crate::event::EventWriter) -//! - [`NonSend`] and `Option` -//! - [`NonSendMut`] and `Option` //! - [`&World`](crate::world::World) //! - [`RemovedComponents`](crate::removal_detection::RemovedComponents) //! - [`SystemName`] @@ -550,8 +548,8 @@ mod tests { Schedule, }, system::{ - Commands, In, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, Res, ResMut, - Resource, System, SystemState, + Commands, In, IntoSystem, Local, ParamSet, Query, Res, ResMut, Resource, System, + SystemState, }, world::{FromWorld, World}, }; @@ -1096,52 +1094,6 @@ mod tests { assert_eq!(*world.resource::(), SystemRan::Yes); } - #[test] - fn non_send_option_system() { - let mut world = World::default(); - - world.insert_resource(SystemRan::No); - struct NotSend1(std::rc::Rc); - struct NotSend2(std::rc::Rc); - world.insert_non_send_resource(NotSend1(std::rc::Rc::new(0))); - - fn sys( - op: Option>, - mut _op2: Option>, - mut system_ran: ResMut, - ) { - op.expect("NonSend should exist"); - *system_ran = SystemRan::Yes; - } - - run_system(&mut world, sys); - // ensure the system actually ran - assert_eq!(*world.resource::(), SystemRan::Yes); - } - - #[test] - fn non_send_system() { - let mut world = World::default(); - - world.insert_resource(SystemRan::No); - struct NotSend1(std::rc::Rc); - struct NotSend2(std::rc::Rc); - - world.insert_non_send_resource(NotSend1(std::rc::Rc::new(1))); - world.insert_non_send_resource(NotSend2(std::rc::Rc::new(2))); - - fn sys( - _op: NonSend, - mut _op2: NonSendMut, - mut system_ran: ResMut, - ) { - *system_ran = SystemRan::Yes; - } - - run_system(&mut world, sys); - assert_eq!(*world.resource::(), SystemRan::Yes); - } - #[test] fn removal_tracking() { let mut world = World::new(); diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index fd03b94fa2b55..af7ba0c7d6410 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -36,12 +36,8 @@ pub trait System: Send + Sync + 'static { fn component_access(&self) -> &Access; /// Returns the system's archetype component [`Access`]. fn archetype_component_access(&self) -> &Access; - /// Returns true if the system is [`Send`]. - fn is_send(&self) -> bool; - /// Returns true if the system must be run exclusively. fn is_exclusive(&self) -> bool; - /// Runs the system with the given input in the world. Unlike [`System::run`], this function /// can be called in parallel with other systems and may break Rust's aliasing rules /// if used incorrectly, making it unsafe to call. @@ -155,7 +151,6 @@ impl Debug for dyn System { f.debug_struct("System") .field("name", &self.name()) .field("is_exclusive", &self.is_exclusive()) - .field("is_send", &self.is_send()) .finish_non_exhaustive() } } @@ -337,17 +332,4 @@ mod tests { world.run_system_once(spawn_entity); assert_eq!(world.entities.len(), 1); } - - #[test] - fn non_send_resources() { - fn non_send_count_down(mut ns: NonSendMut) { - ns.0 -= 1; - } - - let mut world = World::new(); - world.insert_non_send_resource(Counter(10)); - assert_eq!(*world.non_send_resource::(), Counter(10)); - world.run_system_once(non_send_count_down); - assert_eq!(*world.non_send_resource::(), Counter(9)); - } } diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 2b5d73d1ccfe0..7a14b33b25c96 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -1,9 +1,9 @@ -pub use crate::change_detection::{NonSendMut, Res, ResMut}; +pub use crate::change_detection::{Res, ResMut}; use crate::{ archetype::{Archetype, Archetypes}, bundle::Bundles, change_detection::{Ticks, TicksMut}, - component::{ComponentId, ComponentTicks, Components, Tick}, + component::{ComponentId, Components, Tick}, entity::Entities, query::{ Access, FilteredAccess, FilteredAccessSet, QueryState, ReadOnlyWorldQuery, WorldQuery, @@ -919,239 +919,6 @@ unsafe impl SystemParam for Deferred<'_, T> { } } -/// Shared borrow of a non-[`Send`] resource. -/// -/// Only `Send` resources may be accessed with the [`Res`] [`SystemParam`]. In case that the -/// resource does not implement `Send`, this `SystemParam` wrapper can be used. This will instruct -/// the scheduler to instead run the system on the main thread so that it doesn't send the resource -/// over to another thread. -/// -/// # Panics -/// -/// Panics when used as a `SystemParameter` if the resource does not exist. -/// -/// Use `Option>` instead if the resource might not always exist. -pub struct NonSend<'w, T: 'static> { - pub(crate) value: &'w T, - ticks: ComponentTicks, - last_run: Tick, - this_run: Tick, -} - -// SAFETY: Only reads a single World non-send resource -unsafe impl<'w, T> ReadOnlySystemParam for NonSend<'w, T> {} - -impl<'w, T> Debug for NonSend<'w, T> -where - T: Debug, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("NonSend").field(&self.value).finish() - } -} - -impl<'w, T: 'static> NonSend<'w, T> { - /// Returns `true` if the resource was added after the system last ran. - pub fn is_added(&self) -> bool { - self.ticks.is_added(self.last_run, self.this_run) - } - - /// Returns `true` if the resource was added or mutably dereferenced after the system last ran. - pub fn is_changed(&self) -> bool { - self.ticks.is_changed(self.last_run, self.this_run) - } -} - -impl<'w, T> Deref for NonSend<'w, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - self.value - } -} -impl<'a, T> From> for NonSend<'a, T> { - fn from(nsm: NonSendMut<'a, T>) -> Self { - Self { - value: nsm.value, - ticks: ComponentTicks { - added: nsm.ticks.added.to_owned(), - changed: nsm.ticks.changed.to_owned(), - }, - this_run: nsm.ticks.this_run, - last_run: nsm.ticks.last_run, - } - } -} - -// SAFETY: NonSendComponentId and ArchetypeComponentId access is applied to SystemMeta. If this -// NonSend conflicts with any prior access, a panic will occur. -unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { - type State = ComponentId; - type Item<'w, 's> = NonSend<'w, T>; - - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - system_meta.set_non_send(); - - let component_id = world.initialize_non_send_resource::(); - let combined_access = system_meta.component_access_set.combined_access(); - assert!( - !combined_access.has_write(component_id), - "error[B0002]: NonSend<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access.", - std::any::type_name::(), - system_meta.name, - ); - system_meta - .component_access_set - .add_unfiltered_read(component_id); - - let archetype_component_id = world - .get_non_send_archetype_component_id(component_id) - .unwrap(); - system_meta - .archetype_component_access - .add_read(archetype_component_id); - - component_id - } - - #[inline] - unsafe fn get_param<'w, 's>( - &mut component_id: &'s mut Self::State, - system_meta: &SystemMeta, - world: UnsafeWorldCell<'w>, - change_tick: Tick, - ) -> Self::Item<'w, 's> { - let (ptr, ticks) = world - .get_non_send_with_ticks(component_id) - .unwrap_or_else(|| { - panic!( - "Non-send resource requested by {} does not exist: {}", - system_meta.name, - std::any::type_name::() - ) - }); - - NonSend { - value: ptr.deref(), - ticks: ticks.read(), - last_run: system_meta.last_run, - this_run: change_tick, - } - } -} - -// SAFETY: Only reads a single World non-send resource -unsafe impl ReadOnlySystemParam for Option> {} - -// SAFETY: this impl defers to `NonSend`, which initializes and validates the correct world access. -unsafe impl SystemParam for Option> { - type State = ComponentId; - type Item<'w, 's> = Option>; - - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - NonSend::::init_state(world, system_meta) - } - - #[inline] - unsafe fn get_param<'w, 's>( - &mut component_id: &'s mut Self::State, - system_meta: &SystemMeta, - world: UnsafeWorldCell<'w>, - change_tick: Tick, - ) -> Self::Item<'w, 's> { - world - .get_non_send_with_ticks(component_id) - .map(|(ptr, ticks)| NonSend { - value: ptr.deref(), - ticks: ticks.read(), - last_run: system_meta.last_run, - this_run: change_tick, - }) - } -} - -// SAFETY: NonSendMut ComponentId and ArchetypeComponentId access is applied to SystemMeta. If this -// NonSendMut conflicts with any prior access, a panic will occur. -unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { - type State = ComponentId; - type Item<'w, 's> = NonSendMut<'w, T>; - - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - system_meta.set_non_send(); - - let component_id = world.initialize_non_send_resource::(); - let combined_access = system_meta.component_access_set.combined_access(); - if combined_access.has_write(component_id) { - panic!( - "error[B0002]: NonSendMut<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access.", - std::any::type_name::(), system_meta.name); - } else if combined_access.has_read(component_id) { - panic!( - "error[B0002]: NonSendMut<{}> in system {} conflicts with a previous immutable resource access ({0}). Consider removing the duplicate access.", - std::any::type_name::(), system_meta.name); - } - system_meta - .component_access_set - .add_unfiltered_write(component_id); - - let archetype_component_id = world - .get_non_send_archetype_component_id(component_id) - .unwrap(); - system_meta - .archetype_component_access - .add_write(archetype_component_id); - - component_id - } - - #[inline] - unsafe fn get_param<'w, 's>( - &mut component_id: &'s mut Self::State, - system_meta: &SystemMeta, - world: UnsafeWorldCell<'w>, - change_tick: Tick, - ) -> Self::Item<'w, 's> { - let (ptr, ticks) = world - .get_non_send_with_ticks(component_id) - .unwrap_or_else(|| { - panic!( - "Non-send resource requested by {} does not exist: {}", - system_meta.name, - std::any::type_name::() - ) - }); - NonSendMut { - value: ptr.assert_unique().deref_mut(), - ticks: TicksMut::from_tick_cells(ticks, system_meta.last_run, change_tick), - } - } -} - -// SAFETY: this impl defers to `NonSendMut`, which initializes and validates the correct world access. -unsafe impl<'a, T: 'static> SystemParam for Option> { - type State = ComponentId; - type Item<'w, 's> = Option>; - - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - NonSendMut::::init_state(world, system_meta) - } - - #[inline] - unsafe fn get_param<'w, 's>( - &mut component_id: &'s mut Self::State, - system_meta: &SystemMeta, - world: UnsafeWorldCell<'w>, - change_tick: Tick, - ) -> Self::Item<'w, 's> { - world - .get_non_send_with_ticks(component_id) - .map(|(ptr, ticks)| NonSendMut { - value: ptr.assert_unique().deref_mut(), - ticks: TicksMut::from_tick_cells(ticks, system_meta.last_run, change_tick), - }) - } -} - // SAFETY: Only reads World archetypes unsafe impl<'a> ReadOnlySystemParam for &'a Archetypes {} @@ -1739,34 +1506,4 @@ mod tests { schedule.add_systems(non_sync_system); schedule.run(&mut world); } - - // Regression test for https://github.com/bevyengine/bevy/issues/10207. - #[test] - fn param_set_non_send_first() { - fn non_send_param_set(mut p: ParamSet<(NonSend<*mut u8>, ())>) { - let _ = p.p0(); - p.p1(); - } - - let mut world = World::new(); - world.insert_non_send_resource(std::ptr::null_mut::()); - let mut schedule = crate::schedule::Schedule::default(); - schedule.add_systems((non_send_param_set, non_send_param_set, non_send_param_set)); - schedule.run(&mut world); - } - - // Regression test for https://github.com/bevyengine/bevy/issues/10207. - #[test] - fn param_set_non_send_second() { - fn non_send_param_set(mut p: ParamSet<((), NonSendMut<*mut u8>)>) { - p.p0(); - let _ = p.p1(); - } - - let mut world = World::new(); - world.insert_non_send_resource(std::ptr::null_mut::()); - let mut schedule = crate::schedule::Schedule::default(); - schedule.add_systems((non_send_param_set, non_send_param_set, non_send_param_set)); - schedule.run(&mut world); - } } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 995b503788a84..016df0e7a59d4 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1077,58 +1077,7 @@ impl World { }); } - /// Initializes a new non-send resource and returns the [`ComponentId`] created for it. - /// - /// If the resource already exists, nothing happens. - /// - /// The value given by the [`FromWorld::from_world`] method will be used. - /// Note that any resource with the `Default` trait automatically implements `FromWorld`, - /// and those default values will be here instead. - /// - /// # Panics - /// - /// Panics if called from a thread other than the main thread. - #[inline] - pub fn init_non_send_resource(&mut self) -> ComponentId { - let component_id = self.components.init_non_send::(); - if self - .storages - .non_send_resources - .get(component_id) - .map_or(true, |data| !data.is_present()) - { - let value = R::from_world(self); - OwningPtr::make(value, |ptr| { - // SAFETY: component_id was just initialized and corresponds to resource of type R. - unsafe { - self.insert_non_send_by_id(component_id, ptr); - } - }); - } - component_id - } - - /// Inserts a new non-send resource with the given `value`. - /// - /// `NonSend` resources cannot be sent across threads, - /// and do not need the `Send + Sync` bounds. - /// Systems with `NonSend` resources are always scheduled on the main thread. - /// - /// # Panics - /// If a value is already present, this function will panic if called - /// from a different thread than where the original value was inserted from. - #[inline] - pub fn insert_non_send_resource(&mut self, value: R) { - let component_id = self.components.init_non_send::(); - OwningPtr::make(value, |ptr| { - // SAFETY: component_id was just initialized and corresponds to resource of type R. - unsafe { - self.insert_non_send_by_id(component_id, ptr); - } - }); - } - - /// Removes the resource of a given type and returns it, if it exists. Otherwise returns `None`. + /// Removes the resource of a given type and returns it, if it exists. Otherwise returns [None]. #[inline] pub fn remove_resource(&mut self) -> Option { let component_id = self.components.get_resource_id(TypeId::of::())?; @@ -1137,29 +1086,6 @@ impl World { unsafe { Some(ptr.read::()) } } - /// Removes a `!Send` resource from the world and returns it, if present. - /// - /// `NonSend` resources cannot be sent across threads, - /// and do not need the `Send + Sync` bounds. - /// Systems with `NonSend` resources are always scheduled on the main thread. - /// - /// Returns `None` if a value was not previously present. - /// - /// # Panics - /// If a value is present, this function will panic if called from a different - /// thread than where the value was inserted from. - #[inline] - pub fn remove_non_send_resource(&mut self) -> Option { - let component_id = self.components.get_resource_id(TypeId::of::())?; - let (ptr, _) = self - .storages - .non_send_resources - .get_mut(component_id)? - .remove()?; - // SAFETY: `component_id` was gotten via looking up the `R` type - unsafe { Some(ptr.read::()) } - } - /// Returns `true` if a resource of type `R` exists. Otherwise returns `false`. #[inline] pub fn contains_resource(&self) -> bool { @@ -1170,23 +1096,13 @@ impl World { .unwrap_or(false) } - /// Returns `true` if a resource of type `R` exists. Otherwise returns `false`. - #[inline] - pub fn contains_non_send(&self) -> bool { - self.components - .get_resource_id(TypeId::of::()) - .and_then(|component_id| self.storages.non_send_resources.get(component_id)) - .map(|info| info.is_present()) - .unwrap_or(false) - } - - /// Return's `true` if a resource of type `R` exists and was added since the world's - /// [`last_change_tick`](World::last_change_tick()). Otherwise, this return's `false`. + /// Returns `true` if a resource of type `R` exists and was added since the world's + /// [`last_change_tick`](World::last_change_tick()). /// /// This means that: - /// - When called from an exclusive system, this will check for additions since the system last ran. - /// - When called elsewhere, this will check for additions since the last time that [`World::clear_trackers`] - /// was called. + /// - When called from an exclusive system, this will check for added since the system last ran. + /// - When called elsewhere, this will check for added since the last call to [`World::clear_trackers`]. + #[inline] pub fn is_resource_added(&self) -> bool { self.components .get_resource_id(TypeId::of::()) @@ -1196,12 +1112,11 @@ impl World { } /// Return's `true` if a resource of type `R` exists and was modified since the world's - /// [`last_change_tick`](World::last_change_tick()). Otherwise, this return's `false`. + /// [`last_change_tick`](World::last_change_tick()). /// /// This means that: /// - When called from an exclusive system, this will check for changes since the system last ran. - /// - When called elsewhere, this will check for changes since the last time that [`World::clear_trackers`] - /// was called. + /// - When called elsewhere, this will check for changes since the last call to [`World::clear_trackers`]. pub fn is_resource_changed(&self) -> bool { self.components .get_resource_id(TypeId::of::()) @@ -1306,76 +1221,6 @@ impl World { unsafe { data.with_type::() } } - /// Gets an immutable reference to the non-send resource of the given type, if it exists. - /// - /// # Panics - /// - /// Panics if the resource does not exist. - /// Use [`get_non_send_resource`](World::get_non_send_resource) instead if you want to handle this case. - /// - /// This function will panic if it isn't called from the same thread that the resource was inserted from. - #[inline] - #[track_caller] - pub fn non_send_resource(&self) -> &R { - match self.get_non_send_resource() { - Some(x) => x, - None => panic!( - "Requested non-send resource {} does not exist in the `World`. - Did you forget to add it using `app.insert_non_send_resource` / `app.init_non_send_resource`? - Non-send resources can also be be added by plugins.", - std::any::type_name::() - ), - } - } - - /// Gets a mutable reference to the non-send resource of the given type, if it exists. - /// - /// # Panics - /// - /// Panics if the resource does not exist. - /// Use [`get_non_send_resource_mut`](World::get_non_send_resource_mut) instead if you want to handle this case. - /// - /// This function will panic if it isn't called from the same thread that the resource was inserted from. - #[inline] - #[track_caller] - pub fn non_send_resource_mut(&mut self) -> Mut<'_, R> { - match self.get_non_send_resource_mut() { - Some(x) => x, - None => panic!( - "Requested non-send resource {} does not exist in the `World`. - Did you forget to add it using `app.insert_non_send_resource` / `app.init_non_send_resource`? - Non-send resources can also be be added by plugins.", - std::any::type_name::() - ), - } - } - - /// Gets a reference to the non-send resource of the given type, if it exists. - /// Otherwise returns `None`. - /// - /// # Panics - /// This function will panic if it isn't called from the same thread that the resource was inserted from. - #[inline] - pub fn get_non_send_resource(&self) -> Option<&R> { - // SAFETY: - // - `as_unsafe_world_cell_readonly` gives permission to access the entire world immutably - // - `&self` ensures that there are no mutable borrows of world data - unsafe { self.as_unsafe_world_cell_readonly().get_non_send_resource() } - } - - /// Gets a mutable reference to the non-send resource of the given type, if it exists. - /// Otherwise returns `None`. - /// - /// # Panics - /// This function will panic if it isn't called from the same thread that the resource was inserted from. - #[inline] - pub fn get_non_send_resource_mut(&mut self) -> Option> { - // SAFETY: - // - `as_unsafe_world_cell` gives permission to access the entire world mutably - // - `&mut self` ensures that there are no borrows of world data - unsafe { self.as_unsafe_world_cell().get_non_send_resource_mut() } - } - // Shorthand helper function for getting the [`ArchetypeComponentId`] for a resource. #[inline] pub(crate) fn get_resource_archetype_component_id( @@ -1386,16 +1231,6 @@ impl World { Some(resource.id()) } - // Shorthand helper function for getting the [`ArchetypeComponentId`] for a resource. - #[inline] - pub(crate) fn get_non_send_archetype_component_id( - &self, - component_id: ComponentId, - ) -> Option { - let resource = self.storages.non_send_resources.get(component_id)?; - Some(resource.id()) - } - /// For a given batch of ([`Entity`], [`Bundle`]) pairs, either spawns each [`Entity`] with the given /// bundle (if the entity does not exist), or inserts the [`Bundle`] (if the entity already exists). /// This is faster than doing equivalent operations one-by-one. @@ -1637,31 +1472,6 @@ impl World { .insert(value, change_tick); } - /// Inserts a new `!Send` resource with the given `value`. Will replace the value if it already - /// existed. - /// - /// **You should prefer to use the typed API [`World::insert_non_send_resource`] where possible and only - /// use this in cases where the actual types are not known at compile time.** - /// - /// # Panics - /// If a value is already present, this function will panic if not called from the same - /// thread that the original value was inserted from. - /// - /// # Safety - /// The value referenced by `value` must be valid for the given [`ComponentId`] of this world. - #[inline] - pub unsafe fn insert_non_send_by_id( - &mut self, - component_id: ComponentId, - value: OwningPtr<'_>, - ) { - let change_tick = self.change_tick(); - - // SAFETY: value is valid for component_id, ensured by caller - self.initialize_non_send_internal(component_id) - .insert(value, change_tick); - } - /// # Panics /// Panics if `component_id` is not registered as a `Send` component type in this `World` #[inline] @@ -1679,35 +1489,12 @@ impl World { }) } - /// # Panics - /// panics if `component_id` is not registered in this world - #[inline] - fn initialize_non_send_internal( - &mut self, - component_id: ComponentId, - ) -> &mut ResourceData { - let archetype_component_count = &mut self.archetypes.archetype_component_count; - self.storages - .non_send_resources - .initialize_with(component_id, &self.components, || { - let id = ArchetypeComponentId::new(*archetype_component_count); - *archetype_component_count += 1; - id - }) - } - pub(crate) fn initialize_resource(&mut self) -> ComponentId { let component_id = self.components.init_resource::(); self.initialize_resource_internal(component_id); component_id } - pub(crate) fn initialize_non_send_resource(&mut self) -> ComponentId { - let component_id = self.components.init_non_send::(); - self.initialize_non_send_internal(component_id); - component_id - } - /// Empties queued entities and adds them to the empty [`Archetype`](crate::archetype::Archetype). /// This should be called before doing operations that might operate on queued entities, /// such as inserting a [`Component`]. @@ -1779,7 +1566,6 @@ impl World { ref mut tables, ref mut sparse_sets, ref mut resources, - ref mut non_send_resources, } = self.storages; #[cfg(feature = "trace")] @@ -1787,7 +1573,6 @@ impl World { tables.check_change_ticks(change_tick); sparse_sets.check_change_ticks(change_tick); resources.check_change_ticks(change_tick); - non_send_resources.check_change_ticks(change_tick); if let Some(mut schedules) = self.get_resource_mut::() { schedules.check_change_ticks(change_tick); @@ -1820,7 +1605,6 @@ impl World { /// Use with caution. pub fn clear_resources(&mut self) { self.storages.resources.clear(); - self.storages.non_send_resources.clear(); } } @@ -1859,46 +1643,6 @@ impl World { } } - /// Gets a `!Send` resource to the resource with the id [`ComponentId`] if it exists. - /// The returned pointer must not be used to modify the resource, and must not be - /// dereferenced after the immutable borrow of the [`World`] ends. - /// - /// **You should prefer to use the typed API [`World::get_resource`] where possible and only - /// use this in cases where the actual types are not known at compile time.** - /// - /// # Panics - /// This function will panic if it isn't called from the same thread that the resource was inserted from. - #[inline] - pub fn get_non_send_by_id(&self, component_id: ComponentId) -> Option> { - // SAFETY: - // - `as_unsafe_world_cell_readonly` gives permission to access the whole world immutably - // - `&self` ensures there are no mutable borrows on world data - unsafe { - self.as_unsafe_world_cell_readonly() - .get_non_send_resource_by_id(component_id) - } - } - - /// Gets a `!Send` resource to the resource with the id [`ComponentId`] if it exists. - /// The returned pointer may be used to modify the resource, as long as the mutable borrow - /// of the [`World`] is still valid. - /// - /// **You should prefer to use the typed API [`World::get_resource_mut`] where possible and only - /// use this in cases where the actual types are not known at compile time.** - /// - /// # Panics - /// This function will panic if it isn't called from the same thread that the resource was inserted from. - #[inline] - pub fn get_non_send_mut_by_id(&mut self, component_id: ComponentId) -> Option> { - // SAFETY: - // - `&mut self` ensures that all accessed data is unaliased - // - `as_unsafe_world_cell` provides mutable permission to the whole world - unsafe { - self.as_unsafe_world_cell() - .get_non_send_resource_mut_by_id(component_id) - } - } - /// Removes the resource of a given type, if it exists. Otherwise returns `None`. /// /// **You should prefer to use the typed API [`World::remove_resource`] where possible and only @@ -1911,21 +1655,6 @@ impl World { Some(()) } - /// Removes the resource of a given type, if it exists. Otherwise returns `None`. - /// - /// **You should prefer to use the typed API [`World::remove_resource`] where possible and only - /// use this in cases where the actual types are not known at compile time.** - /// - /// # Panics - /// This function will panic if it isn't called from the same thread that the resource was inserted from. - pub fn remove_non_send_by_id(&mut self, component_id: ComponentId) -> Option<()> { - self.storages - .non_send_resources - .get_mut(component_id)? - .remove_and_drop(); - Some(()) - } - /// Retrieves an immutable untyped reference to the given `entity`'s [`Component`] of the given [`ComponentId`]. /// Returns `None` if the `entity` does not have a [`Component`] of the given type. /// @@ -2383,19 +2112,6 @@ mod tests { assert_eq!(resource.0, 0); } - #[test] - fn init_non_send_resource_does_not_overwrite() { - let mut world = World::new(); - world.insert_resource(TestResource(0)); - world.init_non_send_resource::(); - world.insert_resource(TestResource(1)); - world.init_non_send_resource::(); - - let resource = world.non_send_resource::(); - - assert_eq!(resource.0, 0); - } - #[derive(Component)] struct Foo; diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index f5dd64c72530b..6d9779ee3363e 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -301,21 +301,6 @@ impl<'w> UnsafeWorldCell<'w> { Some(resource.id()) } - /// Shorthand helper function for getting the [`ArchetypeComponentId`] for a resource. - #[inline] - pub(crate) fn get_non_send_archetype_component_id( - self, - component_id: ComponentId, - ) -> Option { - // SAFETY: - // - we only access world metadata - let resource = unsafe { self.world_metadata() } - .storages - .non_send_resources - .get(component_id)?; - Some(resource.id()) - } - /// Retrieves an [`UnsafeEntityCell`] that exposes read and write operations for the given `entity`. /// Similar to the [`UnsafeWorldCell`], you are in charge of making sure that no aliasing rules are violated. #[inline] @@ -363,48 +348,6 @@ impl<'w> UnsafeWorldCell<'w> { .get_data() } - /// Gets a reference to the non-send resource of the given type if it exists - /// - /// # Safety - /// It is the callers responsibility to ensure that - /// - the [`UnsafeWorldCell`] has permission to access the resource - /// - no mutable reference to the resource exists at the same time - #[inline] - pub unsafe fn get_non_send_resource(self) -> Option<&'w R> { - let component_id = self.components().get_resource_id(TypeId::of::())?; - // SAFETY: caller ensures that `self` has permission to access `R` - // caller ensures that no mutable reference exists to `R` - unsafe { - self.get_non_send_resource_by_id(component_id) - // SAFETY: `component_id` was obtained from `TypeId::of::()` - .map(|ptr| ptr.deref::()) - } - } - - /// Gets a `!Send` resource to the resource with the id [`ComponentId`] if it exists. - /// The returned pointer must not be used to modify the resource, and must not be - /// dereferenced after the immutable borrow of the [`World`] ends. - /// - /// **You should prefer to use the typed API [`UnsafeWorldCell::get_non_send_resource`] where possible and only - /// use this in cases where the actual types are not known at compile time.** - /// - /// # Panics - /// This function will panic if it isn't called from the same thread that the resource was inserted from. - /// - /// # Safety - /// It is the callers responsibility to ensure that - /// - the [`UnsafeWorldCell`] has permission to access the resource - /// - no mutable reference to the resource exists at the same time - #[inline] - pub unsafe fn get_non_send_resource_by_id(self, component_id: ComponentId) -> Option> { - // SAFETY: we only access data on world that the caller has ensured is unaliased and we have - // permission to access. - unsafe { self.storages() } - .non_send_resources - .get(component_id)? - .get_data() - } - /// Gets a mutable reference to the resource of the given type if it exists /// /// # Safety @@ -463,65 +406,6 @@ impl<'w> UnsafeWorldCell<'w> { }) } - /// Gets a mutable reference to the non-send resource of the given type if it exists - /// - /// # Safety - /// It is the callers responsibility to ensure that - /// - the [`UnsafeWorldCell`] has permission to access the resource mutably - /// - no other references to the resource exist at the same time - #[inline] - pub unsafe fn get_non_send_resource_mut(self) -> Option> { - let component_id = self.components().get_resource_id(TypeId::of::())?; - // SAFETY: - // - caller ensures that `self` has permission to access the resource - // - caller ensures that the resource is unaliased - unsafe { - self.get_non_send_resource_mut_by_id(component_id) - // SAFETY: `component_id` was gotten by `TypeId::of::()` - .map(|ptr| ptr.with_type::()) - } - } - - /// Gets a `!Send` resource to the resource with the id [`ComponentId`] if it exists. - /// The returned pointer may be used to modify the resource, as long as the mutable borrow - /// of the [`World`] is still valid. - /// - /// **You should prefer to use the typed API [`UnsafeWorldCell::get_non_send_resource_mut`] where possible and only - /// use this in cases where the actual types are not known at compile time.** - /// - /// # Panics - /// This function will panic if it isn't called from the same thread that the resource was inserted from. - /// - /// # Safety - /// It is the callers responsibility to ensure that - /// - the [`UnsafeWorldCell`] has permission to access the resource mutably - /// - no other references to the resource exist at the same time - #[inline] - pub unsafe fn get_non_send_resource_mut_by_id( - self, - component_id: ComponentId, - ) -> Option> { - let change_tick = self.change_tick(); - // SAFETY: we only access data that the caller has ensured is unaliased and `self` - // has permission to access. - let (ptr, ticks) = unsafe { self.storages() } - .non_send_resources - .get(component_id)? - .get_with_ticks()?; - - let ticks = - // SAFETY: This function has exclusive access to the world so nothing aliases `ticks`. - // - index is in-bounds because the column is initialized and non-empty - // - no other reference to the ticks of the same row can exist at the same time - unsafe { TicksMut::from_tick_cells(ticks, self.last_change_tick(), change_tick) }; - - Some(MutUntyped { - // SAFETY: This function has exclusive access to the world so nothing aliases `ptr`. - value: unsafe { ptr.assert_unique() }, - ticks, - }) - } - // Shorthand helper function for getting the data and change ticks for a resource. /// /// # Safety @@ -542,30 +426,6 @@ impl<'w> UnsafeWorldCell<'w> { .get(component_id)? .get_with_ticks() } - - // Shorthand helper function for getting the data and change ticks for a resource. - /// - /// # Panics - /// This function will panic if it isn't called from the same thread that the resource was inserted from. - /// - /// # Safety - /// It is the callers responsibility to ensure that - /// - the [`UnsafeWorldCell`] has permission to access the resource mutably - /// - no mutable references to the resource exist at the same time - #[inline] - pub(crate) unsafe fn get_non_send_with_ticks( - self, - component_id: ComponentId, - ) -> Option<(Ptr<'w>, TickCells<'w>)> { - // SAFETY: - // - caller ensures there is no `&mut World` - // - caller ensures there are no mutable borrows of this resource - // - caller ensures that we have permission to access this resource - unsafe { self.storages() } - .non_send_resources - .get(component_id)? - .get_with_ticks() - } } impl Debug for UnsafeWorldCell<'_> { diff --git a/crates/bevy_ecs/src/world/world_cell.rs b/crates/bevy_ecs/src/world/world_cell.rs index 852858ba607b3..43276d56ff26d 100644 --- a/crates/bevy_ecs/src/world/world_cell.rs +++ b/crates/bevy_ecs/src/world/world_cell.rs @@ -268,74 +268,6 @@ impl<'w> WorldCell<'w> { } } - /// Gets an immutable reference to the non-send resource of the given type, if it exists. - pub fn get_non_send_resource(&self) -> Option> { - let component_id = self.world.components().get_resource_id(TypeId::of::())?; - - let archetype_component_id = self - .world - .get_non_send_archetype_component_id(component_id)?; - WorldBorrow::try_new( - // SAFETY: access is checked by WorldBorrowMut - || unsafe { self.world.get_non_send_resource::() }, - archetype_component_id, - self.access.clone(), - ) - } - - /// Gets an immutable reference to the non-send resource of the given type, if it exists. - /// - /// # Panics - /// - /// Panics if the resource does not exist. Use - /// [`get_non_send_resource`](WorldCell::get_non_send_resource) instead if you want to handle - /// this case. - pub fn non_send_resource(&self) -> WorldBorrow<'_, T> { - match self.get_non_send_resource() { - Some(x) => x, - None => panic!( - "Requested non-send resource {} does not exist in the `World`. - Did you forget to add it using `app.insert_non_send_resource` / `app.init_non_send_resource`? - Non-send resources can also be be added by plugins.", - std::any::type_name::() - ), - } - } - - /// Gets a mutable reference to the non-send resource of the given type, if it exists. - pub fn get_non_send_resource_mut(&self) -> Option> { - let component_id = self.world.components().get_resource_id(TypeId::of::())?; - - let archetype_component_id = self - .world - .get_non_send_archetype_component_id(component_id)?; - WorldBorrowMut::try_new( - // SAFETY: access is checked by WorldBorrowMut - || unsafe { self.world.get_non_send_resource_mut::() }, - archetype_component_id, - self.access.clone(), - ) - } - - /// Gets a mutable reference to the non-send resource of the given type, if it exists. - /// - /// # Panics - /// - /// Panics if the resource does not exist. Use - /// [`get_non_send_resource_mut`](WorldCell::get_non_send_resource_mut) instead if you want to - /// handle this case. - pub fn non_send_resource_mut(&self) -> WorldBorrowMut<'_, T> { - match self.get_non_send_resource_mut() { - Some(x) => x, - None => panic!( - "Requested non-send resource {} does not exist in the `World`. - Did you forget to add it using `app.insert_non_send_resource` / `app.init_non_send_resource`? - Non-send resources can also be be added by plugins.", - std::any::type_name::() - ), - } - } - /// Sends an [`Event`]. #[inline] pub fn send_event(&self, event: E) { diff --git a/crates/bevy_gilrs/src/gilrs_system.rs b/crates/bevy_gilrs/src/gilrs_system.rs index 553192810381c..bd2f68e8847b5 100644 --- a/crates/bevy_gilrs/src/gilrs_system.rs +++ b/crates/bevy_gilrs/src/gilrs_system.rs @@ -1,6 +1,6 @@ use crate::converter::{convert_axis, convert_button, convert_gamepad_id}; use bevy_ecs::event::EventWriter; -use bevy_ecs::system::{NonSend, NonSendMut, Res, ResMut}; +use bevy_ecs::prelude::{Mut, Res, ResMut, ThreadLocal}; use bevy_input::gamepad::{ GamepadAxisChangedEvent, GamepadButtonChangedEvent, GamepadConnection, GamepadConnectionEvent, GamepadSettings, @@ -8,85 +8,111 @@ use bevy_input::gamepad::{ use bevy_input::gamepad::{GamepadEvent, GamepadInfo}; use bevy_input::prelude::{GamepadAxis, GamepadButton}; use bevy_input::Axis; -use gilrs::{ev::filter::axis_dpad_to_button, EventType, Filter, Gilrs}; +use gilrs::{ev::filter::axis_dpad_to_button, EventType, Filter}; pub fn gilrs_event_startup_system( - gilrs: NonSend, mut connection_events: EventWriter, + mut main_thread: ThreadLocal, ) { - for (id, gamepad) in gilrs.gamepads() { - let info = GamepadInfo { - name: gamepad.name().into(), - }; + main_thread.run(|tls| { + tls.resource_scope(|_tls, gilrs: Mut| { + for (id, gamepad) in gilrs.gamepads() { + let info = GamepadInfo { + name: gamepad.name().into(), + }; - connection_events.send(GamepadConnectionEvent { - gamepad: convert_gamepad_id(id), - connection: GamepadConnection::Connected(info), + connection_events.send(GamepadConnectionEvent { + gamepad: convert_gamepad_id(id), + connection: GamepadConnection::Connected(info), + }); + } }); - } + }); } pub fn gilrs_event_system( - mut gilrs: NonSendMut, mut events: EventWriter, mut gamepad_buttons: ResMut>, gamepad_axis: Res>, gamepad_settings: Res, + mut main_thread: ThreadLocal, ) { - while let Some(gilrs_event) = gilrs - .next_event() - .filter_ev(&axis_dpad_to_button, &mut gilrs) - { - gilrs.update(&gilrs_event); + main_thread.run(|tls| { + tls.resource_scope(|_tls, mut gilrs: Mut| { + while let Some(gilrs_event) = gilrs + .next_event() + .filter_ev(&axis_dpad_to_button, &mut gilrs) + { + gilrs.update(&gilrs_event); - let gamepad = convert_gamepad_id(gilrs_event.id); - match gilrs_event.event { - EventType::Connected => { - let pad = gilrs.gamepad(gilrs_event.id); - let info = GamepadInfo { - name: pad.name().into(), - }; - - events.send( - GamepadConnectionEvent::new(gamepad, GamepadConnection::Connected(info)).into(), - ); - } - EventType::Disconnected => events - .send(GamepadConnectionEvent::new(gamepad, GamepadConnection::Disconnected).into()), - EventType::ButtonChanged(gilrs_button, raw_value, _) => { - if let Some(button_type) = convert_button(gilrs_button) { - let button = GamepadButton::new(gamepad, button_type); - let old_value = gamepad_buttons.get(button); - let button_settings = gamepad_settings.get_button_axis_settings(button); + let gamepad = convert_gamepad_id(gilrs_event.id); + match gilrs_event.event { + EventType::Connected => { + let pad = gilrs.gamepad(gilrs_event.id); + let info = GamepadInfo { + name: pad.name().into(), + }; - // Only send events that pass the user-defined change threshold - if let Some(filtered_value) = button_settings.filter(raw_value, old_value) { events.send( - GamepadButtonChangedEvent::new(gamepad, button_type, filtered_value) - .into(), + GamepadConnectionEvent::new( + gamepad, + GamepadConnection::Connected(info), + ) + .into(), ); - // Update the current value prematurely so that `old_value` is correct in - // future iterations of the loop. - gamepad_buttons.set(button, filtered_value); } - } - } - EventType::AxisChanged(gilrs_axis, raw_value, _) => { - if let Some(axis_type) = convert_axis(gilrs_axis) { - let axis = GamepadAxis::new(gamepad, axis_type); - let old_value = gamepad_axis.get(axis); - let axis_settings = gamepad_settings.get_axis_settings(axis); + EventType::Disconnected => events.send( + GamepadConnectionEvent::new(gamepad, GamepadConnection::Disconnected) + .into(), + ), + EventType::ButtonChanged(gilrs_button, raw_value, _) => { + if let Some(button_type) = convert_button(gilrs_button) { + let button = GamepadButton::new(gamepad, button_type); + let old_value = gamepad_buttons.get(button); + let button_settings = gamepad_settings.get_button_axis_settings(button); - // Only send events that pass the user-defined change threshold - if let Some(filtered_value) = axis_settings.filter(raw_value, old_value) { - events.send( - GamepadAxisChangedEvent::new(gamepad, axis_type, filtered_value).into(), - ); + // Only send events that pass the user-defined change threshold + if let Some(filtered_value) = + button_settings.filter(raw_value, old_value) + { + events.send( + GamepadButtonChangedEvent::new( + gamepad, + button_type, + filtered_value, + ) + .into(), + ); + // Update the current value prematurely so that `old_value` is + // correct in future iterations of the loop. + gamepad_buttons.set(button, filtered_value); + } + } } - } + EventType::AxisChanged(gilrs_axis, raw_value, _) => { + if let Some(axis_type) = convert_axis(gilrs_axis) { + let axis = GamepadAxis::new(gamepad, axis_type); + let old_value = gamepad_axis.get(axis); + let axis_settings = gamepad_settings.get_axis_settings(axis); + + // Only send events that pass the user-defined change threshold + if let Some(filtered_value) = axis_settings.filter(raw_value, old_value) + { + events.send( + GamepadAxisChangedEvent::new( + gamepad, + axis_type, + filtered_value, + ) + .into(), + ); + } + } + } + _ => (), + }; } - _ => (), - }; - } - gilrs.inc(); + gilrs.inc(); + }); + }); } diff --git a/crates/bevy_gilrs/src/lib.rs b/crates/bevy_gilrs/src/lib.rs index 39172a4053c2b..1e57aaf05859d 100644 --- a/crates/bevy_gilrs/src/lib.rs +++ b/crates/bevy_gilrs/src/lib.rs @@ -18,6 +18,22 @@ use gilrs::GilrsBuilder; use gilrs_system::{gilrs_event_startup_system, gilrs_event_system}; use rumble::{play_gilrs_rumble, RunningRumbleEffects}; +// TODO: This can be a normal resource on all targets except WASM. +/// An instance of [`gilrs::Gilrs`]. +#[derive(ThreadLocalResource)] +pub struct Gilrs(gilrs::Gilrs); +impl std::ops::Deref for Gilrs { + type Target = gilrs::Gilrs; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl std::ops::DerefMut for Gilrs { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + /// Plugin that provides gamepad handling to an [`App`]. #[derive(Default)] pub struct GilrsPlugin; @@ -34,7 +50,7 @@ impl Plugin for GilrsPlugin { .build() { Ok(gilrs) => { - app.insert_non_send_resource(gilrs) + app.insert_non_send_resource(crate::Gilrs(gilrs)) .init_non_send_resource::() .add_systems(PreStartup, gilrs_event_startup_system) .add_systems(PreUpdate, gilrs_event_system.before(InputSystem)) diff --git a/crates/bevy_gilrs/src/rumble.rs b/crates/bevy_gilrs/src/rumble.rs index 2a1a016a2fa2d..4620bb10b4fcf 100644 --- a/crates/bevy_gilrs/src/rumble.rs +++ b/crates/bevy_gilrs/src/rumble.rs @@ -1,8 +1,7 @@ //! Handle user specified rumble request events. -use bevy_ecs::{ - prelude::{EventReader, Res}, - system::NonSendMut, -}; + +use bevy_ecs::prelude::{EventReader, Mut, Res, ThreadLocal}; +use bevy_ecs::storage::ThreadLocalResource; use bevy_input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest}; use bevy_log::{debug, warn}; use bevy_time::{Real, Time}; @@ -17,11 +16,11 @@ use crate::converter::convert_gamepad_id; /// A rumble effect that is currently in effect. struct RunningRumble { - /// Duration from app startup when this effect will be finished + /// The time (since app startup) when this effect will be finished. deadline: Duration, - /// A ref-counted handle to the specific force-feedback effect + /// A ref-counted handle to the specific force-feedback effect. /// - /// Dropping it will cause the effect to stop + /// Dropping it will end the effect. #[allow(dead_code)] effect: ff::Effect, } @@ -35,7 +34,7 @@ enum RumbleError { } /// Contains the gilrs rumble effects that are currently running for each gamepad -#[derive(Default)] +#[derive(Default, ThreadLocalResource)] pub(crate) struct RunningRumbleEffects { /// If multiple rumbles are running at the same time, their resulting rumble /// will be the saturated sum of their strengths up until [`u16::MAX`] @@ -121,39 +120,47 @@ fn handle_rumble_request( } pub(crate) fn play_gilrs_rumble( time: Res>, - mut gilrs: NonSendMut, mut requests: EventReader, - mut running_rumbles: NonSendMut, + mut main_thread: ThreadLocal, ) { - 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, &mut gilrs, 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}" - ); + main_thread.run(|tls| { + let current_time = time.elapsed(); + tls.resource_scope(|tls, mut gilrs: Mut| { + tls.resource_scope(|_tls, mut running_rumbles: Mut| { + // 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); } - } - Err(RumbleError::GamepadNotFound) => { - warn!("Tried to handle rumble request {gamepad:?} but it doesn't exist!"); - } - }; - } + 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, &mut gilrs, 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!"); + } + }; + } + }); + }); + + }); } #[cfg(test)] diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 446605b43e4f8..ed242bb3339dd 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -93,7 +93,7 @@ impl Plugin for GizmoPlugin { .after(TransformSystem::TransformPropagate), ); - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; @@ -111,11 +111,11 @@ impl Plugin for GizmoPlugin { } fn finish(&self, app: &mut bevy_app::App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; - let render_device = render_app.world.resource::(); + let render_device = render_app.world().resource::(); let layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { entries: &[BindGroupLayoutEntry { binding: 0, diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index 5b64598eb403f..ada9b84ac14c2 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -27,7 +27,7 @@ pub struct LineGizmo2dPlugin; impl Plugin for LineGizmo2dPlugin { fn build(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; @@ -43,7 +43,7 @@ impl Plugin for LineGizmo2dPlugin { } fn finish(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 2cb016753513b..77403ef2dc712 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -29,7 +29,7 @@ use bevy_render::{ pub struct LineGizmo3dPlugin; impl Plugin for LineGizmo3dPlugin { fn build(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; @@ -45,7 +45,7 @@ impl Plugin for LineGizmo3dPlugin { } fn finish(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index 559c8ae3c8e63..839f117b271c1 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -59,9 +59,8 @@ impl Plugin for GltfPlugin { } fn finish(&self, app: &mut App) { - let supported_compressed_formats = match app.world.get_resource::() { + let supported_compressed_formats = match app.world().get_resource::() { Some(render_device) => CompressedImageFormats::from_features(render_device.features()), - None => CompressedImageFormats::NONE, }; app.register_asset_loader(GltfLoader { diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index a912d7d984b5c..05092fe9316d4 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -31,17 +31,25 @@ pub mod prelude { }; } +#[cfg(feature = "tracing-chrome")] +use bevy_utils::synccell::SyncCell; pub use bevy_utils::tracing::{ debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn, warn_span, Level, }; use bevy_app::{App, Plugin}; +#[cfg(feature = "tracing-chrome")] +use bevy_ecs::system::Resource; use tracing_log::LogTracer; #[cfg(feature = "tracing-chrome")] use tracing_subscriber::fmt::{format::DefaultFields, FormattedFields}; use tracing_subscriber::{prelude::*, registry::Registry, EnvFilter}; +#[cfg(feature = "tracing-chrome")] +#[derive(Resource)] +pub(crate) struct FlushGuard(SyncCell); + /// Adds logging to Apps. This plugin is part of the `DefaultPlugins`. Adding /// this plugin will setup a collector appropriate to your target platform: /// * Using [`tracing-subscriber`](https://crates.io/crates/tracing-subscriber) by default, @@ -152,7 +160,7 @@ impl Plugin for LogPlugin { } })) .build(); - app.world.insert_non_send_resource(guard); + app.insert_resource(FlushGuard(SyncCell::new(guard))); chrome_layer }; diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index f679f638d42ac..aa8bbdc1e3faa 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -109,7 +109,7 @@ impl Plugin for DeferredPbrLightingPlugin { Shader::from_wgsl ); - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; @@ -134,7 +134,7 @@ impl Plugin for DeferredPbrLightingPlugin { } fn finish(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 8e1cfdd41d34b..80b7dc102ef4f 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -322,18 +322,19 @@ impl Plugin for PbrPlugin { app.add_plugins(DeferredPbrLightingPlugin); } - app.world.resource_mut::>().insert( - Handle::::default(), - StandardMaterial { - base_color: Color::rgb(1.0, 0.0, 0.5), - unlit: true, - ..Default::default() - }, - ); + app.world_mut() + .resource_mut::>() + .insert( + Handle::::default(), + StandardMaterial { + base_color: Color::rgb(1.0, 0.0, 0.5), + unlit: true, + ..Default::default() + }, + ); - let render_app = match app.get_sub_app_mut(RenderApp) { - Ok(render_app) => render_app, - Err(_) => return, + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; }; // Extract the required data from the main world @@ -354,8 +355,8 @@ impl Plugin for PbrPlugin { ) .init_resource::(); - let shadow_pass_node = ShadowPassNode::new(&mut render_app.world); - let mut graph = render_app.world.resource_mut::(); + let shadow_pass_node = ShadowPassNode::new(render_app.world_mut()); + let mut graph = render_app.world_mut().resource_mut::(); let draw_3d_graph = graph .get_sub_graph_mut(bevy_core_pipeline::core_3d::graph::NAME) .unwrap(); @@ -367,9 +368,8 @@ impl Plugin for PbrPlugin { } fn finish(&self, app: &mut App) { - let render_app = match app.get_sub_app_mut(RenderApp) { - Ok(render_app) => render_app, - Err(_) => return, + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; }; // Extract the required data from the main world diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index d5884bb65530c..04640437ff617 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -211,7 +211,7 @@ where app.init_asset::() .add_plugins(ExtractInstancesPlugin::>::extract_visible()); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::>() .add_render_command::>() @@ -248,7 +248,7 @@ where } fn finish(&self, app: &mut App) { - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.init_resource::>(); } } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 6f2dfed69e12d..d8cafa9a6c024 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -86,7 +86,7 @@ where Shader::from_wgsl ); - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; @@ -101,7 +101,7 @@ where } fn finish(&self, app: &mut bevy_app::App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; @@ -125,7 +125,10 @@ where M::Data: PartialEq + Eq + Hash + Clone, { fn build(&self, app: &mut bevy_app::App) { - let no_prepass_plugin_loaded = app.world.get_resource::().is_none(); + let no_prepass_plugin_loaded = app + .world() + .get_resource::() + .is_none(); if no_prepass_plugin_loaded { app.insert_resource(AnyPrepassPluginLoaded) @@ -140,7 +143,7 @@ where ); } - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; diff --git a/crates/bevy_pbr/src/render/fog.rs b/crates/bevy_pbr/src/render/fog.rs index 1738b261b578e..ceab829935229 100644 --- a/crates/bevy_pbr/src/render/fog.rs +++ b/crates/bevy_pbr/src/render/fog.rs @@ -146,7 +146,7 @@ impl Plugin for FogPlugin { app.register_type::(); app.add_plugins(ExtractComponentPlugin::::default()); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::() .add_systems(Render, prepare_fog.in_set(RenderSet::PrepareResources)); diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 8c87d27129c2d..b38f01aca1f32 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -115,7 +115,7 @@ impl Plugin for MeshRenderPlugin { (no_automatic_skin_batching, no_automatic_morph_batching), ); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::() .init_resource::() @@ -154,9 +154,9 @@ impl Plugin for MeshRenderPlugin { fn finish(&self, app: &mut bevy_app::App) { let mut mesh_bindings_shader_defs = Vec::with_capacity(1); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { if let Some(per_object_buffer_batch_size) = GpuArrayBuffer::::batch_size( - render_app.world.resource::(), + render_app.world().resource::(), ) { mesh_bindings_shader_defs.push(ShaderDefVal::UInt( "PER_OBJECT_BUFFER_BATCH_SIZE".into(), @@ -166,7 +166,7 @@ impl Plugin for MeshRenderPlugin { render_app .insert_resource(GpuArrayBuffer::::new( - render_app.world.resource::(), + render_app.world().resource::(), )) .init_resource::(); } diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index 62a7d4e5a4a6a..4e1c53e4e06f0 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -73,12 +73,12 @@ impl Plugin for ScreenSpaceAmbientOcclusionPlugin { } fn finish(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; if !render_app - .world + .world() .resource::() .get_texture_format_features(TextureFormat::R16Float) .allowed_usages @@ -89,7 +89,7 @@ impl Plugin for ScreenSpaceAmbientOcclusionPlugin { } if render_app - .world + .world() .resource::() .limits() .max_storage_textures_per_shader_stage diff --git a/crates/bevy_render/src/camera/mod.rs b/crates/bevy_render/src/camera/mod.rs index 2a92ff159692c..d24ff7baca955 100644 --- a/crates/bevy_render/src/camera/mod.rs +++ b/crates/bevy_render/src/camera/mod.rs @@ -35,13 +35,13 @@ impl Plugin for CameraPlugin { ExtractResourcePlugin::::default(), )); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::() .add_systems(ExtractSchedule, extract_cameras) .add_systems(Render, sort_cameras.in_set(RenderSet::ManageViews)); - let camera_driver_node = CameraDriverNode::new(&mut render_app.world); - let mut render_graph = render_app.world.resource_mut::(); + let camera_driver_node = CameraDriverNode::new(render_app.world_mut()); + let mut render_graph = render_app.world_mut().resource_mut::(); render_graph.add_node(crate::main_graph::node::CAMERA_DRIVER, camera_driver_node); } } diff --git a/crates/bevy_render/src/extract_component.rs b/crates/bevy_render/src/extract_component.rs index 7577517f12197..8dc22e359533b 100644 --- a/crates/bevy_render/src/extract_component.rs +++ b/crates/bevy_render/src/extract_component.rs @@ -80,7 +80,7 @@ impl Default for UniformComponentPlugin { impl Plugin for UniformComponentPlugin { fn build(&self, app: &mut App) { - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .insert_resource(ComponentUniforms::::default()) .add_systems( @@ -184,7 +184,7 @@ impl ExtractComponentPlugin { impl Plugin for ExtractComponentPlugin { fn build(&self, app: &mut App) { - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { if self.only_extract_visible { render_app.add_systems(ExtractSchedule, extract_visible_components::); } else { diff --git a/crates/bevy_render/src/extract_instances.rs b/crates/bevy_render/src/extract_instances.rs index 82c04dba1003f..10d5a13425ecc 100644 --- a/crates/bevy_render/src/extract_instances.rs +++ b/crates/bevy_render/src/extract_instances.rs @@ -94,7 +94,7 @@ where EI: ExtractInstance, { fn build(&self, app: &mut App) { - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.init_resource::>(); if self.only_extract_visible { render_app.add_systems(ExtractSchedule, extract_visible::); diff --git a/crates/bevy_render/src/extract_resource.rs b/crates/bevy_render/src/extract_resource.rs index 37a21f45bf84c..4d07bdcab908d 100644 --- a/crates/bevy_render/src/extract_resource.rs +++ b/crates/bevy_render/src/extract_resource.rs @@ -31,7 +31,7 @@ impl Default for ExtractResourcePlugin { impl Plugin for ExtractResourcePlugin { fn build(&self, app: &mut App) { - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.add_systems(ExtractSchedule, extract_resource::); } } diff --git a/crates/bevy_render/src/globals.rs b/crates/bevy_render/src/globals.rs index d337be44b93bc..549f163b0763f 100644 --- a/crates/bevy_render/src/globals.rs +++ b/crates/bevy_render/src/globals.rs @@ -21,7 +21,7 @@ impl Plugin for GlobalsPlugin { load_internal_asset!(app, GLOBALS_TYPE_HANDLE, "globals.wgsl", Shader::from_wgsl); app.register_type::(); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::() .init_resource::)); } } diff --git a/crates/bevy_render/src/render_graph/app.rs b/crates/bevy_render/src/render_graph/app.rs index 185260f95b7d9..0dc118b0a3c42 100644 --- a/crates/bevy_render/src/render_graph/app.rs +++ b/crates/bevy_render/src/render_graph/app.rs @@ -1,10 +1,10 @@ -use bevy_app::App; +use bevy_app::{App, SubApp}; use bevy_ecs::world::FromWorld; use bevy_log::warn; use super::{Node, RenderGraph}; -/// Adds common [`RenderGraph`] operations to [`App`]. +/// Adds common [`RenderGraph`] operations to [`SubApp`] (and [`App`]). pub trait RenderGraphApp { // Add a sub graph to the [`RenderGraph`] fn add_render_sub_graph(&mut self, sub_graph_name: &'static str) -> &mut Self; @@ -31,14 +31,14 @@ pub trait RenderGraphApp { ) -> &mut Self; } -impl RenderGraphApp for App { +impl RenderGraphApp for SubApp { fn add_render_graph_node( &mut self, sub_graph_name: &'static str, node_name: &'static str, ) -> &mut Self { - let node = T::from_world(&mut self.world); - let mut render_graph = self.world.get_resource_mut::().expect( + let node = T::from_world(self.world_mut()); + let mut render_graph = self.world_mut().get_resource_mut::().expect( "RenderGraph not found. Make sure you are using add_render_graph_node on the RenderApp", ); if let Some(graph) = render_graph.get_sub_graph_mut(sub_graph_name) { @@ -54,7 +54,7 @@ impl RenderGraphApp for App { sub_graph_name: &'static str, edges: &[&'static str], ) -> &mut Self { - let mut render_graph = self.world.get_resource_mut::().expect( + let mut render_graph = self.world_mut().get_resource_mut::().expect( "RenderGraph not found. Make sure you are using add_render_graph_edges on the RenderApp", ); if let Some(graph) = render_graph.get_sub_graph_mut(sub_graph_name) { @@ -71,7 +71,7 @@ impl RenderGraphApp for App { output_edge: &'static str, input_edge: &'static str, ) -> &mut Self { - let mut render_graph = self.world.get_resource_mut::().expect( + let mut render_graph = self.world_mut().get_resource_mut::().expect( "RenderGraph not found. Make sure you are using add_render_graph_edge on the RenderApp", ); if let Some(graph) = render_graph.get_sub_graph_mut(sub_graph_name) { @@ -83,10 +83,45 @@ impl RenderGraphApp for App { } fn add_render_sub_graph(&mut self, sub_graph_name: &'static str) -> &mut Self { - let mut render_graph = self.world.get_resource_mut::().expect( + let mut render_graph = self.world_mut().get_resource_mut::().expect( "RenderGraph not found. Make sure you are using add_render_sub_graph on the RenderApp", ); render_graph.add_sub_graph(sub_graph_name, RenderGraph::default()); self } } + +impl RenderGraphApp for App { + fn add_render_graph_node( + &mut self, + sub_graph_name: &'static str, + node_name: &'static str, + ) -> &mut Self { + SubApp::add_render_graph_node::(self.main_mut(), sub_graph_name, node_name); + self + } + + fn add_render_graph_edge( + &mut self, + sub_graph_name: &'static str, + output_edge: &'static str, + input_edge: &'static str, + ) -> &mut Self { + SubApp::add_render_graph_edge(self.main_mut(), sub_graph_name, output_edge, input_edge); + self + } + + fn add_render_graph_edges( + &mut self, + sub_graph_name: &'static str, + edges: &[&'static str], + ) -> &mut Self { + SubApp::add_render_graph_edges(self.main_mut(), sub_graph_name, edges); + self + } + + fn add_render_sub_graph(&mut self, sub_graph_name: &'static str) -> &mut Self { + SubApp::add_render_sub_graph(self.main_mut(), sub_graph_name); + self + } +} diff --git a/crates/bevy_render/src/render_phase/draw.rs b/crates/bevy_render/src/render_phase/draw.rs index 1d439e501db44..87e6ea989fe82 100644 --- a/crates/bevy_render/src/render_phase/draw.rs +++ b/crates/bevy_render/src/render_phase/draw.rs @@ -1,5 +1,5 @@ use crate::render_phase::{PhaseItem, TrackedRenderPass}; -use bevy_app::App; +use bevy_app::{App, SubApp}; use bevy_ecs::{ entity::Entity, query::{QueryState, ROQueryItem, ReadOnlyWorldQuery}, @@ -289,16 +289,16 @@ pub trait AddRenderCommand { C::Param: ReadOnlySystemParam; } -impl AddRenderCommand for App { +impl AddRenderCommand for SubApp { fn add_render_command + Send + Sync + 'static>( &mut self, ) -> &mut Self where C::Param: ReadOnlySystemParam, { - let draw_function = RenderCommandState::::new(&mut self.world); + let draw_function = RenderCommandState::::new(self.world_mut()); let draw_functions = self - .world + .world() .get_resource::>() .unwrap_or_else(|| { panic!( @@ -311,3 +311,15 @@ impl AddRenderCommand for App { self } } + +impl AddRenderCommand for App { + fn add_render_command + Send + Sync + 'static>( + &mut self, + ) -> &mut Self + where + C::Param: ReadOnlySystemParam, + { + SubApp::add_render_command::(self.main_mut()); + self + } +} diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index d7a31a2bebf27..cdfe4e258266f 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -86,12 +86,14 @@ impl Plugin for ImagePlugin { .register_type::() .init_asset::() .register_asset_reflect::(); - app.world + + app.world_mut() .resource_mut::>() .insert(Handle::default(), Image::default()); + #[cfg(feature = "basis-universal")] if let Some(processor) = app - .world + .world() .get_resource::() { processor.register_processor::>( @@ -101,7 +103,7 @@ impl Plugin for ImagePlugin { .set_default_processor::>("png"); } - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.init_resource::().add_systems( Render, update_texture_cache_system.in_set(RenderSet::Cleanup), @@ -134,9 +136,9 @@ impl Plugin for ImagePlugin { app.init_asset_loader::(); } - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { let default_sampler = { - let device = render_app.world.resource::(); + let device = render_app.world().resource::(); device.create_sampler(&self.default_sampler.as_wgpu()) }; render_app diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 73d8bf9c24e80..f72ccc66c5e35 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -52,7 +52,7 @@ impl Plugin for ViewPlugin { // NOTE: windows.is_changed() handles cases where a window was resized .add_plugins((ExtractResourcePlugin::::default(), VisibilityPlugin)); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.init_resource::().add_systems( Render, ( diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 0bd224f4d4dd4..1cf52c38ba9f5 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -474,42 +474,51 @@ mod test { let mut app = App::new(); app.add_systems(Update, visibility_propagate_system); - let root1 = app.world.spawn(visibility_bundle(Visibility::Hidden)).id(); - let root1_child1 = app.world.spawn(VisibilityBundle::default()).id(); - let root1_child2 = app.world.spawn(visibility_bundle(Visibility::Hidden)).id(); - let root1_child1_grandchild1 = app.world.spawn(VisibilityBundle::default()).id(); - let root1_child2_grandchild1 = app.world.spawn(VisibilityBundle::default()).id(); - - app.world + let root1 = app + .world_mut() + .spawn(visibility_bundle(Visibility::Hidden)) + .id(); + let root1_child1 = app.world_mut().spawn(VisibilityBundle::default()).id(); + let root1_child2 = app + .world_mut() + .spawn(visibility_bundle(Visibility::Hidden)) + .id(); + let root1_child1_grandchild1 = app.world_mut().spawn(VisibilityBundle::default()).id(); + let root1_child2_grandchild1 = app.world_mut().spawn(VisibilityBundle::default()).id(); + + app.world_mut() .entity_mut(root1) .push_children(&[root1_child1, root1_child2]); - app.world + app.world_mut() .entity_mut(root1_child1) .push_children(&[root1_child1_grandchild1]); - app.world + app.world_mut() .entity_mut(root1_child2) .push_children(&[root1_child2_grandchild1]); - let root2 = app.world.spawn(VisibilityBundle::default()).id(); - let root2_child1 = app.world.spawn(VisibilityBundle::default()).id(); - let root2_child2 = app.world.spawn(visibility_bundle(Visibility::Hidden)).id(); - let root2_child1_grandchild1 = app.world.spawn(VisibilityBundle::default()).id(); - let root2_child2_grandchild1 = app.world.spawn(VisibilityBundle::default()).id(); + let root2 = app.world_mut().spawn(VisibilityBundle::default()).id(); + let root2_child1 = app.world_mut().spawn(VisibilityBundle::default()).id(); + let root2_child2 = app + .world_mut() + .spawn(visibility_bundle(Visibility::Hidden)) + .id(); + let root2_child1_grandchild1 = app.world_mut().spawn(VisibilityBundle::default()).id(); + let root2_child2_grandchild1 = app.world_mut().spawn(VisibilityBundle::default()).id(); - app.world + app.world_mut() .entity_mut(root2) .push_children(&[root2_child1, root2_child2]); - app.world + app.world_mut() .entity_mut(root2_child1) .push_children(&[root2_child1_grandchild1]); - app.world + app.world_mut() .entity_mut(root2_child2) .push_children(&[root2_child2_grandchild1]); app.update(); let is_visible = |e: Entity| { - app.world + app.world() .entity(e) .get::() .unwrap() @@ -565,29 +574,29 @@ mod test { let mut app = App::new(); app.add_systems(Update, visibility_propagate_system); - let root1 = app.world.spawn(visibility_bundle(Visible)).id(); - let root1_child1 = app.world.spawn(visibility_bundle(Inherited)).id(); - let root1_child2 = app.world.spawn(visibility_bundle(Hidden)).id(); - let root1_child1_grandchild1 = app.world.spawn(visibility_bundle(Visible)).id(); - let root1_child2_grandchild1 = app.world.spawn(visibility_bundle(Visible)).id(); + let root1 = app.world_mut().spawn(visibility_bundle(Visible)).id(); + let root1_child1 = app.world_mut().spawn(visibility_bundle(Inherited)).id(); + let root1_child2 = app.world_mut().spawn(visibility_bundle(Hidden)).id(); + let root1_child1_grandchild1 = app.world_mut().spawn(visibility_bundle(Visible)).id(); + let root1_child2_grandchild1 = app.world_mut().spawn(visibility_bundle(Visible)).id(); - let root2 = app.world.spawn(visibility_bundle(Inherited)).id(); - let root3 = app.world.spawn(visibility_bundle(Hidden)).id(); + let root2 = app.world_mut().spawn(visibility_bundle(Inherited)).id(); + let root3 = app.world_mut().spawn(visibility_bundle(Hidden)).id(); - app.world + app.world_mut() .entity_mut(root1) .push_children(&[root1_child1, root1_child2]); - app.world + app.world_mut() .entity_mut(root1_child1) .push_children(&[root1_child1_grandchild1]); - app.world + app.world_mut() .entity_mut(root1_child2) .push_children(&[root1_child2_grandchild1]); app.update(); let is_visible = |e: Entity| { - app.world + app.world() .entity(e) .get::() .unwrap() diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index 31ac4fca2cee3..56614f839dba8 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -26,28 +26,23 @@ use screenshot::{ use super::Msaa; -/// Token to ensure a system runs on the main thread. -#[derive(Resource, Default)] -pub struct NonSendMarker; - pub struct WindowRenderPlugin; impl Plugin for WindowRenderPlugin { fn build(&self, app: &mut App) { app.add_plugins(ScreenshotPlugin); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::() .init_resource::() - .init_non_send_resource::() .add_systems(ExtractSchedule, extract_windows) .add_systems(Render, prepare_windows.in_set(RenderSet::ManageViews)); } } fn finish(&self, app: &mut App) { - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.init_resource::(); } } @@ -213,30 +208,28 @@ impl WindowSurfaces { /// Creates and (re)configures window surfaces, and obtains a swapchain texture for rendering. /// -/// NOTE: `get_current_texture` in `prepare_windows` can take a long time if the GPU workload is -/// the performance bottleneck. This can be seen in profiles as multiple prepare-set systems all -/// taking an unusually long time to complete, and all finishing at about the same time as the -/// `prepare_windows` system. Improvements in bevy are planned to avoid this happening when it -/// should not but it will still happen as it is easy for a user to create a large GPU workload -/// relative to the GPU performance and/or CPU workload. -/// This can be caused by many reasons, but several of them are: -/// - GPU workload is more than your current GPU can manage -/// - Error / performance bug in your custom shaders -/// - wgpu was unable to detect a proper GPU hardware-accelerated device given the chosen +/// **NOTE:** `get_current_texture` (acquiring the next framebuffer) in `prepare_windows` can take +/// a long time if the GPU workload is heavy. This can be seen in profiling views with many prepare +/// systems taking an unusually long time to complete, but all finishing at around the same time +/// `prepare_windows` does. Performance improvements are planned to reduce how often this happens, +/// but it will still be possible, since it's easy to create a heavy GPU workload. +/// +/// These are some contributing factors: +/// - The GPU workload is more than your GPU can handle. +/// - There are custom shaders with an error / performance bug. +/// - wgpu could not detect a proper GPU hardware-accelerated device given the chosen /// [`Backends`](crate::settings::Backends), [`WgpuLimits`](crate::settings::WgpuLimits), -/// and/or [`WgpuFeatures`](crate::settings::WgpuFeatures). For example, on Windows currently -/// `DirectX 11` is not supported by wgpu 0.12 and so if your GPU/drivers do not support Vulkan, -/// it may be that a software renderer called "Microsoft Basic Render Driver" using `DirectX 12` -/// will be chosen and performance will be very poor. This is visible in a log message that is -/// output during renderer initialization. Future versions of wgpu will support `DirectX 11`, but -/// another alternative is to try to use [`ANGLE`](https://github.com/gfx-rs/wgpu#angle) and -/// [`Backends::GL`](crate::settings::Backends::GL) if your GPU/drivers support `OpenGL 4.3` / `OpenGL ES 3.0` or -/// later. +/// and/or [`WgpuFeatures`](crate::settings::WgpuFeatures). +/// - On Windows, DirectX 11 is not supported by wgpu 0.12, and if your GPU/drivers do not +/// support Vulkan, a software renderer called "Microsoft Basic Render Driver" using DirectX 12 +/// may be used and performance will be very poor. This will be logged as a message when the +/// renderer is initialized. Future versions of wgpu will support DirectX 11, but an +/// alternative is to try to use [`ANGLE`](https://github.com/gfx-rs/wgpu#angle) and +/// [`Backends::GL`](crate::settings::Backends::GL) if your GPU/drivers support OpenGL 4.3, +/// OpenGL ES 3.0, or later. #[allow(clippy::too_many_arguments)] pub fn prepare_windows( - // By accessing a NonSend resource, we tell the scheduler to put this system on the main thread, - // which is necessary for some OS s - _marker: NonSend, + mut main_thread: ThreadLocal, mut windows: ResMut, mut window_surfaces: ResMut, render_device: Res, @@ -252,20 +245,28 @@ pub fn prepare_windows( let surface_data = window_surfaces .surfaces .entry(window.entity) - .or_insert_with(|| unsafe { - // NOTE: On some OSes this MUST be called from the main thread. - // As of wgpu 0.15, only fallible if the given window is a HTML canvas and obtaining a WebGPU or WebGL2 context fails. - let surface = render_instance - .create_surface(&window.handle.get_handle()) - .expect("Failed to create wgpu surface"); + .or_insert_with(|| { + let surface = main_thread.run(|_| { + // SAFETY: raw window handle is valid + unsafe { + render_instance + // Some operating systems only allow dereferencing window handles in + // the *main* thread (and may panic if done in another thread). + .create_surface(&window.handle.get_handle()) + // As of wgpu 0.15, this can only fail if the window is a HTML canvas + // and obtaining a WebGPU/WebGL2 context fails. + .expect("failed to create wgpu surface") + } + }); let caps = surface.get_capabilities(&render_adapter); let formats = caps.formats; - // For future HDR output support, we'll need to request a format that supports HDR, - // but as of wgpu 0.15 that is not yet supported. - // Prefer sRGB formats for surfaces, but fall back to first available format if no sRGB formats are available. - let mut format = *formats.get(0).expect("No supported formats for surface"); + // Prefer sRGB formats, but fall back to first available format if none available. + // NOTE: To support HDR output in the future, we'll need to request a format that + // supports HDR, but as of wgpu 0.15 that is still unsupported. + let mut format = *formats.get(0).expect("no supported formats for surface"); for available_format in formats { - // Rgba8UnormSrgb and Bgra8UnormSrgb and the only sRGB formats wgpu exposes that we can use for surfaces. + // Rgba8UnormSrgb and Bgra8UnormSrgb and the only sRGB formats wgpu exposes + // that we can use for surfaces. if available_format == TextureFormat::Rgba8UnormSrgb || available_format == TextureFormat::Bgra8UnormSrgb { diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index db3a034744be0..c730c04f9530b 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -137,13 +137,13 @@ impl Plugin for ScreenshotPlugin { } fn finish(&self, app: &mut bevy_app::App) { - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.init_resource::>(); } #[cfg(feature = "bevy_ci_testing")] if app - .world + .world() .contains_resource::() { app.add_systems(bevy_app::Update, ci_testing_screenshot_at); diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 9f68a9a3b981e..9de94e398c0ff 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -72,7 +72,7 @@ impl Plugin for SpritePlugin { calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds), ); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::() .init_resource::>() @@ -100,7 +100,7 @@ impl Plugin for SpritePlugin { } fn finish(&self, app: &mut App) { - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.init_resource::(); } } diff --git a/crates/bevy_sprite/src/mesh2d/color_material.rs b/crates/bevy_sprite/src/mesh2d/color_material.rs index 8e5e39b006a00..e3c319e2abb95 100644 --- a/crates/bevy_sprite/src/mesh2d/color_material.rs +++ b/crates/bevy_sprite/src/mesh2d/color_material.rs @@ -25,13 +25,15 @@ impl Plugin for ColorMaterialPlugin { app.add_plugins(Material2dPlugin::::default()) .register_asset_reflect::(); - app.world.resource_mut::>().insert( - Handle::::default(), - ColorMaterial { - color: Color::rgb(1.0, 0.0, 1.0), - ..Default::default() - }, - ); + app.world_mut() + .resource_mut::>() + .insert( + Handle::::default(), + ColorMaterial { + color: Color::rgb(1.0, 0.0, 1.0), + ..Default::default() + }, + ); } } diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 30593c875e208..43a8f0541000b 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -141,7 +141,7 @@ where fn build(&self, app: &mut App) { app.init_asset::(); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .add_render_command::>() .init_resource::>() @@ -167,7 +167,7 @@ where } fn finish(&self, app: &mut App) { - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.init_resource::>(); } } diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 9a63de7d2eaaf..4d76ce1ed1f24 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -92,7 +92,7 @@ impl Plugin for Mesh2dRenderPlugin { ); load_internal_asset!(app, MESH2D_SHADER_HANDLE, "mesh2d.wgsl", Shader::from_wgsl); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::() .init_resource::>() @@ -114,9 +114,9 @@ impl Plugin for Mesh2dRenderPlugin { fn finish(&self, app: &mut bevy_app::App) { let mut mesh_bindings_shader_defs = Vec::with_capacity(1); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { if let Some(per_object_buffer_batch_size) = GpuArrayBuffer::::batch_size( - render_app.world.resource::(), + render_app.world().resource::(), ) { mesh_bindings_shader_defs.push(ShaderDefVal::UInt( "PER_OBJECT_BUFFER_BATCH_SIZE".into(), @@ -126,7 +126,7 @@ impl Plugin for Mesh2dRenderPlugin { render_app .insert_resource(GpuArrayBuffer::::new( - render_app.world.resource::(), + render_app.world().resource::(), )) .init_resource::(); } diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index 6d52a0d4de575..b29676f5883fc 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -13,7 +13,7 @@ multi-threaded = [] [dependencies] futures-lite = "1.4.0" -async-executor = "1.3.0" +async-executor = ">= 1.3.0, < 1.5.2" async-channel = "1.4.2" async-io = { version = "2.0.0", optional = true } async-task = "4.2.0" diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 6a1981fa6110b..3bb43a61535eb 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -105,7 +105,7 @@ impl Plugin for TextPlugin { ), ); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.add_systems( ExtractSchedule, extract_text2d_sprite.after(SpriteSystem::ExtractSprites), diff --git a/crates/bevy_time/src/lib.rs b/crates/bevy_time/src/lib.rs index d5611ec58ec36..bee22007701a4 100644 --- a/crates/bevy_time/src/lib.rs +++ b/crates/bevy_time/src/lib.rs @@ -63,11 +63,11 @@ impl Plugin for TimePlugin { #[cfg(feature = "bevy_ci_testing")] if let Some(ci_testing_config) = app - .world + .world() .get_resource::() { if let Some(frame_time) = ci_testing_config.frame_time { - app.world + app.world_mut() .insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_secs_f32( frame_time, ))); diff --git a/crates/bevy_transform/src/helper.rs b/crates/bevy_transform/src/helper.rs index d1eca1d1df160..b1be978f196c6 100644 --- a/crates/bevy_transform/src/helper.rs +++ b/crates/bevy_transform/src/helper.rs @@ -121,7 +121,7 @@ mod tests { let mut entity = None; for transform in transforms { - let mut e = app.world.spawn(TransformBundle::from(transform)); + let mut e = app.world_mut().spawn(TransformBundle::from(transform)); if let Some(entity) = entity { e.set_parent(entity); @@ -134,10 +134,10 @@ mod tests { app.update(); - let transform = *app.world.get::(leaf_entity).unwrap(); + let transform = *app.world().get::(leaf_entity).unwrap(); - let mut state = SystemState::::new(&mut app.world); - let helper = state.get(&app.world); + let mut state = SystemState::::new(app.world_mut()); + let helper = state.get(app.world()); let computed_transform = helper.compute_global_transform(leaf_entity).unwrap(); diff --git a/crates/bevy_transform/src/systems.rs b/crates/bevy_transform/src/systems.rs index 8f6bac916a739..a300d4f4ed28a 100644 --- a/crates/bevy_transform/src/systems.rs +++ b/crates/bevy_transform/src/systems.rs @@ -414,7 +414,7 @@ mod test { let mut child = Entity::from_raw(0); let mut grandchild = Entity::from_raw(1); let parent = app - .world + .world_mut() .spawn(( Transform::from_translation(translation), GlobalTransform::IDENTITY, @@ -432,13 +432,16 @@ mod test { app.update(); // check the `Children` structure is spawned - assert_eq!(&**app.world.get::(parent).unwrap(), &[child]); - assert_eq!(&**app.world.get::(child).unwrap(), &[grandchild]); + assert_eq!(&**app.world().get::(parent).unwrap(), &[child]); + assert_eq!( + &**app.world().get::(child).unwrap(), + &[grandchild] + ); // Note that at this point, the `GlobalTransform`s will not have updated yet, due to `Commands` delay app.update(); - let mut state = app.world.query::<&GlobalTransform>(); - for global in state.iter(&app.world) { + let mut state = app.world_mut().query::<&GlobalTransform>(); + for global in state.iter(app.world()) { assert_eq!(global, &GlobalTransform::from_translation(translation)); } } @@ -466,16 +469,16 @@ mod test { } let (temp_child, temp_grandchild) = setup_world(&mut temp); - let (child, grandchild) = setup_world(&mut app.world); + let (child, grandchild) = setup_world(app.world_mut()); assert_eq!(temp_child, child); assert_eq!(temp_grandchild, grandchild); - app.world + app.world_mut() .spawn(TransformBundle::IDENTITY) .push_children(&[child]); std::mem::swap( - &mut *app.world.get_mut::(child).unwrap(), + &mut *app.world_mut().get_mut::(child).unwrap(), &mut *temp.get_mut::(grandchild).unwrap(), ); diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index e72eb5ea777d2..35c1c2a56f84d 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -197,9 +197,8 @@ impl Plugin for UiPlugin { } fn finish(&self, app: &mut App) { - let render_app = match app.get_sub_app_mut(RenderApp) { - Ok(render_app) => render_app, - Err(_) => return, + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; }; render_app.init_resource::(); diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index f63fd1a0dc36c..2f2b8216886c5 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -64,9 +64,8 @@ pub enum RenderUiSystem { pub fn build_ui_render(app: &mut App) { load_internal_asset!(app, UI_SHADER_HANDLE, "ui.wgsl", Shader::from_wgsl); - let render_app = match app.get_sub_app_mut(RenderApp) { - Ok(render_app) => render_app, - Err(_) => return, + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; }; render_app @@ -103,7 +102,7 @@ pub fn build_ui_render(app: &mut App) { // Render graph let ui_graph_2d = get_ui_graph(render_app); let ui_graph_3d = get_ui_graph(render_app); - let mut graph = render_app.world.resource_mut::(); + let mut graph = render_app.world_mut().resource_mut::(); if let Some(graph_2d) = graph.get_sub_graph_mut(bevy_core_pipeline::core_2d::graph::NAME) { graph_2d.add_sub_graph(draw_ui_graph::NAME, ui_graph_2d); @@ -146,8 +145,8 @@ pub fn build_ui_render(app: &mut App) { } } -fn get_ui_graph(render_app: &mut App) -> RenderGraph { - let ui_pass_node = UiPassNode::new(&mut render_app.world); +fn get_ui_graph(render_app: &mut SubApp) -> RenderGraph { + let ui_pass_node = UiPassNode::new(render_app.world_mut()); let mut ui_graph = RenderGraph::default(); ui_graph.add_node(draw_ui_graph::node::UI_PASS, ui_pass_node); ui_graph diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 11885942822a4..268183a34de42 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -64,7 +64,7 @@ where app.init_asset::() .add_plugins(ExtractComponentPlugin::>::extract_visible()); - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .add_render_command::>() .init_resource::>() @@ -91,7 +91,7 @@ where } fn finish(&self, app: &mut App) { - if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.init_resource::>(); } } diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index f1e8a304c52a5..6cd8275146771 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -103,11 +103,11 @@ impl Plugin for WindowPlugin { if let Some(primary_window) = &self.primary_window { let initial_focus = app - .world + .world_mut() .spawn(primary_window.clone()) .insert(PrimaryWindow) .id(); - if let Some(mut focus) = app.world.get_resource_mut::() { + if let Some(mut focus) = app.world_mut().get_resource_mut::() { **focus = Some(initial_focus); } } diff --git a/crates/bevy_winit/src/accessibility.rs b/crates/bevy_winit/src/accessibility.rs index b0f4b6b1229a0..96e9c22118352 100644 --- a/crates/bevy_winit/src/accessibility.rs +++ b/crates/bevy_winit/src/accessibility.rs @@ -16,17 +16,20 @@ use bevy_a11y::{ActionRequest as ActionRequestWrapper, ManageAccessibilityUpdate use bevy_app::{App, Plugin, PostUpdate}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - prelude::{DetectChanges, Entity, EventReader, EventWriter}, + prelude::{DetectChanges, Entity, EventReader, EventWriter, ThreadLocal, ThreadLocalResource}, query::With, schedule::IntoSystemConfigs, - system::{NonSend, NonSendMut, Query, Res, ResMut, Resource}, + system::{Query, Res, ResMut, Resource}, }; use bevy_hierarchy::{Children, Parent}; use bevy_utils::HashMap; use bevy_window::{PrimaryWindow, Window, WindowClosed}; -/// Maps window entities to their `AccessKit` [`Adapter`]s. -#[derive(Default, Deref, DerefMut)] +/// Maps each window entity to its `AccessKit` [`Adapter`]. +/// +/// **Note:** This is a [`ThreadLocalResource`] because the macOS implementation of [`Adapter`] +/// is not [`Send`]. +#[derive(ThreadLocalResource, Default, Deref, DerefMut)] pub struct AccessKitAdapters(pub HashMap); /// Maps window entities to their respective [`WinitActionHandler`]s. @@ -45,14 +48,17 @@ impl ActionHandler for WinitActionHandler { } fn window_closed( - mut adapters: NonSendMut, mut receivers: ResMut, mut events: EventReader, + mut main_thread: ThreadLocal, ) { - for WindowClosed { window, .. } in events.read() { - adapters.remove(window); - receivers.remove(window); - } + main_thread.run(|tls| { + let mut adapters = tls.resource_mut::(); + for WindowClosed { window, .. } in events.read() { + adapters.remove(window); + receivers.remove(window); + } + }); } fn poll_receivers( @@ -75,7 +81,6 @@ fn should_update_accessibility_nodes( } fn update_accessibility_nodes( - adapters: NonSend, focus: Res, primary_window: Query<(Entity, &Window), With>, nodes: Query<( @@ -85,61 +90,65 @@ fn update_accessibility_nodes( Option<&Parent>, )>, node_entities: Query>, + mut main_thread: ThreadLocal, ) { if let Ok((primary_window_id, primary_window)) = primary_window.get_single() { - if let Some(adapter) = adapters.get(&primary_window_id) { - let should_run = focus.is_changed() || !nodes.is_empty(); - if should_run { - adapter.update_if_active(|| { - let mut to_update = vec![]; - let mut name = None; - if primary_window.focused { - let title = primary_window.title.clone(); - name = Some(title.into_boxed_str()); - } - let focus_id = (*focus).unwrap_or_else(|| primary_window_id).to_bits(); - let mut root_children = vec![]; - for (entity, node, children, parent) in &nodes { - let mut node = (**node).clone(); - if let Some(parent) = parent { - if !node_entities.contains(**parent) { + main_thread.run(|tls| { + let adapters = tls.resource::(); + if let Some(adapter) = adapters.get(&primary_window_id) { + let should_run = focus.is_changed() || !nodes.is_empty(); + if should_run { + adapter.update_if_active(|| { + let mut to_update = vec![]; + let mut name = None; + if primary_window.focused { + let title = primary_window.title.clone(); + name = Some(title.into_boxed_str()); + } + let focus_id = (*focus).unwrap_or_else(|| primary_window_id).to_bits(); + let mut root_children = vec![]; + for (entity, node, children, parent) in &nodes { + let mut node = (**node).clone(); + if let Some(parent) = parent { + if !node_entities.contains(**parent) { + root_children.push(NodeId(entity.to_bits())); + } + } else { root_children.push(NodeId(entity.to_bits())); } - } else { - root_children.push(NodeId(entity.to_bits())); - } - if let Some(children) = children { - for child in children { - if node_entities.contains(*child) { - node.push_child(NodeId(child.to_bits())); + if let Some(children) = children { + for child in children { + if node_entities.contains(*child) { + node.push_child(NodeId(child.to_bits())); + } } } + to_update.push(( + NodeId(entity.to_bits()), + node.build(&mut NodeClassSet::lock_global()), + )); + } + let mut root = NodeBuilder::new(Role::Window); + if let Some(name) = name { + root.set_name(name); } - to_update.push(( - NodeId(entity.to_bits()), - node.build(&mut NodeClassSet::lock_global()), - )); - } - let mut root = NodeBuilder::new(Role::Window); - if let Some(name) = name { - root.set_name(name); - } - root.set_children(root_children); - let root = root.build(&mut NodeClassSet::lock_global()); - let window_update = (NodeId(primary_window_id.to_bits()), root); - to_update.insert(0, window_update); - TreeUpdate { - nodes: to_update, - tree: None, - focus: NodeId(focus_id), - } - }); + root.set_children(root_children); + let root = root.build(&mut NodeClassSet::lock_global()); + let window_update = (NodeId(primary_window_id.to_bits()), root); + to_update.insert(0, window_update); + TreeUpdate { + nodes: to_update, + tree: None, + focus: NodeId(focus_id), + } + }); + } } - } + }); } } -/// Implements winit-specific `AccessKit` functionality. +/// Implements [`winit`]-specific `AccessKit` functionality. pub struct AccessibilityPlugin; impl Plugin for AccessibilityPlugin { diff --git a/crates/bevy_winit/src/converters.rs b/crates/bevy_winit/src/converters.rs index 85302ecb0d68c..c13c2a7e21fb8 100644 --- a/crates/bevy_winit/src/converters.rs +++ b/crates/bevy_winit/src/converters.rs @@ -307,3 +307,241 @@ pub fn convert_enabled_buttons(enabled_buttons: EnabledButtons) -> winit::window } window_buttons } + +#[cfg(not(target_arch = "wasm32"))] +use std::path::PathBuf; +#[cfg(not(target_arch = "wasm32"))] +use winit::dpi::{PhysicalPosition, PhysicalSize}; +#[cfg(not(target_arch = "wasm32"))] +use winit::event::{ + AxisId, DeviceEvent, DeviceId, ElementState, Ime, ModifiersState, StartCause, Touch, +}; +#[cfg(not(target_arch = "wasm32"))] +use winit::window::{Theme, WindowId}; + +#[cfg(not(target_arch = "wasm32"))] +// TODO: can remove all these types when we upgrade to winit 0.29 +#[derive(Debug, PartialEq)] +#[allow(clippy::enum_variant_names)] +pub(crate) enum Event { + NewEvents(StartCause), + WindowEvent { + window_id: WindowId, + event: WindowEvent, + }, + DeviceEvent { + device_id: DeviceId, + event: DeviceEvent, + }, + UserEvent(T), + Suspended, + Resumed, + MainEventsCleared, + RedrawRequested(WindowId), + RedrawEventsCleared, + LoopDestroyed, +} + +#[cfg(not(target_arch = "wasm32"))] +#[derive(Debug, PartialEq)] +pub(crate) enum WindowEvent { + Resized(PhysicalSize), + Moved(PhysicalPosition), + CloseRequested, + Destroyed, + DroppedFile(PathBuf), + HoveredFile(PathBuf), + HoveredFileCancelled, + ReceivedCharacter(char), + Focused(bool), + KeyboardInput { + device_id: DeviceId, + input: winit::event::KeyboardInput, + is_synthetic: bool, + }, + ModifiersChanged(ModifiersState), + Ime(Ime), + CursorMoved { + device_id: DeviceId, + position: PhysicalPosition, + }, + + CursorEntered { + device_id: DeviceId, + }, + CursorLeft { + device_id: DeviceId, + }, + MouseWheel { + device_id: DeviceId, + delta: winit::event::MouseScrollDelta, + phase: winit::event::TouchPhase, + }, + MouseInput { + device_id: DeviceId, + state: ElementState, + button: winit::event::MouseButton, + }, + TouchpadMagnify { + device_id: DeviceId, + delta: f64, + phase: winit::event::TouchPhase, + }, + SmartMagnify { + device_id: DeviceId, + }, + TouchpadRotate { + device_id: DeviceId, + delta: f32, + phase: winit::event::TouchPhase, + }, + TouchpadPressure { + device_id: DeviceId, + pressure: f32, + stage: i64, + }, + AxisMotion { + device_id: DeviceId, + axis: AxisId, + value: f64, + }, + Touch(Touch), + ScaleFactorChanged { + scale_factor: f64, + new_inner_size: PhysicalSize, + }, + ThemeChanged(Theme), + Occluded(bool), +} + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn convert_event(event: winit::event::Event<'_, T>) -> Event { + match event { + winit::event::Event::NewEvents(start_cause) => Event::NewEvents(start_cause), + winit::event::Event::WindowEvent { window_id, event } => Event::WindowEvent { + window_id, + event: convert_window_event(event), + }, + winit::event::Event::DeviceEvent { device_id, event } => { + Event::DeviceEvent { device_id, event } + } + winit::event::Event::UserEvent(value) => Event::UserEvent(value), + winit::event::Event::Suspended => Event::Suspended, + winit::event::Event::Resumed => Event::Resumed, + winit::event::Event::MainEventsCleared => Event::MainEventsCleared, + winit::event::Event::RedrawRequested(window_id) => Event::RedrawRequested(window_id), + winit::event::Event::RedrawEventsCleared => Event::RedrawEventsCleared, + winit::event::Event::LoopDestroyed => Event::LoopDestroyed, + } +} + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn convert_window_event(event: winit::event::WindowEvent<'_>) -> WindowEvent { + match event { + winit::event::WindowEvent::AxisMotion { + device_id, + axis, + value, + } => WindowEvent::AxisMotion { + device_id, + axis, + value, + }, + winit::event::WindowEvent::CloseRequested => WindowEvent::CloseRequested, + winit::event::WindowEvent::CursorEntered { device_id } => { + WindowEvent::CursorEntered { device_id } + } + winit::event::WindowEvent::CursorLeft { device_id } => { + WindowEvent::CursorLeft { device_id } + } + winit::event::WindowEvent::CursorMoved { + device_id, + position, + .. + } => WindowEvent::CursorMoved { + device_id, + position, + }, + winit::event::WindowEvent::Destroyed => WindowEvent::Destroyed, + winit::event::WindowEvent::DroppedFile(path_buf) => WindowEvent::DroppedFile(path_buf), + winit::event::WindowEvent::Focused(b) => WindowEvent::Focused(b), + winit::event::WindowEvent::HoveredFile(path_buf) => WindowEvent::HoveredFile(path_buf), + winit::event::WindowEvent::HoveredFileCancelled => WindowEvent::HoveredFileCancelled, + winit::event::WindowEvent::Ime(ime) => WindowEvent::Ime(ime), + winit::event::WindowEvent::KeyboardInput { + device_id, + input, + is_synthetic, + } => WindowEvent::KeyboardInput { + device_id, + input, + is_synthetic, + }, + winit::event::WindowEvent::ModifiersChanged(modifiers_state) => { + WindowEvent::ModifiersChanged(modifiers_state) + } + winit::event::WindowEvent::MouseInput { + device_id, + state, + button, + .. + } => WindowEvent::MouseInput { + device_id, + state, + button, + }, + winit::event::WindowEvent::MouseWheel { + device_id, + delta, + phase, + .. + } => WindowEvent::MouseWheel { + device_id, + delta, + phase, + }, + winit::event::WindowEvent::Moved(new_position) => WindowEvent::Moved(new_position), + winit::event::WindowEvent::Occluded(b) => WindowEvent::Occluded(b), + winit::event::WindowEvent::ReceivedCharacter(char) => WindowEvent::ReceivedCharacter(char), + winit::event::WindowEvent::Resized(new_size) => WindowEvent::Resized(new_size), + winit::event::WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + } => WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size: *new_inner_size, + }, + winit::event::WindowEvent::SmartMagnify { device_id } => { + WindowEvent::SmartMagnify { device_id } + } + winit::event::WindowEvent::ThemeChanged(theme) => WindowEvent::ThemeChanged(theme), + winit::event::WindowEvent::Touch(touch) => WindowEvent::Touch(touch), + winit::event::WindowEvent::TouchpadMagnify { + device_id, + delta, + phase, + } => WindowEvent::TouchpadMagnify { + device_id, + delta, + phase, + }, + winit::event::WindowEvent::TouchpadPressure { + device_id, + pressure, + stage, + } => WindowEvent::TouchpadPressure { + device_id, + pressure, + stage, + }, + winit::event::WindowEvent::TouchpadRotate { + device_id, + delta, + phase, + } => WindowEvent::TouchpadRotate { + device_id, + delta, + phase, + }, + } +} diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 91c9858c79293..364f45ab21f18 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -1,11 +1,11 @@ #![allow(clippy::type_complexity)] #![warn(missing_docs)] -//! `bevy_winit` provides utilities to handle window creation and the eventloop through [`winit`] +//! `bevy_winit` provides utilities to create and manage windows through [`winit`] //! -//! Most commonly, the [`WinitPlugin`] is used as part of -//! [`DefaultPlugins`](https://docs.rs/bevy/latest/bevy/struct.DefaultPlugins.html). -//! The app's [runner](bevy_app::App::runner) is set by `WinitPlugin` and handles the `winit` [`EventLoop`]. -//! See `winit_runner` for details. +//! The [`WinitPlugin`] is one of the [`DefaultPlugins`]. It registers an [`App`] +//! runner that manages the [`App`] using an [`EventLoop`](winit::event_loop::EventLoop). +//! +//! [`DefaultPlugins`]: https://docs.rs/bevy/latest/bevy/struct.DefaultPlugins.html pub mod accessibility; mod converters; @@ -15,51 +15,36 @@ mod web_resize; mod winit_config; mod winit_windows; -use bevy_a11y::AccessibilityRequested; -use system::{changed_windows, create_windows, despawn_windows, CachedWindow}; +use accessibility::AccessibilityPlugin; +use runner::*; +use system::{changed_windows, create_windows, despawn_windows}; +#[cfg(target_arch = "wasm32")] +use web_resize::CanvasParentResizePlugin; pub use winit_config::*; pub use winit_windows::*; -use bevy_app::{App, AppExit, Last, Plugin, PluginsState}; -use bevy_ecs::event::{Events, ManualEventReader}; +use winit::event_loop::EventLoopBuilder; +#[cfg(target_os = "android")] +pub use winit::platform::android::activity::AndroidApp; + +use bevy_app::{App, AppEvent, Last, Plugin}; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; -use bevy_ecs::system::{SystemParam, SystemState}; +use bevy_ecs::system::SystemParam; use bevy_input::{ keyboard::KeyboardInput, - mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, + mouse::{MouseButtonInput, MouseMotion, MouseWheel}, touch::TouchInput, touchpad::{TouchpadMagnify, TouchpadRotate}, }; -use bevy_math::{ivec2, DVec2, Vec2}; -#[cfg(not(target_arch = "wasm32"))] -use bevy_tasks::tick_global_task_pools_on_main_thread; -use bevy_utils::{ - tracing::{trace, warn}, - Duration, Instant, -}; +use bevy_utils::{tracing::warn, Instant}; use bevy_window::{ exit_on_all_closed, ApplicationLifetime, CursorEntered, CursorLeft, CursorMoved, - FileDragAndDrop, Ime, ReceivedCharacter, RequestRedraw, Window, - WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, WindowDestroyed, - WindowFocused, WindowMoved, WindowResized, WindowScaleFactorChanged, WindowThemeChanged, -}; -#[cfg(target_os = "android")] -use bevy_window::{PrimaryWindow, RawHandleWrapper}; - -#[cfg(target_os = "android")] -pub use winit::platform::android::activity::AndroidApp; - -use winit::{ - event::{self, DeviceEvent, Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopWindowTarget}, + FileDragAndDrop, Ime, ReceivedCharacter, WindowBackendScaleFactorChanged, WindowCloseRequested, + WindowDestroyed, WindowFocused, WindowMoved, WindowResized, WindowScaleFactorChanged, + WindowThemeChanged, }; -use crate::accessibility::{AccessKitAdapters, AccessibilityPlugin, WinitActionHandlers}; - -use crate::converters::convert_winit_theme; -#[cfg(target_arch = "wasm32")] -use crate::web_resize::{CanvasParentResizeEventChannel, CanvasParentResizePlugin}; - /// [`AndroidApp`] provides an interface to query the application state as well as monitor events /// (for example lifecycle and input events). #[cfg(target_os = "android")] @@ -68,9 +53,11 @@ pub static ANDROID_APP: std::sync::OnceLock = std::sync::OnceLock::n /// A [`Plugin`] that uses `winit` to create and manage windows, and receive window and input /// events. /// -/// This plugin will add systems and resources that sync with the `winit` backend and also -/// replace the existing [`App`] runner with one that constructs an [event loop](EventLoop) to -/// receive window and input events from the OS. +/// This plugin will add systems and resources that sync with the `winit` backend and also replace +/// the exising [`App`] runner with one that constructs an [event loop] to receive window and input +/// events from the OS. +/// +/// [event loop]: winit::event_loop::EventLoop #[derive(Default)] pub struct WinitPlugin { /// Allows the window (and the event loop) to be created on any thread @@ -87,7 +74,8 @@ pub struct WinitPlugin { impl Plugin for WinitPlugin { fn build(&self, app: &mut App) { - let mut event_loop_builder = EventLoopBuilder::<()>::with_user_event(); + // setup event loop + let mut event_loop_builder = EventLoopBuilder::::with_user_event(); // This is needed because the features checked in the inner // block might be enabled on other platforms than linux. @@ -128,17 +116,25 @@ impl Plugin for WinitPlugin { ); } + let event_loop = crate::EventLoop::new(event_loop_builder.build()); + + #[cfg(not(target_arch = "wasm32"))] + app.init_resource::(); + + // setup app app.init_non_send_resource::() .init_resource::() .set_runner(winit_runner) .add_systems( Last, ( - // `exit_on_all_closed` only checks if windows exist but doesn't access data, - // so we don't need to care about its ordering relative to `changed_windows` + // `exit_on_all_closed` seemingly conflicts with `changed_windows` + // but does not actually access any data that would alias (only metadata) changed_windows.ambiguous_with(exit_on_all_closed), despawn_windows, + create_windows::, ) + // apply all changes before despawning windows for consistent event ordering .chain(), ); @@ -147,90 +143,47 @@ impl Plugin for WinitPlugin { #[cfg(target_arch = "wasm32")] app.add_plugins(CanvasParentResizePlugin); - let event_loop = event_loop_builder.build(); - - // iOS, macOS, and Android don't like it if you create windows before the event loop is - // initialized. + // iOS, macOS, and Android don't like it if you create windows before the + // event loop is initialized. // // See: // - https://github.com/rust-windowing/winit/blob/master/README.md#macos // - https://github.com/rust-windowing/winit/blob/master/README.md#ios #[cfg(not(any(target_os = "android", target_os = "ios", target_os = "macos")))] { - // Otherwise, we want to create a window before `bevy_render` initializes the renderer - // so that we have a surface to use as a hint. This improves compatibility with `wgpu` - // backends, especially WASM/WebGL2. - #[cfg(not(target_arch = "wasm32"))] - let mut create_window_system_state: SystemState<( - Commands, - Query<(Entity, &mut Window)>, - EventWriter, - NonSendMut, - NonSendMut, - ResMut, - ResMut, - )> = SystemState::from_world(&mut app.world); - - #[cfg(target_arch = "wasm32")] - let mut create_window_system_state: SystemState<( - Commands, - Query<(Entity, &mut Window)>, - EventWriter, - NonSendMut, - NonSendMut, - ResMut, - ResMut, - ResMut, - )> = SystemState::from_world(&mut app.world); + app.insert_tls_channel(); - #[cfg(not(target_arch = "wasm32"))] - let ( - commands, - mut windows, - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - ) = create_window_system_state.get_mut(&mut app.world); - - #[cfg(target_arch = "wasm32")] - let ( - commands, - mut windows, - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - event_channel, - ) = create_window_system_state.get_mut(&mut app.world); - - create_windows( - &event_loop, - commands, - windows.iter_mut(), - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - #[cfg(target_arch = "wasm32")] - event_channel, - ); + app.tls + .insert_resource(crate::EventLoopWindowTarget::new(&event_loop)); + + let world = app.sub_apps.main.world_mut(); + + // Otherwise, create a window before `bevy_render` initializes + // the renderer, so that we have a surface to use as a hint. + // This improves compatibility with wgpu backends, especially WASM/WebGL2. + let mut create_windows = IntoSystem::into_system(create_windows::); + create_windows.initialize(world); + create_windows.run((), world); + create_windows.apply_deferred(world); + + app.tls + .remove_resource::>(); - create_window_system_state.apply(&mut app.world); + app.remove_tls_channel(); } - // `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); } } -fn run(event_loop: EventLoop, event_handler: F) -> ! +pub(crate) fn run(event_loop: winit::event_loop::EventLoop, event_handler: F) -> ! where - F: 'static + FnMut(Event<'_, T>, &EventLoopWindowTarget, &mut ControlFlow), + F: 'static + + FnMut( + winit::event::Event<'_, T>, + &winit::event_loop::EventLoopWindowTarget, + &mut winit::event_loop::ControlFlow, + ), { event_loop.run(event_handler) } @@ -244,9 +197,13 @@ where target_os = "netbsd", target_os = "openbsd" ))] -fn run_return(event_loop: &mut EventLoop, event_handler: F) +pub(crate) fn run_return(event_loop: &mut winit::event_loop::EventLoop, event_handler: F) where - F: FnMut(Event<'_, T>, &EventLoopWindowTarget, &mut ControlFlow), + F: FnMut( + winit::event::Event<'_, T>, + &winit::event_loop::EventLoopWindowTarget, + &mut winit::event_loop::ControlFlow, + ), { use winit::platform::run_return::EventLoopExtRunReturn; event_loop.run_return(event_handler); @@ -261,9 +218,13 @@ where target_os = "netbsd", target_os = "openbsd" )))] -fn run_return(_event_loop: &mut EventLoop, _event_handler: F) +pub(crate) fn run_return(_event_loop: &mut winit::event_loop::EventLoop, _event_handler: F) where - F: FnMut(Event<'_, T>, &EventLoopWindowTarget, &mut ControlFlow), + F: FnMut( + winit::event::Event<'_, T>, + &winit::event_loop::EventLoopWindowTarget, + &mut winit::event_loop::ControlFlow, + ), { panic!("Run return is not supported on this platform!") } @@ -296,25 +257,30 @@ struct WindowAndInputEventWriters<'w> { mouse_motion: EventWriter<'w, MouseMotion>, } -/// Persistent state that is used to run the [`App`] according to the current -/// [`UpdateMode`]. +/// Persistent state that is used to run the [`App`] according to the current [`UpdateMode`]. struct WinitAppRunnerState { /// Current active state of the app. active: ActiveState, - /// Is `true` if a new [`WindowEvent`] has been received since the last update. + /// Is `true` if active state just went from `NotYetStarted` to `Active`. + just_started: bool, + /// Is `true` if a new [`WindowEvent`](winit::event::WindowEvent) has been received since the + /// last update. window_event_received: bool, - /// Is `true` if the app has requested a redraw since the last update. + #[cfg(not(target_arch = "wasm32"))] + /// Is `true` if a new [`DeviceEvent`](winit::event::DeviceEvent) has been received. + device_event_received: bool, + /// Is `true` if the app has requested a redraw. redraw_requested: bool, - /// Is `true` if enough time has elapsed since `last_update` to run another update. + /// Is `true` if enough time has elapsed since `last_update`. wait_elapsed: bool, - /// The time the last update started. + /// The time the most recent update started. last_update: Instant, /// The time the next update is scheduled to start. scheduled_update: Option, } #[derive(PartialEq, Eq)] -enum ActiveState { +pub(crate) enum ActiveState { NotYetStarted, Active, Suspended, @@ -323,7 +289,7 @@ enum ActiveState { impl ActiveState { #[inline] - fn should_run(&self) -> bool { + pub(crate) fn should_run(&self) -> bool { match self { ActiveState::NotYetStarted | ActiveState::Suspended => false, ActiveState::Active | ActiveState::WillSuspend => true, @@ -335,7 +301,10 @@ impl Default for WinitAppRunnerState { fn default() -> Self { Self { active: ActiveState::NotYetStarted, + just_started: false, window_event_received: false, + #[cfg(not(target_arch = "wasm32"))] + device_event_received: false, redraw_requested: false, wait_elapsed: false, last_update: Instant::now(), @@ -344,574 +313,1505 @@ impl Default for WinitAppRunnerState { } } -/// The default [`App::runner`] for the [`WinitPlugin`] plugin. +#[derive(ThreadLocalResource, Deref, DerefMut)] +pub(crate) struct EventLoop(winit::event_loop::EventLoop); + +impl EventLoop { + pub fn new(value: winit::event_loop::EventLoop) -> Self { + Self(value) + } + + pub fn into_inner(self) -> winit::event_loop::EventLoop { + self.0 + } +} + +/// [`EventLoopWindowTarget`] that systems can access through [`ThreadLocal`]. /// -/// Overriding the app's [runner](bevy_app::App::runner) while using `WinitPlugin` will bypass the -/// `EventLoop`. -pub fn winit_runner(mut app: App) { - let mut event_loop = app - .world - .remove_non_send_resource::>() - .unwrap(); +/// [`EventLoopWindowTarget`]: winit::event_loop::EventLoopWindowTarget +// +// SAFETY: This type cannot be made `pub`. If it was `pub`, user code could overwrite one whose +// pointer is valid with one whose pointer is invalid, which would break safety invariants. +#[derive(ThreadLocalResource, Deref)] +pub(crate) struct EventLoopWindowTarget { + ptr: *const winit::event_loop::EventLoopWindowTarget, +} - let return_from_run = app.world.resource::().return_from_run; +impl EventLoopWindowTarget { + pub(crate) fn new(target: &winit::event_loop::EventLoopWindowTarget) -> Self { + Self { + ptr: target as *const _, + } + } - app.world - .insert_non_send_resource(event_loop.create_proxy()); + /// Returns a reference to the [`EventLoopWindowTarget`]. + /// + /// # Safety + /// + /// The target pointer must be valid. That means the [`EventLoop`] used to create the target + /// must still exist and must not have moved. + /// + /// [`EventLoopWindowTarget`]: winit::event_loop::EventLoopWindowTarget + /// [`EventLoop`]: winit::event_loop::EventLoop + pub unsafe fn get(&self) -> &'_ winit::event_loop::EventLoopWindowTarget { + &*self.ptr + } +} - let mut runner_state = WinitAppRunnerState::default(); +#[cfg(target_arch = "wasm32")] +mod runner { + #[cfg(target_os = "android")] + use crate::accessibility::WinitActionHandlers; + use crate::{ + accessibility::AccessKitAdapters, converters, run, run_return, system::CachedWindow, + ActiveState, UpdateMode, WindowAndInputEventWriters, WinitAppRunnerState, WinitSettings, + WinitWindows, + }; - // prepare structures to access data in the world - let mut app_exit_event_reader = ManualEventReader::::default(); - let mut redraw_event_reader = ManualEventReader::::default(); + #[cfg(target_os = "android")] + use bevy_a11y::AccessibilityRequested; + use bevy_app::{App, AppEvent, AppExit, PluginsState}; + use bevy_ecs::{event::ManualEventReader, prelude::*, system::SystemState}; + use bevy_input::{ + mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, + touchpad::{TouchpadMagnify, TouchpadRotate}, + }; + use bevy_math::{ivec2, DVec2, Vec2}; + use bevy_utils::{ + tracing::{trace, warn}, + Duration, Instant, + }; + use bevy_window::{ + ApplicationLifetime, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, + ReceivedCharacter, RequestRedraw, Window, WindowBackendScaleFactorChanged, + WindowCloseRequested, WindowDestroyed, WindowFocused, WindowMoved, WindowResized, + WindowScaleFactorChanged, WindowThemeChanged, + }; + #[cfg(target_os = "android")] + use bevy_window::{PrimaryWindow, RawHandleWrapper}; - let mut focused_windows_state: SystemState<(Res, Query<&Window>)> = - SystemState::new(&mut app.world); + use winit::{event::StartCause, event_loop::ControlFlow}; - let mut event_writer_system_state: SystemState<( - WindowAndInputEventWriters, - NonSend, - Query<(&mut Window, &mut CachedWindow)>, - NonSend, - )> = SystemState::new(&mut app.world); + /// The default [`App::runner`] for the [`WinitPlugin`](super::WinitPlugin). + pub(crate) fn winit_runner(mut app: App) { + let return_from_run = app.world().resource::().return_from_run; + let mut event_loop = app + .tls + .remove_resource::>() + .unwrap() + .into_inner(); - #[cfg(not(target_arch = "wasm32"))] - let mut create_window_system_state: SystemState<( - Commands, - Query<(Entity, &mut Window), Added>, - EventWriter, - NonSendMut, - NonSendMut, - ResMut, - ResMut, - )> = SystemState::from_world(&mut app.world); - - #[cfg(target_arch = "wasm32")] - let mut create_window_system_state: SystemState<( - Commands, - Query<(Entity, &mut Window), Added>, - EventWriter, - NonSendMut, - NonSendMut, - ResMut, - ResMut, - ResMut, - )> = SystemState::from_world(&mut app.world); - - // setup up the event loop - let event_handler = move |event: Event<()>, - event_loop: &EventLoopWindowTarget<()>, - control_flow: &mut ControlFlow| { - #[cfg(feature = "trace")] - let _span = bevy_utils::tracing::info_span!("winit event_handler").entered(); - - if app.plugins_state() != PluginsState::Cleaned { - if app.plugins_state() != PluginsState::Ready { - #[cfg(not(target_arch = "wasm32"))] - tick_global_task_pools_on_main_thread(); - } else { - app.finish(); - app.cleanup(); - } + let mut runner_state = WinitAppRunnerState::default(); - if let Some(app_exit_events) = app.world.get_resource::>() { - if app_exit_event_reader.read(app_exit_events).last().is_some() { - *control_flow = ControlFlow::Exit; - return; - } - } - } + // prepare structures to access data in the world + let mut event_writer_system_state: SystemState<( + WindowAndInputEventWriters, + Query<(&mut Window, &mut CachedWindow)>, + )> = SystemState::new(app.world_mut()); - match event { - event::Event::NewEvents(start_cause) => match start_cause { - StartCause::Init => { - #[cfg(any(target_os = "ios", target_os = "macos"))] - { - #[cfg(not(target_arch = "wasm32"))] - let ( - commands, - mut windows, - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - ) = create_window_system_state.get_mut(&mut app.world); - - #[cfg(target_arch = "wasm32")] - let ( - commands, - mut windows, - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - event_channel, - ) = create_window_system_state.get_mut(&mut app.world); - - create_windows( - event_loop, - commands, - windows.iter_mut(), - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - #[cfg(target_arch = "wasm32")] - event_channel, - ); + #[cfg(target_os = "android")] + let mut create_window_system_state: SystemState<( + ResMut, + ResMut, + )> = SystemState::from_world(app.world_mut()); + + let mut app_exit_event_reader = ManualEventReader::::default(); + let mut redraw_event_reader = ManualEventReader::::default(); + + let mut focused_windows_state: SystemState<(Res, Query<&Window>)> = + SystemState::from_world(app.world_mut()); + + let event_cb = move |event: winit::event::Event, + event_loop: &winit::event_loop::EventLoopWindowTarget, + control_flow: &mut ControlFlow| { + #[cfg(feature = "trace")] + let _span = bevy_utils::tracing::info_span!("winit event_handler").entered(); + + let plugins_state = app.plugins_state(); + if plugins_state != PluginsState::Cleaned { + if plugins_state == PluginsState::Ready { + app.finish(); + app.cleanup(); + } - create_window_system_state.apply(&mut app.world); + if let Some(app_exit_events) = app.world().get_resource::>() { + if app_exit_event_reader.read(app_exit_events).last().is_some() { + *control_flow = ControlFlow::Exit; + return; } } - _ => { - if let Some(t) = runner_state.scheduled_update { - let now = Instant::now(); - let remaining = t.checked_duration_since(now).unwrap_or(Duration::ZERO); - runner_state.wait_elapsed = remaining.is_zero(); + + *control_flow = ControlFlow::Poll; + } + + app.tls + .insert_resource(crate::EventLoopWindowTarget::new(event_loop)); + + match event { + winit::event::Event::NewEvents(start_cause) => match start_cause { + StartCause::Init => { + #[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))] + { + let mut create_windows = + IntoSystem::into_system(create_windows::); + create_windows.initialize(app.world_mut()); + create_windows.run((), app.world_mut()); + create_windows.apply_deferred(app.world_mut()); + } } - } - }, - event::Event::WindowEvent { - event, window_id, .. - } => { - let (mut event_writers, winit_windows, mut windows, access_kit_adapters) = - event_writer_system_state.get_mut(&mut app.world); - - let Some(window_entity) = winit_windows.get_window_entity(window_id) else { - warn!( - "Skipped event {:?} for unknown winit Window Id {:?}", - event, window_id - ); - return; - }; - - let Ok((mut window, mut cache)) = windows.get_mut(window_entity) else { - warn!( - "Window {:?} is missing `Window` component, skipping event {:?}", - window_entity, event - ); - return; - }; - - // Allow AccessKit to respond to `WindowEvent`s before they reach - // the engine. - if let Some(adapter) = access_kit_adapters.get(&window_entity) { - if let Some(window) = winit_windows.get_window(window_entity) { - // Somewhat surprisingly, this call has meaningful side effects - // See https://github.com/AccessKit/accesskit/issues/300 - // AccessKit might later need to filter events based on this, but we currently do not. - // See https://github.com/bevyengine/bevy/pull/10239#issuecomment-1775572176 - let _ = adapter.on_event(window, &event); + _ => { + if let Some(next) = runner_state.scheduled_update { + let now = Instant::now(); + let remaining = + next.checked_duration_since(now).unwrap_or(Duration::ZERO); + runner_state.wait_elapsed = remaining.is_zero(); + } } - } + }, + winit::event::Event::WindowEvent { + window_id, event, .. + } => 'window_event: { + let (mut event_writers, mut windows) = + event_writer_system_state.get_mut(app.sub_apps.main.world_mut()); + + let Some(window_entity) = app.tls.run(|tls| { + let winit_windows = tls.resource::(); + winit_windows.get_window_entity(window_id) + }) else { + warn!( + "Skipped event {:?} for unknown winit Window Id {:?}", + event, window_id + ); + break 'window_event; + }; - runner_state.window_event_received = true; + let Ok((mut window, mut cache)) = windows.get_mut(window_entity) else { + warn!( + "Window {:?} is missing `Window` component, skipping event {:?}", + window_entity, event + ); + break 'window_event; + }; - match event { - WindowEvent::Resized(size) => { - window - .resolution - .set_physical_resolution(size.width, size.height); + app.tls.run(|tls| { + let access_kit_adapters = tls.resource::(); + let winit_windows = tls.resource::(); + + // Allow AccessKit to respond to `WindowEvent`s before they reach the engine. + if let Some(adapter) = access_kit_adapters.get(&window_entity) { + if let Some(window) = winit_windows.get_window(window_entity) { + // Somewhat surprisingly, this call has meaningful side effects + // See https://github.com/AccessKit/accesskit/issues/300 + // AccessKit might later need to filter events based on this, but we currently do not. + // See https://github.com/bevyengine/bevy/pull/10239#issuecomment-1775572176 + let _ = adapter.on_event(window, &event); + } + } + }); - event_writers.window_resized.send(WindowResized { - window: window_entity, - width: window.width(), - height: window.height(), - }); - } - WindowEvent::CloseRequested => { - event_writers - .window_close_requested - .send(WindowCloseRequested { + runner_state.window_event_received = true; + + match event { + winit::event::WindowEvent::Resized(size) => { + window + .resolution + .set_physical_resolution(size.width, size.height); + + event_writers.window_resized.send(WindowResized { window: window_entity, + width: window.width(), + height: window.height(), }); - } - WindowEvent::KeyboardInput { ref input, .. } => { - event_writers - .keyboard_input - .send(converters::convert_keyboard_input(input, window_entity)); - } - WindowEvent::CursorMoved { position, .. } => { - let physical_position = DVec2::new(position.x, position.y); - window.set_physical_cursor_position(Some(physical_position)); - event_writers.cursor_moved.send(CursorMoved { - window: window_entity, - position: (physical_position / window.resolution.scale_factor()) - .as_vec2(), - }); - } - WindowEvent::CursorEntered { .. } => { - event_writers.cursor_entered.send(CursorEntered { - window: window_entity, - }); - } - WindowEvent::CursorLeft { .. } => { - window.set_physical_cursor_position(None); - event_writers.cursor_left.send(CursorLeft { - window: window_entity, - }); - } - WindowEvent::MouseInput { state, button, .. } => { - event_writers.mouse_button_input.send(MouseButtonInput { - button: converters::convert_mouse_button(button), - state: converters::convert_element_state(state), - window: window_entity, - }); - } - WindowEvent::TouchpadMagnify { delta, .. } => { - event_writers - .touchpad_magnify_input - .send(TouchpadMagnify(delta as f32)); - } - WindowEvent::TouchpadRotate { delta, .. } => { - event_writers - .touchpad_rotate_input - .send(TouchpadRotate(delta)); - } - WindowEvent::MouseWheel { delta, .. } => match delta { - event::MouseScrollDelta::LineDelta(x, y) => { - event_writers.mouse_wheel_input.send(MouseWheel { - unit: MouseScrollUnit::Line, - x, - y, + } + winit::event::WindowEvent::CloseRequested => { + event_writers + .window_close_requested + .send(WindowCloseRequested { + window: window_entity, + }); + } + winit::event::WindowEvent::KeyboardInput { ref input, .. } => { + event_writers + .keyboard_input + .send(converters::convert_keyboard_input(input, window_entity)); + } + winit::event::WindowEvent::CursorMoved { position, .. } => { + let physical_position = DVec2::new(position.x, position.y); + window.set_physical_cursor_position(Some(physical_position)); + event_writers.cursor_moved.send(CursorMoved { window: window_entity, + position: (physical_position / window.resolution.scale_factor()) + .as_vec2(), }); } - event::MouseScrollDelta::PixelDelta(p) => { - event_writers.mouse_wheel_input.send(MouseWheel { - unit: MouseScrollUnit::Pixel, - x: p.x as f32, - y: p.y as f32, + winit::event::WindowEvent::CursorEntered { .. } => { + event_writers.cursor_entered.send(CursorEntered { window: window_entity, }); } - }, - WindowEvent::Touch(touch) => { - let location = touch.location.to_logical(window.resolution.scale_factor()); - event_writers - .touch_input - .send(converters::convert_touch_input(touch, location)); - } - WindowEvent::ReceivedCharacter(char) => { - event_writers.character_input.send(ReceivedCharacter { - window: window_entity, - char, - }); - } - WindowEvent::ScaleFactorChanged { - scale_factor, - new_inner_size, - } => { - event_writers.window_backend_scale_factor_changed.send( - WindowBackendScaleFactorChanged { + winit::event::WindowEvent::CursorLeft { .. } => { + window.set_physical_cursor_position(None); + event_writers.cursor_left.send(CursorLeft { window: window_entity, - scale_factor, - }, - ); - - let prior_factor = window.resolution.scale_factor(); - window.resolution.set_scale_factor(scale_factor); - let new_factor = window.resolution.scale_factor(); - - if let Some(forced_factor) = window.resolution.scale_factor_override() { - // This window is overriding the OS-suggested DPI, so its physical size - // should be set based on the overriding value. Its logical size already - // incorporates any resize constraints. - *new_inner_size = - winit::dpi::LogicalSize::new(window.width(), window.height()) - .to_physical::(forced_factor); - } else if approx::relative_ne!(new_factor, prior_factor) { - event_writers.window_scale_factor_changed.send( - WindowScaleFactorChanged { + }); + } + winit::event::WindowEvent::MouseInput { state, button, .. } => { + event_writers.mouse_button_input.send(MouseButtonInput { + button: converters::convert_mouse_button(button), + state: converters::convert_element_state(state), + window: window_entity, + }); + } + winit::event::WindowEvent::TouchpadMagnify { delta, .. } => { + event_writers + .touchpad_magnify_input + .send(TouchpadMagnify(delta as f32)); + } + winit::event::WindowEvent::TouchpadRotate { delta, .. } => { + event_writers + .touchpad_rotate_input + .send(TouchpadRotate(delta)); + } + winit::event::WindowEvent::MouseWheel { delta, .. } => match delta { + winit::event::MouseScrollDelta::LineDelta(x, y) => { + event_writers.mouse_wheel_input.send(MouseWheel { + unit: MouseScrollUnit::Line, + x, + y, + window: window_entity, + }); + } + winit::event::MouseScrollDelta::PixelDelta(p) => { + event_writers.mouse_wheel_input.send(MouseWheel { + unit: MouseScrollUnit::Pixel, + x: p.x as f32, + y: p.y as f32, + window: window_entity, + }); + } + }, + winit::event::WindowEvent::Touch(touch) => { + let location = + touch.location.to_logical(window.resolution.scale_factor()); + event_writers + .touch_input + .send(converters::convert_touch_input(touch, location)); + } + winit::event::WindowEvent::ReceivedCharacter(char) => { + event_writers.character_input.send(ReceivedCharacter { + window: window_entity, + char, + }); + } + winit::event::WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + } => { + event_writers.window_backend_scale_factor_changed.send( + WindowBackendScaleFactorChanged { window: window_entity, scale_factor, }, ); - } - let new_logical_width = (new_inner_size.width as f64 / new_factor) as f32; - let new_logical_height = (new_inner_size.height as f64 / new_factor) as f32; - if approx::relative_ne!(window.width(), new_logical_width) - || approx::relative_ne!(window.height(), new_logical_height) - { - event_writers.window_resized.send(WindowResized { + let prior_factor = window.resolution.scale_factor(); + window.resolution.set_scale_factor(scale_factor); + let new_factor = window.resolution.scale_factor(); + + if let Some(forced_factor) = window.resolution.scale_factor_override() { + // This window is overriding the OS-suggested DPI, so its physical + // size should be set based on the overriding value. Its logical + // size already incorporates any resize constraints. + *new_inner_size = + winit::dpi::LogicalSize::new(window.width(), window.height()) + .to_physical::(forced_factor); + } else if approx::relative_ne!(new_factor, prior_factor) { + event_writers.window_scale_factor_changed.send( + WindowScaleFactorChanged { + window: window_entity, + scale_factor, + }, + ); + } + + let new_logical_width = + (new_inner_size.width as f64 / new_factor) as f32; + let new_logical_height = + (new_inner_size.height as f64 / new_factor) as f32; + if approx::relative_ne!(window.width(), new_logical_width) + || approx::relative_ne!(window.height(), new_logical_height) + { + event_writers.window_resized.send(WindowResized { + window: window_entity, + width: new_logical_width, + height: new_logical_height, + }); + } + window.resolution.set_physical_resolution( + new_inner_size.width, + new_inner_size.height, + ); + } + winit::event::WindowEvent::Focused(focused) => { + window.focused = focused; + event_writers.window_focused.send(WindowFocused { window: window_entity, - width: new_logical_width, - height: new_logical_height, + focused, }); } - window - .resolution - .set_physical_resolution(new_inner_size.width, new_inner_size.height); - } - WindowEvent::Focused(focused) => { - window.focused = focused; - event_writers.window_focused.send(WindowFocused { - window: window_entity, - focused, - }); - } - WindowEvent::DroppedFile(path_buf) => { - event_writers - .file_drag_and_drop - .send(FileDragAndDrop::DroppedFile { + winit::event::WindowEvent::DroppedFile(path_buf) => { + event_writers + .file_drag_and_drop + .send(FileDragAndDrop::DroppedFile { + window: window_entity, + path_buf, + }); + } + winit::event::WindowEvent::HoveredFile(path_buf) => { + event_writers + .file_drag_and_drop + .send(FileDragAndDrop::HoveredFile { + window: window_entity, + path_buf, + }); + } + winit::event::WindowEvent::HoveredFileCancelled => { + event_writers.file_drag_and_drop.send( + FileDragAndDrop::HoveredFileCanceled { + window: window_entity, + }, + ); + } + winit::event::WindowEvent::Moved(position) => { + let position = ivec2(position.x, position.y); + window.position.set(position); + event_writers.window_moved.send(WindowMoved { + entity: window_entity, + position, + }); + } + winit::event::WindowEvent::Ime(event) => match event { + winit::event::Ime::Preedit(value, cursor) => { + event_writers.ime_input.send(Ime::Preedit { + window: window_entity, + value, + cursor, + }); + } + winit::event::Ime::Commit(value) => { + event_writers.ime_input.send(Ime::Commit { + window: window_entity, + value, + }) + } + winit::event::Ime::Enabled => { + event_writers.ime_input.send(Ime::Enabled { + window: window_entity, + }) + } + winit::event::Ime::Disabled => { + event_writers.ime_input.send(Ime::Disabled { + window: window_entity, + }) + } + }, + winit::event::WindowEvent::ThemeChanged(theme) => { + event_writers.window_theme_changed.send(WindowThemeChanged { window: window_entity, - path_buf, + theme: converters::convert_winit_theme(theme), }); - } - WindowEvent::HoveredFile(path_buf) => { - event_writers - .file_drag_and_drop - .send(FileDragAndDrop::HoveredFile { + } + winit::event::WindowEvent::Destroyed => { + event_writers.window_destroyed.send(WindowDestroyed { window: window_entity, - path_buf, }); + } + _ => {} } - WindowEvent::HoveredFileCancelled => { - event_writers.file_drag_and_drop.send( - FileDragAndDrop::HoveredFileCanceled { - window: window_entity, - }, - ); + + if window.is_changed() { + cache.window = window.clone(); } - WindowEvent::Moved(position) => { - let position = ivec2(position.x, position.y); - window.position.set(position); - event_writers.window_moved.send(WindowMoved { - entity: window_entity, - position, - }); + } + winit::event::Event::DeviceEvent { + event: winit::event::DeviceEvent::MouseMotion { delta: (x, y) }, + .. + } => { + let (mut event_writers, ..) = + event_writer_system_state.get_mut(app.world_mut()); + event_writers.mouse_motion.send(MouseMotion { + delta: Vec2::new(x as f32, y as f32), + }); + } + winit::event::Event::Suspended => { + let (mut event_writers, ..) = + event_writer_system_state.get_mut(app.world_mut()); + event_writers.lifetime.send(ApplicationLifetime::Suspended); + // Mark the state as `WillSuspend` so the application can react to the suspend + // before actually suspending. + runner_state.active = ActiveState::WillSuspend; + } + winit::event::Event::Resumed => { + let (mut event_writers, ..) = + event_writer_system_state.get_mut(app.world_mut()); + if runner_state.active == ActiveState::NotYetStarted { + event_writers.lifetime.send(ApplicationLifetime::Started); + } else { + event_writers.lifetime.send(ApplicationLifetime::Resumed); } - WindowEvent::Ime(event) => match event { - event::Ime::Preedit(value, cursor) => { - event_writers.ime_input.send(Ime::Preedit { - window: window_entity, - value, - cursor, - }); + + runner_state.active = ActiveState::Active; + #[cfg(target_os = "android")] + { + let mut query = app + .world_mut() + .query_filtered::<(Entity, &Window), (With, Without)>(); + + if let Ok((entity, window)) = query.get_single(app.world()) { + // Re-create the window on the backend and link it to its entity. + use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; + let window = window.clone(); + + let (mut handlers, accessibility_requested) = + create_window_system_state.get_mut(app.world_mut()); + + let tls = &mut app.tls; + + let raw_handle_wrapper = + tls.resource_scope(|tls, mut winit_windows: Mut| { + tls.resource_scope( + |tls, mut adapters: Mut| { + let winit_window = winit_windows.create_window( + event_loop, + entity, + &window, + &mut adapters, + &mut handlers, + &accessibility_requested, + ); + + RawHandleWrapper { + window_handle: winit_window.raw_window_handle(), + display_handle: winit_window.raw_display_handle(), + } + }, + ) + }); + + app.world_mut() + .entity_mut(entity) + .insert(raw_handle_wrapper); } - event::Ime::Commit(value) => event_writers.ime_input.send(Ime::Commit { - window: window_entity, - value, - }), - event::Ime::Enabled => event_writers.ime_input.send(Ime::Enabled { - window: window_entity, - }), - event::Ime::Disabled => event_writers.ime_input.send(Ime::Disabled { - window: window_entity, - }), - }, - WindowEvent::ThemeChanged(theme) => { - event_writers.window_theme_changed.send(WindowThemeChanged { - window: window_entity, - theme: convert_winit_theme(theme), - }); + + // Set the control flow to run the handler again immediately. + *control_flow = ControlFlow::Poll; } - WindowEvent::Destroyed => { - event_writers.window_destroyed.send(WindowDestroyed { - window: window_entity, - }); + } + winit::event::Event::MainEventsCleared => { + if runner_state.active.should_run() { + if runner_state.active == ActiveState::WillSuspend { + runner_state.active = ActiveState::Suspended; + #[cfg(target_os = "android")] + { + // Android sending this event invalidates the existing + // surfaces/windows. + // + // Remove and drop the `RawHandleWrapper` from the entity so + // Android will destroy the surface/window. + let mut query = app + .world_mut() + .query_filtered::>(); + let entity = query.single(app.world()); + app.world_mut() + .entity_mut(entity) + .remove::(); + } + } + + let (config, windows) = focused_windows_state.get(app.world()); + let focused = windows.iter().any(|window| window.focused); + let should_update = match config.update_mode(focused) { + UpdateMode::Continuous | UpdateMode::Reactive { .. } => { + // `Reactive`: In order for `event_handler` to have been called, + // either we received a window or raw input event, the `wait` + // elapsed, or a redraw was requested. There are no other + // conditions, so we can just return `true` here. + true + } + UpdateMode::ReactiveLowPower { .. } => { + runner_state.wait_elapsed + || runner_state.redraw_requested + || runner_state.window_event_received + } + }; + + if should_update { + // reset these on each update + runner_state.wait_elapsed = false; + runner_state.window_event_received = false; + runner_state.redraw_requested = false; + runner_state.last_update = Instant::now(); + + app.update(); + + // decide when to run the next update + let (config, windows) = focused_windows_state.get(app.world()); + let focused = windows.iter().any(|window| window.focused); + match config.update_mode(focused) { + UpdateMode::Continuous => *control_flow = ControlFlow::Poll, + UpdateMode::Reactive { wait } + | UpdateMode::ReactiveLowPower { wait } => { + if let Some(next) = runner_state.last_update.checked_add(wait) { + runner_state.scheduled_update = Some(next); + *control_flow = ControlFlow::WaitUntil(next); + } else { + runner_state.scheduled_update = None; + *control_flow = ControlFlow::Wait; + } + } + } + + if let Some(redraw_events) = + app.world().get_resource::>() + { + if redraw_event_reader.read(redraw_events).last().is_some() { + runner_state.redraw_requested = true; + *control_flow = ControlFlow::Poll; + } + } + + if runner_state.active == ActiveState::Suspended { + // Wait for a `Resume` event. + *control_flow = ControlFlow::Wait; + } + + if let Some(exit_events) = app.world().get_resource::>() + { + if app_exit_event_reader.read(exit_events).last().is_some() { + trace!("exiting app"); + *control_flow = ControlFlow::Exit; + } + } + } } - _ => {} } + _ => (), + } + + app.tls + .remove_resource::>(); + }; + + trace!("starting winit event loop"); + if return_from_run { + run_return(&mut event_loop, event_cb); + } else { + run(event_loop, event_cb); + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +mod runner { + use std::collections::VecDeque; + use std::mem; + use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe}; + use std::sync::mpsc::{ + channel, Receiver, RecvError, RecvTimeoutError, SendError, Sender, TryRecvError, + }; + + #[cfg(target_os = "android")] + use bevy_a11y::AccessibilityRequested; + use bevy_app::{App, AppEvent, AppExit, First, PluginsState, SubApps}; + #[cfg(target_os = "android")] + use bevy_ecs::system::SystemParam; + use bevy_ecs::{event::ManualEventReader, prelude::*, system::SystemState}; + use bevy_input::{ + mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, + touchpad::{TouchpadMagnify, TouchpadRotate}, + }; + use bevy_math::{ivec2, DVec2, Vec2}; + use bevy_tasks::tick_global_task_pools_on_main_thread; + use bevy_utils::{ + default, + synccell::SyncCell, + tracing::{trace, warn}, + Duration, Instant, + }; + use bevy_window::{ + ApplicationLifetime, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, + ReceivedCharacter, RequestRedraw, Window, WindowBackendScaleFactorChanged, + WindowCloseRequested, WindowDestroyed, WindowFocused, WindowMoved, WindowResized, + WindowScaleFactorChanged, WindowThemeChanged, + }; + #[cfg(target_os = "android")] + use bevy_window::{PrimaryWindow, RawHandleWrapper}; + + use winit::event_loop::ControlFlow; + + #[cfg(target_os = "android")] + use crate::accessibility::WinitActionHandlers; + use crate::{ + accessibility::AccessKitAdapters, + converters::{self, convert_event, convert_window_event}, + run, run_return, + system::CachedWindow, + ActiveState, UpdateMode, WindowAndInputEventWriters, WinitAppRunnerState, WinitSettings, + WinitWindowEntityMap, WinitWindows, + }; - if window.is_changed() { - cache.window = window.clone(); + /// Sending half of an [`Event`] channel. + pub struct WinitEventSender { + pub(crate) event_send: Sender>, + pub(crate) clear_send: Sender, + pub(crate) last_event_sent: u64, + } + + /// Receiving half of an [`Event`] channel. + #[derive(Resource)] + pub struct WinitEventReceiver { + pub(crate) event_recv: SyncCell>>, + pub(crate) clear_recv: SyncCell>, + pub(crate) processed: SyncCell>>, + pub(crate) last_event_processed: u64, + pub(crate) state: WinitAppRunnerState, + } + + /// Constructs a new [`WinitEventSender`] and [`WinitEventReceiver`] channel pair. + pub fn winit_channel() -> (WinitEventSender, WinitEventReceiver) { + let (clear_send, clear_recv) = channel(); + let (event_send, event_recv) = channel(); + let processed = VecDeque::new(); + + let sender = WinitEventSender { + clear_send, + event_send, + last_event_sent: 0, + }; + + let receiver = WinitEventReceiver { + event_recv: SyncCell::new(event_recv), + clear_recv: SyncCell::new(clear_recv), + processed: SyncCell::new(processed), + last_event_processed: 0, + state: default(), + }; + + (sender, receiver) + } + + impl WinitEventSender + where + T: Send + 'static, + { + pub(crate) fn send( + &mut self, + event: converters::Event, + ) -> Result<(), SendError>> { + self.last_event_sent = self.last_event_sent.checked_add(1).unwrap(); + self.event_send.send(event) + } + + /// Informs the receiver that there is a new batch of events to be read. + pub(crate) fn send_clear( + &mut self, + event: converters::Event, + ) -> Result<(), SendError>> { + assert!(matches!(event, converters::Event::MainEventsCleared)); + self.send(event)?; + self.clear_send.send(self.last_event_sent).unwrap(); + Ok(()) + } + } + + impl WinitEventReceiver + where + T: Send + 'static, + { + fn process_event(&mut self, event: converters::Event) { + match &event { + converters::Event::WindowEvent { .. } => { + self.state.window_event_received = true; + } + converters::Event::DeviceEvent { .. } => { + self.state.device_event_received = true; + } + converters::Event::Suspended => { + self.state.active = ActiveState::WillSuspend; } + converters::Event::Resumed => { + self.state.just_started = self.state.active == ActiveState::NotYetStarted; + self.state.active = ActiveState::Active; + } + converters::Event::RedrawRequested(_) => { + self.state.redraw_requested = true; + } + _ => (), } - event::Event::DeviceEvent { - event: DeviceEvent::MouseMotion { delta: (x, y) }, - .. - } => { - let (mut event_writers, ..) = event_writer_system_state.get_mut(&mut app.world); - event_writers.mouse_motion.send(MouseMotion { - delta: Vec2::new(x as f32, y as f32), - }); + self.last_event_processed = self.last_event_processed.checked_add(1).unwrap(); + self.processed.get().push_back(event); + } + + fn process_events_until(&mut self, clear_event: u64) { + while self.last_event_processed < clear_event { + let event = self.event_recv.get().try_recv().unwrap(); + self.process_event(event); } - event::Event::Suspended => { - let (mut event_writers, ..) = event_writer_system_state.get_mut(&mut app.world); - event_writers.lifetime.send(ApplicationLifetime::Suspended); - // Mark the state as `WillSuspend`. This will let the schedule run one last time - // before actually suspending to let the application react - runner_state.active = ActiveState::WillSuspend; + } + + pub(crate) fn recv(&mut self) -> Result<(), RecvError> { + let rx = self.clear_recv.get(); + let mut event = rx.recv()?; + while let Ok(n) = rx.try_recv() { + assert!(n > event); + event = n; } - event::Event::Resumed => { - let (mut event_writers, ..) = event_writer_system_state.get_mut(&mut app.world); - match runner_state.active { - ActiveState::NotYetStarted => { - event_writers.lifetime.send(ApplicationLifetime::Started); + self.process_events_until(event); + Ok(()) + } + + pub(crate) fn try_recv(&mut self) -> Result<(), TryRecvError> { + let rx = self.clear_recv.get(); + let mut event = rx.try_recv()?; + while let Ok(n) = rx.try_recv() { + assert!(n > event); + event = n; + } + self.process_events_until(event); + Ok(()) + } + + pub(crate) fn recv_timeout(&mut self, timeout: Duration) -> Result<(), RecvTimeoutError> { + let rx = self.clear_recv.get(); + let mut event = rx.recv_timeout(timeout)?; + while let Ok(n) = rx.try_recv() { + assert!(n > event); + event = n; + } + self.process_events_until(event); + Ok(()) + } + } + + #[cfg(target_os = "android")] + #[derive(SystemParam)] + struct ExtraAndroidParams<'w, 's> { + resume: + Query<'w, 's, Entity, (With, With, Without)>, + suspend: Query<'w, 's, Entity, With>, + handlers: ResMut<'w, WinitActionHandlers>, + accessibility_requested: Res<'w, AccessibilityRequested>, + main_thread: ThreadLocal<'w, 's>, + commands: Commands<'w, 's>, + } + + pub(crate) fn flush_winit_events( + mut queue: ResMut>, + mut windows: Query<(&mut Window, &mut CachedWindow)>, + mut event_writers: WindowAndInputEventWriters, + #[cfg(not(target_os = "android"))] map: Res, + #[cfg(target_os = "android")] mut map: ResMut, + #[cfg(target_os = "android")] mut extra: ExtraAndroidParams, + ) { + // TODO: Use system local instead? + let just_started = queue.state.just_started; + + for event in queue.processed.get().drain(..) { + match event { + crate::converters::Event::WindowEvent { + window_id, event, .. + } => { + let Some(window_entity) = map.get_window_entity(window_id) else { + warn!( + "Skipped event {:?} for unknown winit Window Id {:?}", + event, window_id + ); + continue; + }; + + let Ok((mut window, mut cache)) = windows.get_mut(window_entity) else { + warn!( + "Window {:?} is missing `Window` component, skipping event {:?}", + window_entity, event + ); + continue; + }; + + match event { + converters::WindowEvent::Resized(size) => { + window + .resolution + .set_physical_resolution(size.width, size.height); + + event_writers.window_resized.send(WindowResized { + window: window_entity, + width: window.width(), + height: window.height(), + }); + } + converters::WindowEvent::CloseRequested => { + event_writers + .window_close_requested + .send(WindowCloseRequested { + window: window_entity, + }); + } + converters::WindowEvent::KeyboardInput { ref input, .. } => { + event_writers + .keyboard_input + .send(converters::convert_keyboard_input(input, window_entity)); + } + converters::WindowEvent::CursorMoved { position, .. } => { + let physical_position = DVec2::new(position.x, position.y); + window.set_physical_cursor_position(Some(physical_position)); + event_writers.cursor_moved.send(CursorMoved { + window: window_entity, + position: (physical_position / window.resolution.scale_factor()) + .as_vec2(), + }); + } + converters::WindowEvent::CursorEntered { .. } => { + event_writers.cursor_entered.send(CursorEntered { + window: window_entity, + }); + } + converters::WindowEvent::CursorLeft { .. } => { + window.set_physical_cursor_position(None); + event_writers.cursor_left.send(CursorLeft { + window: window_entity, + }); + } + converters::WindowEvent::MouseInput { state, button, .. } => { + event_writers.mouse_button_input.send(MouseButtonInput { + button: converters::convert_mouse_button(button), + state: converters::convert_element_state(state), + window: window_entity, + }); + } + converters::WindowEvent::TouchpadMagnify { delta, .. } => { + event_writers + .touchpad_magnify_input + .send(TouchpadMagnify(delta as f32)); + } + converters::WindowEvent::TouchpadRotate { delta, .. } => { + event_writers + .touchpad_rotate_input + .send(TouchpadRotate(delta)); + } + converters::WindowEvent::MouseWheel { delta, .. } => match delta { + winit::event::MouseScrollDelta::LineDelta(x, y) => { + event_writers.mouse_wheel_input.send(MouseWheel { + unit: MouseScrollUnit::Line, + x, + y, + window: window_entity, + }); + } + winit::event::MouseScrollDelta::PixelDelta(p) => { + event_writers.mouse_wheel_input.send(MouseWheel { + unit: MouseScrollUnit::Pixel, + x: p.x as f32, + y: p.y as f32, + window: window_entity, + }); + } + }, + converters::WindowEvent::Touch(touch) => { + let location = + touch.location.to_logical(window.resolution.scale_factor()); + event_writers + .touch_input + .send(converters::convert_touch_input(touch, location)); + } + converters::WindowEvent::ReceivedCharacter(char) => { + event_writers.character_input.send(ReceivedCharacter { + window: window_entity, + char, + }); + } + converters::WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + } => { + event_writers.window_backend_scale_factor_changed.send( + WindowBackendScaleFactorChanged { + window: window_entity, + scale_factor, + }, + ); + + let prior_factor = window.resolution.scale_factor(); + window.resolution.set_scale_factor(scale_factor); + + if window.resolution.scale_factor_override().is_none() + && approx::relative_ne!(scale_factor, prior_factor) + { + event_writers.window_scale_factor_changed.send( + WindowScaleFactorChanged { + window: window_entity, + scale_factor, + }, + ); + } + + let new_factor = window.resolution.scale_factor(); + let new_logical_width = + (new_inner_size.width as f64 / new_factor) as f32; + let new_logical_height = + (new_inner_size.height as f64 / new_factor) as f32; + if approx::relative_ne!(window.width(), new_logical_width) + || approx::relative_ne!(window.height(), new_logical_height) + { + event_writers.window_resized.send(WindowResized { + window: window_entity, + width: new_logical_width, + height: new_logical_height, + }); + } + window.resolution.set_physical_resolution( + new_inner_size.width, + new_inner_size.height, + ); + } + converters::WindowEvent::Focused(focused) => { + window.focused = focused; + event_writers.window_focused.send(WindowFocused { + window: window_entity, + focused, + }); + } + converters::WindowEvent::DroppedFile(path_buf) => { + event_writers + .file_drag_and_drop + .send(FileDragAndDrop::DroppedFile { + window: window_entity, + path_buf, + }); + } + converters::WindowEvent::HoveredFile(path_buf) => { + event_writers + .file_drag_and_drop + .send(FileDragAndDrop::HoveredFile { + window: window_entity, + path_buf, + }); + } + converters::WindowEvent::HoveredFileCancelled => { + event_writers.file_drag_and_drop.send( + FileDragAndDrop::HoveredFileCanceled { + window: window_entity, + }, + ); + } + converters::WindowEvent::Moved(position) => { + let position = ivec2(position.x, position.y); + window.position.set(position); + event_writers.window_moved.send(WindowMoved { + entity: window_entity, + position, + }); + } + converters::WindowEvent::Ime(event) => match event { + winit::event::Ime::Preedit(value, cursor) => { + event_writers.ime_input.send(Ime::Preedit { + window: window_entity, + value, + cursor, + }); + } + winit::event::Ime::Commit(value) => { + event_writers.ime_input.send(Ime::Commit { + window: window_entity, + value, + }); + } + winit::event::Ime::Enabled => { + event_writers.ime_input.send(Ime::Enabled { + window: window_entity, + }); + } + winit::event::Ime::Disabled => { + event_writers.ime_input.send(Ime::Disabled { + window: window_entity, + }); + } + }, + converters::WindowEvent::ThemeChanged(theme) => { + event_writers.window_theme_changed.send(WindowThemeChanged { + window: window_entity, + theme: converters::convert_winit_theme(theme), + }); + } + converters::WindowEvent::Destroyed => { + event_writers.window_destroyed.send(WindowDestroyed { + window: window_entity, + }); + } + _ => {} } - _ => { - event_writers.lifetime.send(ApplicationLifetime::Resumed); + + if window.is_changed() { + cache.window = window.clone(); } } - runner_state.active = ActiveState::Active; - #[cfg(target_os = "android")] - { - // Get windows that are cached but without raw handles. Those window were already created, but got their - // handle wrapper removed when the app was suspended. - let mut query = app - .world - .query_filtered::<(Entity, &Window), (With, Without)>(); - if let Ok((entity, window)) = query.get_single(&app.world) { - use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; - let window = window.clone(); - - let ( - _, - _, - _, - mut winit_windows, - mut adapters, - mut handlers, - accessibility_requested, - ) = create_window_system_state.get_mut(&mut app.world); - - let winit_window = winit_windows.create_window( - event_loop, - entity, - &window, - &mut adapters, - &mut handlers, - &accessibility_requested, - ); + crate::converters::Event::DeviceEvent { + event: winit::event::DeviceEvent::MouseMotion { delta: (x, y) }, + .. + } => { + event_writers.mouse_motion.send(MouseMotion { + delta: Vec2::new(x as f32, y as f32), + }); + } + crate::converters::Event::Suspended => { + event_writers.lifetime.send(ApplicationLifetime::Suspended); + #[cfg(target_os = "android")] + { + // Android sending this event invalidates the existing surfaces/windows. + // + // Remove and drop the `RawHandleWrapper` from the entity so Android will + // destroy the surface/window. + if let Ok(entity) = extra.suspend.get_single() { + extra.commands.entity(entity).remove::(); + } + } + } + crate::converters::Event::Resumed => { + if just_started { + event_writers.lifetime.send(ApplicationLifetime::Started); + } else { + event_writers.lifetime.send(ApplicationLifetime::Resumed); + } - let wrapper = RawHandleWrapper { - window_handle: winit_window.raw_window_handle(), - display_handle: winit_window.raw_display_handle(), - }; + #[cfg(target_os = "android")] + { + if let Ok(entity) = extra.resume.get_single() { + use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; + + // Re-create the window on the backend and link it to its entity. + let (window, _) = windows.get(entity).unwrap(); + + let raw_handle_wrapper = extra.main_thread.run(|tls| { + tls.resource_scope(|tls, mut winit_windows: Mut| { + tls.resource_scope(|tls, mut adapters: Mut| { + // SAFETY: `bevy_winit` guarantees that this resource can + // only be inserted by its `App` runner and that the stored + // pointer is valid. + let event_loop = unsafe { + tls.resource::>() + .get() + }; + + let winit_window = winit_windows.create_window( + event_loop, + entity, + window, + &mut map, + &mut adapters, + &mut extra.handlers, + &extra.accessibility_requested, + ); + + RawHandleWrapper { + window_handle: winit_window.raw_window_handle(), + display_handle: winit_window.raw_display_handle(), + } + }) + }) + }); - app.world.entity_mut(entity).insert(wrapper); + // Re-insert the component. + extra.commands.entity(entity).insert(raw_handle_wrapper); + } } - *control_flow = ControlFlow::Poll; } + _ => (), } - event::Event::MainEventsCleared => { - if runner_state.active.should_run() { - if runner_state.active == ActiveState::WillSuspend { - runner_state.active = ActiveState::Suspended; - #[cfg(target_os = "android")] - { - // Remove the `RawHandleWrapper` from the primary window. - // This will trigger the surface destruction. - let mut query = - app.world.query_filtered::>(); - let entity = query.single(&app.world); - app.world.entity_mut(entity).remove::(); - *control_flow = ControlFlow::Wait; - } + } + } + + pub(crate) fn spawn_app_thread( + mut sub_apps: SubApps, + send: bevy_app::AppEventSender, + recv: bevy_app::AppEventReceiver, + event_loop_proxy: winit::event_loop::EventLoopProxy, + ) { + std::thread::Builder::new() + .name("winit-proxy-listener".to_string()) + .spawn(move || { + while let Ok(event) = recv.recv() { + // got something from the app, forward it to the event loop + if event_loop_proxy.send_event(event).is_err() { + break; + } + } + }) + .unwrap(); + + std::thread::Builder::new() + .name("app".to_string()) + .spawn(move || { + let result = catch_unwind(AssertUnwindSafe(|| { + let mut app_exit_event_reader = ManualEventReader::::default(); + let mut redraw_event_reader = ManualEventReader::::default(); + let mut focused_windows_state: SystemState<( + Res, + Query<&Window>, + )> = SystemState::from_world(sub_apps.main.world_mut()); + + let mut rx = sub_apps + .main + .world_mut() + .remove_resource::>() + .unwrap(); + + #[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))] + { + let mut create_windows = + IntoSystem::into_system(crate::system::create_windows::); + create_windows.initialize(sub_apps.main.world_mut()); + create_windows.run((), sub_apps.main.world_mut()); + create_windows.apply_deferred(sub_apps.main.world_mut()); } - let (config, windows) = focused_windows_state.get(&app.world); - let focused = windows.iter().any(|window| window.focused); - let should_update = match config.update_mode(focused) { - UpdateMode::Continuous | UpdateMode::Reactive { .. } => { - // `Reactive`: In order for `event_handler` to have been called, either - // we received a window or raw input event, the `wait` elapsed, or a - // redraw was requested (by the app or the OS). There are no other - // conditions, so we can just return `true` here. - true - } - UpdateMode::ReactiveLowPower { .. } => { - runner_state.wait_elapsed - || runner_state.redraw_requested - || runner_state.window_event_received + + let mut control_flow = ControlFlow::Poll; + loop { + let now = Instant::now(); + match control_flow { + ControlFlow::Poll => match rx.try_recv() { + Ok(_) | Err(TryRecvError::Empty) => {} + Err(TryRecvError::Disconnected) => { + trace!("terminating app because event loop disconnected"); + return; + } + }, + ControlFlow::Wait => match rx.recv() { + Ok(_) => {} + Err(_) => { + trace!("terminating app because event loop disconnected"); + return; + } + }, + ControlFlow::WaitUntil(next) => { + let timeout = + next.checked_duration_since(now).unwrap_or(Duration::ZERO); + rx.state.wait_elapsed = timeout.is_zero(); + match rx.recv_timeout(timeout) { + Ok(_) | Err(RecvTimeoutError::Timeout) => {} + Err(RecvTimeoutError::Disconnected) => { + trace!("terminating app because event loop disconnected"); + return; + } + } + } + ControlFlow::ExitWithCode(_) => { + trace!("exiting app"); + // return sub-apps to the main thread + if send.send(AppEvent::Exit(sub_apps)).is_err() { + trace!("terminating app because event loop disconnected"); + } + return; + } } - }; - if app.plugins_state() == PluginsState::Cleaned && should_update { - // reset these on each update - runner_state.wait_elapsed = false; - runner_state.window_event_received = false; - runner_state.redraw_requested = false; - runner_state.last_update = Instant::now(); + if rx.state.active.should_run() { + if rx.state.active == ActiveState::WillSuspend { + rx.state.active = ActiveState::Suspended; + #[cfg(target_os = "android")] + { + // Android sending this event invalidates the existing + // surfaces/windows. + // + // Remove and drop the `RawHandleWrapper` from the entity so + // Android will destroy the surface/window. + let mut query = sub_apps + .main + .world_mut() + .query_filtered::>(); + let entity = query.single(sub_apps.main.world()); + sub_apps + .main + .world_mut() + .entity_mut(entity) + .remove::(); + } + } + + let (config, windows) = + focused_windows_state.get(sub_apps.main.world()); + let focused = windows.iter().any(|window| window.focused); + let should_update = match config.update_mode(focused) { + UpdateMode::Continuous => true, + UpdateMode::Reactive { .. } => { + rx.state.wait_elapsed + || rx.state.redraw_requested + || rx.state.window_event_received + || rx.state.device_event_received + } + UpdateMode::ReactiveLowPower { .. } => { + rx.state.wait_elapsed + || rx.state.redraw_requested + || rx.state.window_event_received + } + }; + + if should_update { + // reset these flags + rx.state.wait_elapsed = false; + rx.state.redraw_requested = false; + rx.state.window_event_received = false; + rx.state.device_event_received = false; + rx.state.last_update = now; + + sub_apps.main.world_mut().insert_resource(rx); + sub_apps.update(); + rx = sub_apps + .main + .world_mut() + .remove_resource::>() + .unwrap(); + + // decide when to run the next update + let (config, windows) = + focused_windows_state.get(sub_apps.main.world()); + let focused = windows.iter().any(|window| window.focused); + match config.update_mode(focused) { + UpdateMode::Continuous => control_flow = ControlFlow::Poll, + UpdateMode::Reactive { wait } + | UpdateMode::ReactiveLowPower { wait } => { + if let Some(next) = rx.state.last_update.checked_add(wait) { + rx.state.scheduled_update = Some(next); + control_flow = ControlFlow::WaitUntil(next); + } else { + rx.state.scheduled_update = None; + control_flow = ControlFlow::Wait; + } + } + } + + if let Some(redraw_events) = sub_apps + .main + .world() + .get_resource::>() + { + if redraw_event_reader.read(redraw_events).last().is_some() { + rx.state.redraw_requested = true; + control_flow = ControlFlow::Poll; + } + } - app.update(); + if rx.state.active == ActiveState::Suspended { + // Wait for a `Resume` event. + control_flow = ControlFlow::Wait; + } - // decide when to run the next update - let (config, windows) = focused_windows_state.get(&app.world); - let focused = windows.iter().any(|window| window.focused); - match config.update_mode(focused) { - UpdateMode::Continuous => *control_flow = ControlFlow::Poll, - UpdateMode::Reactive { wait } - | UpdateMode::ReactiveLowPower { wait } => { - if let Some(next) = runner_state.last_update.checked_add(*wait) { - runner_state.scheduled_update = Some(next); - *control_flow = ControlFlow::WaitUntil(next); - } else { - runner_state.scheduled_update = None; - *control_flow = ControlFlow::Wait; + if let Some(exit_events) = + sub_apps.main.world().get_resource::>() + { + if app_exit_event_reader.read(exit_events).last().is_some() { + control_flow = ControlFlow::Exit; + } } } } + } + })); - if let Some(app_redraw_events) = - app.world.get_resource::>() - { - if redraw_event_reader.read(app_redraw_events).last().is_some() { - runner_state.redraw_requested = true; - *control_flow = ControlFlow::Poll; - } + if let Some(payload) = result.err() { + send.send(AppEvent::Error(payload)).unwrap(); + } + }) + .unwrap(); + } + + /// The default [`App::runner`] for the [`WinitPlugin`](super::WinitPlugin). + pub(crate) fn winit_runner(mut app: App) { + let return_from_run = app.world().resource::().return_from_run; + let mut event_loop = app + .tls + .remove_resource::>() + .unwrap() + .into_inner(); + + let event_loop_proxy = event_loop.create_proxy(); + + let mut app_exit_event_reader = ManualEventReader::::default(); + + let (mut winit_send, winit_recv) = winit_channel::(); + let mut winit_recv = Some(winit_recv); + let mut locals = None; + + enum Status { + Initializing, + Ready, + Running, + } + + let mut status = if app.plugins_state() == PluginsState::Cleaned { + Status::Ready + } else { + Status::Initializing + }; + + let event_cb = move |event: winit::event::Event, + event_loop: &winit::event_loop::EventLoopWindowTarget, + control_flow: &mut ControlFlow| { + #[cfg(feature = "trace")] + let _span = bevy_utils::tracing::info_span!("winit event_handler").entered(); + + match status { + Status::Initializing => { + if app.plugins_state() == PluginsState::Adding { + tick_global_task_pools_on_main_thread(); + } else { + app.finish(); + app.cleanup(); + status = Status::Ready; + } + + if let Some(app_exit_events) = app.world().get_resource::>() { + if app_exit_event_reader.read(app_exit_events).last().is_some() { + *control_flow = ControlFlow::Exit; + return; } + } + + *control_flow = ControlFlow::Poll; + } + Status::Ready => { + // TODO: schedule this after the system that updates time + #[cfg(not(target_arch = "wasm32"))] + app.add_systems(First, flush_winit_events::); + + // split app + let (mut sub_apps, tls, send, recv, _) = mem::take(&mut app).into_parts(); + locals = Some(tls); + + // insert winit -> app channel + let winit_recv = winit_recv.take().unwrap(); + sub_apps.main.world_mut().insert_resource(winit_recv); + + // send sub-apps to separate thread + spawn_app_thread(sub_apps, send, recv, event_loop_proxy.clone()); + + status = Status::Running; + + // Since the app runs in its own thread, this thread should sleep when + // it has no events to process. + *control_flow = ControlFlow::Wait; + } + Status::Running => (), + } - if let Some(app_exit_events) = app.world.get_resource::>() { - if app_exit_event_reader.read(app_exit_events).last().is_some() { - *control_flow = ControlFlow::Exit; + match event { + winit::event::Event::WindowEvent { window_id, event } => { + if let Some(tls) = locals.as_mut() { + tls.run(|tls| { + let winit_windows = tls.resource::(); + let access_kit_adapters = tls.resource::(); + + // Let AccessKit process the event before it reaches the engine. + if let Some(entity) = winit_windows.get_window_entity(window_id) { + if let Some(window) = winit_windows.get_window(entity) { + if let Some(adapter) = access_kit_adapters.get(&entity) { + // Unlike `on_event` suggests, this call has meaningful side + // effects. AccessKit may eventually filter events here, but + // they currently don't. + // See: + // - https://github.com/AccessKit/accesskit/issues/300 + // - https://github.com/bevyengine/bevy/pull/10239#issuecomment-1775572176 + let _ = adapter.on_event(window, &event); + } + } + }; + }); + } + + match event { + winit::event::WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + } => { + if let Some(tls) = locals.as_mut() { + tls.run(|tls| { + // This event requires special handling because writes to `new_inner_size` + // must happen here. It can't be written asynchronously. + let winit_windows = tls.resource::(); + if let Some(window) = + winit_windows.cached_windows.get(&window_id) + { + if let Some(sf_override) = + window.resolution.scale_factor_override() + { + // This window is overriding the OS-suggested DPI, so its physical + // size should be set based on the overriding value. Its logical + // size already incorporates any resize constraints. + *new_inner_size = winit::dpi::LogicalSize::new( + window.width(), + window.height(), + ) + .to_physical::(sf_override); + } + } + }); } + + let _ = winit_send.send(converters::Event::WindowEvent { + window_id, + event: converters::WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size: *new_inner_size, + }, + }); + } + _ => { + let _ = winit_send.send(converters::Event::WindowEvent { + window_id, + event: convert_window_event(event), + }); } } + } + winit::event::Event::UserEvent(event) => { + assert!(matches!(status, Status::Running)); + match event { + AppEvent::Task(task) => { + let tls = locals.as_mut().unwrap(); + + tls.insert_resource(crate::EventLoopWindowTarget::new(event_loop)); + + { + #[cfg(feature = "trace")] + let _span = bevy_utils::tracing::info_span!("TLS access").entered(); + task(); + } - // create any new windows - // (even if app did not update, some may have been created by plugin setup) - #[cfg(not(target_arch = "wasm32"))] - let ( - commands, - mut windows, - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - ) = create_window_system_state.get_mut(&mut app.world); - - #[cfg(target_arch = "wasm32")] - let ( - commands, - mut windows, - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - event_channel, - ) = create_window_system_state.get_mut(&mut app.world); - - create_windows( - event_loop, - commands, - windows.iter_mut(), - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - #[cfg(target_arch = "wasm32")] - event_channel, - ); - - create_window_system_state.apply(&mut app.world); + tls.remove_resource::>(); + } + AppEvent::Exit(_) => { + let tls = locals.as_mut().unwrap(); + tls.clear(); + + *control_flow = ControlFlow::Exit; + } + AppEvent::Error(payload) => { + resume_unwind(payload); + } + } + } + winit::event::Event::MainEventsCleared => { + let _ = winit_send.send_clear(convert_event(event)); + } + _ => { + let _ = winit_send.send(convert_event(event)); } } - _ => (), - } - }; + }; - trace!("starting winit event loop"); - if return_from_run { - run_return(&mut event_loop, event_handler); - } else { - run(event_loop, event_handler); + trace!("starting winit event loop"); + if return_from_run { + run_return(&mut event_loop, event_cb); + } else { + run(event_loop, event_cb); + } } } diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 896466f3c7073..223a7030b75c0 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -1,12 +1,5 @@ use bevy_a11y::AccessibilityRequested; -use bevy_ecs::{ - entity::Entity, - event::EventWriter, - prelude::{Changed, Component, Resource}, - removal_detection::RemovedComponents, - system::{Commands, NonSendMut, Query, ResMut}, - world::Mut, -}; +use bevy_ecs::prelude::*; use bevy_utils::{ tracing::{error, info, warn}, HashMap, @@ -14,20 +7,20 @@ use bevy_utils::{ use bevy_window::{RawHandleWrapper, Window, WindowClosed, WindowCreated}; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; -use winit::{ - dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, - event_loop::EventLoopWindowTarget, -}; +use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; #[cfg(target_arch = "wasm32")] use crate::web_resize::{CanvasParentResizeEventChannel, WINIT_CANVAS_SELECTOR}; +#[cfg(not(target_arch = "wasm32"))] +use crate::WinitWindowEntityMap; use crate::{ accessibility::{AccessKitAdapters, WinitActionHandlers}, + attempt_grab, converters::{ - self, convert_enabled_buttons, convert_window_level, convert_window_theme, + convert_cursor_icon, convert_enabled_buttons, convert_window_level, convert_window_theme, convert_winit_theme, }, - get_best_videomode, get_fitting_videomode, WinitWindows, + get_best_videomode, get_fitting_videomode, EventLoopWindowTarget, WinitWindows, }; /// Creates new windows on the [`winit`] backend for each entity with a newly-added @@ -36,68 +29,89 @@ use crate::{ /// If any of these entities are missing required components, those will be added with their /// default values. #[allow(clippy::too_many_arguments)] -pub(crate) fn create_windows<'a>( - event_loop: &EventLoopWindowTarget<()>, +pub(crate) fn create_windows( mut commands: Commands, - created_windows: impl Iterator)>, + mut created_windows: Query<(Entity, &mut Window)>, mut event_writer: EventWriter, - mut winit_windows: NonSendMut, - mut adapters: NonSendMut, + #[cfg(not(target_arch = "wasm32"))] mut window_entity_map: ResMut, mut handlers: ResMut, - accessibility_requested: ResMut, + accessibility_requested: Res, + mut main_thread: ThreadLocal, #[cfg(target_arch = "wasm32")] event_channel: ResMut, ) { - for (entity, mut window) in created_windows { - if winit_windows.get_window(entity).is_some() { - continue; - } + main_thread.run(|tls| { + tls.resource_scope(|tls, mut winit_windows: Mut| { + tls.resource_scope(|tls, mut adapters: Mut| { + for (entity, mut window) in created_windows.iter_mut() { + if winit_windows.get_window(entity).is_some() { + continue; + } - info!( - "Creating new window {:?} ({:?})", - window.title.as_str(), - entity - ); - - let winit_window = winit_windows.create_window( - event_loop, - entity, - &window, - &mut adapters, - &mut handlers, - &accessibility_requested, - ); - - if let Some(theme) = winit_window.theme() { - window.window_theme = Some(convert_winit_theme(theme)); - } + info!( + "Creating new window {:?} ({:?})", + window.title.as_str(), + entity + ); - window - .resolution - .set_scale_factor(winit_window.scale_factor()); - commands - .entity(entity) - .insert(RawHandleWrapper { - window_handle: winit_window.raw_window_handle(), - display_handle: winit_window.raw_display_handle(), - }) - .insert(CachedWindow { - window: window.clone(), - }); + // SAFETY: `bevy_winit` guarantees that this resource can only be inserted by + // its `App` runner and that the stored pointer is valid. + let event_loop = unsafe { tls.resource::>().get() }; + + #[cfg(not(target_arch = "wasm32"))] + let winit_window = winit_windows.create_window( + event_loop, + entity, + &window, + &mut window_entity_map, + &mut adapters, + &mut handlers, + &accessibility_requested, + ); - #[cfg(target_arch = "wasm32")] - { - if window.fit_canvas_to_parent { - let selector = if let Some(selector) = &window.canvas { - selector - } else { - WINIT_CANVAS_SELECTOR - }; - event_channel.listen_to_selector(entity, selector); - } - } + #[cfg(target_arch = "wasm32")] + let winit_window = winit_windows.create_window( + event_loop, + entity, + &window, + &mut adapters, + &mut handlers, + &accessibility_requested, + ); + + if let Some(theme) = winit_window.theme() { + window.window_theme = Some(convert_winit_theme(theme)); + } + + window + .resolution + .set_scale_factor(winit_window.scale_factor()); + commands + .entity(entity) + .insert(RawHandleWrapper { + window_handle: winit_window.raw_window_handle(), + display_handle: winit_window.raw_display_handle(), + }) + .insert(CachedWindow { + window: window.clone(), + }); + + #[cfg(target_arch = "wasm32")] + { + if window.fit_canvas_to_parent { + let selector = if let Some(selector) = &window.canvas { + selector + } else { + WINIT_CANVAS_SELECTOR + }; + event_channel.listen_to_selector(entity, selector); + } + } - event_writer.send(WindowCreated { window: entity }); - } + event_writer.send(WindowCreated { window: entity }); + } + }); + }); + }); } /// Cache for closing windows so we can get better debug information. @@ -108,17 +122,20 @@ pub(crate) fn despawn_windows( mut closed: RemovedComponents, window_entities: Query<&Window>, mut close_events: EventWriter, - mut winit_windows: NonSendMut, + mut main_thread: ThreadLocal, ) { - for window in closed.read() { - info!("Closing window {:?}", window); - // Guard to verify that the window is in fact actually gone, - // rather than having the component added and removed in the same frame. - if !window_entities.contains(window) { - winit_windows.remove_window(window); - close_events.send(WindowClosed { window }); + main_thread.run(|tls| { + let mut winit_windows = tls.resource_mut::(); + for window in closed.read() { + info!("Closing window {:?}", window); + // Guard to verify that the window is in fact actually gone, + // rather than having the component added and removed in the same frame. + if !window_entities.contains(window) { + winit_windows.remove_window(window); + close_events.send(WindowClosed { window }); + } } - } + }); } /// The cached state of the window so we can check which properties were changed from within the app. @@ -127,7 +144,7 @@ pub struct CachedWindow { pub window: Window, } -/// Propagates changes from [`Window`] entities to the [`winit`] backend. +/// Propagates changes from window entities to the [`winit`] backend. /// /// # Notes /// @@ -137,182 +154,196 @@ pub struct CachedWindow { /// - [`Window::focused`] cannot be manually changed to `false` after the window is created. pub(crate) fn changed_windows( mut changed_windows: Query<(Entity, &mut Window, &mut CachedWindow), Changed>, - winit_windows: NonSendMut, + mut main_thread: ThreadLocal, ) { - for (entity, mut window, mut cache) in &mut changed_windows { - if let Some(winit_window) = winit_windows.get_window(entity) { - if window.title != cache.window.title { - winit_window.set_title(window.title.as_str()); - } + main_thread.run(|tls| { + #[cfg(not(target_arch = "wasm32"))] + let mut winit_windows = tls.resource_mut::(); - if window.mode != cache.window.mode { - let new_mode = match window.mode { - bevy_window::WindowMode::BorderlessFullscreen => { - Some(winit::window::Fullscreen::Borderless(None)) - } - bevy_window::WindowMode::Fullscreen => { - Some(winit::window::Fullscreen::Exclusive(get_best_videomode( - &winit_window.current_monitor().unwrap(), - ))) - } - bevy_window::WindowMode::SizedFullscreen => { - Some(winit::window::Fullscreen::Exclusive(get_fitting_videomode( - &winit_window.current_monitor().unwrap(), - window.width() as u32, - window.height() as u32, - ))) - } - bevy_window::WindowMode::Windowed => None, - }; + #[cfg(target_arch = "wasm32")] + let winit_windows = tls.resource::(); - if winit_window.fullscreen() != new_mode { - winit_window.set_fullscreen(new_mode); + for (entity, mut window, mut cache) in &mut changed_windows { + if let Some(winit_window) = winit_windows.get_window(entity) { + if window.title != cache.window.title { + winit_window.set_title(window.title.as_str()); } - } - if window.resolution != cache.window.resolution { - let physical_size = PhysicalSize::new( - window.resolution.physical_width(), - window.resolution.physical_height(), - ); - winit_window.set_inner_size(physical_size); - } - if window.physical_cursor_position() != cache.window.physical_cursor_position() { - if let Some(physical_position) = window.physical_cursor_position() { - let position = PhysicalPosition::new(physical_position.x, physical_position.y); + if window.mode != cache.window.mode { + let new_mode = match window.mode { + bevy_window::WindowMode::BorderlessFullscreen => { + Some(winit::window::Fullscreen::Borderless(None)) + } + bevy_window::WindowMode::Fullscreen => { + Some(winit::window::Fullscreen::Exclusive(get_best_videomode( + &winit_window.current_monitor().unwrap(), + ))) + } + bevy_window::WindowMode::SizedFullscreen => { + Some(winit::window::Fullscreen::Exclusive(get_fitting_videomode( + &winit_window.current_monitor().unwrap(), + window.width() as u32, + window.height() as u32, + ))) + } + bevy_window::WindowMode::Windowed => None, + }; - if let Err(err) = winit_window.set_cursor_position(position) { - error!("could not set cursor position: {:?}", err); + if winit_window.fullscreen() != new_mode { + winit_window.set_fullscreen(new_mode); } } - } + if window.resolution != cache.window.resolution { + let physical_size = PhysicalSize::new( + window.resolution.physical_width(), + window.resolution.physical_height(), + ); + winit_window.set_inner_size(physical_size); + } - if window.cursor.icon != cache.window.cursor.icon { - winit_window.set_cursor_icon(converters::convert_cursor_icon(window.cursor.icon)); - } + if window.physical_cursor_position() != cache.window.physical_cursor_position() { + if let Some(physical_position) = window.physical_cursor_position() { + let inner_size = winit_window.inner_size(); - if window.cursor.grab_mode != cache.window.cursor.grab_mode { - crate::winit_windows::attempt_grab(winit_window, window.cursor.grab_mode); - } + let position = PhysicalPosition::new( + physical_position.x, + // Flip the coordinate space back to winit's context. + inner_size.height as f32 - physical_position.y, + ); - if window.cursor.visible != cache.window.cursor.visible { - winit_window.set_cursor_visible(window.cursor.visible); - } + if let Err(err) = winit_window.set_cursor_position(position) { + error!("could not set cursor position: {:?}", err); + } + } + } - if window.cursor.hit_test != cache.window.cursor.hit_test { - if let Err(err) = winit_window.set_cursor_hittest(window.cursor.hit_test) { - window.cursor.hit_test = cache.window.cursor.hit_test; - warn!( - "Could not set cursor hit test for window {:?}: {:?}", - window.title, err - ); + if window.cursor.icon != cache.window.cursor.icon { + winit_window.set_cursor_icon(convert_cursor_icon(window.cursor.icon)); } - } - if window.decorations != cache.window.decorations - && window.decorations != winit_window.is_decorated() - { - winit_window.set_decorations(window.decorations); - } + if window.cursor.grab_mode != cache.window.cursor.grab_mode { + attempt_grab(winit_window, window.cursor.grab_mode); + } - if window.resizable != cache.window.resizable - && window.resizable != winit_window.is_resizable() - { - winit_window.set_resizable(window.resizable); - } + if window.cursor.visible != cache.window.cursor.visible { + winit_window.set_cursor_visible(window.cursor.visible); + } - if window.enabled_buttons != cache.window.enabled_buttons { - winit_window.set_enabled_buttons(convert_enabled_buttons(window.enabled_buttons)); - } + if window.cursor.hit_test != cache.window.cursor.hit_test { + if let Err(err) = winit_window.set_cursor_hittest(window.cursor.hit_test) { + window.cursor.hit_test = cache.window.cursor.hit_test; + warn!( + "Could not set cursor hit test for window {:?}: {:?}", + window.title, err + ); + } + } - if window.resize_constraints != cache.window.resize_constraints { - let constraints = window.resize_constraints.check_constraints(); - let min_inner_size = LogicalSize { - width: constraints.min_width, - height: constraints.min_height, - }; - let max_inner_size = LogicalSize { - width: constraints.max_width, - height: constraints.max_height, - }; - - winit_window.set_min_inner_size(Some(min_inner_size)); - if constraints.max_width.is_finite() && constraints.max_height.is_finite() { - winit_window.set_max_inner_size(Some(max_inner_size)); + if window.decorations != cache.window.decorations + && window.decorations != winit_window.is_decorated() + { + winit_window.set_decorations(window.decorations); + } + + if window.resizable != cache.window.resizable + && window.resizable != winit_window.is_resizable() + { + winit_window.set_resizable(window.resizable); + } + + if window.enabled_buttons != cache.window.enabled_buttons { + winit_window.set_enabled_buttons(convert_enabled_buttons(window.enabled_buttons)); } - } - if window.position != cache.window.position { - if let Some(position) = crate::winit_window_position( - &window.position, - &window.resolution, - winit_window.available_monitors(), - winit_window.primary_monitor(), - winit_window.current_monitor(), - ) { - let should_set = match winit_window.outer_position() { - Ok(current_position) => current_position != position, - _ => true, + if window.resize_constraints != cache.window.resize_constraints { + let constraints = window.resize_constraints.check_constraints(); + let min_inner_size = LogicalSize { + width: constraints.min_width, + height: constraints.min_height, + }; + let max_inner_size = LogicalSize { + width: constraints.max_width, + height: constraints.max_height, }; - if should_set { - winit_window.set_outer_position(position); + winit_window.set_min_inner_size(Some(min_inner_size)); + if constraints.max_width.is_finite() && constraints.max_height.is_finite() { + winit_window.set_max_inner_size(Some(max_inner_size)); } } - } - if let Some(maximized) = window.internal.take_maximize_request() { - winit_window.set_maximized(maximized); - } + if window.position != cache.window.position { + if let Some(position) = crate::winit_window_position( + &window.position, + &window.resolution, + winit_window.available_monitors(), + winit_window.primary_monitor(), + winit_window.current_monitor(), + ) { + let should_set = match winit_window.outer_position() { + Ok(current_position) => current_position != position, + _ => true, + }; + + if should_set { + winit_window.set_outer_position(position); + } + } + } - if let Some(minimized) = window.internal.take_minimize_request() { - winit_window.set_minimized(minimized); - } + if let Some(maximized) = window.internal.take_maximize_request() { + winit_window.set_maximized(maximized); + } - if window.focused != cache.window.focused && window.focused { - winit_window.focus_window(); - } + if let Some(minimized) = window.internal.take_minimize_request() { + winit_window.set_minimized(minimized); + } - if window.window_level != cache.window.window_level { - winit_window.set_window_level(convert_window_level(window.window_level)); - } + if window.focused != cache.window.focused && window.focused { + winit_window.focus_window(); + } - // Currently unsupported changes - if window.transparent != cache.window.transparent { - window.transparent = cache.window.transparent; - warn!( - "Winit does not currently support updating transparency after window creation." - ); - } + if window.window_level != cache.window.window_level { + winit_window.set_window_level(convert_window_level(window.window_level)); + } - #[cfg(target_arch = "wasm32")] - if window.canvas != cache.window.canvas { - window.canvas = cache.window.canvas.clone(); - warn!( - "Bevy currently doesn't support modifying the window canvas after initialization." - ); - } + if window.transparent != cache.window.transparent { + window.transparent = cache.window.transparent; + warn!( + "Currently, `winit` does not support changing a window's transparency after it's been created." + ); + } - if window.ime_enabled != cache.window.ime_enabled { - winit_window.set_ime_allowed(window.ime_enabled); - } + #[cfg(target_arch = "wasm32")] + if window.canvas != cache.window.canvas { + window.canvas = cache.window.canvas.clone(); + warn!( + "Currently, `winit` does not support changing a window's canvas after it's been created." + ); + } - if window.ime_position != cache.window.ime_position { - winit_window.set_ime_position(LogicalPosition::new( - window.ime_position.x, - window.ime_position.y, - )); - } + if window.ime_enabled != cache.window.ime_enabled { + winit_window.set_ime_allowed(window.ime_enabled); + } - if window.window_theme != cache.window.window_theme { - winit_window.set_theme(window.window_theme.map(convert_window_theme)); - } + if window.ime_position != cache.window.ime_position { + winit_window.set_ime_position(LogicalPosition::new( + window.ime_position.x, + window.ime_position.y, + )); + } + + if window.window_theme != cache.window.window_theme { + winit_window.set_theme(window.window_theme.map(convert_window_theme)); + } - if window.visible != cache.window.visible { - winit_window.set_visible(window.visible); + cache.window = window.clone(); } - cache.window = window.clone(); + #[cfg(not(target_arch = "wasm32"))] + if let Some(window_id) = winit_windows.get_window_id(entity) { + winit_windows.cached_windows.insert(window_id, window.clone()); + } } - } + }); } diff --git a/crates/bevy_winit/src/web_resize.rs b/crates/bevy_winit/src/web_resize.rs index a53075dea3883..fcaa3db1caff2 100644 --- a/crates/bevy_winit/src/web_resize.rs +++ b/crates/bevy_winit/src/web_resize.rs @@ -26,14 +26,17 @@ pub(crate) struct CanvasParentResizeEventChannel { } fn canvas_parent_resize_event_handler( - winit_windows: NonSend, resize_events: Res, + mut main_thread: ThreadLocal, ) { - for event in resize_events.receiver.try_iter() { - if let Some(window) = winit_windows.get_window(event.window) { - window.set_inner_size(event.size); + main_thread.run(|tls| { + let winit_windows = tls.resource::(); + for event in resize_events.receiver.try_iter() { + if let Some(window) = winit_windows.get_window(event.window) { + window.set_inner_size(event.size); + } } - } + }); } fn get_size(selector: &str) -> Option> { diff --git a/crates/bevy_winit/src/winit_config.rs b/crates/bevy_winit/src/winit_config.rs index d6b22ecf21535..639a789be0adf 100644 --- a/crates/bevy_winit/src/winit_config.rs +++ b/crates/bevy_winit/src/winit_config.rs @@ -70,10 +70,10 @@ impl WinitSettings { /// Returns the current [`UpdateMode`]. /// /// **Note:** The output depends on whether the window has focus or not. - pub fn update_mode(&self, focused: bool) -> &UpdateMode { + pub fn update_mode(&self, focused: bool) -> UpdateMode { match focused { - true => &self.focused_mode, - false => &self.unfocused_mode, + true => self.focused_mode, + false => self.unfocused_mode, } } } diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 43c0e414b2956..e1b42c7772045 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -1,4 +1,5 @@ #![warn(missing_docs)] +use std::marker::PhantomData; use accesskit_winit::Adapter; use bevy_a11y::{ @@ -6,12 +7,13 @@ use bevy_a11y::{ AccessibilityRequested, }; use bevy_ecs::entity::Entity; - +use bevy_ecs::prelude::{Resource, ThreadLocalResource}; use bevy_utils::{tracing::warn, HashMap}; use bevy_window::{CursorGrabMode, Window, WindowMode, WindowPosition, WindowResolution}; use winit::{ dpi::{LogicalSize, PhysicalPosition}, + event_loop::EventLoopWindowTarget, monitor::MonitorHandle, }; @@ -20,29 +22,57 @@ use crate::{ converters::{convert_enabled_buttons, convert_window_level, convert_window_theme}, }; -/// A resource mapping window entities to their `winit`-backend [`Window`](winit::window::Window) -/// states. -#[derive(Debug, Default)] -pub struct WinitWindows { - /// Stores [`winit`] windows by window identifier. - pub windows: HashMap, +/// A two-way map between [`Window`] entities and [`winit`] library [`Window`](winit::window::Window) instances. +#[derive(Resource, Debug, Default)] +pub struct WinitWindowEntityMap { /// Maps entities to `winit` window identifiers. pub entity_to_winit: HashMap, /// Maps `winit` window identifiers to entities. pub winit_to_entity: HashMap, +} + +impl WinitWindowEntityMap { + /// Returns the [`WindowId`](winit::window::WindowId) that is mapped to the given [`Entity`]. + pub fn get_window_id(&self, entity: Entity) -> Option { + self.entity_to_winit.get(&entity).cloned() + } + + /// Returns the [`Entity`] that is mapped to the given [`WindowId`](winit::window::WindowId). + pub fn get_window_entity(&self, winit_id: winit::window::WindowId) -> Option { + self.winit_to_entity.get(&winit_id).cloned() + } +} + +/// Collection of `winit` [`Window`](winit::window::Window) instances. +#[derive(ThreadLocalResource, Debug, Default)] +pub struct WinitWindows { + /// Collection of [`winit`] windows indexed by [`WindowId`](winit::window::WindowId). + pub windows: HashMap, + /// Two-way mapping between [`Entity`] and [`WindowId`](winit::window::WindowId). + map: WinitWindowEntityMap, + /// Cached copy of the last-known [`Window`] properties. + /// + /// This copy is needed because some `winit` events require immediate handling, but the + /// [`App`](bevy_app::App) lives in a different thread. + #[cfg(not(target_arch = "wasm32"))] + pub(crate) cached_windows: HashMap, // Many `winit` window functions (e.g. `set_window_icon`) can only be called on the main thread. // If they're called on other threads, the program might hang. This marker indicates that this // type is not thread-safe and will be `!Send` and `!Sync`. - _not_send_sync: core::marker::PhantomData<*const ()>, + _not_send_sync: PhantomData<*const ()>, } impl WinitWindows { - /// Creates a `winit` window and associates it with our entity. - pub fn create_window( + /// Constructs a new [`Window`](winit::window::Window) and returns a reference to it. + /// + /// Due to platform limitations, this function can only run on the main thread. + #[allow(clippy::too_many_arguments)] + pub fn create_window( &mut self, - event_loop: &winit::event_loop::EventLoopWindowTarget<()>, + event_loop: &EventLoopWindowTarget, entity: Entity, window: &Window, + #[cfg(not(target_arch = "wasm32"))] entity_map: &mut WinitWindowEntityMap, adapters: &mut AccessKitAdapters, handlers: &mut WinitActionHandlers, accessibility_requested: &AccessibilityRequested, @@ -165,6 +195,7 @@ impl WinitWindows { }, Box::new(handler.clone()), ); + adapters.insert(entity, adapter); handlers.insert(entity, handler); @@ -186,8 +217,15 @@ impl WinitWindows { } } - self.entity_to_winit.insert(entity, winit_window.id()); - self.winit_to_entity.insert(winit_window.id(), entity); + self.map.entity_to_winit.insert(entity, winit_window.id()); + self.map.winit_to_entity.insert(winit_window.id(), entity); + + // save copy of window properties so we don't have to synchronize the threads to read them + #[cfg(not(target_arch = "wasm32"))] + { + entity_map.entity_to_winit = self.map.entity_to_winit.clone(); + entity_map.winit_to_entity = self.map.winit_to_entity.clone(); + } #[cfg(target_arch = "wasm32")] { @@ -211,26 +249,35 @@ impl WinitWindows { .into_mut() } - /// Get the winit window that is associated with our entity. - pub fn get_window(&self, entity: Entity) -> Option<&winit::window::Window> { - self.entity_to_winit - .get(&entity) - .and_then(|winit_id| self.windows.get(winit_id)) + /// Returns the [`WindowId`](winit::window::WindowId) that is mapped to the given [`Entity`]. + pub fn get_window_id(&self, entity: Entity) -> Option { + self.map.get_window_id(entity) } - /// Get the entity associated with the winit window id. - /// - /// This is mostly just an intermediary step between us and winit. + /// Returns the [`Entity`] that is mapped to the given [`WindowId`](winit::window::WindowId). pub fn get_window_entity(&self, winit_id: winit::window::WindowId) -> Option { - self.winit_to_entity.get(&winit_id).cloned() + self.map.get_window_entity(winit_id) + } + + /// Returns the [`Window`](winit::window::Window) that is mapped to the given [`Entity`]. + pub fn get_window(&self, entity: Entity) -> Option<&winit::window::Window> { + self.map + .get_window_id(entity) + .and_then(|id| self.windows.get(&id)) } /// Remove a window from winit. /// /// This should mostly just be called when the window is closing. pub fn remove_window(&mut self, entity: Entity) -> Option { - let winit_id = self.entity_to_winit.remove(&entity)?; - // Don't remove from `winit_to_window_id` so we know the window used to exist. + let winit_id = self.map.entity_to_winit.remove(&entity)?; + + #[cfg(not(target_arch = "wasm32"))] + self.cached_windows.remove(&winit_id); + + // Don't remove from `winit_to_window_id` so we don't forget that the window existed. + // TODO: Investigate the reasoning here. + // self.map.winit_to_entity.remove(&winit_id)?; self.windows.remove(&winit_id) } } @@ -270,7 +317,7 @@ pub fn get_fitting_videomode( modes.first().unwrap().clone() } -/// Gets the "best" videomode from a monitor. +/// Returns the "best" videomode from a monitor. /// /// The heuristic for "best" prioritizes width, height, and refresh rate in that order. pub fn get_best_videomode(monitor: &winit::monitor::MonitorHandle) -> winit::monitor::VideoMode { @@ -314,9 +361,8 @@ pub(crate) fn attempt_grab(winit_window: &winit::window::Window, grab_mode: Curs } } -/// Compute the physical window position for a given [`WindowPosition`]. -// Ideally we could generify this across window backends, but we only really have winit atm -// so whatever. +/// Computes the physical window position for a given [`WindowPosition`]. +// TODO: Ideally, this function is backend-generic, but right now we only internally support winit. pub fn winit_window_position( position: &WindowPosition, resolution: &WindowResolution, @@ -373,9 +419,3 @@ pub fn winit_window_position( } } } - -// WARNING: this only works under the assumption that wasm runtime is single threaded -#[cfg(target_arch = "wasm32")] -unsafe impl Send for WinitWindows {} -#[cfg(target_arch = "wasm32")] -unsafe impl Sync for WinitWindows {} diff --git a/errors/B0002.md b/errors/B0002.md index cfba45ba32578..29160aa75fd77 100644 --- a/errors/B0002.md +++ b/errors/B0002.md @@ -1,6 +1,6 @@ # B0002 -To keep [Rust rules on references](https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#the-rules-of-references) (either one mutable reference or any number of immutable references) on a resource, it is not possible to have more than one resource of a kind if one is mutable in the same system. This can happen between [`Res`](https://docs.rs/bevy/*/bevy/ecs/system/struct.Res.html) and [`ResMut`](https://docs.rs/bevy/*/bevy/ecs/system/struct.ResMut.html) for the same `T`, or between [`NonSend`](https://docs.rs/bevy/*/bevy/ecs/system/struct.NonSend.html) and [`NonSendMut`](https://docs.rs/bevy/*/bevy/ecs/system/struct.NonSendMut.html) for the same `T`. +To keep [Rust rules on references](https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#the-rules-of-references) (either one mutable reference or any number of immutable references) on a resource, it is not possible to have more than one resource of a kind if one is mutable in the same system. This can happen between [`Res`](https://docs.rs/bevy/*/bevy/ecs/system/struct.Res.html) and [`ResMut`](https://docs.rs/bevy/*/bevy/ecs/system/struct.ResMut.html) for the same `T`. Erroneous code example: diff --git a/examples/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index 3e908c49fe222..a720dfbd0dfd5 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -273,7 +273,7 @@ pub const COLORED_MESH2D_SHADER_HANDLE: Handle = impl Plugin for ColoredMesh2dPlugin { fn build(&self, app: &mut App) { // Load our custom shader - let mut shaders = app.world.resource_mut::>(); + let mut shaders = app.world_mut().resource_mut::>(); shaders.insert( COLORED_MESH2D_SHADER_HANDLE, Shader::from_wgsl(COLORED_MESH2D_SHADER, file!()), diff --git a/examples/app/custom_loop.rs b/examples/app/custom_loop.rs index 17f70ae9f6d38..3d76bed2dff91 100644 --- a/examples/app/custom_loop.rs +++ b/examples/app/custom_loop.rs @@ -11,7 +11,7 @@ fn my_runner(mut app: App) { println!("Type stuff into the console"); for line in io::stdin().lines() { { - let mut input = app.world.resource_mut::(); + let mut input = app.world_mut().resource_mut::(); input.0 = line.unwrap(); } app.update(); diff --git a/examples/shader/compute_shader_game_of_life.rs b/examples/shader/compute_shader_game_of_life.rs index 2f8a269e592b8..db9f8679039fb 100644 --- a/examples/shader/compute_shader_game_of_life.rs +++ b/examples/shader/compute_shader_game_of_life.rs @@ -79,7 +79,7 @@ impl Plugin for GameOfLifeComputePlugin { prepare_bind_group.in_set(RenderSet::PrepareBindGroups), ); - let mut render_graph = render_app.world.resource_mut::(); + let mut render_graph = render_app.world_mut().resource_mut::(); render_graph.add_node("game_of_life", GameOfLifeNode::default()); render_graph.add_node_edge( "game_of_life", diff --git a/examples/shader/post_processing.rs b/examples/shader/post_processing.rs index 66d26cffbc271..51d5e5940c18d 100644 --- a/examples/shader/post_processing.rs +++ b/examples/shader/post_processing.rs @@ -62,7 +62,7 @@ impl Plugin for PostProcessPlugin { )); // We need to get the render app from the main app - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; @@ -100,7 +100,7 @@ impl Plugin for PostProcessPlugin { fn finish(&self, app: &mut App) { // We need to get the render app from the main app - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; diff --git a/examples/shader/texture_binding_array.rs b/examples/shader/texture_binding_array.rs index 82bcf34acc6ab..eb3fdb934472e 100644 --- a/examples/shader/texture_binding_array.rs +++ b/examples/shader/texture_binding_array.rs @@ -33,11 +33,11 @@ impl Plugin for GpuFeatureSupportChecker { fn build(&self, _app: &mut App) {} fn finish(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; - let render_device = render_app.world.resource::(); + let render_device = render_app.world().resource::(); // Check if the device support the required feature. If not, exit the example. // In a real application, you should setup a fallback for the missing feature diff --git a/examples/stress_tests/many_lights.rs b/examples/stress_tests/many_lights.rs index b42d1b42cc9c5..0697fcffea260 100644 --- a/examples/stress_tests/many_lights.rs +++ b/examples/stress_tests/many_lights.rs @@ -152,9 +152,8 @@ struct LogVisibleLights; impl Plugin for LogVisibleLights { fn build(&self, app: &mut App) { - let render_app = match app.get_sub_app_mut(RenderApp) { - Ok(render_app) => render_app, - Err(_) => return, + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; }; render_app.add_systems(Render, print_visible_light_count.in_set(RenderSet::Prepare)); diff --git a/examples/time/time.rs b/examples/time/time.rs index e0c1d6e427b0a..22f713dab0efb 100644 --- a/examples/time/time.rs +++ b/examples/time/time.rs @@ -43,29 +43,29 @@ fn runner(mut app: App) { } "f" => { println!("FAST: setting relative speed to 2x"); - app.world + app.world_mut() .resource_mut::>() .set_relative_speed(2.0); } "n" => { println!("NORMAL: setting relative speed to 1x"); - app.world + app.world_mut() .resource_mut::>() .set_relative_speed(1.0); } "s" => { println!("SLOW: setting relative speed to 0.5x"); - app.world + app.world_mut() .resource_mut::>() .set_relative_speed(0.5); } "p" => { println!("PAUSE: pausing virtual clock"); - app.world.resource_mut::>().pause(); + app.world_mut().resource_mut::>().pause(); } "u" => { println!("UNPAUSE: resuming virtual clock"); - app.world.resource_mut::>().unpause(); + app.world_mut().resource_mut::>().unpause(); } "q" => { println!("QUITTING!"); diff --git a/tests/how_to_test_systems.rs b/tests/how_to_test_systems.rs index 23b29bd550c2b..cb7a3d86712e9 100644 --- a/tests/how_to_test_systems.rs +++ b/tests/how_to_test_systems.rs @@ -62,7 +62,7 @@ fn did_hurt_enemy() { // Setup test entities let enemy_id = app - .world + .world_mut() .spawn(Enemy { hit_points: 5, score_value: 3, @@ -73,8 +73,8 @@ fn did_hurt_enemy() { app.update(); // Check resulting changes - assert!(app.world.get::(enemy_id).is_some()); - assert_eq!(app.world.get::(enemy_id).unwrap().hit_points, 4); + assert!(app.world().get::(enemy_id).is_some()); + assert_eq!(app.world().get::(enemy_id).unwrap().hit_points, 4); } #[test] @@ -93,7 +93,7 @@ fn did_despawn_enemy() { // Setup test entities let enemy_id = app - .world + .world_mut() .spawn(Enemy { hit_points: 1, score_value: 1, @@ -104,10 +104,10 @@ fn did_despawn_enemy() { app.update(); // Check enemy was despawned - assert!(app.world.get::(enemy_id).is_none()); + assert!(app.world().get::(enemy_id).is_none()); // Get `EnemyDied` event reader - let enemy_died_events = app.world.resource::>(); + let enemy_died_events = app.world().resource::>(); let mut enemy_died_reader = enemy_died_events.get_reader(); let enemy_died = enemy_died_reader.read(enemy_died_events).next().unwrap(); @@ -132,16 +132,16 @@ fn spawn_enemy_using_input_resource() { app.update(); // Check resulting changes, one entity has been spawned with `Enemy` component - assert_eq!(app.world.query::<&Enemy>().iter(&app.world).len(), 1); + assert_eq!(app.world_mut().query::<&Enemy>().iter(app.world()).len(), 1); // Clear the `just_pressed` status for all `KeyCode`s - app.world.resource_mut::>().clear(); + app.world_mut().resource_mut::>().clear(); // Run systems app.update(); // Check resulting changes, no new entity has been spawned - assert_eq!(app.world.query::<&Enemy>().iter(&app.world).len(), 1); + assert_eq!(app.world_mut().query::<&Enemy>().iter(app.world()).len(), 1); } #[test] @@ -159,7 +159,7 @@ fn update_score_on_event() { app.add_systems(Update, update_score); // Send an `EnemyDied` event - app.world + app.world_mut() .resource_mut::>() .send(EnemyDied(3)); @@ -167,5 +167,5 @@ fn update_score_on_event() { app.update(); // Check resulting changes - assert_eq!(app.world.resource::().0, 3); + assert_eq!(app.world().resource::().0, 3); }