kms: skip cursor updates for fullscreen content above the minimum refresh rate

This commit is contained in:
Victoria Brekenfeld 2025-01-02 20:23:15 +01:00 committed by Victoria Brekenfeld
parent adcb81bbe0
commit b5cd62fd7a
3 changed files with 111 additions and 16 deletions

View file

@ -239,6 +239,26 @@ pub fn calculate_refresh_rate(mode: Mode) -> u32 {
refresh as u32
}
pub fn get_minimum_refresh_rate(
device: &impl ControlDevice,
connector: connector::Handle,
) -> Result<Option<u32>> {
let info = edid_info(device, connector)?;
let edid = info.edid().context("EDID lacking into")?;
for descriptor in edid.display_descriptors() {
if descriptor.tag() == DisplayDescriptorTag::RangeLimits {
return Ok(Some(
descriptor
.range_limits()
.context("Invalid range limits descriptor")?
.min_vert_rate_hz as u32,
));
}
}
Ok(None)
}
pub fn get_max_bpc(
dev: &impl ControlDevice,
conn: connector::Handle,

View file

@ -531,7 +531,7 @@ fn surface_thread(
vrr_mode: AdaptiveSync::Disabled,
state: QueueState::Idle,
timings: Timings::new(None, false),
timings: Timings::new(None, None, false),
frame_callback_seq: 0,
thread_sender,
@ -667,11 +667,28 @@ impl SurfaceThreadState {
}
fn resume(&mut self, compositor: GbmDrmOutput) -> Result<()> {
let mode = compositor.with_compositor(|c| c.surface().pending_mode());
self.timings
.set_refresh_interval(Some(Duration::from_secs_f64(
1_000.0 / drm_helpers::calculate_refresh_rate(mode) as f64,
)));
let (mode, min_hz) = compositor.with_compositor(|c| {
(
c.surface().pending_mode(),
drm_helpers::get_minimum_refresh_rate(
c.surface(),
c.pending_connectors().into_iter().next().unwrap(),
)
.ok()
.flatten(),
)
});
let interval =
Duration::from_secs_f64(1_000. / drm_helpers::calculate_refresh_rate(mode) as f64);
self.timings.set_refresh_interval(Some(interval));
let min_min_refresh_interval = Duration::from_secs_f64(1. / 30.); // 30Hz
self.timings.set_min_refresh_interval(Some(
min_hz
.map(|min| Duration::from_secs_f64(1. / min as f64))
.unwrap_or(min_min_refresh_interval) // alternatively use 30Hz
.max(min_min_refresh_interval),
));
if crate::utils::env::bool_var("COSMIC_DISABLE_DIRECT_SCANOUT").unwrap_or(false) {
self.frame_flags.remove(FrameFlags::ALLOW_SCANOUT);
@ -799,7 +816,7 @@ impl SurfaceThreadState {
}
}
fn on_estimated_vblank(&mut self) {
fn on_estimated_vblank(&mut self, force: bool) {
match mem::replace(&mut self.state, QueueState::Idle) {
QueueState::Idle => unreachable!(),
QueueState::Queued(_) => unreachable!(),
@ -814,7 +831,7 @@ impl SurfaceThreadState {
self.frame_callback_seq = self.frame_callback_seq.wrapping_add(1);
if self.shell.read().unwrap().animations_going() {
if force || self.shell.read().unwrap().animations_going() {
self.queue_redraw(false);
} else {
self.send_frame_callbacks();
@ -924,10 +941,14 @@ impl SurfaceThreadState {
_ => false,
};
if self.vrr_mode == AdaptiveSync::Enabled {
let has_active_fullscreen = {
let shell = self.shell.read().unwrap();
let output = self.mirroring.as_ref().unwrap_or(&self.output);
vrr = shell.workspaces.active(output).1.get_fullscreen().is_some();
shell.workspaces.active(output).1.get_fullscreen().is_some()
};
if self.vrr_mode == AdaptiveSync::Enabled {
vrr = has_active_fullscreen;
}
let mut elements = output_elements(
@ -945,6 +966,14 @@ impl SurfaceThreadState {
.map_err(|err| {
anyhow::format_err!("Failed to accumulate elements for rendering: {:?}", err)
})?;
let additional_frame_flags = if vrr
&& has_active_fullscreen
&& !self.timings.past_min_presentation_time(&self.clock)
{
FrameFlags::SKIP_CURSOR_ONLY_UPDATES
} else {
FrameFlags::empty()
};
self.timings.set_vrr(vrr);
self.timings.elements_done(&self.clock);
@ -1108,7 +1137,7 @@ impl SurfaceThreadState {
&mut renderer,
&elements,
[0.0, 0.0, 0.0, 1.0],
self.frame_flags,
self.frame_flags.union(additional_frame_flags),
)
} else {
if let Err(err) = compositor.with_compositor(|c| c.use_vrr(vrr)) {
@ -1118,7 +1147,7 @@ impl SurfaceThreadState {
&mut renderer,
&elements,
CLEAR_COLOR, // TODO use a theme neutral color
self.frame_flags,
self.frame_flags.union(additional_frame_flags),
)
};
self.timings.draw_done(&self.clock);
@ -1321,7 +1350,12 @@ impl SurfaceThreadState {
self.send_frame_callbacks();
}
} else {
self.queue_estimated_vblank(estimated_presentation);
self.queue_estimated_vblank(
estimated_presentation,
// Make sure we redraw to reevaluate, if we intentionally missed content
additional_frame_flags
.contains(FrameFlags::SKIP_CURSOR_ONLY_UPDATES),
);
}
}
Err(err) => {
@ -1348,7 +1382,7 @@ impl SurfaceThreadState {
Ok(())
}
fn queue_estimated_vblank(&mut self, target_presentation_time: Duration) {
fn queue_estimated_vblank(&mut self, target_presentation_time: Duration, force: bool) {
match mem::take(&mut self.state) {
QueueState::Idle => unreachable!(),
QueueState::Queued(_) => (),
@ -1378,7 +1412,7 @@ impl SurfaceThreadState {
let token = self
.loop_handle
.insert_source(timer, move |_, _, data| {
data.on_estimated_vblank();
data.on_estimated_vblank(force);
TimeoutAction::Drop
})
.unwrap();

View file

@ -8,6 +8,7 @@ const FRAME_TIME_WINDOW: usize = 3;
pub struct Timings {
refresh_interval_ns: Option<NonZeroU64>,
min_refresh_interval_ns: Option<NonZeroU64>,
vrr: bool,
pub pending_frame: Option<PendingFrame>,
@ -44,7 +45,11 @@ impl Frame {
impl Timings {
const WINDOW_SIZE: usize = 360;
pub fn new(refresh_interval: Option<Duration>, vrr: bool) -> Self {
pub fn new(
refresh_interval: Option<Duration>,
min_interval: Option<Duration>,
vrr: bool,
) -> Self {
let refresh_interval_ns = if let Some(interval) = &refresh_interval {
assert_eq!(interval.as_secs(), 0);
Some(NonZeroU64::new(interval.subsec_nanos().into()).unwrap())
@ -52,8 +57,16 @@ impl Timings {
None
};
let min_refresh_interval_ns = if let Some(interval) = &min_interval {
assert_eq!(interval.as_secs(), 0);
Some(NonZeroU64::new(interval.subsec_nanos().into()).unwrap())
} else {
None
};
Self {
refresh_interval_ns,
min_refresh_interval_ns,
vrr,
pending_frame: None,
@ -76,6 +89,12 @@ impl Timings {
self.previous_frames.clear();
}
pub fn set_min_refresh_interval(&mut self, min_interval: Option<Duration>) {
self.min_refresh_interval_ns = min_interval
.map(|duration| duration.subsec_nanos() as u64)
.and_then(NonZeroU64::new);
}
pub fn set_vrr(&mut self, vrr: bool) {
self.vrr = vrr;
}
@ -255,6 +274,28 @@ impl Timings {
}
}
pub fn past_min_presentation_time(&self, clock: &Clock<Monotonic>) -> bool {
let now: Duration = clock.now().into();
let Some(refresh_interval_ns) = self.min_refresh_interval_ns else {
return true;
};
let Some(last_presentation_time): Option<Duration> = self
.previous_frames
.back()
.map(|frame| frame.presentation_presented.into())
else {
return true;
};
let refresh_interval_ns = refresh_interval_ns.get();
if now <= last_presentation_time {
return false;
}
let next = last_presentation_time + Duration::from_nanos(refresh_interval_ns);
now >= next
}
pub fn next_render_time(&self, clock: &Clock<Monotonic>) -> Duration {
let estimated_presentation_time = self.next_presentation_time(clock);
if estimated_presentation_time.is_zero() {