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

Conversation

aevyrie
Copy link
Member

@aevyrie aevyrie commented Feb 18, 2022

Objective

  • Reduce power usage for games when not focused.
  • Reduce power usage to ~0 when a desktop application is minimized (opt-in).
  • Reduce power usage when focused, only updating on a winit event, or the user sends a redraw request. (opt-in)
2022-03-05.16-36-35_Trim.mp4

Note resource usage in the Task Manager in the above video.

Solution

  • Added a type UpdateMode that allows users to specify how the winit event loop is updated, without exposing winit types.
  • Added two fields to WinitConfig, both with the UpdateMode type. One configures how the application updates when focused, and the other configures how the application behaves when it is not focused. Users can modify this resource manually to set the type of event loop control flow they want.
  • For convenience, two functions were added to WinitConfig, that provide reasonable presets: game() (default) and desktop_app().
    • The game() preset, which is used by default, is unchanged from current behavior with one exception: when the app is out of focus the app updates at a minimum of 10fps, or every time a winit event is received. This has a huge positive impact on power use and responsiveness on my machine, which will otherwise continue running the app at many hundreds of fps when out of focus or minimized.
    • The desktop_app() preset is fully reactive, only updating when user input (winit event) is supplied or a RedrawRequest event is sent. When the app is out of focus, it only updates on Window events - i.e. any winit event that directly interacts with the window. What this means in practice is that the app uses zero resources when minimized or not interacted with, but still updates fluidly when the app is out of focus and the user mouses over the application.
  • Added a RedrawRequest event so users can force an update even if there are no events. This is useful in an application when you want to, say, run an animation even when the user isn't providing input.
  • Added an example low_power to demonstrate these changes

Usage

Configuring the event loop:

use bevy::winit::{WinitConfig};
// ...
.insert_resource(WinitConfig::desktop_app()) // preset
// or
.insert_resource(WinitConfig::game()) // preset
// or
.insert_resource(WinitConfig{ .. }) // manual

Requesting a redraw:

use bevy::window::RequestRedraw;
// ...
fn request_redraw(mut event: EventWriter<RequestRedraw>) {
    event.send(RequestRedraw);
}

Other details

  • Because we have a single event loop for multiple windows, every time I've mentioned "focused" above, I more precisely mean, "if at least one bevy window is focused".
  • Due to a platform bug in winit (ControlFlow::Wait suspends app even if redraw is requested. rust-windowing/winit#1619), we can't simply use Window::request_redraw(). As a workaround, this PR will temporarily set the window mode to Poll when a redraw is requested. This is then reset to the user's WinitConfig setting on the next frame.

@github-actions github-actions bot added the S-Needs-Triage This issue needs to be labelled label Feb 18, 2022
@alice-i-cecile alice-i-cecile added A-Windowing Platform-agnostic interface layer to run your app in C-Enhancement A new feature C-Performance A change motivated by improving speed, memory usage or compile times and removed S-Needs-Triage This issue needs to be labelled labels Feb 18, 2022
Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feature is very useful, the strategy checks out and the code quality and docs are good. All I want is a more advanced example and I'll be happy with this.

@HackerFoo
Copy link
Contributor

I've been testing a similar change, where the app can dynamically control the update interval: HackerFoo@653b825

I've been testing this for a while on iOS, but I'm still not quite happy with how it works, because there is a pause when resuming the app (between NewEvents and the next event) depending on the interval, which should not be there. I think it's a bug in winit on iOS, but I haven't been able to fix it yet.

@aevyrie
Copy link
Member Author

aevyrie commented Feb 18, 2022

@alice-i-cecile I've updated the example, squashed all the bugs I could find, and tested async tasks using the async_compute example. Async tasks worked nicely - spawned tasks ran in the background, and as soon as the application was interacted with, all the results showed up instantly.

@aevyrie
Copy link
Member Author

aevyrie commented Feb 18, 2022

@alice-i-cecile Okay, now it's really ready for review. 😆 I found a few more bugs and tried to make the example much clearer.

Some interesting notes:

  • Running in Wait mode and sending redraw requests every frame appears to be just as performant as using Poll. In my CPU-bound example, I'm hitting ~540fps in both scenarios. This suggests we could consider switching to Wait mode by default, this would make it easier to have control over the winit event loop, so we can do fancier things for choosing how frequently to update the event loop - whether that's for power use, latency, @HackerFoo's dynamic update interval, or other shenanigans.
  • Redraw requests don't build up, so there's no risk of "overusing" the feature. This is useful for composability: a system can submit a redraw request and the app will update to the next frame regardless of what other systems are doing, there is no performance impact from having various systems cumulatively submitting 1,000 redraw requests. Essentially, Wait mode will only ever pause if there are no winit or redraw events.
  • Once paused, there is no way to make the app start updating when using Wait, unless a winit event comes in. There are two solutions I can see: 1) add a custom winit event on construction of the eventloop, and pass the proxy into the app world. An external thread can then send an event to trigger the winit loop to continue. This will require more work. 2) Use WaitUntil(Instant) instead of Wait. This will ensure the app will eventually wake up once the timeout is reached. It's less flexible than (1), but easier to use and already possible with this PR.

@alice-i-cecile
Copy link
Member

Running in Wait mode and sending redraw requests every frame appears to be just as performant as using Poll. In my CPU-bound example, I'm hitting ~540fps in both scenarios. This suggests we could consider switching to Wait mode by default, this would make it easier to have control over the winit event loop, so we can do fancier things for choosing how frequently to update the event loop - whether that's for power use, latency, @HackerFoo's dynamic update interval, or other shenanigans.

I'm in favor of this change; this seems like a sensible place to make it.

Redraw requests don't build up, so there's no risk of "overusing" the feature. This is useful for composability: a system can submit a redraw request and the app will update to the next frame regardless of what other systems are doing, there is no performance impact from having various systems cumulatively submitting 1,000 redraw requests. Essentially, Wait mode will only ever pause if there are no winit or redraw events.

Excellent.

Once paused, there is no way to make the app start updating when using Wait, unless a winit event comes in. There are two solutions I can see: 1) add a custom winit event on construction of the eventloop, and pass the proxy into the app world. An external thread can then send an event to trigger the winit loop to continue. This will require more work. 2) Use WaitUntil(Instant) instead of Wait. This will ensure the app will eventually wake up once the timeout is reached. It's less flexible than (1), but easier to use and already possible with this PR.

I prefer the second approach. We can investigate the first approach later in its own PR if clear use cases emerge for that flexibility.

@superdump
Copy link
Contributor

Tested on macOS on an M1 Max:
Screenshot 2022-02-25 at 23 07 21
The drop in the CPU/GPU usage to much lower levels is when Wait mode is used. The higher parts are with WaitAndRedraw or Poll. Very nice!

@aevyrie
Copy link
Member Author

aevyrie commented Feb 28, 2022

I have noticed what I would consider a bug. When the window is in the background, it still updates with events when it shouldn't. Strangely, in some cases it does work as expect - if I select the Task Manger, the bevy app will stop updating unless the mouse is actually hovering over the background bevy window. My guess is this behavior may be platform dependent.

I'm tempted to special-case this, such that while the window does not have focus, the only events that trigger an update are mouse window events (or explicit redraw requests).

aevyrie and others added 3 commits March 6, 2022 10:52
Co-authored-by: François <mockersf@gmail.com>
Co-authored-by: François <mockersf@gmail.com>
Co-authored-by: François <mockersf@gmail.com>
@aevyrie
Copy link
Member Author

aevyrie commented Mar 6, 2022

This now has all feedback incorporated from all reviewers. 😅

I'm going to stop all changes here until cart's review.

Copy link
Member

@cart cart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! I think this is generally a good direction to take this. Might need a few changes here and there when "actual pipelining" gets merged, but it largely seems compatible with that.
Just a couple of comments to resolve, then I think this is good to go!

crates/bevy_winit/src/winit_config.rs Outdated Show resolved Hide resolved
crates/bevy_winit/src/winit_config.rs Outdated Show resolved Hide resolved
* Rename `WinitConfig` to `WinitSettings`.
* Disable power saving for game mode.
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
@cart
Copy link
Member

cart commented Mar 7, 2022

bors r+

bors bot pushed a commit that referenced this pull request Mar 7, 2022
# Objective

- Reduce power usage for games when not focused.
- Reduce power usage to ~0 when a desktop application is minimized (opt-in).
- Reduce power usage when focused, only updating on a `winit` event, or the user sends a redraw request. (opt-in)

https://user-images.githubusercontent.com/2632925/156904387-ec47d7de-7f06-4c6f-8aaf-1e952c1153a2.mp4

Note resource usage in the Task Manager in the above video.

## Solution

- Added a type `UpdateMode` that allows users to specify how the winit event loop is updated, without exposing winit types.
- Added two fields to `WinitConfig`, both with the `UpdateMode` type. One configures how the application updates when focused, and the other configures how the application behaves when it is not focused. Users can modify this resource manually to set the type of event loop control flow they want.
- For convenience, two functions were added to `WinitConfig`, that provide reasonable presets: `game()` (default) and `desktop_app()`.
    - The `game()` preset, which is used by default, is unchanged from current behavior with one exception: when the app is out of focus the app updates at a minimum of 10fps, or every time a winit event is received. This has a huge positive impact on power use and responsiveness on my machine, which will otherwise continue running the app at many hundreds of fps when out of focus or minimized.
    - The `desktop_app()` preset is fully reactive, only updating when user input (winit event) is supplied or a `RedrawRequest` event is sent. When the app is out of focus, it only updates on `Window` events - i.e. any winit event that directly interacts with the window. What this means in practice is that the app uses *zero* resources when minimized or not interacted with, but still updates fluidly when the app is out of focus and the user mouses over the application.
- Added a `RedrawRequest` event so users can force an update even if there are no events. This is useful in an application when you want to, say, run an animation even when the user isn't providing input.
- Added an example `low_power` to demonstrate these changes

## Usage

Configuring the event loop:
```rs
use bevy::winit::{WinitConfig};
// ...
.insert_resource(WinitConfig::desktop_app()) // preset
// or
.insert_resource(WinitConfig::game()) // preset
// or
.insert_resource(WinitConfig{ .. }) // manual
```

Requesting a redraw:
```rs
use bevy::window::RequestRedraw;
// ...
fn request_redraw(mut event: EventWriter<RequestRedraw>) {
    event.send(RequestRedraw);
}
```

## Other details

- Because we have a single event loop for multiple windows, every time I've mentioned "focused" above, I more precisely mean, "if at least one bevy window is focused".
- Due to a platform bug in winit (rust-windowing/winit#1619), we can't simply use `Window::request_redraw()`. As a workaround, this PR will temporarily set the window mode to `Poll` when a redraw is requested. This is then reset to the user's `WinitConfig` setting on the next frame.
@bors
Copy link
Contributor

bors bot commented Mar 7, 2022

@bors bors bot changed the title Reduce power usage with configurable event loop [Merged by Bors] - Reduce power usage with configurable event loop Mar 7, 2022
@bors bors bot closed this Mar 7, 2022
@aevyrie aevyrie mentioned this pull request Mar 8, 2022
@expikr
Copy link

expikr commented Apr 16, 2022

Due to a platform bug in winit (rust-windowing/winit#1619), we can't simply use Window::request_redraw(). As a workaround, this PR will temporarily set the window mode to Poll when a redraw is requested. This is then reset to the user's WinitConfig setting on the next frame.

Would this mean that the timestamping requirement I described in #4502 would not be achievable with Bevy or winit in general?

@aevyrie
Copy link
Member Author

aevyrie commented Apr 16, 2022

Due to a platform bug in winit (rust-windowing/winit#1619), we can't simply use Window::request_redraw(). As a workaround, this PR will temporarily set the window mode to Poll when a redraw is requested. This is then reset to the user's WinitConfig setting on the next frame.

Would this mean that the timestamping requirement I described in #4502 would not be achievable with Bevy or winit in general?

I'm not sure, I don't understand your request. I'll reply in that thread.

aevyrie added a commit to aevyrie/bevy that referenced this pull request Jun 7, 2022
# Objective

- Reduce power usage for games when not focused.
- Reduce power usage to ~0 when a desktop application is minimized (opt-in).
- Reduce power usage when focused, only updating on a `winit` event, or the user sends a redraw request. (opt-in)

https://user-images.githubusercontent.com/2632925/156904387-ec47d7de-7f06-4c6f-8aaf-1e952c1153a2.mp4

Note resource usage in the Task Manager in the above video.

## Solution

- Added a type `UpdateMode` that allows users to specify how the winit event loop is updated, without exposing winit types.
- Added two fields to `WinitConfig`, both with the `UpdateMode` type. One configures how the application updates when focused, and the other configures how the application behaves when it is not focused. Users can modify this resource manually to set the type of event loop control flow they want.
- For convenience, two functions were added to `WinitConfig`, that provide reasonable presets: `game()` (default) and `desktop_app()`.
    - The `game()` preset, which is used by default, is unchanged from current behavior with one exception: when the app is out of focus the app updates at a minimum of 10fps, or every time a winit event is received. This has a huge positive impact on power use and responsiveness on my machine, which will otherwise continue running the app at many hundreds of fps when out of focus or minimized.
    - The `desktop_app()` preset is fully reactive, only updating when user input (winit event) is supplied or a `RedrawRequest` event is sent. When the app is out of focus, it only updates on `Window` events - i.e. any winit event that directly interacts with the window. What this means in practice is that the app uses *zero* resources when minimized or not interacted with, but still updates fluidly when the app is out of focus and the user mouses over the application.
- Added a `RedrawRequest` event so users can force an update even if there are no events. This is useful in an application when you want to, say, run an animation even when the user isn't providing input.
- Added an example `low_power` to demonstrate these changes

## Usage

Configuring the event loop:
```rs
use bevy::winit::{WinitConfig};
// ...
.insert_resource(WinitConfig::desktop_app()) // preset
// or
.insert_resource(WinitConfig::game()) // preset
// or
.insert_resource(WinitConfig{ .. }) // manual
```

Requesting a redraw:
```rs
use bevy::window::RequestRedraw;
// ...
fn request_redraw(mut event: EventWriter<RequestRedraw>) {
    event.send(RequestRedraw);
}
```

## Other details

- Because we have a single event loop for multiple windows, every time I've mentioned "focused" above, I more precisely mean, "if at least one bevy window is focused".
- Due to a platform bug in winit (rust-windowing/winit#1619), we can't simply use `Window::request_redraw()`. As a workaround, this PR will temporarily set the window mode to `Poll` when a redraw is requested. This is then reset to the user's `WinitConfig` setting on the next frame.
ItsDoot pushed a commit to ItsDoot/bevy that referenced this pull request Feb 1, 2023
# Objective

- Reduce power usage for games when not focused.
- Reduce power usage to ~0 when a desktop application is minimized (opt-in).
- Reduce power usage when focused, only updating on a `winit` event, or the user sends a redraw request. (opt-in)

https://user-images.githubusercontent.com/2632925/156904387-ec47d7de-7f06-4c6f-8aaf-1e952c1153a2.mp4

Note resource usage in the Task Manager in the above video.

## Solution

- Added a type `UpdateMode` that allows users to specify how the winit event loop is updated, without exposing winit types.
- Added two fields to `WinitConfig`, both with the `UpdateMode` type. One configures how the application updates when focused, and the other configures how the application behaves when it is not focused. Users can modify this resource manually to set the type of event loop control flow they want.
- For convenience, two functions were added to `WinitConfig`, that provide reasonable presets: `game()` (default) and `desktop_app()`.
    - The `game()` preset, which is used by default, is unchanged from current behavior with one exception: when the app is out of focus the app updates at a minimum of 10fps, or every time a winit event is received. This has a huge positive impact on power use and responsiveness on my machine, which will otherwise continue running the app at many hundreds of fps when out of focus or minimized.
    - The `desktop_app()` preset is fully reactive, only updating when user input (winit event) is supplied or a `RedrawRequest` event is sent. When the app is out of focus, it only updates on `Window` events - i.e. any winit event that directly interacts with the window. What this means in practice is that the app uses *zero* resources when minimized or not interacted with, but still updates fluidly when the app is out of focus and the user mouses over the application.
- Added a `RedrawRequest` event so users can force an update even if there are no events. This is useful in an application when you want to, say, run an animation even when the user isn't providing input.
- Added an example `low_power` to demonstrate these changes

## Usage

Configuring the event loop:
```rs
use bevy::winit::{WinitConfig};
// ...
.insert_resource(WinitConfig::desktop_app()) // preset
// or
.insert_resource(WinitConfig::game()) // preset
// or
.insert_resource(WinitConfig{ .. }) // manual
```

Requesting a redraw:
```rs
use bevy::window::RequestRedraw;
// ...
fn request_redraw(mut event: EventWriter<RequestRedraw>) {
    event.send(RequestRedraw);
}
```

## Other details

- Because we have a single event loop for multiple windows, every time I've mentioned "focused" above, I more precisely mean, "if at least one bevy window is focused".
- Due to a platform bug in winit (rust-windowing/winit#1619), we can't simply use `Window::request_redraw()`. As a workaround, this PR will temporarily set the window mode to `Poll` when a redraw is requested. This is then reset to the user's `WinitConfig` setting on the next frame.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Rendering Drawing game state to the screen A-Windowing Platform-agnostic interface layer to run your app in C-Enhancement A new feature C-Performance A change motivated by improving speed, memory usage or compile times S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

10 participants