Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Reduce power usage with configurable event loop #3974

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f7ca793
Implement low power updates
aevyrie Feb 18, 2022
564d9eb
Move camera closer
aevyrie Feb 18, 2022
828090b
Make example more resource intensive
aevyrie Feb 18, 2022
08b590b
Fix bug with delayed events
aevyrie Feb 18, 2022
1e380e6
Bugfixes
aevyrie Feb 18, 2022
a41fe02
Improve example clarity
aevyrie Feb 18, 2022
2cc429c
Fix bug where redraw requests prevent app exit
aevyrie Feb 18, 2022
4856da1
Switch example from array of cubes to disabled vsync
aevyrie Feb 18, 2022
b8d0531
Add example to readme
aevyrie Feb 18, 2022
94c60cc
Fix bug preventing `WinitConfig` resource changes from updating in th…
aevyrie Feb 18, 2022
a889087
Fleshed out example
aevyrie Feb 18, 2022
e4a0d61
Improve example comments for new functionality
aevyrie Feb 18, 2022
6af14ce
Fix duplicate app update and perf bugs
aevyrie Feb 18, 2022
989f128
clippy
aevyrie Feb 18, 2022
aefd419
Add bevy type `UpdateMode` to config event loop.
aevyrie Mar 4, 2022
6299712
improve presets and docs
aevyrie Mar 4, 2022
5917fdc
Merge remote-tracking branch 'upstream/main' into low-power_winit
aevyrie Mar 4, 2022
75a6bfe
lint
aevyrie Mar 4, 2022
75693ad
Update crates/bevy_winit/src/lib.rs
aevyrie Mar 4, 2022
f764b78
Update crates/bevy_winit/src/lib.rs
aevyrie Mar 4, 2022
4895873
Update crates/bevy_winit/src/lib.rs
aevyrie Mar 4, 2022
24903fa
Apply suggestions from code review
aevyrie Mar 4, 2022
de19a81
Code review cleanup
aevyrie Mar 4, 2022
464f610
Doc corrections.
aevyrie Mar 4, 2022
350d67a
Switch example to RMB
aevyrie Mar 5, 2022
33413c1
Add frame counter and final touches to example
aevyrie Mar 6, 2022
ac921b3
Contextualize example comments, improvements
aevyrie Mar 6, 2022
016a6f2
Update examples/window/low_power.rs
aevyrie Mar 6, 2022
c259331
Apply suggestions from code review
aevyrie Mar 6, 2022
8df90d3
Review feedback: use captured var instead of Res<>
aevyrie Mar 6, 2022
f4d3e44
PR review feedback
aevyrie Mar 7, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions crates/bevy_window/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_window/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ impl Plugin for WindowPlugin {
.add_event::<CreateWindow>()
.add_event::<WindowCreated>()
.add_event::<WindowCloseRequested>()
.add_event::<RequestRedraw>()
.add_event::<CloseWindow>()
.add_event::<CursorMoved>()
.add_event::<CursorEntered>()
Expand Down
156 changes: 128 additions & 28 deletions crates/bevy_winit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use bevy_input::{
mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel},
touch::TouchInput,
};
pub use winit::event_loop::ControlFlow;
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
pub use winit_config::*;
pub use winit_windows::*;

Expand All @@ -17,16 +18,19 @@ 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_loop::{ControlFlow, EventLoop, EventLoopWindowTarget},
event::{self, DeviceEvent, Event, StartCause, WindowEvent},
event_loop::{EventLoop, EventLoopWindowTarget},
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
};

use winit::dpi::LogicalSize;
Expand All @@ -37,6 +41,8 @@ pub struct WinitPlugin;
impl Plugin for WinitPlugin {
fn build(&self, app: &mut App) {
app.init_non_send_resource::<WinitWindows>()
.init_resource::<WinitPersistentState>()
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
.init_resource::<WinitConfig>()
.set_runner(winit_runner)
.add_system_to_stage(CoreStage::PostUpdate, change_window.exclusive_system());
let event_loop = EventLoop::new();
Expand Down Expand Up @@ -227,41 +233,73 @@ 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
.remove_non_send_resource::<EventLoop<()>>()
.unwrap();
let mut create_window_event_reader = ManualEventReader::<CreateWindow>::default();
let mut app_exit_event_reader = ManualEventReader::<AppExit>::default();
let mut redraw_event_reader = ManualEventReader::<RequestRedraw>::default();
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::<WinitConfig>()
.map_or(false, |config| config.return_from_run);

let mut active = true;
.resource::<WinitConfig>()
.return_from_run;
trace!("Entering winit event loop");

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::<Events<AppExit>>() {
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_state = app.world.resource::<WinitPersistentState>();
let winit_config = app.world.resource::<WinitConfig>();
let windows = app.world.resource::<Windows>();
let focused = windows.iter().any(|w| w.is_focused());
// Check if either the `WaitUntil` timeout was reached, or
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
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
}
};
let mut winit_state = app
.world
.resource_mut::<WinitPersistentState>();
// 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,
Expand All @@ -288,6 +326,10 @@ pub fn winit_runner_with(mut app: App) {
return;
};

world
.resource_mut::<WinitPersistentState>()
.low_power_event = true;

match event {
WindowEvent::Resized(size) => {
window.update_actual_size_from_backend(size.width, size.height);
Expand Down Expand Up @@ -497,25 +539,83 @@ pub fn winit_runner_with(mut app: App) {
});
}
event::Event::Suspended => {
active = false;
app.world
.resource_mut::<WinitPersistentState>()
.active = false;
}
event::Event::Resumed => {
active = true;
app.world
.resource_mut::<WinitPersistentState>()
.active = true;
}
event::Event::MainEventsCleared => {
handle_create_window_events(
&mut app.world,
event_loop,
&mut create_window_event_reader,
);
if active {
let winit_state = app.world.resource::<WinitPersistentState>();
let winit_config = app.world.resource::<WinitConfig>();
let update = if winit_state.active {
let windows = app.world.resource::<Windows>();
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 {
app.world
.resource_mut::<WinitPersistentState>()
.last_update = Instant::now();
app.update();
}
}
Event::RedrawEventsCleared => {
{
let world = app.world.cell();
let winit_config = world.resource::<WinitConfig>();
let windows = world.resource::<Windows>();
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::<Events<RequestRedraw>>() {
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::<Events<AppExit>>() {
if app_exit_event_reader.iter(app_exit_events).last().is_some() {
*control_flow = ControlFlow::Exit;
}
}
app.world
.resource_mut::<WinitPersistentState>()
.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);
Expand Down
79 changes: 78 additions & 1 deletion crates/bevy_winit/src/winit_config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use bevy_utils::Duration;

/// A resource for configuring usage of the `rust_winit` library.
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct WinitConfig {
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
/// Configures the winit library to return control to the main thread after
/// the [run](bevy_app::App::run) loop is exited. Winit strongly recommends
Expand All @@ -12,4 +14,79 @@ pub struct WinitConfig {
/// `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 WinitConfig {
/// Configure winit with common settings for a game.
pub fn game() -> Self {
WinitConfig::default()
}

/// Configure winit with common settings for a desktop application.
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
pub fn desktop_app() -> Self {
WinitConfig {
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 {
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
match focused {
true => &self.focused_mode,
false => &self.unfocused_mode,
}
}
}
impl Default for WinitConfig {
fn default() -> Self {
WinitConfig {
return_from_run: false,
focused_mode: UpdateMode::Continuous,
unfocused_mode: UpdateMode::ReactiveLowPower {
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
max_wait: Duration::from_millis(100),
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
},
}
}
}

/// 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
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
/// 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 },
}
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions examples/app/return_after_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ fn main() {
App::new()
.insert_resource(WinitConfig {
return_from_run: true,
..Default::default()
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
})
.insert_resource(ClearColor(Color::rgb(0.2, 0.2, 0.8)))
.add_plugins(DefaultPlugins)
Expand All @@ -14,6 +15,7 @@ fn main() {
App::new()
.insert_resource(WinitConfig {
return_from_run: true,
..Default::default()
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
})
.insert_resource(ClearColor(Color::rgb(0.2, 0.8, 0.2)))
.add_plugins_with(DefaultPlugins, |group| {
Expand Down
Loading