From a1430d76c683562f7a30aba8e972c04f497c25a3 Mon Sep 17 00:00:00 2001 From: Nicola Papale Date: Fri, 5 Jan 2024 18:13:28 +0100 Subject: [PATCH 1/4] Fix perf degradation on web builds Fixes https://github.com/bevyengine/bevy/issues/11122 --- crates/bevy_winit/src/lib.rs | 233 ++++++++++++++++++++--------------- 1 file changed, 133 insertions(+), 100 deletions(-) diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 5e347e656d2d2..ec38670e7f395 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -412,7 +412,7 @@ pub fn winit_runner(mut app: App) { return; }; - let Ok((mut window, mut cache)) = windows.get_mut(window_entity) else { + let Ok((mut window, _)) = windows.get_mut(window_entity) else { warn!( "Window {:?} is missing `Window` component, skipping event {:?}", window_entity, event @@ -655,9 +655,23 @@ pub fn winit_runner(mut app: App) { window: window_entity, }); } + WindowEvent::RedrawRequested => { + redraw_requested( + &mut runner_state, + &mut app, + &mut focused_windows_state, + event_loop, + &mut create_window_system_state, + &mut app_exit_event_reader, + &mut redraw_event_reader, + ); + } _ => {} } + let mut windows = app.world.query::<(&mut Window, &mut CachedWindow)>(); + let (window, mut cache) = windows.get_mut(&mut app.world, window_entity).unwrap(); + if window.is_changed() { cache.window = window.clone(); } @@ -730,105 +744,9 @@ pub fn winit_runner(mut app: App) { } } Event::AboutToWait => { - if runner_state.active.should_run() { - if runner_state.active == ActiveState::WillSuspend { - runner_state.active = ActiveState::Suspended; - #[cfg(target_os = "android")] - { - // Remove the `RawHandleWrapper` from the primary window. - // This will trigger the surface destruction. - let mut query = - app.world.query_filtered::>(); - let entity = query.single(&app.world); - app.world.entity_mut(entity).remove::(); - event_loop.set_control_flow(ControlFlow::Wait); - } - } - let (config, windows) = focused_windows_state.get(&app.world); - let focused = windows.iter().any(|window| window.focused); - let should_update = match config.update_mode(focused) { - UpdateMode::Continuous | UpdateMode::Reactive { .. } => { - // `Reactive`: In order for `event_handler` to have been called, either - // we received a window or raw input event, the `wait` elapsed, or a - // redraw was requested (by the app or the OS). There are no other - // conditions, so we can just return `true` here. - true - } - UpdateMode::ReactiveLowPower { .. } => { - runner_state.wait_elapsed - || runner_state.redraw_requested - || runner_state.window_event_received - } - }; - - if app.plugins_state() == PluginsState::Cleaned && should_update { - // reset these on each update - runner_state.wait_elapsed = false; - runner_state.window_event_received = false; - runner_state.redraw_requested = false; - runner_state.last_update = Instant::now(); - - app.update(); - - // decide when to run the next update - let (config, windows) = focused_windows_state.get(&app.world); - let focused = windows.iter().any(|window| window.focused); - match config.update_mode(focused) { - UpdateMode::Continuous => { - event_loop.set_control_flow(ControlFlow::Poll); - } - UpdateMode::Reactive { wait } - | UpdateMode::ReactiveLowPower { wait } => { - if let Some(next) = runner_state.last_update.checked_add(*wait) { - runner_state.scheduled_update = Some(next); - event_loop.set_control_flow(ControlFlow::WaitUntil(next)); - } else { - runner_state.scheduled_update = None; - event_loop.set_control_flow(ControlFlow::Wait); - } - } - } - - if let Some(app_redraw_events) = - app.world.get_resource::>() - { - if redraw_event_reader.read(app_redraw_events).last().is_some() { - runner_state.redraw_requested = true; - event_loop.set_control_flow(ControlFlow::Poll); - } - } - - if let Some(app_exit_events) = app.world.get_resource::>() { - if app_exit_event_reader.read(app_exit_events).last().is_some() { - event_loop.exit(); - } - } - } - - // create any new windows - // (even if app did not update, some may have been created by plugin setup) - let ( - commands, - mut windows, - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - ) = create_window_system_state.get_mut(&mut app.world); - - create_windows( - event_loop, - commands, - windows.iter_mut(), - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - ); - - create_window_system_state.apply(&mut app.world); + let (_, winit_windows, _, _) = event_writer_system_state.get_mut(&mut app.world); + for window in winit_windows.windows.values() { + window.request_redraw(); } } _ => (), @@ -857,3 +775,118 @@ fn react_to_resize( height: window.height(), }); } + +fn redraw_requested( + runner_state: &mut WinitAppRunnerState, + app: &mut App, + focused_windows_state: &mut SystemState<(Res, Query<&Window>)>, + event_loop: &EventLoopWindowTarget<()>, + create_window_system_state: &mut SystemState<( + Commands, + Query<(Entity, &mut Window), Added>, + EventWriter, + NonSendMut, + NonSendMut, + ResMut, + ResMut, + )>, + app_exit_event_reader: &mut ManualEventReader, + redraw_event_reader: &mut ManualEventReader, +) { + if runner_state.active.should_run() { + if runner_state.active == ActiveState::WillSuspend { + runner_state.active = ActiveState::Suspended; + #[cfg(target_os = "android")] + { + // Remove the `RawHandleWrapper` from the primary window. + // This will trigger the surface destruction. + let mut query = app.world.query_filtered::>(); + let entity = query.single(&app.world); + app.world.entity_mut(entity).remove::(); + event_loop.set_control_flow(ControlFlow::Wait); + } + } + let (config, windows) = focused_windows_state.get(&app.world); + let focused = windows.iter().any(|window| window.focused); + let should_update = match config.update_mode(focused) { + UpdateMode::Continuous | UpdateMode::Reactive { .. } => { + // `Reactive`: In order for `event_handler` to have been called, either + // we received a window or raw input event, the `wait` elapsed, or a + // redraw was requested (by the app or the OS). There are no other + // conditions, so we can just return `true` here. + true + } + UpdateMode::ReactiveLowPower { .. } => { + runner_state.wait_elapsed + || runner_state.redraw_requested + || runner_state.window_event_received + } + }; + + if app.plugins_state() == PluginsState::Cleaned && should_update { + // reset these on each update + runner_state.wait_elapsed = false; + runner_state.window_event_received = false; + runner_state.redraw_requested = false; + runner_state.last_update = Instant::now(); + + app.update(); + + // decide when to run the next update + let (config, windows) = focused_windows_state.get(&app.world); + let focused = windows.iter().any(|window| window.focused); + match config.update_mode(focused) { + UpdateMode::Continuous => { + event_loop.set_control_flow(ControlFlow::Poll); + } + UpdateMode::Reactive { wait } | UpdateMode::ReactiveLowPower { wait } => { + if let Some(next) = runner_state.last_update.checked_add(*wait) { + runner_state.scheduled_update = Some(next); + event_loop.set_control_flow(ControlFlow::WaitUntil(next)); + } else { + runner_state.scheduled_update = None; + event_loop.set_control_flow(ControlFlow::Wait); + } + } + } + + if let Some(app_redraw_events) = app.world.get_resource::>() { + if redraw_event_reader.read(app_redraw_events).last().is_some() { + runner_state.redraw_requested = true; + event_loop.set_control_flow(ControlFlow::Poll); + } + } + + if let Some(app_exit_events) = app.world.get_resource::>() { + if app_exit_event_reader.read(app_exit_events).last().is_some() { + event_loop.exit(); + } + } + } + + // create any new windows + // (even if app did not update, some may have been created by plugin setup) + let ( + commands, + mut windows, + event_writer, + winit_windows, + adapters, + handlers, + accessibility_requested, + ) = create_window_system_state.get_mut(&mut app.world); + + create_windows( + event_loop, + commands, + windows.iter_mut(), + event_writer, + winit_windows, + adapters, + handlers, + accessibility_requested, + ); + + create_window_system_state.apply(&mut app.world); + } +} From 4e6d8f71398ce30b455adffe7906e0b70287013c Mon Sep 17 00:00:00 2001 From: Nicola Papale Date: Sat, 6 Jan 2024 15:27:32 +0100 Subject: [PATCH 2/4] Use Window::request_redraw over ControlFlow::Poll --- crates/bevy_render/src/renderer/mod.rs | 4 + crates/bevy_winit/src/lib.rs | 198 +++++++++++++------------ crates/bevy_winit/src/winit_config.rs | 4 +- 3 files changed, 109 insertions(+), 97 deletions(-) diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index 7a606e3027e00..9e09dfba9ce52 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -71,6 +71,10 @@ pub fn render_system(world: &mut World) { for window in windows.values_mut() { if let Some(wrapped_texture) = window.swap_chain_texture.take() { if let Some(surface_texture) = wrapped_texture.try_unwrap() { + // TODO(clean): winit docs recommends calling pre_present_notify before this. + // though `present()` doesn't present the frame, it schedules it to be presented + // by wgpu. + // https://docs.rs/winit/0.29.9/wasm32-unknown-unknown/winit/window/struct.Window.html#method.pre_present_notify surface_texture.present(); } } diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index ec38670e7f395..130acd18434d7 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -142,6 +142,7 @@ impl Plugin for WinitPlugin { let event_loop = event_loop_builder .build() .expect("Failed to build event loop"); + event_loop.set_control_flow(ControlFlow::Wait); // iOS, macOS, and Android don't like it if you create windows before the event loop is // initialized. @@ -665,15 +666,22 @@ pub fn winit_runner(mut app: App) { &mut app_exit_event_reader, &mut redraw_event_reader, ); + if runner_state.redraw_requested { + let (_, winit_windows, _, _) = + event_writer_system_state.get_mut(&mut app.world); + if let Some(window) = winit_windows.get_window(window_entity) { + window.request_redraw(); + } + } } _ => {} } let mut windows = app.world.query::<(&mut Window, &mut CachedWindow)>(); - let (window, mut cache) = windows.get_mut(&mut app.world, window_entity).unwrap(); - - if window.is_changed() { - cache.window = window.clone(); + if let Ok((window, mut cache)) = windows.get_mut(&mut app.world, window_entity) { + if window.is_changed() { + cache.window = window.clone(); + } } } Event::DeviceEvent { @@ -740,13 +748,7 @@ pub fn winit_runner(mut app: App) { app.world.entity_mut(entity).insert(wrapper); } - event_loop.set_control_flow(ControlFlow::Poll); - } - } - Event::AboutToWait => { - let (_, winit_windows, _, _) = event_writer_system_state.get_mut(&mut app.world); - for window in winit_windows.windows.values() { - window.request_redraw(); + event_loop.set_control_flow(ControlFlow::Wait); } } _ => (), @@ -754,6 +756,7 @@ pub fn winit_runner(mut app: App) { }; trace!("starting winit event loop"); + // TODO(clean): the winit docs mention using `spawn` instead of `run` on WASM. if let Err(err) = event_loop.run(event_handler) { error!("winit event loop returned an error: {err}"); } @@ -793,100 +796,105 @@ fn redraw_requested( app_exit_event_reader: &mut ManualEventReader, redraw_event_reader: &mut ManualEventReader, ) { - if runner_state.active.should_run() { - if runner_state.active == ActiveState::WillSuspend { - runner_state.active = ActiveState::Suspended; - #[cfg(target_os = "android")] - { - // Remove the `RawHandleWrapper` from the primary window. - // This will trigger the surface destruction. - let mut query = app.world.query_filtered::>(); - let entity = query.single(&app.world); - app.world.entity_mut(entity).remove::(); - event_loop.set_control_flow(ControlFlow::Wait); - } + if !runner_state.active.should_run() { + return; + } + if runner_state.active == ActiveState::WillSuspend { + runner_state.active = ActiveState::Suspended; + #[cfg(target_os = "android")] + { + // Remove the `RawHandleWrapper` from the primary window. + // This will trigger the surface destruction. + let mut query = app.world.query_filtered::>(); + let entity = query.single(&app.world); + app.world.entity_mut(entity).remove::(); + event_loop.set_control_flow(ControlFlow::Wait); } + } + let (config, windows) = focused_windows_state.get(&app.world); + let focused = windows.iter().any(|window| window.focused); + let should_update = match config.update_mode(focused) { + UpdateMode::Continuous | UpdateMode::Reactive { .. } => { + // `Reactive`: In order for `event_handler` to have been called, either + // we received a window or raw input event, the `wait` elapsed, or a + // redraw was requested (by the app or the OS). There are no other + // conditions, so we can just return `true` here. + true + } + UpdateMode::ReactiveLowPower { .. } => { + runner_state.wait_elapsed + || runner_state.redraw_requested + || runner_state.window_event_received + } + }; + + if app.plugins_state() == PluginsState::Cleaned && should_update { + // reset these on each update + runner_state.wait_elapsed = false; + runner_state.window_event_received = false; + runner_state.redraw_requested = false; + runner_state.last_update = Instant::now(); + + app.update(); + + // decide when to run the next update let (config, windows) = focused_windows_state.get(&app.world); let focused = windows.iter().any(|window| window.focused); - let should_update = match config.update_mode(focused) { - UpdateMode::Continuous | UpdateMode::Reactive { .. } => { - // `Reactive`: In order for `event_handler` to have been called, either - // we received a window or raw input event, the `wait` elapsed, or a - // redraw was requested (by the app or the OS). There are no other - // conditions, so we can just return `true` here. - true - } - UpdateMode::ReactiveLowPower { .. } => { - runner_state.wait_elapsed - || runner_state.redraw_requested - || runner_state.window_event_received + match config.update_mode(focused) { + UpdateMode::Continuous => { + runner_state.redraw_requested = true; } - }; - - if app.plugins_state() == PluginsState::Cleaned && should_update { - // reset these on each update - runner_state.wait_elapsed = false; - runner_state.window_event_received = false; - runner_state.redraw_requested = false; - runner_state.last_update = Instant::now(); - - app.update(); - - // decide when to run the next update - let (config, windows) = focused_windows_state.get(&app.world); - let focused = windows.iter().any(|window| window.focused); - match config.update_mode(focused) { - UpdateMode::Continuous => { - event_loop.set_control_flow(ControlFlow::Poll); - } - UpdateMode::Reactive { wait } | UpdateMode::ReactiveLowPower { wait } => { - if let Some(next) = runner_state.last_update.checked_add(*wait) { - runner_state.scheduled_update = Some(next); - event_loop.set_control_flow(ControlFlow::WaitUntil(next)); - } else { - runner_state.scheduled_update = None; - event_loop.set_control_flow(ControlFlow::Wait); - } + UpdateMode::Reactive { wait } | UpdateMode::ReactiveLowPower { wait } => { + // TODO(bug): this is unexpected behavior. + // When Reactive, user expects bevy to actually wait that amount of time, + // and not potentially infinitely depending on plateform specifics (which this does) + // Need to verify the plateform specifics (whether this can occur in + // rare-but-possible cases) and replace this with a panic or a log warn! + if let Some(next) = runner_state.last_update.checked_add(*wait) { + runner_state.scheduled_update = Some(next); + event_loop.set_control_flow(ControlFlow::WaitUntil(next)); + } else { + runner_state.scheduled_update = None; + event_loop.set_control_flow(ControlFlow::Wait); } } + } - if let Some(app_redraw_events) = app.world.get_resource::>() { - if redraw_event_reader.read(app_redraw_events).last().is_some() { - runner_state.redraw_requested = true; - event_loop.set_control_flow(ControlFlow::Poll); - } + if let Some(app_redraw_events) = app.world.get_resource::>() { + if redraw_event_reader.read(app_redraw_events).last().is_some() { + runner_state.redraw_requested = true; } + } - if let Some(app_exit_events) = app.world.get_resource::>() { - if app_exit_event_reader.read(app_exit_events).last().is_some() { - event_loop.exit(); - } + if let Some(app_exit_events) = app.world.get_resource::>() { + if app_exit_event_reader.read(app_exit_events).last().is_some() { + event_loop.exit(); } } - - // create any new windows - // (even if app did not update, some may have been created by plugin setup) - let ( - commands, - mut windows, - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - ) = create_window_system_state.get_mut(&mut app.world); - - create_windows( - event_loop, - commands, - windows.iter_mut(), - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - ); - - create_window_system_state.apply(&mut app.world); } + + // create any new windows + // (even if app did not update, some may have been created by plugin setup) + let ( + commands, + mut windows, + event_writer, + winit_windows, + adapters, + handlers, + accessibility_requested, + ) = create_window_system_state.get_mut(&mut app.world); + + create_windows( + event_loop, + commands, + windows.iter_mut(), + event_writer, + winit_windows, + adapters, + handlers, + accessibility_requested, + ); + + create_window_system_state.apply(&mut app.world); } diff --git a/crates/bevy_winit/src/winit_config.rs b/crates/bevy_winit/src/winit_config.rs index b91e25d340afa..ecece207d6b3d 100644 --- a/crates/bevy_winit/src/winit_config.rs +++ b/crates/bevy_winit/src/winit_config.rs @@ -76,7 +76,7 @@ pub enum UpdateMode { /// - new [window](`winit::event::WindowEvent`) or [raw input](`winit::event::DeviceEvent`) /// events have appeared Reactive { - /// The minimum time from the start of one update to the next. + /// The approximate time from the start of one update to the next. /// /// **Note:** This has no upper limit. /// The [`App`](bevy_app::App) will wait indefinitely if you set this to [`Duration::MAX`]. @@ -93,7 +93,7 @@ pub enum UpdateMode { /// Use this mode if, for example, you only want your app to update when the mouse cursor is /// moving over a window, not just moving in general. This can greatly reduce power consumption. ReactiveLowPower { - /// The minimum time from the start of one update to the next. + /// The approximate time from the start of one update to the next. /// /// **Note:** This has no upper limit. /// The [`App`](bevy_app::App) will wait indefinitely if you set this to [`Duration::MAX`]. From 6ec7ac0ebcad25ad198b1a1558edf90e74ccf059 Mon Sep 17 00:00:00 2001 From: Nicola Papale Date: Sat, 6 Jan 2024 15:32:06 +0100 Subject: [PATCH 3/4] Move react_to_resize --- crates/bevy_winit/src/lib.rs | 51 +++++++++++++++++------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 130acd18434d7..77746ccbe2f4b 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -142,7 +142,6 @@ impl Plugin for WinitPlugin { let event_loop = event_loop_builder .build() .expect("Failed to build event loop"); - event_loop.set_control_flow(ControlFlow::Wait); // iOS, macOS, and Android don't like it if you create windows before the event loop is // initialized. @@ -657,7 +656,7 @@ pub fn winit_runner(mut app: App) { }); } WindowEvent::RedrawRequested => { - redraw_requested( + run_app_update_if_should( &mut runner_state, &mut app, &mut focused_windows_state, @@ -762,24 +761,7 @@ pub fn winit_runner(mut app: App) { } } -fn react_to_resize( - window: &mut Mut<'_, Window>, - size: winit::dpi::PhysicalSize, - event_writers: &mut WindowAndInputEventWriters<'_>, - window_entity: Entity, -) { - window - .resolution - .set_physical_resolution(size.width, size.height); - - event_writers.window_resized.send(WindowResized { - window: window_entity, - width: window.width(), - height: window.height(), - }); -} - -fn redraw_requested( +fn run_app_update_if_should( runner_state: &mut WinitAppRunnerState, app: &mut App, focused_windows_state: &mut SystemState<(Res, Query<&Window>)>, @@ -814,13 +796,11 @@ fn redraw_requested( let (config, windows) = focused_windows_state.get(&app.world); let focused = windows.iter().any(|window| window.focused); let should_update = match config.update_mode(focused) { - UpdateMode::Continuous | UpdateMode::Reactive { .. } => { - // `Reactive`: In order for `event_handler` to have been called, either - // we received a window or raw input event, the `wait` elapsed, or a - // redraw was requested (by the app or the OS). There are no other - // conditions, so we can just return `true` here. - true - } + // `Reactive`: In order for `event_handler` to have been called, either + // we received a window or raw input event, the `wait` elapsed, or a + // redraw was requested (by the app or the OS). There are no other + // conditions, so we can just return `true` here. + UpdateMode::Continuous | UpdateMode::Reactive { .. } => true, UpdateMode::ReactiveLowPower { .. } => { runner_state.wait_elapsed || runner_state.redraw_requested @@ -898,3 +878,20 @@ fn redraw_requested( create_window_system_state.apply(&mut app.world); } + +fn react_to_resize( + window: &mut Mut<'_, Window>, + size: winit::dpi::PhysicalSize, + event_writers: &mut WindowAndInputEventWriters<'_>, + window_entity: Entity, +) { + window + .resolution + .set_physical_resolution(size.width, size.height); + + event_writers.window_resized.send(WindowResized { + window: window_entity, + width: window.width(), + height: window.height(), + }); +} From c4ad9ec4c264d78a2d9adb8bd5d890f18f0d4c1c Mon Sep 17 00:00:00 2001 From: Nicola Papale Date: Sat, 6 Jan 2024 17:54:02 +0100 Subject: [PATCH 4/4] Move around redraw_requested handling --- crates/bevy_winit/src/lib.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 77746ccbe2f4b..4f3a34ab71841 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -360,6 +360,7 @@ pub fn winit_runner(mut app: App) { } } } + runner_state.redraw_requested = false; match event { Event::NewEvents(start_cause) => match start_cause { @@ -656,6 +657,7 @@ pub fn winit_runner(mut app: App) { }); } WindowEvent::RedrawRequested => { + runner_state.redraw_requested = false; run_app_update_if_should( &mut runner_state, &mut app, @@ -665,13 +667,6 @@ pub fn winit_runner(mut app: App) { &mut app_exit_event_reader, &mut redraw_event_reader, ); - if runner_state.redraw_requested { - let (_, winit_windows, _, _) = - event_writer_system_state.get_mut(&mut app.world); - if let Some(window) = winit_windows.get_window(window_entity) { - window.request_redraw(); - } - } } _ => {} } @@ -687,6 +682,7 @@ pub fn winit_runner(mut app: App) { event: DeviceEvent::MouseMotion { delta: (x, y) }, .. } => { + runner_state.redraw_requested = true; let (mut event_writers, ..) = event_writer_system_state.get_mut(&mut app.world); event_writers.mouse_motion.send(MouseMotion { delta: Vec2::new(x as f32, y as f32), @@ -752,6 +748,12 @@ pub fn winit_runner(mut app: App) { } _ => (), } + if runner_state.redraw_requested { + let (_, winit_windows, _, _) = event_writer_system_state.get_mut(&mut app.world); + for window in winit_windows.windows.values() { + window.request_redraw(); + } + } }; trace!("starting winit event loop"); @@ -801,6 +803,8 @@ fn run_app_update_if_should( // redraw was requested (by the app or the OS). There are no other // conditions, so we can just return `true` here. UpdateMode::Continuous | UpdateMode::Reactive { .. } => true, + // TODO(bug): This is currently always true since we only run this function + // if we received a `RequestRedraw` event. UpdateMode::ReactiveLowPower { .. } => { runner_state.wait_elapsed || runner_state.redraw_requested @@ -811,8 +815,6 @@ fn run_app_update_if_should( if app.plugins_state() == PluginsState::Cleaned && should_update { // reset these on each update runner_state.wait_elapsed = false; - runner_state.window_event_received = false; - runner_state.redraw_requested = false; runner_state.last_update = Instant::now(); app.update();