halley-wl 0.2.0

Wayland backend and rendering implementation for the Halley Wayland compositor.
use std::time::{Duration, Instant};

use calloop::timer::{TimeoutAction, Timer};
use calloop::{LoopHandle, RegistrationToken};
use eventline::debug;

use crate::compositor::root::Halley;

#[derive(Debug)]
pub(crate) struct VBlankThrottle {
    event_loop: LoopHandle<'static, Halley>,
    last_vblank_timestamp: Option<Duration>,
    throttle_timer_token: Option<RegistrationToken>,
    printed_warning: bool,
    output_name: String,
    samples: u64,
    throttled_count: u64,
    estimated_missed_vblanks: u64,
    smoothed_interval: Option<Duration>,
    max_jitter: Duration,
    min_interval: Option<Duration>,
    max_interval: Duration,
    window_throttled_count: u64,
    window_estimated_missed_vblanks: u64,
    window_max_jitter: Duration,
    window_min_interval: Option<Duration>,
    window_max_interval: Duration,
    last_report_at: Option<Instant>,
}

impl VBlankThrottle {
    pub(crate) fn new(event_loop: LoopHandle<'static, Halley>, output_name: String) -> Self {
        Self {
            event_loop,
            last_vblank_timestamp: None,
            throttle_timer_token: None,
            printed_warning: false,
            output_name,
            samples: 0,
            throttled_count: 0,
            estimated_missed_vblanks: 0,
            smoothed_interval: None,
            max_jitter: Duration::ZERO,
            min_interval: None,
            max_interval: Duration::ZERO,
            window_throttled_count: 0,
            window_estimated_missed_vblanks: 0,
            window_max_jitter: Duration::ZERO,
            window_min_interval: None,
            window_max_interval: Duration::ZERO,
            last_report_at: None,
        }
    }

    fn maybe_report_metrics(&mut self, timestamp: Instant) {
        const REPORT_INTERVAL: Duration = Duration::from_secs(30);
        const REPORT_JITTER_THRESHOLD: Duration = Duration::from_millis(250);

        let should_report = match self.last_report_at {
            Some(last) => timestamp.saturating_duration_since(last) >= REPORT_INTERVAL,
            None => self.samples >= 240,
        };
        if !should_report {
            return;
        }

        self.last_report_at = Some(timestamp);

        let interesting = self.window_throttled_count > 0
            || self.window_estimated_missed_vblanks > 0
            || self.window_max_jitter >= REPORT_JITTER_THRESHOLD;
        if interesting {
            debug!(
                "vblank pacing [{}]: throttled={} est_missed={} avg={:?} min={:?} max={:?} max_jitter={:?}",
                self.output_name,
                self.window_throttled_count,
                self.window_estimated_missed_vblanks,
                self.smoothed_interval.unwrap_or(Duration::ZERO),
                self.window_min_interval.unwrap_or(Duration::ZERO),
                self.window_max_interval,
                self.window_max_jitter,
            );
        }

        self.window_throttled_count = 0;
        self.window_estimated_missed_vblanks = 0;
        self.window_max_jitter = Duration::ZERO;
        self.window_min_interval = None;
        self.window_max_interval = Duration::ZERO;
    }

    fn update_metrics(
        &mut self,
        refresh_interval: Option<Duration>,
        passed: Duration,
        timestamp: Instant,
    ) {
        self.samples = self.samples.saturating_add(1);
        self.min_interval = Some(match self.min_interval {
            Some(current) => current.min(passed),
            None => passed,
        });
        self.max_interval = self.max_interval.max(passed);
        self.window_min_interval = Some(match self.window_min_interval {
            Some(current) => current.min(passed),
            None => passed,
        });
        self.window_max_interval = self.window_max_interval.max(passed);

        let smoothed = match self.smoothed_interval {
            Some(current) => {
                let current_ns = current.as_nanos();
                let passed_ns = passed.as_nanos();
                let blended = ((current_ns * 7) + passed_ns) / 8;
                let blended = blended.min(u128::from(u64::MAX));
                Duration::from_nanos(blended as u64)
            }
            None => passed,
        };
        self.smoothed_interval = Some(smoothed);

        if let Some(refresh) = refresh_interval {
            let jitter = if passed >= refresh {
                passed - refresh
            } else {
                refresh - passed
            };
            self.max_jitter = self.max_jitter.max(jitter);
            self.window_max_jitter = self.window_max_jitter.max(jitter);

            let refresh_ns = refresh.as_nanos();
            let passed_ns = passed.as_nanos();
            if refresh_ns > 0 && passed_ns > refresh_ns + (refresh_ns / 2) {
                let missed = (passed_ns / refresh_ns).saturating_sub(1);
                let missed = missed.min(u128::from(u64::MAX)) as u64;
                self.estimated_missed_vblanks =
                    self.estimated_missed_vblanks.saturating_add(missed);
                self.window_estimated_missed_vblanks =
                    self.window_estimated_missed_vblanks.saturating_add(missed);
            }
        }

        self.maybe_report_metrics(timestamp);
    }

    pub(crate) fn throttle(
        &mut self,
        refresh_interval: Option<Duration>,
        timestamp: Duration,
        mut call_vblank: impl FnMut(&mut Halley) + 'static,
    ) -> bool {
        let now = Instant::now();
        if let Some(token) = self.throttle_timer_token.take() {
            self.event_loop.remove(token);
        }

        if let Some(last) = self.last_vblank_timestamp {
            let passed = timestamp.saturating_sub(last);
            self.update_metrics(refresh_interval, passed, now);

            if let Some(refresh) = refresh_interval {
                if passed < refresh / 2 {
                    if !self.printed_warning {
                        self.printed_warning = true;
                        debug!(
                            "output {} running faster than expected, throttling vblanks: expected refresh {:?}, got vblank after {:?}",
                            self.output_name, refresh, passed
                        );
                    }

                    self.throttled_count = self.throttled_count.saturating_add(1);
                    self.window_throttled_count = self.window_throttled_count.saturating_add(1);
                    let remaining = refresh.saturating_sub(passed);
                    let token = self
                        .event_loop
                        .insert_source(Timer::from_duration(remaining), move |_, _, state| {
                            call_vblank(state);
                            TimeoutAction::Drop
                        })
                        .expect("vblank throttle timer should insert");
                    self.throttle_timer_token = Some(token);
                    return true;
                }
            }
        }

        self.last_vblank_timestamp = Some(timestamp);
        false
    }
}