Skip to content

Commit 79021c7

Browse files
authored
Fix perf degradation on web builds (#11227)
# Objective - Since #10702, the way bevy updates the window leads to major slowdowns as seen in - #11122 - #11220 - Slow is bad, furthermore, _very_ slow is _very_ bad. We should fix this issue. ## Solution - Move the app update code into the `Event::WindowEvent { event: WindowEvent::RedrawRequested }` branch of the event loop. - Run `window.request_redraw()` When `runner_state.redraw_requested` - Instead of swapping `ControlFlow` between `Poll` and `Wait`, we always keep it at `Wait`, and use `window.request_redraw()` to schedule an immediate call to the event loop. - `runner_state.redraw_requested` is set to `true` when `UpdateMode::Continuous` and when a `RequestRedraw` event is received. - Extract the redraw code into a separate function, because otherwise I'd go crazy with the indentation level. - Fix #11122. ## Testing I tested the WASM builds as follow: ```sh cargo run -p build-wasm-example -- --api webgl2 bevymark python -m http.server --directory examples/wasm/ 8080 # Open browser at http://localhost:8080 ``` On main, even spawning a couple sprites is super choppy. Even if it says "300 FPS". While on this branch, it is smooth as butter. I also found that it fixes all choppiness on window resize (tested on Linux/X11). This was another issue from #10702 IIRC. So here is what I tested: - On `wasm`: `many_foxes` and `bevymark`, with `argh::from_env()` commented out, otherwise we get a cryptic error. - Both with `PresentMode::AutoVsync` and `PresentMode::AutoNoVsync` - On main, it is consistently choppy. - With this PR, the visible frame rate is consistent with the diagnostic numbers - On native (linux/x11) I ran similar tests, making sure that `AutoVsync` limits to monitor framerate, and `AutoNoVsync` doesn't. ## Future work Code could be improved, I wanted a quick solution easy to review, but we really need to make the code more accessible. - #9768 - ~~**`WinitSettings::desktop_app()` is completely borked.**~~ actually broken on main as well ### Review guide Consider enable the non-whitespace diff to see the _real_ change set.
1 parent a35a151 commit 79021c7

File tree

3 files changed

+150
-106
lines changed

3 files changed

+150
-106
lines changed

crates/bevy_render/src/renderer/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ pub fn render_system(world: &mut World) {
7171
for window in windows.values_mut() {
7272
if let Some(wrapped_texture) = window.swap_chain_texture.take() {
7373
if let Some(surface_texture) = wrapped_texture.try_unwrap() {
74+
// TODO(clean): winit docs recommends calling pre_present_notify before this.
75+
// though `present()` doesn't present the frame, it schedules it to be presented
76+
// by wgpu.
77+
// https://docs.rs/winit/0.29.9/wasm32-unknown-unknown/winit/window/struct.Window.html#method.pre_present_notify
7478
surface_texture.present();
7579
}
7680
}

crates/bevy_winit/src/lib.rs

Lines changed: 144 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ pub fn winit_runner(mut app: App) {
360360
}
361361
}
362362
}
363+
runner_state.redraw_requested = false;
363364

364365
match event {
365366
Event::NewEvents(start_cause) => match start_cause {
@@ -412,7 +413,7 @@ pub fn winit_runner(mut app: App) {
412413
return;
413414
};
414415

415-
let Ok((mut window, mut cache)) = windows.get_mut(window_entity) else {
416+
let Ok((mut window, _)) = windows.get_mut(window_entity) else {
416417
warn!(
417418
"Window {:?} is missing `Window` component, skipping event {:?}",
418419
window_entity, event
@@ -655,17 +656,33 @@ pub fn winit_runner(mut app: App) {
655656
window: window_entity,
656657
});
657658
}
659+
WindowEvent::RedrawRequested => {
660+
runner_state.redraw_requested = false;
661+
run_app_update_if_should(
662+
&mut runner_state,
663+
&mut app,
664+
&mut focused_windows_state,
665+
event_loop,
666+
&mut create_window_system_state,
667+
&mut app_exit_event_reader,
668+
&mut redraw_event_reader,
669+
);
670+
}
658671
_ => {}
659672
}
660673

661-
if window.is_changed() {
662-
cache.window = window.clone();
674+
let mut windows = app.world.query::<(&mut Window, &mut CachedWindow)>();
675+
if let Ok((window, mut cache)) = windows.get_mut(&mut app.world, window_entity) {
676+
if window.is_changed() {
677+
cache.window = window.clone();
678+
}
663679
}
664680
}
665681
Event::DeviceEvent {
666682
event: DeviceEvent::MouseMotion { delta: (x, y) },
667683
..
668684
} => {
685+
runner_state.redraw_requested = true;
669686
let (mut event_writers, ..) = event_writer_system_state.get_mut(&mut app.world);
670687
event_writers.mouse_motion.send(MouseMotion {
671688
delta: Vec2::new(x as f32, y as f32),
@@ -726,119 +743,142 @@ pub fn winit_runner(mut app: App) {
726743

727744
app.world.entity_mut(entity).insert(wrapper);
728745
}
729-
event_loop.set_control_flow(ControlFlow::Poll);
746+
event_loop.set_control_flow(ControlFlow::Wait);
730747
}
731748
}
732-
Event::AboutToWait => {
733-
if runner_state.active.should_run() {
734-
if runner_state.active == ActiveState::WillSuspend {
735-
runner_state.active = ActiveState::Suspended;
736-
#[cfg(target_os = "android")]
737-
{
738-
// Remove the `RawHandleWrapper` from the primary window.
739-
// This will trigger the surface destruction.
740-
let mut query =
741-
app.world.query_filtered::<Entity, With<PrimaryWindow>>();
742-
let entity = query.single(&app.world);
743-
app.world.entity_mut(entity).remove::<RawHandleWrapper>();
744-
event_loop.set_control_flow(ControlFlow::Wait);
745-
}
746-
}
747-
let (config, windows) = focused_windows_state.get(&app.world);
748-
let focused = windows.iter().any(|window| window.focused);
749-
let should_update = match config.update_mode(focused) {
750-
UpdateMode::Continuous | UpdateMode::Reactive { .. } => {
751-
// `Reactive`: In order for `event_handler` to have been called, either
752-
// we received a window or raw input event, the `wait` elapsed, or a
753-
// redraw was requested (by the app or the OS). There are no other
754-
// conditions, so we can just return `true` here.
755-
true
756-
}
757-
UpdateMode::ReactiveLowPower { .. } => {
758-
runner_state.wait_elapsed
759-
|| runner_state.redraw_requested
760-
|| runner_state.window_event_received
761-
}
762-
};
763-
764-
if app.plugins_state() == PluginsState::Cleaned && should_update {
765-
// reset these on each update
766-
runner_state.wait_elapsed = false;
767-
runner_state.window_event_received = false;
768-
runner_state.redraw_requested = false;
769-
runner_state.last_update = Instant::now();
770-
771-
app.update();
749+
_ => (),
750+
}
751+
if runner_state.redraw_requested {
752+
let (_, winit_windows, _, _) = event_writer_system_state.get_mut(&mut app.world);
753+
for window in winit_windows.windows.values() {
754+
window.request_redraw();
755+
}
756+
}
757+
};
772758

773-
// decide when to run the next update
774-
let (config, windows) = focused_windows_state.get(&app.world);
775-
let focused = windows.iter().any(|window| window.focused);
776-
match config.update_mode(focused) {
777-
UpdateMode::Continuous => {
778-
event_loop.set_control_flow(ControlFlow::Poll);
779-
}
780-
UpdateMode::Reactive { wait }
781-
| UpdateMode::ReactiveLowPower { wait } => {
782-
if let Some(next) = runner_state.last_update.checked_add(*wait) {
783-
runner_state.scheduled_update = Some(next);
784-
event_loop.set_control_flow(ControlFlow::WaitUntil(next));
785-
} else {
786-
runner_state.scheduled_update = None;
787-
event_loop.set_control_flow(ControlFlow::Wait);
788-
}
789-
}
790-
}
759+
trace!("starting winit event loop");
760+
// TODO(clean): the winit docs mention using `spawn` instead of `run` on WASM.
761+
if let Err(err) = event_loop.run(event_handler) {
762+
error!("winit event loop returned an error: {err}");
763+
}
764+
}
791765

792-
if let Some(app_redraw_events) =
793-
app.world.get_resource::<Events<RequestRedraw>>()
794-
{
795-
if redraw_event_reader.read(app_redraw_events).last().is_some() {
796-
runner_state.redraw_requested = true;
797-
event_loop.set_control_flow(ControlFlow::Poll);
798-
}
799-
}
766+
fn run_app_update_if_should(
767+
runner_state: &mut WinitAppRunnerState,
768+
app: &mut App,
769+
focused_windows_state: &mut SystemState<(Res<WinitSettings>, Query<&Window>)>,
770+
event_loop: &EventLoopWindowTarget<()>,
771+
create_window_system_state: &mut SystemState<(
772+
Commands,
773+
Query<(Entity, &mut Window), Added<Window>>,
774+
EventWriter<WindowCreated>,
775+
NonSendMut<WinitWindows>,
776+
NonSendMut<AccessKitAdapters>,
777+
ResMut<WinitActionHandlers>,
778+
ResMut<AccessibilityRequested>,
779+
)>,
780+
app_exit_event_reader: &mut ManualEventReader<AppExit>,
781+
redraw_event_reader: &mut ManualEventReader<RequestRedraw>,
782+
) {
783+
if !runner_state.active.should_run() {
784+
return;
785+
}
786+
if runner_state.active == ActiveState::WillSuspend {
787+
runner_state.active = ActiveState::Suspended;
788+
#[cfg(target_os = "android")]
789+
{
790+
// Remove the `RawHandleWrapper` from the primary window.
791+
// This will trigger the surface destruction.
792+
let mut query = app.world.query_filtered::<Entity, With<PrimaryWindow>>();
793+
let entity = query.single(&app.world);
794+
app.world.entity_mut(entity).remove::<RawHandleWrapper>();
795+
event_loop.set_control_flow(ControlFlow::Wait);
796+
}
797+
}
798+
let (config, windows) = focused_windows_state.get(&app.world);
799+
let focused = windows.iter().any(|window| window.focused);
800+
let should_update = match config.update_mode(focused) {
801+
// `Reactive`: In order for `event_handler` to have been called, either
802+
// we received a window or raw input event, the `wait` elapsed, or a
803+
// redraw was requested (by the app or the OS). There are no other
804+
// conditions, so we can just return `true` here.
805+
UpdateMode::Continuous | UpdateMode::Reactive { .. } => true,
806+
// TODO(bug): This is currently always true since we only run this function
807+
// if we received a `RequestRedraw` event.
808+
UpdateMode::ReactiveLowPower { .. } => {
809+
runner_state.wait_elapsed
810+
|| runner_state.redraw_requested
811+
|| runner_state.window_event_received
812+
}
813+
};
800814

801-
if let Some(app_exit_events) = app.world.get_resource::<Events<AppExit>>() {
802-
if app_exit_event_reader.read(app_exit_events).last().is_some() {
803-
event_loop.exit();
804-
}
805-
}
806-
}
815+
if app.plugins_state() == PluginsState::Cleaned && should_update {
816+
// reset these on each update
817+
runner_state.wait_elapsed = false;
818+
runner_state.last_update = Instant::now();
807819

808-
// create any new windows
809-
// (even if app did not update, some may have been created by plugin setup)
810-
let (
811-
commands,
812-
mut windows,
813-
event_writer,
814-
winit_windows,
815-
adapters,
816-
handlers,
817-
accessibility_requested,
818-
) = create_window_system_state.get_mut(&mut app.world);
819-
820-
create_windows(
821-
event_loop,
822-
commands,
823-
windows.iter_mut(),
824-
event_writer,
825-
winit_windows,
826-
adapters,
827-
handlers,
828-
accessibility_requested,
829-
);
820+
app.update();
830821

831-
create_window_system_state.apply(&mut app.world);
822+
// decide when to run the next update
823+
let (config, windows) = focused_windows_state.get(&app.world);
824+
let focused = windows.iter().any(|window| window.focused);
825+
match config.update_mode(focused) {
826+
UpdateMode::Continuous => {
827+
runner_state.redraw_requested = true;
828+
}
829+
UpdateMode::Reactive { wait } | UpdateMode::ReactiveLowPower { wait } => {
830+
// TODO(bug): this is unexpected behavior.
831+
// When Reactive, user expects bevy to actually wait that amount of time,
832+
// and not potentially infinitely depending on plateform specifics (which this does)
833+
// Need to verify the plateform specifics (whether this can occur in
834+
// rare-but-possible cases) and replace this with a panic or a log warn!
835+
if let Some(next) = runner_state.last_update.checked_add(*wait) {
836+
runner_state.scheduled_update = Some(next);
837+
event_loop.set_control_flow(ControlFlow::WaitUntil(next));
838+
} else {
839+
runner_state.scheduled_update = None;
840+
event_loop.set_control_flow(ControlFlow::Wait);
832841
}
833842
}
834-
_ => (),
835843
}
836-
};
837844

838-
trace!("starting winit event loop");
839-
if let Err(err) = event_loop.run(event_handler) {
840-
error!("winit event loop returned an error: {err}");
845+
if let Some(app_redraw_events) = app.world.get_resource::<Events<RequestRedraw>>() {
846+
if redraw_event_reader.read(app_redraw_events).last().is_some() {
847+
runner_state.redraw_requested = true;
848+
}
849+
}
850+
851+
if let Some(app_exit_events) = app.world.get_resource::<Events<AppExit>>() {
852+
if app_exit_event_reader.read(app_exit_events).last().is_some() {
853+
event_loop.exit();
854+
}
855+
}
841856
}
857+
858+
// create any new windows
859+
// (even if app did not update, some may have been created by plugin setup)
860+
let (
861+
commands,
862+
mut windows,
863+
event_writer,
864+
winit_windows,
865+
adapters,
866+
handlers,
867+
accessibility_requested,
868+
) = create_window_system_state.get_mut(&mut app.world);
869+
870+
create_windows(
871+
event_loop,
872+
commands,
873+
windows.iter_mut(),
874+
event_writer,
875+
winit_windows,
876+
adapters,
877+
handlers,
878+
accessibility_requested,
879+
);
880+
881+
create_window_system_state.apply(&mut app.world);
842882
}
843883

844884
fn react_to_resize(

crates/bevy_winit/src/winit_config.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ pub enum UpdateMode {
7676
/// - new [window](`winit::event::WindowEvent`) or [raw input](`winit::event::DeviceEvent`)
7777
/// events have appeared
7878
Reactive {
79-
/// The minimum time from the start of one update to the next.
79+
/// The approximate time from the start of one update to the next.
8080
///
8181
/// **Note:** This has no upper limit.
8282
/// The [`App`](bevy_app::App) will wait indefinitely if you set this to [`Duration::MAX`].
@@ -93,7 +93,7 @@ pub enum UpdateMode {
9393
/// Use this mode if, for example, you only want your app to update when the mouse cursor is
9494
/// moving over a window, not just moving in general. This can greatly reduce power consumption.
9595
ReactiveLowPower {
96-
/// The minimum time from the start of one update to the next.
96+
/// The approximate time from the start of one update to the next.
9797
///
9898
/// **Note:** This has no upper limit.
9999
/// The [`App`](bevy_app::App) will wait indefinitely if you set this to [`Duration::MAX`].

0 commit comments

Comments
 (0)