diff --git a/Cargo.toml b/Cargo.toml index b14b51b22ef91..38cb2a817812e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,7 +98,9 @@ bevy_dylib = { path = "crates/bevy_dylib", version = "0.6.0", default-features = bevy_internal = { path = "crates/bevy_internal", version = "0.6.0", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] -bevy_internal = { path = "crates/bevy_internal", version = "0.6.0", default-features = false, features = ["webgl"] } +bevy_internal = { path = "crates/bevy_internal", version = "0.6.0", default-features = false, features = [ + "webgl", +] } [dev-dependencies] anyhow = "1.0.4" @@ -522,6 +524,10 @@ path = "examples/ui/ui.rs" name = "clear_color" path = "examples/window/clear_color.rs" +[[example]] +name = "low_power" +path = "examples/window/low_power.rs" + [[example]] name = "multiple_windows" path = "examples/window/multiple_windows.rs" diff --git a/crates/bevy_window/src/event.rs b/crates/bevy_window/src/event.rs index 72d6483d9b0df..8673edf550c89 100644 --- a/crates/bevy_window/src/event.rs +++ b/crates/bevy_window/src/event.rs @@ -20,6 +20,11 @@ pub struct CreateWindow { pub descriptor: WindowDescriptor, } +/// An event that indicates the window should redraw, even if its control flow is set to `Wait` and +/// there have been no window events. +#[derive(Debug, Clone)] +pub struct RequestRedraw; + /// An event that indicates a window should be closed. #[derive(Debug, Clone)] pub struct CloseWindow { diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index a686e8b2b2f6e..37cac585e0701 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -43,6 +43,7 @@ impl Plugin for WindowPlugin { .add_event::() .add_event::() .add_event::() + .add_event::() .add_event::() .add_event::() .add_event::() diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index f59d15e41dfca..eb588e132f41a 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -17,15 +17,18 @@ use bevy_ecs::{ world::World, }; use bevy_math::{ivec2, DVec2, Vec2}; -use bevy_utils::tracing::{error, trace, warn}; +use bevy_utils::{ + tracing::{error, trace, warn}, + Instant, +}; use bevy_window::{ CreateWindow, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, ReceivedCharacter, - WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, WindowFocused, - WindowMoved, WindowResized, WindowScaleFactorChanged, Windows, + RequestRedraw, WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, + WindowFocused, WindowMoved, WindowResized, WindowScaleFactorChanged, Windows, }; use winit::{ dpi::PhysicalPosition, - event::{self, DeviceEvent, Event, WindowEvent}, + event::{self, DeviceEvent, Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, }; @@ -37,6 +40,7 @@ pub struct WinitPlugin; impl Plugin for WinitPlugin { fn build(&self, app: &mut App) { app.init_non_send_resource::() + .init_resource::() .set_runner(winit_runner) .add_system_to_stage(CoreStage::PostUpdate, change_window.exclusive_system()); let event_loop = EventLoop::new(); @@ -227,6 +231,31 @@ pub fn winit_runner(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 `WaitUntil` timeout. + timeout_reached: bool, + last_update: Instant, +} +impl Default for WinitPersistentState { + fn default() -> Self { + Self { + active: true, + low_power_event: false, + redraw_request_sent: false, + timeout_reached: false, + last_update: Instant::now(), + } + } +} + pub fn winit_runner_with(mut app: App) { let mut event_loop = app .world @@ -234,34 +263,39 @@ pub fn winit_runner_with(mut app: App) { .unwrap(); let mut create_window_event_reader = ManualEventReader::::default(); let mut app_exit_event_reader = ManualEventReader::::default(); + let mut redraw_event_reader = ManualEventReader::::default(); + let mut winit_state = WinitPersistentState::default(); app.world .insert_non_send_resource(event_loop.create_proxy()); + let return_from_run = app.world.resource::().return_from_run; trace!("Entering winit event loop"); - let should_return_from_run = app - .world - .get_resource::() - .map_or(false, |config| config.return_from_run); - - let mut active = true; - let event_handler = move |event: Event<()>, event_loop: &EventLoopWindowTarget<()>, control_flow: &mut ControlFlow| { - *control_flow = ControlFlow::Poll; - - if let Some(app_exit_events) = app.world.get_resource_mut::>() { - if app_exit_event_reader - .iter(&app_exit_events) - .next_back() - .is_some() - { - *control_flow = ControlFlow::Exit; - } - } - match event { + event::Event::NewEvents(start) => { + let winit_config = app.world.resource::(); + let windows = app.world.resource::(); + let focused = windows.iter().any(|w| w.is_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(focused) { + UpdateMode::Continuous => false, + UpdateMode::Reactive { max_wait } + | UpdateMode::ReactiveLowPower { max_wait } => { + now.duration_since(winit_state.last_update) >= *max_wait + } + }; + // 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; + } event::Event::WindowEvent { event, window_id: winit_window_id, @@ -287,6 +321,7 @@ pub fn winit_runner_with(mut app: App) { warn!("Skipped event for unknown Window Id {:?}", winit_window_id); return; }; + winit_state.low_power_event = true; match event { WindowEvent::Resized(size) => { @@ -497,10 +532,10 @@ pub fn winit_runner_with(mut app: App) { }); } event::Event::Suspended => { - active = false; + winit_state.active = false; } event::Event::Resumed => { - active = true; + winit_state.active = true; } event::Event::MainEventsCleared => { handle_create_window_events( @@ -508,14 +543,62 @@ pub fn winit_runner_with(mut app: App) { event_loop, &mut create_window_event_reader, ); - if active { + let winit_config = app.world.resource::(); + let update = if winit_state.active { + let windows = app.world.resource::(); + let focused = windows.iter().any(|w| w.is_focused()); + match winit_config.update_mode(focused) { + UpdateMode::Continuous | UpdateMode::Reactive { .. } => true, + UpdateMode::ReactiveLowPower { .. } => { + winit_state.low_power_event + || winit_state.redraw_request_sent + || winit_state.timeout_reached + } + } + } else { + false + }; + if update { + winit_state.last_update = Instant::now(); app.update(); } } + Event::RedrawEventsCleared => { + { + let winit_config = app.world.resource::(); + let windows = app.world.resource::(); + let focused = windows.iter().any(|w| w.is_focused()); + let now = Instant::now(); + use UpdateMode::*; + *control_flow = match winit_config.update_mode(focused) { + Continuous => ControlFlow::Poll, + Reactive { max_wait } | ReactiveLowPower { max_wait } => { + ControlFlow::WaitUntil(now + *max_wait) + } + }; + } + // 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; + } + } + winit_state.redraw_request_sent = redraw; + } _ => (), } }; - if should_return_from_run { + + if return_from_run { run_return(&mut event_loop, event_handler); } else { run(event_loop, event_handler); diff --git a/crates/bevy_winit/src/winit_config.rs b/crates/bevy_winit/src/winit_config.rs index 20a72870b16de..0c93ee77b8c87 100644 --- a/crates/bevy_winit/src/winit_config.rs +++ b/crates/bevy_winit/src/winit_config.rs @@ -1,15 +1,89 @@ +use bevy_utils::Duration; + /// A resource for configuring usage of the `rust_winit` library. -#[derive(Debug, Default)] -pub struct WinitConfig { - /// Configures the winit library to return control to the main thread after - /// the [run](bevy_app::App::run) loop is exited. Winit strongly recommends - /// avoiding this when possible. Before using this please read and understand - /// the [caveats](winit::platform::run_return::EventLoopExtRunReturn::run_return) - /// in the winit documentation. - /// - /// This feature is only available on desktop `target_os` configurations. - /// Namely `windows`, `macos`, `linux`, `dragonfly`, `freebsd`, `netbsd`, and - /// `openbsd`. If set to true on an unsupported platform - /// [run](bevy_app::App::run) will panic. +#[derive(Debug)] +pub struct WinitSettings { + /// Configures the winit library to return control to the main thread after the + /// [run](bevy_app::App::run) loop is exited. Winit strongly recommends avoiding this when + /// possible. Before using this please read and understand the + /// [caveats](winit::platform::run_return::EventLoopExtRunReturn::run_return) in the winit + /// documentation. + /// + /// This feature is only available on desktop `target_os` configurations. Namely `windows`, + /// `macos`, `linux`, `dragonfly`, `freebsd`, `netbsd`, and `openbsd`. If set to true on an + /// unsupported platform [run](bevy_app::App::run) will panic. pub return_from_run: bool, + /// Configures how the winit event loop updates while the window is focused. + pub focused_mode: UpdateMode, + /// Configures how the winit event loop updates while the window is *not* focused. + pub unfocused_mode: UpdateMode, +} +impl WinitSettings { + /// Configure winit with common settings for a game. + pub fn game() -> Self { + WinitSettings::default() + } + + /// Configure winit with common settings for a desktop application. + pub fn desktop_app() -> Self { + WinitSettings { + focused_mode: UpdateMode::Reactive { + max_wait: Duration::from_secs(5), + }, + unfocused_mode: UpdateMode::ReactiveLowPower { + max_wait: Duration::from_secs(60), + }, + ..Default::default() + } + } + + /// Gets the configured `UpdateMode` depending on whether the window is focused or not + pub fn update_mode(&self, focused: bool) -> &UpdateMode { + match focused { + true => &self.focused_mode, + false => &self.unfocused_mode, + } + } +} +impl Default for WinitSettings { + fn default() -> Self { + WinitSettings { + return_from_run: false, + focused_mode: UpdateMode::Continuous, + unfocused_mode: UpdateMode::Continuous, + } + } +} + +/// Configure how the winit event loop should update. +#[derive(Debug)] +pub enum UpdateMode { + /// The event loop will update continuously, running as fast as possible. + 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. + Reactive { 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`] + /// + /// 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. + ReactiveLowPower { max_wait: Duration }, } diff --git a/examples/README.md b/examples/README.md index 340918cd1e443..bbf1278529428 100644 --- a/examples/README.md +++ b/examples/README.md @@ -261,6 +261,7 @@ Example | File | Description Example | File | Description --- | --- | --- `clear_color` | [`window/clear_color.rs`](./window/clear_color.rs) | Creates a solid color window +`low_power` | [`window/low_power.rs`](./window/low_power.rs) | Demonstrates settings to reduce power use for bevy applications `multiple_windows` | [`window/multiple_windows.rs`](./window/multiple_windows.rs) | Demonstrates creating multiple windows, and rendering to them `scale_factor_override` | [`window/scale_factor_override.rs`](./window/scale_factor_override.rs) | Illustrates how to customize the default window settings `transparent_window` | [`window/transparent_window.rs`](./window/transparent_window.rs) | Illustrates making the window transparent and hiding the window decoration diff --git a/examples/app/return_after_run.rs b/examples/app/return_after_run.rs index ff73928a11b4a..7d4968c83e10b 100644 --- a/examples/app/return_after_run.rs +++ b/examples/app/return_after_run.rs @@ -1,10 +1,11 @@ -use bevy::{prelude::*, winit::WinitConfig}; +use bevy::{prelude::*, winit::WinitSettings}; fn main() { println!("Running first App."); App::new() - .insert_resource(WinitConfig { + .insert_resource(WinitSettings { return_from_run: true, + ..default() }) .insert_resource(ClearColor(Color::rgb(0.2, 0.2, 0.8))) .add_plugins(DefaultPlugins) @@ -12,8 +13,9 @@ fn main() { .run(); println!("Running another App."); App::new() - .insert_resource(WinitConfig { + .insert_resource(WinitSettings { return_from_run: true, + ..default() }) .insert_resource(ClearColor(Color::rgb(0.2, 0.8, 0.2))) .add_plugins_with(DefaultPlugins, |group| { diff --git a/examples/window/low_power.rs b/examples/window/low_power.rs new file mode 100644 index 0000000000000..a23c460255b03 --- /dev/null +++ b/examples/window/low_power.rs @@ -0,0 +1,220 @@ +use std::time::Duration; + +use bevy::{ + prelude::*, + window::{PresentMode, RequestRedraw}, + winit::WinitSettings, +}; + +/// This example illustrates how to run a winit window in a reactive, low power mode. This is useful +/// for making desktop applications, or any other program that doesn't need to be running the event +/// loop non-stop. +fn main() { + App::new() + // Continuous rendering for games - bevy's default. + .insert_resource(WinitSettings::game()) + // Power-saving reactive rendering for applications. + .insert_resource(WinitSettings::desktop_app()) + // You can also customize update behavior with the fields of [`WinitConfig`] + .insert_resource(WinitSettings { + focused_mode: bevy::winit::UpdateMode::Continuous, + unfocused_mode: bevy::winit::UpdateMode::ReactiveLowPower { + max_wait: Duration::from_millis(10), + }, + ..default() + }) + // Turn off vsync to maximize CPU/GPU usage + .insert_resource(WindowDescriptor { + present_mode: PresentMode::Immediate, + ..default() + }) + .insert_resource(ExampleMode::Game) + .add_plugins(DefaultPlugins) + .add_startup_system(test_setup::setup) + .add_system(test_setup::cycle_modes) + .add_system(test_setup::rotate_cube) + .add_system(test_setup::update_text) + .add_system(update_winit) + .run(); +} + +#[derive(Debug)] +enum ExampleMode { + Game, + Application, + ApplicationWithRedraw, +} + +/// Update winit based on the current `ExampleMode` +fn update_winit( + mode: Res, + mut event: EventWriter, + mut winit_config: ResMut, +) { + use ExampleMode::*; + *winit_config = match *mode { + Game => { + // In the default `WinitConfig::game()` mode: + // * When focused: the event loop runs as fast as possible + // * When not focused: the event loop runs as fast as possible + WinitSettings::game() + } + Application => { + // While in `WinitConfig::desktop_app()` mode: + // * When focused: the app will update any time a winit event (e.g. the window is + // moved/resized, the mouse moves, a button is pressed, etc.), a [`RequestRedraw`] + // event is received, or after 5 seconds if the app has not updated. + // * When not focused: the app will update when the window is directly interacted with + // (e.g. the mouse hovers over a visible part of the out of focus window), a + // [`RequestRedraw`] event is received, or one minute has passed without the app + // updating. + WinitSettings::desktop_app() + } + ApplicationWithRedraw => { + // Sending a `RequestRedraw` event is useful when you want the app to update the next + // frame regardless of any user input. For example, your application might use + // `WinitConfig::desktop_app()` to reduce power use, but UI animations need to play even + // when there are no inputs, so you send redraw requests while the animation is playing. + event.send(RequestRedraw); + WinitSettings::desktop_app() + } + }; +} + +/// Everything in this module is for setting up and animating the scene, and is not important to the +/// demonstrated features. +pub(crate) mod test_setup { + use crate::ExampleMode; + use bevy::{prelude::*, window::RequestRedraw}; + + /// Switch between update modes when the mouse is clicked. + pub(crate) fn cycle_modes( + mut mode: ResMut, + mouse_button_input: Res>, + ) { + if mouse_button_input.just_pressed(KeyCode::Space) { + *mode = match *mode { + ExampleMode::Game => ExampleMode::Application, + ExampleMode::Application => ExampleMode::ApplicationWithRedraw, + ExampleMode::ApplicationWithRedraw => ExampleMode::Game, + }; + } + } + + #[derive(Component)] + pub(crate) struct Rotator; + + /// Rotate the cube to make it clear when the app is updating + pub(crate) fn rotate_cube( + time: Res