Skip to content

Commit

Permalink
Improve composition scheduling
Browse files Browse the repository at this point in the history
Previously Catacomb would always start compositing 10ms before the
vblank. However in some instances, like with a large number of surfaces,
rendering could exceed that deadline and cause Catacomb to miss the
vblank entirely.

This patch adds a dynamic scheduling model, which uses the average
rendering time and the last 16 frames to predict the next frame's
rendering time. Since rendering does tend to fluctuate and Catacomb is
lacking insight into when rendering actually completed, a buffer is
added on top of the prediction.

Since compositing times in the worst-case are very high, the predictions
generally exceed the vblank interval, effectively making this solution
identical to always compositing right after the vblank. However under
ideal circumstances this solution manages to provide at least a couple
milliseconds for fast clients to render, without significantly affecting
the worst-case. This also can likely be improved in the future with more
insight into the rendering pipeline.

Closes #146.
  • Loading branch information
chrisduerr committed Feb 17, 2024
1 parent b0e0323 commit 0409604
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 7 deletions.
58 changes: 57 additions & 1 deletion src/catacomb.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//! Catacomb compositor state.
use std::cell::RefCell;
use std::env;
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::{cmp, env};

use _decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode;
use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as ManagerMode;
Expand Down Expand Up @@ -102,6 +102,12 @@ const ACTIVATION_TIMEOUT: Duration = Duration::from_secs(10);
/// The script to run after compositor start.
const POST_START_SCRIPT: &str = "post_start.sh";

/// Number of frames considered for best-case rendering times.
const RECENT_FRAME_COUNT: usize = 16;

/// Padding added to render time predictions.
const PREDICTION_PADDING: Duration = Duration::from_millis(8);

/// Shared compositor state.
pub struct Catacomb {
pub suspend_timer: Option<RegistrationToken>,
Expand All @@ -110,6 +116,7 @@ pub struct Catacomb {
pub display_handle: DisplayHandle,
pub key_bindings: Vec<KeyBinding>,
pub touch_state: TouchState,
pub frame_pacer: FramePacer,
pub last_resume: Instant,
pub seat_name: String,
pub windows: Windows,
Expand Down Expand Up @@ -332,6 +339,7 @@ impl Catacomb {
suspend_timer: Default::default(),
key_bindings: Default::default(),
ime_override: Default::default(),
frame_pacer: Default::default(),
last_focus: Default::default(),
terminated: Default::default(),
idle_timer: Default::default(),
Expand All @@ -358,6 +366,8 @@ impl Catacomb {
return;
}

let frame_start = Instant::now();

// Clear rendering stall status.
self.stalled = false;

Expand Down Expand Up @@ -389,6 +399,10 @@ impl Catacomb {
// Draw all visible clients.
let rendered = self.backend.render(&mut self.windows);

// Update render time prediction.
let frame_interval = self.output().frame_interval();
self.frame_pacer.add_frame(frame_start.elapsed(), frame_interval);

// Create artificial VBlank if renderer didn't draw.
//
// This is necessary, since rendering might have been skipped due to DRM planes
Expand Down Expand Up @@ -876,3 +890,45 @@ impl ClientData for ClientState {
}

delegate_single_pixel_buffer!(Catacomb);

/// Compositor rendering time prediction.
#[derive(Default)]
pub struct FramePacer {
recent_frames: [u64; RECENT_FRAME_COUNT],
frame_count: u64,
total_ns: u64,
}

impl FramePacer {
/// Add a new frame to the tracker.
fn add_frame(&mut self, render_time: Duration, frame_interval: Duration) {
// Limit worst-case to refresh rate, to reduce impact of statistical outliers.
let ns = cmp::min(render_time, frame_interval).as_nanos() as u64;

self.recent_frames.rotate_right(1);
self.recent_frames[0] = ns;

self.total_ns += ns;

self.frame_count += 1;
}

/// Predict time required for next frame.
pub fn predict(&self) -> Option<Duration> {
if self.frame_count == 0 {
return None;
}

// Use total average as the minimum render time.
let min_time_ns = self.total_ns / self.frame_count;

// Use longest recent frame as baseline.
let recent_frames = self.recent_frames.iter().copied().take(self.frame_count as usize);
let recent_worst_ns = recent_frames.max().unwrap_or_default();

let prediction = Duration::from_nanos(recent_worst_ns.max(min_time_ns));

// Add buffer to account for measuring tolerances.
Some(prediction + PREDICTION_PADDING)
}
}
14 changes: 8 additions & 6 deletions src/udev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,6 @@ use crate::windows::Windows;
/// Default background color.
const CLEAR_COLOR: [f32; 4] = [0., 0., 0., 1.];

/// Time before a VBlank reserved for rendering compositor updates.
const RENDER_TIME_OFFSET: Duration = Duration::from_millis(10);

/// Supported DRM color formats.
///
/// These are formats supported by most devices which have at least 8 bits per
Expand Down Expand Up @@ -138,7 +135,7 @@ fn add_device(catacomb: &mut Catacomb, path: PathBuf) {
pub struct Udev {
scheduled_redraws: Vec<RegistrationToken>,
event_loop: LoopHandle<'static, Catacomb>,
pub output_device: Option<OutputDevice>,
output_device: Option<OutputDevice>,
session: LibSeatSession,
gpu: Option<PathBuf>,
}
Expand Down Expand Up @@ -368,8 +365,13 @@ impl Udev {

// Request redraw before the next VBlank.
let frame_interval = catacomb.windows.output().frame_interval();
let duration = frame_interval - RENDER_TIME_OFFSET;
catacomb.backend.schedule_redraw(duration);
let prediction = catacomb.frame_pacer.predict();
match prediction.filter(|prediction| prediction < &frame_interval) {
Some(prediction) => {
catacomb.backend.schedule_redraw(frame_interval - prediction);
},
None => catacomb.create_frame(),
}
},
DrmEvent::Error(error) => error!("DRM error: {error}"),
};
Expand Down

0 comments on commit 0409604

Please sign in to comment.