185 lines
5.9 KiB
Rust
185 lines
5.9 KiB
Rust
//! 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<Option<String>> =
|
|
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<Instant>,
|
|
}
|
|
|
|
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<Duration> {
|
|
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<T, E>(result: Result<Option<T>, E>) -> (Option<T>, bool, Option<E>) {
|
|
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"
|
|
);
|
|
}
|
|
}
|