From f7ca793ab440884d92de48a3f2d4ff4935ca8d67 Mon Sep 17 00:00:00 2001 From: Aevyrie Roessler Date: Thu, 17 Feb 2022 18:16:30 -0800 Subject: [PATCH 01/30] Implement low power updates --- Cargo.toml | 8 +++- crates/bevy_window/src/event.rs | 5 ++ crates/bevy_window/src/lib.rs | 1 + crates/bevy_winit/src/lib.rs | 39 +++++++++++---- crates/bevy_winit/src/winit_config.rs | 3 ++ examples/app/return_after_run.rs | 2 + examples/window/low_power.rs | 69 +++++++++++++++++++++++++++ 7 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 examples/window/low_power.rs diff --git a/Cargo.toml b/Cargo.toml index eb7cab0be8825..6b8240ce7e762 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" @@ -502,6 +504,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 a63ca57a96b7d..bee4d1dd63f57 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -42,6 +42,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 c58c9e4fc8906..06640ed22512a 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -7,6 +7,7 @@ use bevy_input::{ mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, touch::TouchInput, }; +pub use winit::event_loop::ControlFlow; pub use winit_config::*; pub use winit_windows::*; @@ -16,13 +17,13 @@ use bevy_math::{ivec2, DVec2, Vec2}; use bevy_utils::tracing::{error, trace, warn}; 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_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, + event_loop::{EventLoop, EventLoopWindowTarget}, }; use winit::dpi::LogicalSize; @@ -230,23 +231,21 @@ 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(); app.world .insert_non_send_resource(event_loop.create_proxy()); trace!("Entering winit event loop"); - let should_return_from_run = app - .world - .get_resource::() - .map_or(false, |config| config.return_from_run); + let winit_config = app.world.get_resource::(); + let should_return_from_run = winit_config.map_or(false, |config| config.return_from_run); + let config_control_flow = winit_config.map_or(ControlFlow::Poll, |config| config.control_flow); 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) @@ -442,6 +441,10 @@ pub fn winit_runner_with(mut app: App) { ); } WindowEvent::Focused(focused) => { + match control_flow { + ControlFlow::Wait | ControlFlow::WaitUntil(_) => active = focused, + _ => (), + } window.update_focused_status_from_backend(focused); let mut focused_events = world.get_resource_mut::>().unwrap(); @@ -500,6 +503,24 @@ pub fn winit_runner_with(mut app: App) { active = true; } event::Event::MainEventsCleared => { + // Don't use `Event::RedrawRequested(id)`, because it will be generated for each window. + *control_flow = config_control_flow; + // Force a winit redraw by setting the control flow to `Poll`, even if it is currently set + // to `Wait`. Every time the event handler loops, it resets the control_flow to + // match the user-defined [`WinitConfig`] in the line above. This ensures we always + // respect the user choice, but override it when a redraw is needed. + // Why this is required: https://github.com/rust-windowing/winit/issues/1619 + if let Some(app_redraw_events) = + app.world.get_resource_mut::>() + { + if redraw_event_reader + .iter(&app_redraw_events) + .next_back() + .is_some() + { + *control_flow = ControlFlow::Poll; + } + } handle_create_window_events( &mut app.world, event_loop, diff --git a/crates/bevy_winit/src/winit_config.rs b/crates/bevy_winit/src/winit_config.rs index 20a72870b16de..11b01115707da 100644 --- a/crates/bevy_winit/src/winit_config.rs +++ b/crates/bevy_winit/src/winit_config.rs @@ -1,3 +1,5 @@ +use winit::event_loop::ControlFlow; + /// A resource for configuring usage of the `rust_winit` library. #[derive(Debug, Default)] pub struct WinitConfig { @@ -12,4 +14,5 @@ pub struct WinitConfig { /// `openbsd`. If set to true on an unsupported platform /// [run](bevy_app::App::run) will panic. pub return_from_run: bool, + pub control_flow: ControlFlow, } diff --git a/examples/app/return_after_run.rs b/examples/app/return_after_run.rs index ff73928a11b4a..5f56f0bfdb6c9 100644 --- a/examples/app/return_after_run.rs +++ b/examples/app/return_after_run.rs @@ -5,6 +5,7 @@ fn main() { App::new() .insert_resource(WinitConfig { return_from_run: true, + ..Default::default() }) .insert_resource(ClearColor(Color::rgb(0.2, 0.2, 0.8))) .add_plugins(DefaultPlugins) @@ -14,6 +15,7 @@ fn main() { App::new() .insert_resource(WinitConfig { return_from_run: true, + ..Default::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..1693a44cff6c7 --- /dev/null +++ b/examples/window/low_power.rs @@ -0,0 +1,69 @@ +use bevy::{ + prelude::*, + window::RequestRedraw, + winit::{ControlFlow, WinitConfig}, +}; + +/// This example illustrates how to run in low power mode, useful for making desktop applications. +/// The app will only update when there is an event (resize, input, etc.), or you send a redraw +/// request. +fn main() { + App::new() + .insert_resource(WinitConfig { + control_flow: ControlFlow::Wait, + ..Default::default() + }) + .add_plugins(DefaultPlugins) + .add_startup_system(setup) + .add_system(rotate) + // Try uncommenting this system to manually request a redraw every frame: + //.add_system(request_redraw) + .run(); +} + +fn request_redraw(mut event: EventWriter) { + event.send(RequestRedraw); +} + +fn rotate(mut cube_transform: Query<&mut Transform, With>) { + for mut transform in cube_transform.iter_mut() { + transform.rotate(Quat::from_rotation_y(0.05)); + } +} + +#[derive(Component)] +struct Rotator; + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut event: EventWriter, +) { + // cube + commands + .spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), + material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()), + transform: Transform::from_xyz(0.0, 0.5, 0.0), + ..Default::default() + }) + .insert(Rotator); + // light + commands.spawn_bundle(PointLightBundle { + point_light: PointLight { + intensity: 1500.0, + shadows_enabled: true, + ..Default::default() + }, + transform: Transform::from_xyz(4.0, 8.0, 4.0), + ..Default::default() + }); + // camera + commands.spawn_bundle(PerspectiveCameraBundle { + transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + ..Default::default() + }); + event.send(RequestRedraw); +} From 564d9eba55a88328ef92376acd7b27e8fbbe0a0e Mon Sep 17 00:00:00 2001 From: Aevyrie Roessler Date: Thu, 17 Feb 2022 18:57:10 -0800 Subject: [PATCH 02/30] Move camera closer --- examples/window/low_power.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/window/low_power.rs b/examples/window/low_power.rs index 1693a44cff6c7..1a28cb7387d12 100644 --- a/examples/window/low_power.rs +++ b/examples/window/low_power.rs @@ -5,8 +5,8 @@ use bevy::{ }; /// This example illustrates how to run in low power mode, useful for making desktop applications. -/// The app will only update when there is an event (resize, input, etc.), or you send a redraw -/// request. +/// The app will only update when there is an event (resize, mouse input, etc.), or you send a +/// redraw request. fn main() { App::new() .insert_resource(WinitConfig { @@ -46,7 +46,6 @@ fn setup( .spawn_bundle(PbrBundle { mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()), - transform: Transform::from_xyz(0.0, 0.5, 0.0), ..Default::default() }) .insert(Rotator); @@ -62,7 +61,7 @@ fn setup( }); // camera commands.spawn_bundle(PerspectiveCameraBundle { - transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + transform: Transform::from_xyz(-1.0, 1.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y), ..Default::default() }); event.send(RequestRedraw); From 828090b73a79e6dee142bb79903ec0957532293a Mon Sep 17 00:00:00 2001 From: Aevyrie Roessler Date: Thu, 17 Feb 2022 19:40:29 -0800 Subject: [PATCH 03/30] Make example more resource intensive --- examples/window/low_power.rs | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/examples/window/low_power.rs b/examples/window/low_power.rs index 1a28cb7387d12..81f905f48ec79 100644 --- a/examples/window/low_power.rs +++ b/examples/window/low_power.rs @@ -1,6 +1,6 @@ use bevy::{ prelude::*, - window::RequestRedraw, + window::{PresentMode, RequestRedraw}, winit::{ControlFlow, WinitConfig}, }; @@ -27,7 +27,9 @@ fn request_redraw(mut event: EventWriter) { fn rotate(mut cube_transform: Query<&mut Transform, With>) { for mut transform in cube_transform.iter_mut() { + transform.rotate(Quat::from_rotation_x(0.05)); transform.rotate(Quat::from_rotation_y(0.05)); + transform.rotate(Quat::from_rotation_z(0.05)); } } @@ -41,14 +43,21 @@ fn setup( mut materials: ResMut>, mut event: EventWriter, ) { - // cube - commands - .spawn_bundle(PbrBundle { - mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), - material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()), - ..Default::default() - }) - .insert(Rotator); + // Spawn a big block of cubes + for i in -5..5 { + for j in -5..5 { + for k in -5..5 { + commands + .spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), + material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()), + transform: Transform::from_xyz(i as f32, j as f32, k as f32), + ..Default::default() + }) + .insert(Rotator); + } + } + } // light commands.spawn_bundle(PointLightBundle { point_light: PointLight { @@ -61,7 +70,7 @@ fn setup( }); // camera commands.spawn_bundle(PerspectiveCameraBundle { - transform: Transform::from_xyz(-1.0, 1.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y), + transform: Transform::from_xyz(-10.0, 10.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y), ..Default::default() }); event.send(RequestRedraw); From 08b590b875ea11fedf7631ce91bec681596097bd Mon Sep 17 00:00:00 2001 From: Aevyrie Roessler Date: Thu, 17 Feb 2022 20:33:57 -0800 Subject: [PATCH 04/30] Fix bug with delayed events --- crates/bevy_winit/src/lib.rs | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 06640ed22512a..890752990683a 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -246,6 +246,8 @@ pub fn winit_runner_with(mut app: App) { let event_handler = move |event: Event<()>, event_loop: &EventLoopWindowTarget<()>, control_flow: &mut ControlFlow| { + *control_flow = config_control_flow; + if let Some(app_exit_events) = app.world.get_resource_mut::>() { if app_exit_event_reader .iter(&app_exit_events) @@ -503,13 +505,19 @@ pub fn winit_runner_with(mut app: App) { active = true; } event::Event::MainEventsCleared => { - // Don't use `Event::RedrawRequested(id)`, because it will be generated for each window. - *control_flow = config_control_flow; - // Force a winit redraw by setting the control flow to `Poll`, even if it is currently set - // to `Wait`. Every time the event handler loops, it resets the control_flow to - // match the user-defined [`WinitConfig`] in the line above. This ensures we always - // respect the user choice, but override it when a redraw is needed. - // Why this is required: https://github.com/rust-windowing/winit/issues/1619 + handle_create_window_events( + &mut app.world, + event_loop, + &mut create_window_event_reader, + ); + if active { + app.update(); + } + } + Event::RedrawEventsCleared => { + // This needs to run after the app update in `MainEventsCleared`, so we can see if + // any redraw events were generated this frame. Otherwise we won't be able to see + // redraw requests until the next event, defeating the purpose of a redraw request! if let Some(app_redraw_events) = app.world.get_resource_mut::>() { @@ -518,21 +526,14 @@ pub fn winit_runner_with(mut app: App) { .next_back() .is_some() { - *control_flow = ControlFlow::Poll; + *control_flow = ControlFlow::Poll } } - handle_create_window_events( - &mut app.world, - event_loop, - &mut create_window_event_reader, - ); - if active { - app.update(); - } } _ => (), } }; + if should_return_from_run { run_return(&mut event_loop, event_handler); } else { From 1e380e63fbedb676fd4b33fadd237b89250fb97f Mon Sep 17 00:00:00 2001 From: Aevyrie Roessler Date: Thu, 17 Feb 2022 21:11:09 -0800 Subject: [PATCH 05/30] Bugfixes --- crates/bevy_winit/src/lib.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 890752990683a..366c601d886a4 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -510,8 +510,14 @@ pub fn winit_runner_with(mut app: App) { event_loop, &mut create_window_event_reader, ); - if active { - app.update(); + match control_flow { + ControlFlow::Poll => { + if active { + // We only need to update conditionally when in Poll mode. + app.update(); + } + } + _ => app.update(), } } Event::RedrawEventsCleared => { @@ -526,7 +532,10 @@ pub fn winit_runner_with(mut app: App) { .next_back() .is_some() { - *control_flow = ControlFlow::Poll + *control_flow = ControlFlow::Poll; + if !active { + app.update(); + } } } } From a41fe022a6c26e6942e3bb4ea64e326aa95be934 Mon Sep 17 00:00:00 2001 From: Aevyrie Roessler Date: Thu, 17 Feb 2022 21:23:51 -0800 Subject: [PATCH 06/30] Improve example clarity --- examples/window/low_power.rs | 76 +++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/examples/window/low_power.rs b/examples/window/low_power.rs index 81f905f48ec79..3c3b649c9683a 100644 --- a/examples/window/low_power.rs +++ b/examples/window/low_power.rs @@ -1,35 +1,52 @@ use bevy::{ prelude::*, - window::{PresentMode, RequestRedraw}, + window::RequestRedraw, winit::{ControlFlow, WinitConfig}, }; -/// This example illustrates how to run in low power mode, useful for making desktop applications. -/// The app will only update when there is an event (resize, mouse input, etc.), or you send a -/// redraw request. +/// This example illustrates how to run a winit window in a reactive, low power mode, useful for +/// making desktop applications or any other program that doesn't need to be running the main loop +/// non-stop. The app will only update when there is an event (resize, mouse input, etc.), or a +/// redraw request is sent to force an update. +/// +/// When the window is minimized, de-focused, or not receiving input, it will use almost zero power. fn main() { App::new() .insert_resource(WinitConfig { control_flow: ControlFlow::Wait, ..Default::default() }) + .insert_resource(ManuallyRedraw(false)) .add_plugins(DefaultPlugins) .add_startup_system(setup) + .add_system(toggle_mode) .add_system(rotate) - // Try uncommenting this system to manually request a redraw every frame: - //.add_system(request_redraw) .run(); } -fn request_redraw(mut event: EventWriter) { - event.send(RequestRedraw); +struct ManuallyRedraw(bool); + +/// This system runs every update and switches between two modes when the left mouse button is +/// clicked: +/// 1) Continuously sending redraw requests +/// 2) Only updating the app when an input is received +fn toggle_mode( + mut event: EventWriter, + mut redraw: ResMut, + mouse_button_input: Res>, +) { + if mouse_button_input.just_pressed(MouseButton::Left) { + redraw.0 = !redraw.0; + } + if redraw.0 { + event.send(RequestRedraw); + } } fn rotate(mut cube_transform: Query<&mut Transform, With>) { for mut transform in cube_transform.iter_mut() { transform.rotate(Quat::from_rotation_x(0.05)); - transform.rotate(Quat::from_rotation_y(0.05)); - transform.rotate(Quat::from_rotation_z(0.05)); + transform.rotate(Quat::from_rotation_y(0.08)); } } @@ -42,15 +59,18 @@ fn setup( mut meshes: ResMut>, mut materials: ResMut>, mut event: EventWriter, + asset_server: Res, ) { + let mesh = meshes.add(Mesh::from(shape::Cube { size: 0.5 })); + let material = materials.add(Color::rgb(0.8, 0.7, 0.6).into()); // Spawn a big block of cubes - for i in -5..5 { - for j in -5..5 { - for k in -5..5 { + for i in -3..=3 { + for j in -3..=3 { + for k in -3..=3 { commands .spawn_bundle(PbrBundle { - mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), - material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()), + mesh: mesh.clone(), + material: material.clone(), transform: Transform::from_xyz(i as f32, j as f32, k as f32), ..Default::default() }) @@ -74,4 +94,30 @@ fn setup( ..Default::default() }); event.send(RequestRedraw); + // UI camera + commands.spawn_bundle(UiCameraBundle::default()); + // Text with one section + commands.spawn_bundle(TextBundle { + style: Style { + align_self: AlignSelf::FlexEnd, + position_type: PositionType::Absolute, + position: Rect { + bottom: Val::Px(5.0), + right: Val::Px(15.0), + ..Default::default() + }, + ..Default::default() + }, + // Use the `Text::with_section` constructor + text: Text::with_section( + "Click left mouse button to toggle continuous redraw requests", + TextStyle { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 50.0, + color: Color::WHITE, + }, + TextAlignment::default(), + ), + ..Default::default() + }); } From 2cc429cbaafa2eb46b98a267b3ed5e581ce937cc Mon Sep 17 00:00:00 2001 From: Aevyrie Roessler Date: Thu, 17 Feb 2022 21:38:58 -0800 Subject: [PATCH 07/30] Fix bug where redraw requests prevent app exit --- crates/bevy_winit/src/lib.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 366c601d886a4..46f5af8ffb442 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -246,18 +246,6 @@ pub fn winit_runner_with(mut app: App) { let event_handler = move |event: Event<()>, event_loop: &EventLoopWindowTarget<()>, control_flow: &mut ControlFlow| { - *control_flow = config_control_flow; - - 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::WindowEvent { event, @@ -521,6 +509,7 @@ pub fn winit_runner_with(mut app: App) { } } Event::RedrawEventsCleared => { + *control_flow = config_control_flow; // This needs to run after the app update in `MainEventsCleared`, so we can see if // any redraw events were generated this frame. Otherwise we won't be able to see // redraw requests until the next event, defeating the purpose of a redraw request! @@ -538,6 +527,15 @@ pub fn winit_runner_with(mut app: App) { } } } + 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; + } + } } _ => (), } From 4856da106f00e93b8c8099b557f66e097b296e98 Mon Sep 17 00:00:00 2001 From: Aevyrie Roessler Date: Thu, 17 Feb 2022 21:39:18 -0800 Subject: [PATCH 08/30] Switch example from array of cubes to disabled vsync --- examples/window/low_power.rs | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/examples/window/low_power.rs b/examples/window/low_power.rs index 3c3b649c9683a..5999b9acc7d33 100644 --- a/examples/window/low_power.rs +++ b/examples/window/low_power.rs @@ -1,6 +1,6 @@ use bevy::{ prelude::*, - window::RequestRedraw, + window::{PresentMode, RequestRedraw}, winit::{ControlFlow, WinitConfig}, }; @@ -16,6 +16,11 @@ fn main() { control_flow: ControlFlow::Wait, ..Default::default() }) + // Turn off vsync to use maximum CPU/GPU when running all-out + .insert_resource(WindowDescriptor { + present_mode: PresentMode::Immediate, + ..Default::default() + }) .insert_resource(ManuallyRedraw(false)) .add_plugins(DefaultPlugins) .add_startup_system(setup) @@ -61,23 +66,14 @@ fn setup( mut event: EventWriter, asset_server: Res, ) { - let mesh = meshes.add(Mesh::from(shape::Cube { size: 0.5 })); - let material = materials.add(Color::rgb(0.8, 0.7, 0.6).into()); - // Spawn a big block of cubes - for i in -3..=3 { - for j in -3..=3 { - for k in -3..=3 { - commands - .spawn_bundle(PbrBundle { - mesh: mesh.clone(), - material: material.clone(), - transform: Transform::from_xyz(i as f32, j as f32, k as f32), - ..Default::default() - }) - .insert(Rotator); - } - } - } + // cube + commands + .spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Cube { size: 0.5 })), + material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()), + ..Default::default() + }) + .insert(Rotator); // light commands.spawn_bundle(PointLightBundle { point_light: PointLight { @@ -90,7 +86,7 @@ fn setup( }); // camera commands.spawn_bundle(PerspectiveCameraBundle { - transform: Transform::from_xyz(-10.0, 10.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y), + transform: Transform::from_xyz(-2.0, 2.0, 2.0).looking_at(Vec3::ZERO, Vec3::Y), ..Default::default() }); event.send(RequestRedraw); From b8d0531fddd94cc80bcaa0f408119d01f43aad38 Mon Sep 17 00:00:00 2001 From: Aevyrie Roessler Date: Thu, 17 Feb 2022 21:43:45 -0800 Subject: [PATCH 09/30] Add example to readme --- examples/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/README.md b/examples/README.md index aeaff8c7845f2..9f601adf404fd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -256,6 +256,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 From 94c60cce16666d3ffce90fbb3b0ba7fc2ae72e15 Mon Sep 17 00:00:00 2001 From: Aevyrie Roessler Date: Fri, 18 Feb 2022 05:24:55 -0800 Subject: [PATCH 10/30] Fix bug preventing `WinitConfig` resource changes from updating in the winit event loop --- crates/bevy_winit/src/lib.rs | 17 +++++++++++------ examples/window/low_power.rs | 1 + 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 46f5af8ffb442..9087bd059502f 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -237,9 +237,10 @@ pub fn winit_runner_with(mut app: App) { trace!("Entering winit event loop"); - let winit_config = app.world.get_resource::(); - let should_return_from_run = winit_config.map_or(false, |config| config.return_from_run); - let config_control_flow = winit_config.map_or(ControlFlow::Poll, |config| config.control_flow); + let should_return_from_run = app + .world + .get_resource::() + .map_or(false, |config| config.return_from_run); let mut active = true; @@ -509,10 +510,14 @@ pub fn winit_runner_with(mut app: App) { } } Event::RedrawEventsCleared => { + // 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 config_control_flow = app + .world + .get_resource::() + .map_or(ControlFlow::Poll, |config| config.control_flow); *control_flow = config_control_flow; - // This needs to run after the app update in `MainEventsCleared`, so we can see if - // any redraw events were generated this frame. Otherwise we won't be able to see - // redraw requests until the next event, defeating the purpose of a redraw request! if let Some(app_redraw_events) = app.world.get_resource_mut::>() { diff --git a/examples/window/low_power.rs b/examples/window/low_power.rs index 5999b9acc7d33..d5892130c344b 100644 --- a/examples/window/low_power.rs +++ b/examples/window/low_power.rs @@ -12,6 +12,7 @@ use bevy::{ /// When the window is minimized, de-focused, or not receiving input, it will use almost zero power. fn main() { App::new() + // Note: you can change the control_flow setting while the app is running .insert_resource(WinitConfig { control_flow: ControlFlow::Wait, ..Default::default() From a8890872c32bcdb11731e7e4196d6498fec6c242 Mon Sep 17 00:00:00 2001 From: Aevyrie Roessler Date: Fri, 18 Feb 2022 06:08:34 -0800 Subject: [PATCH 11/30] Fleshed out example --- examples/window/low_power.rs | 121 +++++++++++++++++++++++------------ 1 file changed, 80 insertions(+), 41 deletions(-) diff --git a/examples/window/low_power.rs b/examples/window/low_power.rs index d5892130c344b..3e5696bab453b 100644 --- a/examples/window/low_power.rs +++ b/examples/window/low_power.rs @@ -6,13 +6,19 @@ use bevy::{ /// This example illustrates how to run a winit window in a reactive, low power mode, useful for /// making desktop applications or any other program that doesn't need to be running the main loop -/// non-stop. The app will only update when there is an event (resize, mouse input, etc.), or a -/// redraw request is sent to force an update. +/// non-stop. The `winit` event loop will only update when there is a `winit` event (resize, mouse +/// input, etc.), or a redraw request is sent to force an update. /// -/// When the window is minimized, de-focused, or not receiving input, it will use almost zero power. +/// * While in `Wait` mode: the app will use almost zero power when the window is minimized, +/// de-focused, or not receiving input. +/// +/// * While continuously sending `RequestRedraw` events: the app will use resources regardless of +/// window state. +/// +/// Try setting `ControlFlow` to `Poll` (default for Bevy apps), notice resource usage never drops +/// to zero even when minimized. fn main() { App::new() - // Note: you can change the control_flow setting while the app is running .insert_resource(WinitConfig { control_flow: ControlFlow::Wait, ..Default::default() @@ -22,33 +28,57 @@ fn main() { present_mode: PresentMode::Immediate, ..Default::default() }) - .insert_resource(ManuallyRedraw(false)) + .insert_resource(TestMode::Wait) .add_plugins(DefaultPlugins) .add_startup_system(setup) - .add_system(toggle_mode) + .add_system(cycle_modes) .add_system(rotate) + .add_system(update_text) .run(); } -struct ManuallyRedraw(bool); +#[derive(Debug)] +enum TestMode { + Wait, + WaitAndRedraw, + Poll, +} /// This system runs every update and switches between two modes when the left mouse button is /// clicked: -/// 1) Continuously sending redraw requests -/// 2) Only updating the app when an input is received -fn toggle_mode( +/// * Continuously sending redraw requests +/// * Only updating the app when a `winit` event is received +fn cycle_modes( mut event: EventWriter, - mut redraw: ResMut, + mut mode: ResMut, + mut winit_config: ResMut, mouse_button_input: Res>, ) { if mouse_button_input.just_pressed(MouseButton::Left) { - redraw.0 = !redraw.0; + *mode = match *mode { + TestMode::Wait => TestMode::WaitAndRedraw, + TestMode::WaitAndRedraw => TestMode::Poll, + TestMode::Poll => TestMode::Wait, + } } - if redraw.0 { - event.send(RequestRedraw); + winit_config.control_flow = match *mode { + TestMode::Wait => ControlFlow::Wait, + TestMode::WaitAndRedraw => { + // Sending a `RequestRedraw` event is useful when you want the app to update again + // regardless of any user input. For example, your application might use `ControlFlow::Wait` + // 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); + ControlFlow::Wait + } + TestMode::Poll => ControlFlow::Poll, } } +#[derive(Component)] +struct Rotator; + +/// Rotate the cube to make it clear when the app is updating fn rotate(mut cube_transform: Query<&mut Transform, With>) { for mut transform in cube_transform.iter_mut() { transform.rotate(Quat::from_rotation_x(0.05)); @@ -57,9 +87,13 @@ fn rotate(mut cube_transform: Query<&mut Transform, With>) { } #[derive(Component)] -struct Rotator; +struct ModeText; + +fn update_text(mode: Res, mut query: Query<&mut Text, With>) { + query.get_single_mut().unwrap().sections[1].value = format!("{mode:?}") +} -/// set up a simple 3D scene +/// Set up a scene with a cube and some text fn setup( mut commands: Commands, mut meshes: ResMut>, @@ -67,7 +101,6 @@ fn setup( mut event: EventWriter, asset_server: Res, ) { - // cube commands .spawn_bundle(PbrBundle { mesh: meshes.add(Mesh::from(shape::Cube { size: 0.5 })), @@ -75,7 +108,6 @@ fn setup( ..Default::default() }) .insert(Rotator); - // light commands.spawn_bundle(PointLightBundle { point_light: PointLight { intensity: 1500.0, @@ -85,36 +117,43 @@ fn setup( transform: Transform::from_xyz(4.0, 8.0, 4.0), ..Default::default() }); - // camera commands.spawn_bundle(PerspectiveCameraBundle { transform: Transform::from_xyz(-2.0, 2.0, 2.0).looking_at(Vec3::ZERO, Vec3::Y), ..Default::default() }); event.send(RequestRedraw); - // UI camera commands.spawn_bundle(UiCameraBundle::default()); - // Text with one section - commands.spawn_bundle(TextBundle { - style: Style { - align_self: AlignSelf::FlexEnd, - position_type: PositionType::Absolute, - position: Rect { - bottom: Val::Px(5.0), - right: Val::Px(15.0), + let style = TextStyle { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 50.0, + color: Color::WHITE, + }; + commands + .spawn_bundle(TextBundle { + style: Style { + align_self: AlignSelf::FlexStart, + position_type: PositionType::Absolute, + position: Rect { + top: Val::Px(5.0), + left: Val::Px(5.0), + ..Default::default() + }, ..Default::default() }, - ..Default::default() - }, - // Use the `Text::with_section` constructor - text: Text::with_section( - "Click left mouse button to toggle continuous redraw requests", - TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: 50.0, - color: Color::WHITE, + text: Text { + sections: vec![ + TextSection { + value: "Click left mouse button to cycle modes:\n".into(), + style: style.clone(), + }, + TextSection { + value: "Mode::Wait".into(), + style, + }, + ], + alignment: TextAlignment::default(), }, - TextAlignment::default(), - ), - ..Default::default() - }); + ..Default::default() + }) + .insert(ModeText); } From e4a0d6184d458644178fd405365e719486dcf9f2 Mon Sep 17 00:00:00 2001 From: Aevyrie Roessler Date: Fri, 18 Feb 2022 06:23:58 -0800 Subject: [PATCH 12/30] Improve example comments for new functionality --- examples/window/low_power.rs | 179 ++++++++++++++++++----------------- 1 file changed, 92 insertions(+), 87 deletions(-) diff --git a/examples/window/low_power.rs b/examples/window/low_power.rs index 3e5696bab453b..9c680dc544868 100644 --- a/examples/window/low_power.rs +++ b/examples/window/low_power.rs @@ -4,36 +4,35 @@ use bevy::{ winit::{ControlFlow, WinitConfig}, }; -/// This example illustrates how to run a winit window in a reactive, low power mode, useful for -/// making desktop applications or any other program that doesn't need to be running the main loop -/// non-stop. The `winit` event loop will only update when there is a `winit` event (resize, mouse -/// input, etc.), or a redraw request is sent to force an update. +/// 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 main +/// loop non-stop. /// /// * While in `Wait` mode: the app will use almost zero power when the window is minimized, /// de-focused, or not receiving input. /// -/// * While continuously sending `RequestRedraw` events: the app will use resources regardless of -/// window state. +/// * While continuously sending `RequestRedraw` events in `Wait` mode: the app will use resources +/// regardless of window state. /// -/// Try setting `ControlFlow` to `Poll` (default for Bevy apps), notice resource usage never drops -/// to zero even when minimized. +/// * While in `Poll` mode: the app will update continuously, but will suspend app updates when +/// minimized. Notice resource usage never drops to zero even when minimized. fn main() { App::new() .insert_resource(WinitConfig { control_flow: ControlFlow::Wait, ..Default::default() }) - // Turn off vsync to use maximum CPU/GPU when running all-out + // Turn off vsync to maximize CPU/GPU usage .insert_resource(WindowDescriptor { present_mode: PresentMode::Immediate, ..Default::default() }) .insert_resource(TestMode::Wait) .add_plugins(DefaultPlugins) - .add_startup_system(setup) + .add_startup_system(test_setup::setup) .add_system(cycle_modes) - .add_system(rotate) - .add_system(update_text) + .add_system(test_setup::rotate) + .add_system(test_setup::update_text) .run(); } @@ -44,10 +43,7 @@ enum TestMode { Poll, } -/// This system runs every update and switches between two modes when the left mouse button is -/// clicked: -/// * Continuously sending redraw requests -/// * Only updating the app when a `winit` event is received +/// Handles switching between update modes fn cycle_modes( mut event: EventWriter, mut mode: ResMut, @@ -75,85 +71,94 @@ fn cycle_modes( } } -#[derive(Component)] -struct Rotator; +/// Everything in this module is for setup and is not important to the demonstrated features. +pub(crate) mod test_setup { + use crate::TestMode; + use bevy::{prelude::*, window::RequestRedraw}; -/// Rotate the cube to make it clear when the app is updating -fn rotate(mut cube_transform: Query<&mut Transform, With>) { - for mut transform in cube_transform.iter_mut() { - transform.rotate(Quat::from_rotation_x(0.05)); - transform.rotate(Quat::from_rotation_y(0.08)); + #[derive(Component)] + pub(crate) struct Rotator; + + /// Rotate the cube to make it clear when the app is updating + pub(crate) fn rotate(mut cube_transform: Query<&mut Transform, With>) { + for mut transform in cube_transform.iter_mut() { + transform.rotate(Quat::from_rotation_x(0.05)); + transform.rotate(Quat::from_rotation_y(0.08)); + } } -} -#[derive(Component)] -struct ModeText; + #[derive(Component)] + pub struct ModeText; -fn update_text(mode: Res, mut query: Query<&mut Text, With>) { - query.get_single_mut().unwrap().sections[1].value = format!("{mode:?}") -} + pub(crate) fn update_text(mode: Res, mut query: Query<&mut Text, With>) { + query.get_single_mut().unwrap().sections[1].value = format!("{mode:?}") + } -/// Set up a scene with a cube and some text -fn setup( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, - mut event: EventWriter, - asset_server: Res, -) { - commands - .spawn_bundle(PbrBundle { - mesh: meshes.add(Mesh::from(shape::Cube { size: 0.5 })), - material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()), + /// Set up a scene with a cube and some text + pub fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut event: EventWriter, + asset_server: Res, + ) { + commands + .spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Cube { size: 0.5 })), + material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()), + ..Default::default() + }) + .insert(Rotator); + commands.spawn_bundle(PointLightBundle { + point_light: PointLight { + intensity: 1500.0, + shadows_enabled: true, + ..Default::default() + }, + transform: Transform::from_xyz(4.0, 8.0, 4.0), ..Default::default() - }) - .insert(Rotator); - commands.spawn_bundle(PointLightBundle { - point_light: PointLight { - intensity: 1500.0, - shadows_enabled: true, + }); + commands.spawn_bundle(PerspectiveCameraBundle { + transform: Transform::from_xyz(-2.0, 2.0, 2.0).looking_at(Vec3::ZERO, Vec3::Y), ..Default::default() - }, - transform: Transform::from_xyz(4.0, 8.0, 4.0), - ..Default::default() - }); - commands.spawn_bundle(PerspectiveCameraBundle { - transform: Transform::from_xyz(-2.0, 2.0, 2.0).looking_at(Vec3::ZERO, Vec3::Y), - ..Default::default() - }); - event.send(RequestRedraw); - commands.spawn_bundle(UiCameraBundle::default()); - let style = TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: 50.0, - color: Color::WHITE, - }; - commands - .spawn_bundle(TextBundle { - style: Style { - align_self: AlignSelf::FlexStart, - position_type: PositionType::Absolute, - position: Rect { - top: Val::Px(5.0), - left: Val::Px(5.0), + }); + event.send(RequestRedraw); + commands.spawn_bundle(UiCameraBundle::default()); + commands + .spawn_bundle(TextBundle { + style: Style { + align_self: AlignSelf::FlexStart, + position_type: PositionType::Absolute, + position: Rect { + top: Val::Px(5.0), + left: Val::Px(5.0), + ..Default::default() + }, ..Default::default() }, + text: Text { + sections: vec![ + TextSection { + value: "Click left mouse button to cycle modes:\n".into(), + style: TextStyle { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 50.0, + color: Color::WHITE, + }, + }, + TextSection { + value: "Mode::Wait".into(), + style: TextStyle { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 50.0, + color: Color::GREEN, + }, + }, + ], + alignment: TextAlignment::default(), + }, ..Default::default() - }, - text: Text { - sections: vec![ - TextSection { - value: "Click left mouse button to cycle modes:\n".into(), - style: style.clone(), - }, - TextSection { - value: "Mode::Wait".into(), - style, - }, - ], - alignment: TextAlignment::default(), - }, - ..Default::default() - }) - .insert(ModeText); + }) + .insert(ModeText); + } } From 6af14ce27b5e812b1a9b41ee175da1b3742a0c0e Mon Sep 17 00:00:00 2001 From: Aevyrie Roessler Date: Fri, 18 Feb 2022 06:54:56 -0800 Subject: [PATCH 13/30] Fix duplicate app update and perf bugs --- crates/bevy_winit/src/lib.rs | 11 +++-------- examples/window/low_power.rs | 27 +++++++++++++-------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 9087bd059502f..098fcf03d7041 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -518,21 +518,16 @@ pub fn winit_runner_with(mut app: App) { .get_resource::() .map_or(ControlFlow::Poll, |config| config.control_flow); *control_flow = config_control_flow; - if let Some(app_redraw_events) = - app.world.get_resource_mut::>() - { + if let Some(app_redraw_events) = app.world.get_resource::>() { if redraw_event_reader .iter(&app_redraw_events) - .next_back() + .last() .is_some() { *control_flow = ControlFlow::Poll; - if !active { - app.update(); - } } } - if let Some(app_exit_events) = app.world.get_resource_mut::>() { + if let Some(app_exit_events) = app.world.get_resource::>() { if app_exit_event_reader .iter(&app_exit_events) .next_back() diff --git a/examples/window/low_power.rs b/examples/window/low_power.rs index 9c680dc544868..b2d5180f3ec2f 100644 --- a/examples/window/low_power.rs +++ b/examples/window/low_power.rs @@ -14,8 +14,7 @@ use bevy::{ /// * While continuously sending `RequestRedraw` events in `Wait` mode: the app will use resources /// regardless of window state. /// -/// * While in `Poll` mode: the app will update continuously, but will suspend app updates when -/// minimized. Notice resource usage never drops to zero even when minimized. +/// * While in `Poll` mode: the app will update continuously fn main() { App::new() .insert_resource(WinitConfig { @@ -55,19 +54,19 @@ fn cycle_modes( TestMode::Wait => TestMode::WaitAndRedraw, TestMode::WaitAndRedraw => TestMode::Poll, TestMode::Poll => TestMode::Wait, + }; + winit_config.control_flow = match *mode { + TestMode::Wait => ControlFlow::Wait, + TestMode::WaitAndRedraw => ControlFlow::Wait, + TestMode::Poll => ControlFlow::Poll, } } - winit_config.control_flow = match *mode { - TestMode::Wait => ControlFlow::Wait, - TestMode::WaitAndRedraw => { - // Sending a `RequestRedraw` event is useful when you want the app to update again - // regardless of any user input. For example, your application might use `ControlFlow::Wait` - // 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); - ControlFlow::Wait - } - TestMode::Poll => ControlFlow::Poll, + if let TestMode::WaitAndRedraw = *mode { + // Sending a `RequestRedraw` event is useful when you want the app to update again + // regardless of any user input. For example, your application might use `ControlFlow::Wait` + // 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); } } @@ -82,7 +81,7 @@ pub(crate) mod test_setup { /// Rotate the cube to make it clear when the app is updating pub(crate) fn rotate(mut cube_transform: Query<&mut Transform, With>) { for mut transform in cube_transform.iter_mut() { - transform.rotate(Quat::from_rotation_x(0.05)); + transform.rotate(Quat::from_rotation_x(0.04)); transform.rotate(Quat::from_rotation_y(0.08)); } } From 989f1287591510951cbabafb04a93f6aaf787f91 Mon Sep 17 00:00:00 2001 From: Aevyrie Roessler Date: Fri, 18 Feb 2022 07:02:04 -0800 Subject: [PATCH 14/30] clippy --- crates/bevy_winit/src/lib.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 098fcf03d7041..2f826f8be1829 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -519,20 +519,12 @@ pub fn winit_runner_with(mut app: App) { .map_or(ControlFlow::Poll, |config| config.control_flow); *control_flow = config_control_flow; if let Some(app_redraw_events) = app.world.get_resource::>() { - if redraw_event_reader - .iter(&app_redraw_events) - .last() - .is_some() - { + if redraw_event_reader.iter(app_redraw_events).last().is_some() { *control_flow = ControlFlow::Poll; } } if let Some(app_exit_events) = app.world.get_resource::>() { - if app_exit_event_reader - .iter(&app_exit_events) - .next_back() - .is_some() - { + if app_exit_event_reader.iter(app_exit_events).last().is_some() { *control_flow = ControlFlow::Exit; } } From aefd4197d779fa660f172879a258a46577de46e5 Mon Sep 17 00:00:00 2001 From: Aevyrie Roessler Date: Thu, 3 Mar 2022 19:23:17 -0800 Subject: [PATCH 15/30] Add bevy type `UpdateMode` to config event loop. --- crates/bevy_winit/src/lib.rs | 143 +++++++++++++++++++++----- crates/bevy_winit/src/winit_config.rs | 77 +++++++++++++- examples/window/low_power.rs | 64 ++++++------ 3 files changed, 223 insertions(+), 61 deletions(-) diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 2f826f8be1829..633402c923917 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -14,7 +14,10 @@ pub use winit_windows::*; use bevy_app::{App, AppExit, CoreStage, Events, ManualEventReader, Plugin}; use bevy_ecs::{system::IntoExclusiveSystem, 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, RequestRedraw, WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, @@ -22,7 +25,7 @@ use bevy_window::{ }; use winit::{ dpi::PhysicalPosition, - event::{self, DeviceEvent, Event, WindowEvent}, + event::{self, DeviceEvent, Event, StartCause, WindowEvent}, event_loop::{EventLoop, EventLoopWindowTarget}, }; @@ -34,6 +37,8 @@ pub struct WinitPlugin; impl Plugin for WinitPlugin { fn build(&self, app: &mut App) { app.init_resource::() + .init_resource::() + .init_resource::() .set_runner(winit_runner) .add_system_to_stage(CoreStage::PostUpdate, change_window.exclusive_system()); let event_loop = EventLoop::new(); @@ -224,6 +229,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 @@ -235,19 +265,42 @@ pub fn winit_runner_with(mut app: App) { app.world .insert_non_send_resource(event_loop.create_proxy()); - trace!("Entering winit event loop"); - - let should_return_from_run = app + let return_from_run = app .world .get_resource::() - .map_or(false, |config| config.return_from_run); - - let mut active = true; + .unwrap() + .return_from_run; + trace!("Entering winit event loop"); let event_handler = move |event: Event<()>, event_loop: &EventLoopWindowTarget<()>, control_flow: &mut ControlFlow| { match event { + event::Event::NewEvents(start) => { + let winit_state = app.world.get_resource::().unwrap(); + let winit_config = app.world.get_resource::().unwrap(); + let windows = app.world.get_resource::().unwrap(); + let focused = windows.iter().any(|w| w.is_focused()); + // Check if either the `WaitUntil` timeout was reached, or + 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 } => { + now.duration_since(winit_state.last_update) >= *max_wait + } + UpdateMode::ReactiveLowPower { max_wait } => { + now.duration_since(winit_state.last_update) >= *max_wait + } + }; + let mut winit_state = app + .world + .get_resource_mut::() + .unwrap(); + // The low_power_event state 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, @@ -274,6 +327,11 @@ pub fn winit_runner_with(mut app: App) { return; }; + world + .get_resource_mut::() + .unwrap() + .low_power_event = true; + match event { WindowEvent::Resized(size) => { window.update_actual_size_from_backend(size.width, size.height); @@ -432,10 +490,6 @@ pub fn winit_runner_with(mut app: App) { ); } WindowEvent::Focused(focused) => { - match control_flow { - ControlFlow::Wait | ControlFlow::WaitUntil(_) => active = focused, - _ => (), - } window.update_focused_status_from_backend(focused); let mut focused_events = world.get_resource_mut::>().unwrap(); @@ -488,10 +542,16 @@ pub fn winit_runner_with(mut app: App) { }); } event::Event::Suspended => { - active = false; + app.world + .get_resource_mut::() + .unwrap() + .active = false; } event::Event::Resumed => { - active = true; + app.world + .get_resource_mut::() + .unwrap() + .active = true; } event::Event::MainEventsCleared => { handle_create_window_events( @@ -499,41 +559,70 @@ pub fn winit_runner_with(mut app: App) { event_loop, &mut create_window_event_reader, ); - match control_flow { - ControlFlow::Poll => { - if active { - // We only need to update conditionally when in Poll mode. - app.update(); + let winit_state = app.world.get_resource::().unwrap(); + let winit_config = app.world.get_resource::().unwrap(); + let update = if winit_state.active { + let windows = app.world.get_resource::().unwrap(); + let focused = windows.iter().any(|w| w.is_focused()); + match winit_config.update_mode(focused) { + UpdateMode::Continuous => true, + UpdateMode::Reactive { .. } => true, + UpdateMode::ReactiveLowPower { .. } => { + winit_state.low_power_event + || winit_state.redraw_request_sent + || winit_state.timeout_reached } } - _ => app.update(), + } else { + false + }; + if update { + app.world + .get_resource_mut::() + .unwrap() + .last_update = Instant::now(); + app.update(); } } Event::RedrawEventsCleared => { + { + let world = app.world.cell(); + let winit_config = world.get_resource::().unwrap(); + let windows = world.get_resource::().unwrap(); + 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 } => ControlFlow::WaitUntil(now + *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 config_control_flow = app - .world - .get_resource::() - .map_or(ControlFlow::Poll, |config| config.control_flow); - *control_flow = config_control_flow; + 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; } } + app.world + .get_resource_mut::() + .unwrap() + .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 11b01115707da..47d1f162e568d 100644 --- a/crates/bevy_winit/src/winit_config.rs +++ b/crates/bevy_winit/src/winit_config.rs @@ -1,7 +1,7 @@ -use winit::event_loop::ControlFlow; +use bevy_utils::Duration; /// A resource for configuring usage of the `rust_winit` library. -#[derive(Debug, Default)] +#[derive(Debug)] 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 @@ -14,5 +14,76 @@ pub struct WinitConfig { /// `openbsd`. If set to true on an unsupported platform /// [run](bevy_app::App::run) will panic. pub return_from_run: bool, - pub control_flow: ControlFlow, + /// 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 not_focused_mode: UpdateMode, +} +impl WinitConfig { + /// Configure winit with common settings for a game. + pub fn game() -> Self { + WinitConfig::default() + } + /// Configure winit with common settings for a desktop application. + pub fn desktop_app() -> Self { + WinitConfig { + focused_mode: UpdateMode::Reactive { + max_wait: Duration::from_secs(60), + }, + not_focused_mode: UpdateMode::ReactiveLowPower { + max_wait: Duration::from_secs(300), + }, + ..Default::default() + } + } + pub fn update_mode(&self, focused: bool) -> &UpdateMode { + match focused { + true => &self.focused_mode, + false => &self.not_focused_mode, + } + } +} +impl Default for WinitConfig { + fn default() -> Self { + WinitConfig { + return_from_run: false, + focused_mode: UpdateMode::Continuous, + not_focused_mode: UpdateMode::ReactiveLowPower { + max_wait: Duration::from_millis(100), + }, + } + } +} + +/// 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 wake and update again, unless a winit event 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 wake and update again, unless a winit event 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/window/low_power.rs b/examples/window/low_power.rs index b2d5180f3ec2f..5f315590b62b7 100644 --- a/examples/window/low_power.rs +++ b/examples/window/low_power.rs @@ -1,7 +1,7 @@ use bevy::{ prelude::*, window::{PresentMode, RequestRedraw}, - winit::{ControlFlow, WinitConfig}, + winit::WinitConfig, }; /// This example illustrates how to run a winit window in a reactive, low power mode. This is useful @@ -17,62 +17,64 @@ use bevy::{ /// * While in `Poll` mode: the app will update continuously fn main() { App::new() - .insert_resource(WinitConfig { - control_flow: ControlFlow::Wait, - ..Default::default() - }) + .insert_resource(WinitConfig::game()) // Turn off vsync to maximize CPU/GPU usage .insert_resource(WindowDescriptor { present_mode: PresentMode::Immediate, ..Default::default() }) - .insert_resource(TestMode::Wait) + .insert_resource(ExampleMode::Game) .add_plugins(DefaultPlugins) .add_startup_system(test_setup::setup) .add_system(cycle_modes) + .add_system(update_winit) .add_system(test_setup::rotate) .add_system(test_setup::update_text) .run(); } #[derive(Debug)] -enum TestMode { - Wait, - WaitAndRedraw, - Poll, +enum ExampleMode { + Game, + Application, + ApplicationWithRedraw, } -/// Handles switching between update modes -fn cycle_modes( - mut event: EventWriter, - mut mode: ResMut, - mut winit_config: ResMut, - mouse_button_input: Res>, -) { +/// Switch between update modes when the mouse is clicked. +fn cycle_modes(mut mode: ResMut, mouse_button_input: Res>) { if mouse_button_input.just_pressed(MouseButton::Left) { *mode = match *mode { - TestMode::Wait => TestMode::WaitAndRedraw, - TestMode::WaitAndRedraw => TestMode::Poll, - TestMode::Poll => TestMode::Wait, + ExampleMode::Game => ExampleMode::Application, + ExampleMode::Application => ExampleMode::ApplicationWithRedraw, + ExampleMode::ApplicationWithRedraw => ExampleMode::Game, }; - winit_config.control_flow = match *mode { - TestMode::Wait => ControlFlow::Wait, - TestMode::WaitAndRedraw => ControlFlow::Wait, - TestMode::Poll => ControlFlow::Poll, - } } - if let TestMode::WaitAndRedraw = *mode { +} + +/// Update winit based on the current ExampleMode +fn update_winit( + mode: Res, + mut event: EventWriter, + mut winit_config: ResMut, +) { + *winit_config = match *mode { + ExampleMode::Game => WinitConfig::game(), + ExampleMode::Application => WinitConfig::desktop_app(), + ExampleMode::ApplicationWithRedraw => WinitConfig::desktop_app(), + }; + + if let ExampleMode::ApplicationWithRedraw = *mode { // Sending a `RequestRedraw` event is useful when you want the app to update again - // regardless of any user input. For example, your application might use `ControlFlow::Wait` - // 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. + // 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); } } /// Everything in this module is for setup and is not important to the demonstrated features. pub(crate) mod test_setup { - use crate::TestMode; + use crate::ExampleMode; use bevy::{prelude::*, window::RequestRedraw}; #[derive(Component)] @@ -89,7 +91,7 @@ pub(crate) mod test_setup { #[derive(Component)] pub struct ModeText; - pub(crate) fn update_text(mode: Res, mut query: Query<&mut Text, With>) { + pub(crate) fn update_text(mode: Res, mut query: Query<&mut Text, With>) { query.get_single_mut().unwrap().sections[1].value = format!("{mode:?}") } From 6299712d9eab99fcd87b450467963389a6f843e4 Mon Sep 17 00:00:00 2001 From: Aevyrie Roessler Date: Thu, 3 Mar 2022 20:49:02 -0800 Subject: [PATCH 16/30] improve presets and docs --- crates/bevy_winit/src/winit_config.rs | 12 +++--- examples/window/low_power.rs | 55 ++++++++++++++++----------- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/crates/bevy_winit/src/winit_config.rs b/crates/bevy_winit/src/winit_config.rs index 47d1f162e568d..4249501743043 100644 --- a/crates/bevy_winit/src/winit_config.rs +++ b/crates/bevy_winit/src/winit_config.rs @@ -17,7 +17,7 @@ pub struct WinitConfig { /// 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 not_focused_mode: UpdateMode, + pub unfocused_mode: UpdateMode, } impl WinitConfig { /// Configure winit with common settings for a game. @@ -28,10 +28,10 @@ impl WinitConfig { pub fn desktop_app() -> Self { WinitConfig { focused_mode: UpdateMode::Reactive { - max_wait: Duration::from_secs(60), + max_wait: Duration::from_secs(5), }, - not_focused_mode: UpdateMode::ReactiveLowPower { - max_wait: Duration::from_secs(300), + unfocused_mode: UpdateMode::ReactiveLowPower { + max_wait: Duration::from_secs(60), }, ..Default::default() } @@ -39,7 +39,7 @@ impl WinitConfig { pub fn update_mode(&self, focused: bool) -> &UpdateMode { match focused { true => &self.focused_mode, - false => &self.not_focused_mode, + false => &self.unfocused_mode, } } } @@ -48,7 +48,7 @@ impl Default for WinitConfig { WinitConfig { return_from_run: false, focused_mode: UpdateMode::Continuous, - not_focused_mode: UpdateMode::ReactiveLowPower { + unfocused_mode: UpdateMode::ReactiveLowPower { max_wait: Duration::from_millis(100), }, } diff --git a/examples/window/low_power.rs b/examples/window/low_power.rs index 5f315590b62b7..a93228a1bbc7c 100644 --- a/examples/window/low_power.rs +++ b/examples/window/low_power.rs @@ -5,16 +5,22 @@ use bevy::{ }; /// 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 main +/// for making desktop applications, or any other program that doesn't need to be running the event /// loop non-stop. /// -/// * While in `Wait` mode: the app will use almost zero power when the window is minimized, -/// de-focused, or not receiving input. +/// * In the default `WinitConfig::game()` mode, the event loop runs as fast as possible when the +/// window is focused. When not in focus, the app updates every 100ms. /// -/// * While continuously sending `RequestRedraw` events in `Wait` mode: the app will use resources -/// regardless of window state. +/// * While in [`bevy::winit::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.) or [`RequestRedraw`] event is +/// received, or after 5 seconds if the app has not updated. +/// * When not focused: the app will update when a [`RequestRedraw`] event is received, the +/// window is directly interacted with (e.g. the mouse hovers over a visible part of the out +/// of focus window), or one minute has passed without the app updating. /// -/// * While in `Poll` mode: the app will update continuously +/// These two functions are presets, you can customize the behavior by manually setting the fields +/// of the [`WinitConfig`] resource. fn main() { App::new() .insert_resource(WinitConfig::game()) @@ -40,17 +46,6 @@ enum ExampleMode { ApplicationWithRedraw, } -/// Switch between update modes when the mouse is clicked. -fn cycle_modes(mut mode: ResMut, mouse_button_input: Res>) { - if mouse_button_input.just_pressed(MouseButton::Left) { - *mode = match *mode { - ExampleMode::Game => ExampleMode::Application, - ExampleMode::Application => ExampleMode::ApplicationWithRedraw, - ExampleMode::ApplicationWithRedraw => ExampleMode::Game, - }; - } -} - /// Update winit based on the current ExampleMode fn update_winit( mode: Res, @@ -72,7 +67,19 @@ fn update_winit( } } -/// Everything in this module is for setup and is not important to the demonstrated features. +/// Switch between update modes when the mouse is clicked. +fn cycle_modes(mut mode: ResMut, mouse_button_input: Res>) { + if mouse_button_input.just_pressed(MouseButton::Left) { + *mode = match *mode { + ExampleMode::Game => ExampleMode::Application, + ExampleMode::Application => ExampleMode::ApplicationWithRedraw, + ExampleMode::ApplicationWithRedraw => ExampleMode::Game, + }; + } +} + +/// 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}; @@ -81,10 +88,14 @@ pub(crate) mod test_setup { pub(crate) struct Rotator; /// Rotate the cube to make it clear when the app is updating - pub(crate) fn rotate(mut cube_transform: Query<&mut Transform, With>) { + pub(crate) fn rotate( + time: Res