Rework PW screencast frame timing

- Remove the 0.5 ms hack.
- Add redraw scheduling to fix stuck frame if the last redrawn frame
  happened too soon.
This commit is contained in:
Ivan Molodetskikh
2024-08-24 10:49:32 +03:00
parent 4832924483
commit b2c7d3ad40
2 changed files with 77 additions and 14 deletions
+3 -3
View File
@@ -3664,7 +3664,7 @@ impl Niri {
} }
} }
if cast.should_skip_frame(target_presentation_time) { if cast.check_time_and_schedule(&self.event_loop, output, target_presentation_time) {
continue; continue;
} }
@@ -3726,7 +3726,7 @@ impl Niri {
} }
} }
if cast.should_skip_frame(target_presentation_time) { if cast.check_time_and_schedule(&self.event_loop, output, target_presentation_time) {
continue; continue;
} }
@@ -3805,7 +3805,7 @@ impl Niri {
} }
} }
if cast.should_skip_frame(target_presentation_time) { if cast.check_time_and_schedule(&self.event_loop, output, target_presentation_time) {
continue; continue;
} }
+74 -11
View File
@@ -8,6 +8,8 @@ use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
use anyhow::Context as _; use anyhow::Context as _;
use calloop::timer::{TimeoutAction, Timer};
use calloop::RegistrationToken;
use pipewire::context::Context; use pipewire::context::Context;
use pipewire::core::Core; use pipewire::core::Core;
use pipewire::main_loop::MainLoop; use pipewire::main_loop::MainLoop;
@@ -34,7 +36,7 @@ use smithay::backend::drm::DrmDeviceFd;
use smithay::backend::renderer::damage::OutputDamageTracker; use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::RenderElement; use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer; use smithay::backend::renderer::gles::GlesRenderer;
use smithay::output::{OutputModeSource, WeakOutput}; use smithay::output::{Output, OutputModeSource, WeakOutput};
use smithay::reexports::calloop::generic::Generic; use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction}; use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::gbm::Modifier; use smithay::reexports::gbm::Modifier;
@@ -44,6 +46,10 @@ use zbus::SignalContext;
use crate::dbus::mutter_screen_cast::{self, CursorMode}; use crate::dbus::mutter_screen_cast::{self, CursorMode};
use crate::niri::State; use crate::niri::State;
use crate::render_helpers::render_to_dmabuf; use crate::render_helpers::render_to_dmabuf;
use crate::utils::get_monotonic_time;
// Give a 0.1 ms allowance for presentation time errors.
const CAST_DELAY_ALLOWANCE: Duration = Duration::from_micros(100);
pub struct PipeWire { pub struct PipeWire {
_context: Context, _context: Context,
@@ -70,6 +76,7 @@ pub struct Cast {
pub last_frame_time: Duration, pub last_frame_time: Duration,
min_time_between_frames: Rc<Cell<Duration>>, min_time_between_frames: Rc<Cell<Duration>>,
dmabufs: Rc<RefCell<HashMap<i64, Dmabuf>>>, dmabufs: Rc<RefCell<HashMap<i64, Dmabuf>>>,
scheduled_redraw: Option<RegistrationToken>,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -315,10 +322,9 @@ impl PipeWire {
}; };
let max_frame_rate = format.max_framerate(); let max_frame_rate = format.max_framerate();
// Subtract 0.5 ms to improve edge cases when equal to refresh rate. let min_frame_time = Duration::from_micros(
let min_frame_time = Duration::from_secs_f64( 1_000_000 * u64::from(max_frame_rate.denom) / u64::from(max_frame_rate.num),
max_frame_rate.denom as f64 / max_frame_rate.num as f64, );
) - Duration::from_micros(500);
min_time_between_frames.set(min_frame_time); min_time_between_frames.set(min_frame_time);
let object = pod.as_object().unwrap(); let object = pod.as_object().unwrap();
@@ -651,6 +657,7 @@ impl PipeWire {
last_frame_time: Duration::ZERO, last_frame_time: Duration::ZERO,
min_time_between_frames, min_time_between_frames,
dmabufs, dmabufs,
scheduled_redraw: None,
}; };
Ok(cast) Ok(cast)
} }
@@ -711,13 +718,13 @@ impl Cast {
Ok(()) Ok(())
} }
pub fn should_skip_frame(&self, target_frame_time: Duration) -> bool { fn compute_extra_delay(&self, target_frame_time: Duration) -> Duration {
let last = self.last_frame_time; let last = self.last_frame_time;
let min = self.min_time_between_frames.get(); let min = self.min_time_between_frames.get();
if last.is_zero() { if last.is_zero() {
trace!(?target_frame_time, ?last, "last is zero, recording"); trace!(?target_frame_time, ?last, "last is zero, recording");
return false; return Duration::ZERO;
} }
if target_frame_time < last { if target_frame_time < last {
@@ -727,20 +734,76 @@ impl Cast {
?last, ?last,
"target frame time is below last, did it overflow or did we mispredict?" "target frame time is below last, did it overflow or did we mispredict?"
); );
return false; return Duration::ZERO;
} }
let diff = target_frame_time - last; let diff = target_frame_time - last;
if diff < min { if diff < min {
let delay = min - diff;
trace!( trace!(
?target_frame_time, ?target_frame_time,
?last, ?last,
"skipping frame because it is too soon: diff={diff:?} < min={min:?}", "frame is too soon: min={min:?}, delay={:?}",
delay
); );
return true; return delay;
} else {
trace!("overshoot={:?}", diff - min);
} }
false Duration::ZERO
}
fn schedule_redraw(
&mut self,
event_loop: &LoopHandle<'static, State>,
output: Output,
target_time: Duration,
) {
if self.scheduled_redraw.is_some() {
return;
}
let now = get_monotonic_time();
let duration = target_time.saturating_sub(now);
let timer = Timer::from_duration(duration);
let token = event_loop
.insert_source(timer, move |_, _, state| {
state.niri.queue_redraw(&output);
TimeoutAction::Drop
})
.unwrap();
self.scheduled_redraw = Some(token);
}
fn remove_scheduled_redraw(&mut self, event_loop: &LoopHandle<'static, State>) {
if let Some(token) = self.scheduled_redraw.take() {
event_loop.remove(token);
}
}
/// Checks whether this frame should be skipped because it's too soon.
///
/// If the frame should be skipped, schedules a redraw and returns `true`. Otherwise, removes a
/// scheduled redraw, if any, and returns `false`.
///
/// When this method returns `false`, the calling code is assumed to follow up with
/// [`Cast::dequeue_buffer_and_render()`].
pub fn check_time_and_schedule(
&mut self,
event_loop: &LoopHandle<'static, State>,
output: &Output,
target_frame_time: Duration,
) -> bool {
let delay = self.compute_extra_delay(target_frame_time);
if delay >= CAST_DELAY_ALLOWANCE {
trace!("delay >= allowance, scheduling redraw");
self.schedule_redraw(event_loop, output.clone(), target_frame_time + delay);
true
} else {
self.remove_scheduled_redraw(event_loop);
false
}
} }
pub fn dequeue_buffer_and_render( pub fn dequeue_buffer_and_render(