//! Uncertain KMS completion requires device reconciliation, not buffer reuse. use super::{ DirectOutput, DrmNode, Duration, Ferese, Instant, TimeoutAction, Timer, crtc, reconcile_outputs, }; const COMPLETION_TIMEOUT: Duration = Duration::from_secs(2); #[cfg(feature = "drm-fault-injection")] pub(super) fn inject(mode: &str) -> bool { use std::sync::LazyLock; use std::sync::atomic::{AtomicBool, Ordering}; static MODE: LazyLock> = LazyLock::new(|| std::env::var("FERESE_TEST_DRM_FAILURE").ok()); static USED: AtomicBool = AtomicBool::new(false); MODE.as_deref() == Some(mode) && !USED.swap(true, Ordering::AcqRel) } #[derive(Default)] pub(super) struct Completion { pending: Option, } impl Completion { fn queued(&mut self, now: Instant) { self.pending = Some(now); } pub fn retired(&mut self) { self.pending = None; } fn remaining(&self, now: Instant) -> Option { self.pending .map(|queued| COMPLETION_TIMEOUT.saturating_sub(now.saturating_duration_since(queued))) } } // Keep the submission result and the recovery decision together. An error must // not be mistaken for a completed frame with no presentation feedback. pub(super) fn retirement(result: Result, E>) -> (Option, bool, Option) { match result { Ok(feedback) => { let retired = feedback.is_some(); (feedback, retired, None) } Err(error) => (None, false, Some(error)), } } pub(super) fn arm_completion( handle: &smithay::reexports::calloop::LoopHandle<'static, Ferese>, node: DrmNode, crtc: crtc::Handle, output: &mut DirectOutput, ) -> bool { output.completion.queued(Instant::now()); if output.completion_timer.is_some() { return true; } let identity = output.output.clone(); match handle.insert_source( Timer::from_duration(COMPLETION_TIMEOUT), move |_, _, state| { let Some(output) = state .direct_backend .as_mut() .and_then(|backend| backend.devices.get_mut(&node)) .and_then(|device| device.outputs.get_mut(&crtc)) .filter(|output| output.output == identity) else { return TimeoutAction::Drop; }; if let Some(remaining) = output.completion.remaining(Instant::now()) { if !remaining.is_zero() { return TimeoutAction::ToDuration(remaining); } output.completion_timer = None; tracing::error!( ?node, ?crtc, "DRM frame completion timed out; reconciling device" ); reconcile_device(state, node); } else { output.completion_timer = None; } TimeoutAction::Drop }, ) { Ok(token) => { output.completion_timer = Some(token); true } Err(error) => { tracing::error!(%error, ?node, ?crtc, "could not arm DRM completion watchdog"); false } } } pub(super) fn reconcile_device(state: &mut Ferese, node: DrmNode) { let Some(backend) = state.direct_backend.as_mut() else { return; }; backend.topology.dirty = true; if !backend.topology.invalidated.insert(node) { return; } // Defer teardown until the DRM notifier callback has returned. Inactive // sessions retain the invalidation for fresh enumeration on activation. let active = backend.active; if active { match state .loop_handle .insert_source(Timer::immediate(), |_, _, state| { reconcile_outputs(state, false); TimeoutAction::Drop }) { Ok(_) => (), Err(error) => tracing::error!(%error, "could not schedule DRM device recovery"), } } } #[cfg(test)] mod tests { use super::*; #[test] fn failed_retirement_does_not_release_pending_buffer_ownership() { let now = Instant::now(); let mut completion = Completion::default(); completion.queued(now); let (feedback, retired, error) = retirement::<(), _>(Err("injected KMS retirement failure")); assert_eq!(feedback, None); assert!(!retired); assert!( error.is_some(), "the production handler must reconcile the device" ); assert_eq!(completion.remaining(now), Some(COMPLETION_TIMEOUT)); assert_eq!(retirement::<(), &str>(Ok(None)), (None, false, None)); assert_eq!(retirement::<(), &str>(Ok(Some(()))), (Some(()), true, None)); // Only successful retirement or device teardown can end uncertainty. completion = Completion::default(); assert_eq!(completion.remaining(now), None); } #[test] fn missing_completion_remains_pending_until_hardware_reconciliation() { let now = Instant::now(); let mut completion = Completion::default(); completion.queued(now); assert_eq!( completion.remaining(now + COMPLETION_TIMEOUT - Duration::from_millis(1)), Some(Duration::from_millis(1)) ); // A timeout observes uncertainty; it never frees the current buffer. assert_eq!( completion.remaining(now + COMPLETION_TIMEOUT), Some(Duration::ZERO) ); assert_eq!( completion.remaining(now + COMPLETION_TIMEOUT + Duration::from_secs(1)), Some(Duration::ZERO) ); completion.retired(); assert_eq!(completion.remaining(now), None); completion.queued(now + Duration::from_secs(3)); assert_eq!( completion.remaining(now + Duration::from_secs(4)), Some(Duration::from_secs(1)), "existing timer must respect the newer frame's deadline" ); } }