diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2023-09-29 13:12:50 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2023-09-29 13:12:50 +0400 |
| commit | 404661ed8d9efc60d70547287d81953c1025a95b (patch) | |
| tree | c9030cf6c08cfeaa32acdd10fbbc303e4a7abaf3 /src/backend | |
| parent | 73fd286f3494f2cf7388710624d3736a514b9f1f (diff) | |
| download | niri-404661ed8d9efc60d70547287d81953c1025a95b.tar.gz niri-404661ed8d9efc60d70547287d81953c1025a95b.tar.bz2 niri-404661ed8d9efc60d70547287d81953c1025a95b.zip | |
Throttle frame callbacks to once per monitor refresh
Under some circumstances, the compositor can get into a commit-frame
callback busy loop with a client. For example, if a client redraws on
frame callbacks, but the resulting frame has empty damage (e.g. the
damaged part of the client is outside the monitor). Or if the client
simply commits with empty damage (looking at you, Firefox).
This behavior is compliant with the Wayland specification and with the
intended idea of frame callbacks, but causes a lot of unnecessary CPU
usage in the client and the compositor.
To solve this problem, this commit introduces frame callback throttling.
Every surface may only receive a single frame callback in one
monitor refresh cycle. If a surface commits resulting in no KMS frame
submission, a timer is created, that will fire at the predicted would-
be-VBlank time, and send the accumulated frame callbacks.
This way, a surface that redraws on frame callbacks will not notice
any change in frame callback delivery, if its commits suddenly stop
producing KMS updates.
Diffstat (limited to 'src/backend')
| -rw-r--r-- | src/backend/tty.rs | 75 |
1 files changed, 71 insertions, 4 deletions
diff --git a/src/backend/tty.rs b/src/backend/tty.rs index c7ebab4b..c4ac14e7 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -22,6 +22,7 @@ use smithay::backend::session::{Event as SessionEvent, Session}; use smithay::backend::udev::{self, UdevBackend, UdevEvent}; use smithay::desktop::utils::OutputPresentationFeedback; use smithay::output::{Mode, Output, OutputModeSource, PhysicalProperties, Subpixel, Scale}; +use smithay::reexports::calloop::timer::{Timer, TimeoutAction}; use smithay::reexports::calloop::{Dispatcher, LoopHandle, RegistrationToken}; use smithay::reexports::drm::control::{ connector, crtc, Mode as DrmMode, ModeFlags, ModeTypeFlags, @@ -92,6 +93,7 @@ struct Surface { vblank_plot_name: tracy_client::PlotName, /// Plot name for the presentation target offset plot. presentation_plot_name: tracy_client::PlotName, + sequence_delta_plot_name: tracy_client::PlotName, } impl Tty { @@ -526,6 +528,8 @@ impl Tty { let presentation_plot_name = tracy_client::PlotName::new_leak(format!( "{output_name} presentation target offset, ms" )); + let sequence_delta_plot_name = + tracy_client::PlotName::new_leak(format!("{output_name} sequence delta")); self.connectors .lock() @@ -540,6 +544,7 @@ impl Tty { vblank_frame_name, vblank_plot_name, presentation_plot_name, + sequence_delta_plot_name, }; let res = device.surfaces.insert(crtc, surface); assert!(res.is_none(), "crtc must not have already existed"); @@ -691,11 +696,44 @@ impl Tty { } } + if let Some(last_sequence) = output_state.current_estimated_sequence { + let delta = meta.sequence as f64 - last_sequence as f64; + tracy_client::Client::running() + .unwrap() + .plot(surface.sequence_delta_plot_name, delta); + } + + assert!(output_state.waiting_for_vblank); + assert!(output_state.estimated_vblank_timer.is_none()); + output_state.waiting_for_vblank = false; output_state.frame_clock.presented(presentation_time); + output_state.current_estimated_sequence = Some(meta.sequence); niri.queue_redraw(output); } + fn on_estimated_vblank_timer(&self, niri: &mut Niri, output: &Output) { + let span = tracy_client::span!("Tty::on_estimated_vblank_timer"); + + let name = output.name(); + span.emit_text(&name); + + let Some(output_state) = niri.output_state.get_mut(output) else { + error!("missing output state for {name}"); + return; + }; + + assert!(!output_state.waiting_for_vblank); + let token = output_state.estimated_vblank_timer.take(); + assert!(token.is_some()); + + if let Some(sequence) = output_state.current_estimated_sequence.as_mut() { + *sequence = sequence.wrapping_add(1); + + niri.send_frame_callbacks(output); + } + } + pub fn seat_name(&self) -> String { self.session.seat() } @@ -754,10 +792,11 @@ impl Tty { match drm_compositor.queue_frame(data) { Ok(()) => { - niri.output_state - .get_mut(output) - .unwrap() - .waiting_for_vblank = true; + let output_state = niri.output_state.get_mut(output).unwrap(); + output_state.waiting_for_vblank = true; + if let Some(token) = output_state.estimated_vblank_timer.take() { + niri.event_loop.remove(token); + } return Some(&surface.dmabuf_feedback); } @@ -775,6 +814,10 @@ impl Tty { // We're not expecting a vblank right after this. drop(surface.vblank_frame.take()); + + // Queue a timer to fire at the predicted vblank time. + queue_estimated_vblank_timer(niri, output.clone(), target_presentation_time); + None } @@ -853,3 +896,27 @@ fn suspend() -> anyhow::Result<()> { .context("error creating login manager proxy")?; manager.suspend(true).context("error suspending") } + +fn queue_estimated_vblank_timer( + niri: &mut Niri, + output: Output, + target_presentation_time: Duration, +) { + let output_state = niri.output_state.get_mut(&output).unwrap(); + if output_state.estimated_vblank_timer.is_some() { + return; + } + + let now = get_monotonic_time(); + let timer = Timer::from_duration(target_presentation_time.saturating_sub(now)); + let token = niri + .event_loop + .insert_source(timer, move |_, _, data| { + data.backend + .tty() + .on_estimated_vblank_timer(&mut data.niri, &output); + TimeoutAction::Drop + }) + .unwrap(); + output_state.estimated_vblank_timer = Some(token); +} |
