diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 46b3e3e195f59..2605d45a2b0df 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -16,17 +16,14 @@ mod winit_config; mod winit_windows; use bevy_a11y::AccessibilityRequested; -use bevy_ecs::system::{SystemParam, SystemState}; -#[cfg(not(target_arch = "wasm32"))] -use bevy_tasks::tick_global_task_pools_on_main_thread; -use system::{changed_window, create_window, despawn_window, CachedWindow}; - +use system::{changed_windows, create_windows, despawn_windows, CachedWindow}; pub use winit_config::*; pub use winit_windows::*; use bevy_app::{App, AppExit, Last, Plugin}; use bevy_ecs::event::{Events, ManualEventReader}; use bevy_ecs::prelude::*; +use bevy_ecs::system::{SystemParam, SystemState}; use bevy_input::{ keyboard::KeyboardInput, mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, @@ -34,9 +31,11 @@ use bevy_input::{ 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}, - Instant, + Duration, Instant, }; use bevy_window::{ exit_on_all_closed, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, @@ -63,14 +62,16 @@ use crate::web_resize::{CanvasParentResizeEventChannel, CanvasParentResizePlugin #[cfg(target_os = "android")] pub static ANDROID_APP: std::sync::OnceLock = std::sync::OnceLock::new(); -/// A [`Plugin`] that utilizes [`winit`] for window creation and event loop management. +/// A [`Plugin`] that integrates [`winit`], adding its window management and event loop +/// capabilities to an [`App`]. +/// +/// **NOTE:** This plugin will replace the existing application runner function. #[derive(Default)] pub struct WinitPlugin; impl Plugin for WinitPlugin { fn build(&self, app: &mut App) { let mut event_loop_builder = EventLoopBuilder::<()>::with_user_event(); - #[cfg(target_os = "android")] { use winit::platform::android::EventLoopBuilderExtAndroid; @@ -82,20 +83,17 @@ impl Plugin for WinitPlugin { ); } - let event_loop = event_loop_builder.build(); - app.insert_non_send_resource(event_loop); - app.init_non_send_resource::() .init_resource::() .set_runner(winit_runner) - // exit_on_all_closed only uses the query to determine if the query is empty, - // and so doesn't care about ordering relative to changed_window .add_systems( Last, ( - changed_window.ambiguous_with(exit_on_all_closed), - // Update the state of the window before attempting to despawn to ensure consistent event ordering - despawn_window.after(changed_window), + // `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` + changed_windows.ambiguous_with(exit_on_all_closed), + // apply all changes first, then despawn windows + despawn_windows.after(changed_windows), ), ); @@ -104,40 +102,46 @@ impl Plugin for WinitPlugin { #[cfg(target_arch = "wasm32")] app.add_plugins(CanvasParentResizePlugin); - #[cfg(not(target_arch = "wasm32"))] - let mut create_window_system_state: SystemState<( - Commands, - NonSendMut>, - Query<(Entity, &mut Window)>, - EventWriter, - NonSendMut, - NonSendMut, - ResMut, - ResMut, - )> = SystemState::from_world(&mut app.world); + let event_loop = event_loop_builder.build(); - #[cfg(target_arch = "wasm32")] - let mut create_window_system_state: SystemState<( - Commands, - NonSendMut>, - Query<(Entity, &mut Window)>, - EventWriter, - NonSendMut, - NonSendMut, - ResMut, - ResMut, - ResMut, - )> = SystemState::from_world(&mut app.world); - - // And for ios and macos, we should not create window early, all ui related code should be executed inside - // UIApplicationMain/NSApplicationMain. + // 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); + #[cfg(not(target_arch = "wasm32"))] let ( commands, - event_loop, - mut new_windows, + mut windows, event_writer, winit_windows, adapters, @@ -148,8 +152,7 @@ impl Plugin for WinitPlugin { #[cfg(target_arch = "wasm32")] let ( commands, - event_loop, - mut new_windows, + mut windows, event_writer, winit_windows, adapters, @@ -158,13 +161,10 @@ impl Plugin for WinitPlugin { event_channel, ) = create_window_system_state.get_mut(&mut app.world); - // Here we need to create a winit-window and give it a WindowHandle which the renderer can use. - // It needs to be spawned before the start of the startup schedule, so we cannot use a regular system. - // Instead we need to create the window and spawn it using direct world access - create_window( - commands, + create_windows( &event_loop, - new_windows.iter_mut(), + commands, + windows.iter_mut(), event_writer, winit_windows, adapters, @@ -173,22 +173,21 @@ impl Plugin for WinitPlugin { #[cfg(target_arch = "wasm32")] event_channel, ); + + create_window_system_state.apply(&mut app.world); } - create_window_system_state.apply(&mut app.world); + app.insert_non_send_resource(event_loop); } } -fn run(event_loop: EventLoop<()>, event_handler: F) -> ! +fn run(event_loop: EventLoop, event_handler: F) -> ! where - F: 'static + FnMut(Event<'_, ()>, &EventLoopWindowTarget<()>, &mut ControlFlow), + F: 'static + FnMut(Event<'_, T>, &EventLoopWindowTarget, &mut ControlFlow), { event_loop.run(event_handler) } -// TODO: It may be worth moving this cfg into a procedural macro so that it can be referenced by -// a single name instead of being copied around. -// https://gist.github.com/jakerr/231dee4a138f7a5f25148ea8f39b382e seems to work. #[cfg(any( target_os = "windows", target_os = "macos", @@ -198,9 +197,9 @@ where target_os = "netbsd", target_os = "openbsd" ))] -fn run_return(event_loop: &mut EventLoop<()>, event_handler: F) +fn run_return(event_loop: &mut EventLoop, event_handler: F) where - F: FnMut(Event<'_, ()>, &EventLoopWindowTarget<()>, &mut ControlFlow), + F: FnMut(Event<'_, T>, &EventLoopWindowTarget, &mut ControlFlow), { use winit::platform::run_return::EventLoopExtRunReturn; event_loop.run_return(event_handler); @@ -215,15 +214,16 @@ where target_os = "netbsd", target_os = "openbsd" )))] -fn run_return(_event_loop: &mut EventLoop<()>, _event_handler: F) +fn run_return(_event_loop: &mut EventLoop, _event_handler: F) where - F: FnMut(Event<'_, ()>, &EventLoopWindowTarget<()>, &mut ControlFlow), + F: FnMut(Event<'_, T>, &EventLoopWindowTarget, &mut ControlFlow), { panic!("Run return is not supported on this platform!") } #[derive(SystemParam)] -struct WindowEvents<'w> { +struct WinitEventWriters<'w> { + // window events window_resized: EventWriter<'w, WindowResized>, window_close_requested: EventWriter<'w, WindowCloseRequested>, window_scale_factor_changed: EventWriter<'w, WindowScaleFactorChanged>, @@ -232,10 +232,7 @@ struct WindowEvents<'w> { window_moved: EventWriter<'w, WindowMoved>, window_theme_changed: EventWriter<'w, WindowThemeChanged>, window_destroyed: EventWriter<'w, WindowDestroyed>, -} -#[derive(SystemParam)] -struct InputEvents<'w> { keyboard_input: EventWriter<'w, KeyboardInput>, character_input: EventWriter<'w, ReceivedCharacter>, mouse_button_input: EventWriter<'w, MouseButtonInput>, @@ -244,79 +241,83 @@ struct InputEvents<'w> { mouse_wheel_input: EventWriter<'w, MouseWheel>, touch_input: EventWriter<'w, TouchInput>, ime_input: EventWriter<'w, Ime>, -} + file_drag_and_drop: EventWriter<'w, FileDragAndDrop>, -#[derive(SystemParam)] -struct CursorEvents<'w> { cursor_moved: EventWriter<'w, CursorMoved>, cursor_entered: EventWriter<'w, CursorEntered>, cursor_left: EventWriter<'w, CursorLeft>, + + // device events + mouse_motion: EventWriter<'w, MouseMotion>, } -// #[cfg(any( -// target_os = "linux", -// target_os = "dragonfly", -// target_os = "freebsd", -// target_os = "netbsd", -// target_os = "openbsd" -// ))] -// pub fn winit_runner_any_thread(app: App) { -// winit_runner_with(app, EventLoop::new_any_thread()); -// } - -/// Stores state that must persist between frames. -struct WinitPersistentState { - /// Tracks whether or not the application is active or suspended. - active: bool, - /// Tracks whether or not an event has occurred this frame that would trigger an update in low - /// power mode. Should be reset at the end of every frame. - low_power_event: bool, - /// Tracks whether the event loop was started this frame because of a redraw request. - redraw_request_sent: bool, - /// Tracks if the event loop was started this frame because of a [`ControlFlow::WaitUntil`] - /// timeout. - timeout_reached: bool, +/// Persistent state that is used to run the [`App`] according to the current +/// [`UpdateMode`]. +struct WinitAppRunnerState { + /// Is `true` if the app is running and not suspended. + is_active: bool, + /// Is `true` if a new window event has been received since the last update. + window_event_received: bool, + /// Is `true` if a new device event has been received since the last update. + device_event_received: bool, + /// Is `true` if the app has requested a redraw since the last update. + redraw_requested: bool, + /// Is `true` if enough time has elapsed since `last_update` to run another update. + wait_elapsed: bool, + /// The time the last update started. last_update: Instant, + /// The time the next update is scheduled to start. + scheduled_update: Option, } -impl Default for WinitPersistentState { + +impl Default for WinitAppRunnerState { fn default() -> Self { Self { - active: false, - low_power_event: false, - redraw_request_sent: false, - timeout_reached: false, + is_active: false, + window_event_received: false, + device_event_received: false, + redraw_requested: false, + wait_elapsed: false, last_update: Instant::now(), + scheduled_update: None, } } } /// The default [`App::runner`] for the [`WinitPlugin`] plugin. /// -/// Overriding the app's [runner](bevy_app::App::runner) while using `WinitPlugin` will bypass the `EventLoop`. +/// Overriding the app's [runner](bevy_app::App::runner) while using `WinitPlugin` will bypass the +/// `EventLoop`. pub fn winit_runner(mut app: App) { - // We remove this so that we have ownership over it. let mut event_loop = app .world .remove_non_send_resource::>() .unwrap(); - let mut app_exit_event_reader = ManualEventReader::::default(); - let mut redraw_event_reader = ManualEventReader::::default(); - let mut winit_state = WinitPersistentState::default(); + let return_from_run = app.world.resource::().return_from_run; + app.world .insert_non_send_resource(event_loop.create_proxy()); - let return_from_run = app.world.resource::().return_from_run; + let mut runner_state = WinitAppRunnerState::default(); - trace!("Entering winit event loop"); + // prepare structures to access data in the world + let mut app_exit_event_reader = ManualEventReader::::default(); + let mut redraw_event_reader = ManualEventReader::::default(); - let mut focused_window_state: SystemState<(Res, Query<&Window>)> = - SystemState::from_world(&mut app.world); + let mut focused_windows_state: SystemState<(Res, Query<&Window>)> = + SystemState::new(&mut app.world); + + let mut event_writer_system_state: SystemState<( + WinitEventWriters, + NonSend, + Query<(&mut Window, &mut CachedWindow)>, + )> = SystemState::new(&mut app.world); #[cfg(not(target_arch = "wasm32"))] let mut create_window_system_state: SystemState<( Commands, - Query<(Entity, &mut Window), Added>, + Query<(Entity, &mut Window)>, EventWriter, NonSendMut, NonSendMut, @@ -327,7 +328,7 @@ pub fn winit_runner(mut app: App) { #[cfg(target_arch = "wasm32")] let mut create_window_system_state: SystemState<( Commands, - Query<(Entity, &mut Window), Added>, + Query<(Entity, &mut Window)>, EventWriter, NonSendMut, NonSendMut, @@ -338,6 +339,7 @@ pub fn winit_runner(mut app: App) { let mut finished_and_setup_done = false; + // setup up the event loop let event_handler = move |event: Event<()>, event_loop: &EventLoopWindowTarget<()>, control_flow: &mut ControlFlow| { @@ -353,77 +355,82 @@ pub fn winit_runner(mut app: App) { app.cleanup(); finished_and_setup_done = true; } - } - if let Some(app_exit_events) = app.world.get_resource::>() { - if app_exit_event_reader.iter(app_exit_events).last().is_some() { - *control_flow = ControlFlow::Exit; - return; + if let Some(app_exit_events) = app.world.get_resource::>() { + if app_exit_event_reader.iter(app_exit_events).last().is_some() { + *control_flow = ControlFlow::Exit; + return; + } } } match event { - event::Event::NewEvents(start) => { - let (winit_config, window_focused_query) = focused_window_state.get(&app.world); - - let app_focused = window_focused_query.iter().any(|window| window.focused); - - // Check if either the `WaitUntil` timeout was triggered by winit, or that same - // amount of time has elapsed since the last app update. This manual check is needed - // because we don't know if the criteria for an app update were met until the end of - // the frame. - let auto_timeout_reached = matches!(start, StartCause::ResumeTimeReached { .. }); - let now = Instant::now(); - let manual_timeout_reached = match winit_config.update_mode(app_focused) { - UpdateMode::Continuous => false, - UpdateMode::Reactive { max_wait } - | UpdateMode::ReactiveLowPower { max_wait } => { - now.duration_since(winit_state.last_update) >= *max_wait + event::Event::NewEvents(start_cause) => match start_cause { + StartCause::Init => { + #[cfg(any(target_os = "android", 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, + ); + + create_window_system_state.apply(&mut app.world); } - }; - // The low_power_event state and timeout must be reset at the start of every frame. - winit_state.low_power_event = false; - winit_state.timeout_reached = auto_timeout_reached || manual_timeout_reached; - } + } + _ => { + 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(); + } + } + }, event::Event::WindowEvent { - event, - window_id: winit_window_id, - .. + event, window_id, .. } => { - // Fetch and prepare details from the world - let mut system_state: SystemState<( - NonSend, - Query<(&mut Window, &mut CachedWindow)>, - WindowEvents, - InputEvents, - CursorEvents, - EventWriter, - )> = SystemState::new(&mut app.world); - let ( - winit_windows, - mut window_query, - mut window_events, - mut input_events, - mut cursor_events, - mut file_drag_and_drop_events, - ) = system_state.get_mut(&mut app.world); - - // Entity of this window - let window_entity = - if let Some(entity) = winit_windows.get_window_entity(winit_window_id) { - entity - } else { + let (mut event_writers, winit_windows, mut windows) = + 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, winit_window_id + event, window_id ); return; }; - let (mut window, mut cache) = - if let Ok((window, info)) = window_query.get_mut(window_entity) { - (window, info) - } else { + let Ok((mut window, mut cache)) = windows.get_mut(window_entity) else { warn!( "Window {:?} is missing `Window` component, skipping event {:?}", window_entity, event @@ -431,7 +438,7 @@ pub fn winit_runner(mut app: App) { return; }; - winit_state.low_power_event = true; + runner_state.window_event_received = true; match event { WindowEvent::Resized(size) => { @@ -439,67 +446,64 @@ pub fn winit_runner(mut app: App) { .resolution .set_physical_resolution(size.width, size.height); - window_events.window_resized.send(WindowResized { + event_writers.window_resized.send(WindowResized { window: window_entity, width: window.width(), height: window.height(), }); } WindowEvent::CloseRequested => { - window_events + event_writers .window_close_requested .send(WindowCloseRequested { window: window_entity, }); } WindowEvent::KeyboardInput { ref input, .. } => { - input_events + 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)); - - cursor_events.cursor_moved.send(CursorMoved { + event_writers.cursor_moved.send(CursorMoved { window: window_entity, position: (physical_position / window.resolution.scale_factor()) .as_vec2(), }); } WindowEvent::CursorEntered { .. } => { - cursor_events.cursor_entered.send(CursorEntered { + event_writers.cursor_entered.send(CursorEntered { window: window_entity, }); } WindowEvent::CursorLeft { .. } => { window.set_physical_cursor_position(None); - - cursor_events.cursor_left.send(CursorLeft { + event_writers.cursor_left.send(CursorLeft { window: window_entity, }); } WindowEvent::MouseInput { state, button, .. } => { - input_events.mouse_button_input.send(MouseButtonInput { + 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, .. } => { - input_events + event_writers .touchpad_magnify_input .send(TouchpadMagnify(delta as f32)); } WindowEvent::TouchpadRotate { delta, .. } => { - input_events + event_writers .touchpad_rotate_input .send(TouchpadRotate(delta)); } WindowEvent::MouseWheel { delta, .. } => match delta { event::MouseScrollDelta::LineDelta(x, y) => { - input_events.mouse_wheel_input.send(MouseWheel { + event_writers.mouse_wheel_input.send(MouseWheel { unit: MouseScrollUnit::Line, x, y, @@ -507,7 +511,7 @@ pub fn winit_runner(mut app: App) { }); } event::MouseScrollDelta::PixelDelta(p) => { - input_events.mouse_wheel_input.send(MouseWheel { + event_writers.mouse_wheel_input.send(MouseWheel { unit: MouseScrollUnit::Pixel, x: p.x as f32, y: p.y as f32, @@ -517,23 +521,21 @@ pub fn winit_runner(mut app: App) { }, WindowEvent::Touch(touch) => { let location = touch.location.to_logical(window.resolution.scale_factor()); - - // Event - input_events + event_writers .touch_input .send(converters::convert_touch_input(touch, location)); } - WindowEvent::ReceivedCharacter(c) => { - input_events.character_input.send(ReceivedCharacter { + WindowEvent::ReceivedCharacter(char) => { + event_writers.character_input.send(ReceivedCharacter { window: window_entity, - char: c, + char, }); } WindowEvent::ScaleFactorChanged { scale_factor, new_inner_size, } => { - window_events.window_backend_scale_factor_changed.send( + event_writers.window_backend_scale_factor_changed.send( WindowBackendScaleFactorChanged { window: window_entity, scale_factor, @@ -545,17 +547,14 @@ pub fn winit_runner(mut app: App) { let new_factor = window.resolution.scale_factor(); if let Some(forced_factor) = window.resolution.scale_factor_override() { - // If there is a scale factor override, then force that to be used - // Otherwise, use the OS suggested size - // We have already told the OS about our resize constraints, so - // the new_inner_size should take those into account + // 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); - // TODO: Should this not trigger a WindowsScaleFactorChanged? } else if approx::relative_ne!(new_factor, prior_factor) { - // Trigger a change event if they are approximately different - window_events.window_scale_factor_changed.send( + event_writers.window_scale_factor_changed.send( WindowScaleFactorChanged { window: window_entity, scale_factor, @@ -568,7 +567,7 @@ pub fn winit_runner(mut app: App) { if approx::relative_ne!(window.width(), new_logical_width) || approx::relative_ne!(window.height(), new_logical_height) { - window_events.window_resized.send(WindowResized { + event_writers.window_resized.send(WindowResized { window: window_entity, width: new_logical_width, height: new_logical_height, @@ -579,68 +578,70 @@ pub fn winit_runner(mut app: App) { .set_physical_resolution(new_inner_size.width, new_inner_size.height); } WindowEvent::Focused(focused) => { - // Component window.focused = focused; - - window_events.window_focused.send(WindowFocused { + event_writers.window_focused.send(WindowFocused { window: window_entity, focused, }); } WindowEvent::DroppedFile(path_buf) => { - file_drag_and_drop_events.send(FileDragAndDrop::DroppedFile { - window: window_entity, - path_buf, - }); + event_writers + .file_drag_and_drop + .send(FileDragAndDrop::DroppedFile { + window: window_entity, + path_buf, + }); } WindowEvent::HoveredFile(path_buf) => { - file_drag_and_drop_events.send(FileDragAndDrop::HoveredFile { - window: window_entity, - path_buf, - }); + event_writers + .file_drag_and_drop + .send(FileDragAndDrop::HoveredFile { + window: window_entity, + path_buf, + }); } WindowEvent::HoveredFileCancelled => { - file_drag_and_drop_events.send(FileDragAndDrop::HoveredFileCanceled { - window: window_entity, - }); + event_writers.file_drag_and_drop.send( + FileDragAndDrop::HoveredFileCanceled { + window: window_entity, + }, + ); } WindowEvent::Moved(position) => { let position = ivec2(position.x, position.y); - window.position.set(position); - - window_events.window_moved.send(WindowMoved { + event_writers.window_moved.send(WindowMoved { entity: window_entity, position, }); } WindowEvent::Ime(event) => match event { event::Ime::Preedit(value, cursor) => { - input_events.ime_input.send(Ime::Preedit { + event_writers.ime_input.send(Ime::Preedit { window: window_entity, value, cursor, }); } - event::Ime::Commit(value) => input_events.ime_input.send(Ime::Commit { + event::Ime::Commit(value) => event_writers.ime_input.send(Ime::Commit { window: window_entity, value, }), - event::Ime::Enabled => input_events.ime_input.send(Ime::Enabled { + event::Ime::Enabled => event_writers.ime_input.send(Ime::Enabled { window: window_entity, }), - event::Ime::Disabled => input_events.ime_input.send(Ime::Disabled { + event::Ime::Disabled => event_writers.ime_input.send(Ime::Disabled { window: window_entity, }), }, WindowEvent::ThemeChanged(theme) => { - window_events.window_theme_changed.send(WindowThemeChanged { + event_writers.window_theme_changed.send(WindowThemeChanged { window: window_entity, theme: convert_winit_theme(theme), }); } WindowEvent::Destroyed => { - window_events.window_destroyed.send(WindowDestroyed { + event_writers.window_destroyed.send(WindowDestroyed { window: window_entity, }); } @@ -655,132 +656,134 @@ pub fn winit_runner(mut app: App) { event: DeviceEvent::MouseMotion { delta: (x, y) }, .. } => { - let mut system_state: SystemState> = - SystemState::new(&mut app.world); - let mut mouse_motion = system_state.get_mut(&mut app.world); - - mouse_motion.send(MouseMotion { + 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), }); + + runner_state.device_event_received = true; } event::Event::Suspended => { - winit_state.active = false; + runner_state.is_active = false; #[cfg(target_os = "android")] { - // Bevy doesn't support suspend/resume so we just exit - // and Android will restart the application on resume - // TODO: Save save some state and load on resume + // Android sending this event invalidates all render surfaces. + // TODO + // Upon resume, check if the new render surfaces are compatible with the + // existing render device. If not (which should basically never happen), + // and *then* try to rebuild the renderer. *control_flow = ControlFlow::Exit; } } event::Event::Resumed => { - winit_state.active = true; + runner_state.is_active = true; } event::Event::MainEventsCleared => { - let (winit_config, window_focused_query) = focused_window_state.get(&app.world); - - let update = if winit_state.active { - // True if _any_ windows are currently being focused - let app_focused = window_focused_query.iter().any(|window| window.focused); - match winit_config.update_mode(app_focused) { - UpdateMode::Continuous | UpdateMode::Reactive { .. } => true, + if runner_state.is_active { + 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 => true, + UpdateMode::Reactive { .. } => { + runner_state.wait_elapsed + || runner_state.redraw_requested + || runner_state.window_event_received + || runner_state.device_event_received + } UpdateMode::ReactiveLowPower { .. } => { - winit_state.low_power_event - || winit_state.redraw_request_sent - || winit_state.timeout_reached + runner_state.wait_elapsed + || runner_state.redraw_requested + || runner_state.window_event_received } - } - } else { - false - }; + }; - if update && finished_and_setup_done { - winit_state.last_update = Instant::now(); - app.update(); - } - } - Event::RedrawEventsCleared => { - { - // Fetch from world - let (winit_config, window_focused_query) = focused_window_state.get(&app.world); - - // True if _any_ windows are currently being focused - let app_focused = window_focused_query.iter().any(|window| window.focused); - - let now = Instant::now(); - use UpdateMode::*; - *control_flow = match winit_config.update_mode(app_focused) { - Continuous => ControlFlow::Poll, - Reactive { max_wait } | ReactiveLowPower { max_wait } => { - if let Some(instant) = now.checked_add(*max_wait) { - ControlFlow::WaitUntil(instant) - } else { - ControlFlow::Wait + if should_update { + // reset these on each update + runner_state.wait_elapsed = false; + runner_state.device_event_received = 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(app_redraw_events) = + app.world.get_resource::>() + { + if redraw_event_reader.iter(app_redraw_events).last().is_some() { + runner_state.redraw_requested = true; + *control_flow = ControlFlow::Poll; } } - }; - } - // This block needs to run after `app.update()` in `MainEventsCleared`. Otherwise, - // we won't be able to see redraw requests until the next event, defeating the - // purpose of a redraw request! - let mut redraw = false; - if let Some(app_redraw_events) = app.world.get_resource::>() { - if redraw_event_reader.iter(app_redraw_events).last().is_some() { - *control_flow = ControlFlow::Poll; - redraw = true; + if let Some(app_exit_events) = app.world.get_resource::>() { + if app_exit_event_reader.iter(app_exit_events).last().is_some() { + *control_flow = ControlFlow::Exit; + } + } + + #[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); } } - - winit_state.redraw_request_sent = redraw; } - _ => (), } - - if winit_state.active { - #[cfg(not(target_arch = "wasm32"))] - let ( - commands, - mut new_windows, - created_window_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - ) = create_window_system_state.get_mut(&mut app.world); - - #[cfg(target_arch = "wasm32")] - let ( - commands, - mut new_windows, - created_window_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - canvas_parent_resize_channel, - ) = create_window_system_state.get_mut(&mut app.world); - - // Responsible for creating new windows - create_window( - commands, - event_loop, - new_windows.iter_mut(), - created_window_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - #[cfg(target_arch = "wasm32")] - canvas_parent_resize_channel, - ); - - create_window_system_state.apply(&mut app.world); - } }; - // If true, returns control from Winit back to the main Bevy loop + trace!("starting winit event loop"); if return_from_run { run_return(&mut event_loop, event_handler); } else { diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index a39aa4d560de5..f2dd8d1a2a723 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -30,15 +30,16 @@ use crate::{ get_best_videomode, get_fitting_videomode, WinitWindows, }; -/// System responsible for creating new windows whenever a [`Window`] component is added -/// to an entity. +/// Creates new windows on the [`winit`] backend for each entity with a newly-added +/// [`Window`] component. /// -/// This will default any necessary components if they are not already added. +/// 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_window<'a>( +pub(crate) fn create_windows<'a, T>( + event_loop: &EventLoopWindowTarget, mut commands: Commands, - event_loop: &EventLoopWindowTarget<()>, - created_windows: impl Iterator)>, + mut windows: impl Iterator)>, mut event_writer: EventWriter, mut winit_windows: NonSendMut, mut adapters: NonSendMut, @@ -46,7 +47,7 @@ pub(crate) fn create_window<'a>( mut accessibility_requested: ResMut, #[cfg(target_arch = "wasm32")] event_channel: ResMut, ) { - for (entity, mut window) in created_windows { + for (entity, mut window) in windows { if winit_windows.get_window(entity).is_some() { continue; } @@ -103,7 +104,7 @@ pub(crate) fn create_window<'a>( #[derive(Debug, Clone, Resource)] pub struct WindowTitleCache(HashMap); -pub(crate) fn despawn_window( +pub(crate) fn despawn_windows( mut closed: RemovedComponents, window_entities: Query<&Window>, mut close_events: EventWriter, @@ -126,14 +127,14 @@ pub struct CachedWindow { pub window: Window, } -// Detect changes to the window and update the winit window accordingly. -// -// Notes: -// - [`Window::present_mode`] and [`Window::composite_alpha_mode`] updating should be handled in the bevy render crate. -// - [`Window::transparent`] currently cannot be updated after startup for winit. -// - [`Window::canvas`] currently cannot be updated after startup, not entirely sure if it would work well with the -// event channel stuff. -pub(crate) fn changed_window( +/// Propagates changes from window entities to the [`winit`] backend. +/// +/// # Notes +/// +/// - [`Window::present_mode`] and [`Window::composite_alpha_mode`] changes are handled by the `bevy_render` crate. +/// - [`Window::transparent`] cannot be changed after the window is created. +/// - [`Window::canvas`] cannot be changed after the window is created. +pub(crate) fn changed_windows( mut changed_windows: Query<(Entity, &mut Window, &mut CachedWindow), Changed>, winit_windows: NonSendMut, ) { diff --git a/crates/bevy_winit/src/winit_config.rs b/crates/bevy_winit/src/winit_config.rs index ec2ff83127488..e9b4b12a1abaf 100644 --- a/crates/bevy_winit/src/winit_config.rs +++ b/crates/bevy_winit/src/winit_config.rs @@ -1,57 +1,68 @@ use bevy_ecs::system::Resource; use bevy_utils::Duration; -/// A resource for configuring usage of the [`winit`] library. +/// Settings for the [`WinitPlugin`](super::WinitPlugin). #[derive(Debug, Resource)] pub struct WinitSettings { - /// Configures `winit` to return control to the caller after exiting the - /// event loop, enabling [`App::run()`](bevy_app::App::run()) to return. + /// Controls how the [`EventLoop`](winit::event_loop::EventLoop) is deployed. /// - /// By default, [`return_from_run`](Self::return_from_run) is `false` and *Bevy* - /// will use `winit`'s - /// [`EventLoop::run()`](https://docs.rs/winit/latest/winit/event_loop/struct.EventLoop.html#method.run) - /// to initiate the event loop. - /// [`EventLoop::run()`](https://docs.rs/winit/latest/winit/event_loop/struct.EventLoop.html#method.run) - /// will never return but will terminate the process after the event loop exits. + /// - If this value is set to `false` (default), [`run`] is called, and exiting the loop will + /// terminate the program. + /// - If this value is set to `true`, [`run_return`] is called, and exiting the loop will + /// return control to the caller. /// - /// Setting [`return_from_run`](Self::return_from_run) to `true` will cause *Bevy* - /// to use `winit`'s - /// [`EventLoopExtRunReturn::run_return()`](https://docs.rs/winit/latest/winit/platform/run_return/trait.EventLoopExtRunReturn.html#tymethod.run_return) - /// instead which is strongly discouraged by the `winit` authors. + /// **NOTE:** This cannot be changed while the loop is running. `winit` discourages use of + /// `run_return`. /// /// # Supported platforms /// - /// This feature is only available on the following desktop `target_os` configurations: - /// `windows`, `macos`, `linux`, `dragonfly`, `freebsd`, `netbsd`, and `openbsd`. + /// `run_return` is only available on the following `target_os` environments: + /// - `windows` + /// - `macos` + /// - `linux` + /// - `freebsd` + /// - `openbsd` + /// - `netbsd` + /// - `dragonfly` /// - /// Setting [`return_from_run`](Self::return_from_run) to `true` on - /// unsupported platforms will cause [`App::run()`](bevy_app::App::run()) to panic! + /// The runner will panic if this is set to `true` on other platforms. + /// + /// [`run`]: https://docs.rs/winit/latest/winit/event_loop/struct.EventLoop.html#method.run + /// [`run_return`]: https://docs.rs/winit/latest/winit/platform/run_return/trait.EventLoopExtRunReturn.html#tymethod.run_return pub return_from_run: bool, - /// Configures how the winit event loop updates while the window is focused. + /// Determines how frequently the application can update when it has focus. pub focused_mode: UpdateMode, - /// Configures how the winit event loop updates while the window is *not* focused. + /// Determines how frequently the application can update when it's out of focus. pub unfocused_mode: UpdateMode, } + impl WinitSettings { - /// Configure winit with common settings for a game. + /// Default settings for games. + /// + /// [`Continuous`](UpdateMode::Continuous) if windows have focus. pub fn game() -> Self { WinitSettings::default() } - /// Configure winit with common settings for a desktop application. + /// Default settings for desktop applications. + /// + /// [`Reactive`](UpdateMode::Reactive) if windows have focus, + /// [`ReactiveLowPower`](UpdateMode::ReactiveLowPower) otherwise. pub fn desktop_app() -> Self { WinitSettings { focused_mode: UpdateMode::Reactive { - max_wait: Duration::from_secs(5), + wait: Duration::from_secs(5), }, unfocused_mode: UpdateMode::ReactiveLowPower { - max_wait: Duration::from_secs(60), + wait: Duration::from_secs(60), }, ..Default::default() } } - /// Gets the configured [`UpdateMode`] depending on whether the window is focused or not + /// Returns the current [`UpdateMode`]. + /// + /// **Note:** The output depends on whether the window has focus or not. pub fn update_mode(&self, focused: bool) -> &UpdateMode { match focused { true => &self.focused_mode, @@ -59,6 +70,7 @@ impl WinitSettings { } } } + impl Default for WinitSettings { fn default() -> Self { WinitSettings { @@ -69,45 +81,43 @@ impl Default for WinitSettings { } } -/// Configure how the winit event loop should update. -#[derive(Debug)] +#[allow(clippy::doc_markdown)] +/// Determines how frequently an [`App`](bevy_app::App) should update. +/// +/// **NOTE:** This setting is independent of VSync. VSync is controlled by a window's +/// [`PresentMode`](bevy_window::PresentMode) setting. If an app can update faster than the refresh +/// rate, but VSync is enabled, the update rate will be indirectly limited by the renderer. +#[derive(Debug, Clone, Copy)] pub enum UpdateMode { - /// The event loop will update continuously, running as fast as possible. + /// The [`App`](bevy_app::App) will update over and over, as fast as it possibly can, until an + /// [`AppExit`](bevy_app::AppExit) event appears. Continuous, - /// The event loop will only update if there is a winit event, a redraw is requested, or the - /// maximum wait time has elapsed. - /// - /// ## Note - /// - /// Once the app has executed all bevy systems and reaches the end of the event loop, there is - /// no way to force the app to wake and update again, unless a `winit` event (such as user - /// input, or the window being resized) is received or the time limit is reached. + /// The [`App`](bevy_app::App) will update in response to the following, until an + /// [`AppExit`](bevy_app::AppExit) event appears: + /// - enough time has elapsed since the previous update + /// - a redraw is requested + /// - new window or device events have appeared Reactive { - /// The maximum time to wait before the event loop runs again. + /// The minimum time from the start of one update to the next. /// - /// Note that Bevy will wait indefinitely if the duration is too high (such as [`Duration::MAX`]). - max_wait: Duration, + /// **Note:** This has no upper limit. + /// The [`App`](bevy_app::App) will wait forever if you set this to [`Duration::MAX`]. + wait: Duration, }, - /// The event loop will only update if there is a winit event from direct interaction with the - /// window (e.g. mouseover), a redraw is requested, or the maximum wait time has elapsed. - /// - /// ## Note - /// - /// Once the app has executed all bevy systems and reaches the end of the event loop, there is - /// no way to force the app to wake and update again, unless a `winit` event (such as user - /// input, or the window being resized) is received or the time limit is reached. - /// - /// ## Differences from [`UpdateMode::Reactive`] + /// The [`App`](bevy_app::App) will update in response to the following, until an + /// [`AppExit`](bevy_app::AppExit) event appears: + /// - enough time has elapsed since the previous update + /// - a redraw is requested + /// - new window events have appeared /// - /// Unlike [`UpdateMode::Reactive`], this mode will ignore winit events that aren't directly - /// caused by interaction with the window. For example, you might want to use this mode when the - /// window is not focused, to only re-draw your bevy app when the cursor is over the window, but - /// not when the mouse moves somewhere else on the screen. This helps to significantly reduce - /// power consumption by only updated the app when absolutely necessary. + /// **Note:** Unlike [`Reactive`](`UpdateMode::Reactive`), this mode ignores device events. + /// Use this mode if, for example, you only want your app to update when the mouse cursor is + /// moving over a window, not just moving in general. This can greatly reduce power consumption. ReactiveLowPower { - /// The maximum time to wait before the event loop runs again. + /// The minimum time from the start of one update to the next. /// - /// Note that Bevy will wait indefinitely if the duration is too high (such as [`Duration::MAX`]). - max_wait: Duration, + /// **Note:** This has no upper limit. + /// The [`App`](bevy_app::App) will wait forever if you set this to [`Duration::MAX`]. + wait: Duration, }, } diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 0a0cd1ac01a60..bcd79120fa6ce 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -21,7 +21,8 @@ use crate::{ converters::{convert_enabled_buttons, convert_window_level, convert_window_theme}, }; -/// A resource which maps window entities to [`winit`] library windows. +/// 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. @@ -30,18 +31,17 @@ pub struct WinitWindows { pub entity_to_winit: HashMap, /// Maps `winit` window identifiers to entities. pub winit_to_entity: HashMap, - - // Some winit functions, such as `set_window_icon` can only be used from the main thread. If - // they are used in another thread, the app will hang. This marker ensures `WinitWindows` is - // only ever accessed with bevy's non-send functions and in NonSend systems. + // 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 ()>, } impl WinitWindows { /// Creates a `winit` window and associates it with our entity. - pub fn create_window( + pub fn create_window( &mut self, - event_loop: &winit::event_loop::EventLoopWindowTarget<()>, + event_loop: &winit::event_loop::EventLoopWindowTarget, entity: Entity, window: &Window, adapters: &mut AccessKitAdapters, @@ -169,15 +169,15 @@ impl WinitWindows { handlers.insert(entity, handler); winit_window.set_visible(true); - // Do not set the grab mode on window creation if it's none, this can fail on mobile + // Do not set the grab mode on window creation if it's none. It can fail on mobile. if window.cursor.grab_mode != CursorGrabMode::None { attempt_grab(&winit_window, window.cursor.grab_mode); } winit_window.set_cursor_visible(window.cursor.visible); - // Do not set the cursor hittest on window creation if it's false, as it will always fail on some - // platforms and log an unfixable warning. + // Do not set the cursor hittest on window creation if it's false, as it will always fail on + // some platforms and log an unfixable warning. if !window.cursor.hit_test { if let Err(err) = winit_window.set_cursor_hittest(window.cursor.hit_test) { warn!( @@ -231,7 +231,7 @@ impl WinitWindows { /// 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, to track that we used to know about this winit window + // Don't remove from `winit_to_window_id` so we know the window used to exist. self.windows.remove(&winit_id) } } @@ -346,8 +346,8 @@ pub fn winit_window_position( if let Some(monitor) = maybe_monitor { let screen_size = monitor.size(); - // We use the monitors scale factor here since WindowResolution.scale_factor - // is not yet populated when windows are created at plugin setup + // We use the monitors scale factor here since `WindowResolution.scale_factor` is + // not yet populated when windows are created during plugin setup. let scale_factor = monitor.scale_factor(); // Logical to physical window size diff --git a/examples/window/low_power.rs b/examples/window/low_power.rs index ba890e7ec9d4b..d64f9d36412b6 100644 --- a/examples/window/low_power.rs +++ b/examples/window/low_power.rs @@ -20,7 +20,7 @@ fn main() { .insert_resource(WinitSettings { focused_mode: bevy::winit::UpdateMode::Continuous, unfocused_mode: bevy::winit::UpdateMode::ReactiveLowPower { - max_wait: Duration::from_millis(10), + wait: Duration::from_millis(10), }, ..default() })