fix compositor edge cases, process handing and other cool improvements
This commit is contained in:
1 parent
5706a71941
commit
405c74af9f
38 files changed
+3391
-875
No files matched your search
@@ -10,6 +10,7 @@ mod lid;
|
||||
mod mirror;
|
||||
mod output_power;
|
||||
mod planes;
|
||||
mod recovery;
|
||||
mod redraw;
|
||||
mod scheduling;
|
||||
mod topology;
|
||||
@@ -260,6 +261,8 @@ struct DirectOutput {
|
||||
capture_texture: Option<GlesTexture>,
|
||||
render_metrics: RenderMetrics,
|
||||
frame_pending: bool,
|
||||
completion: recovery::Completion,
|
||||
completion_timer: Option<RegistrationToken>,
|
||||
animation_fallback: AnimationFallback,
|
||||
cursor_visible: bool,
|
||||
scheduler: crate::frame_scheduler::FrameScheduler,
|
||||
@@ -800,23 +803,43 @@ fn open_device(state: &mut Ferese, path: &Path) -> Result<DrmNode, Box<dyn Error
|
||||
.loop_handle
|
||||
.insert_source(notifier, move |event, metadata, state| match event {
|
||||
DrmEvent::VBlank(crtc) => {
|
||||
#[cfg(feature = "drm-fault-injection")]
|
||||
if state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.and_then(|backend| backend.devices.get(&node))
|
||||
.and_then(|device| device.outputs.get(&crtc))
|
||||
.is_some_and(|output| output.frame_pending)
|
||||
&& recovery::inject("missing-completion")
|
||||
{
|
||||
tracing::warn!(?node, ?crtc, "injecting missing DRM completion");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(metadata) = metadata {
|
||||
let mut retired = false;
|
||||
let mut retirement_failed = false;
|
||||
let feedback = state
|
||||
.direct_backend
|
||||
.as_mut()
|
||||
.and_then(|backend| backend.devices.get_mut(&node))
|
||||
.and_then(|device| device.outputs.get_mut(&crtc))
|
||||
.and_then(|output| match output.surface.frame_submitted() {
|
||||
Ok(feedback) => {
|
||||
retired = feedback.is_some();
|
||||
feedback
|
||||
.and_then(|output| {
|
||||
#[cfg(feature = "drm-fault-injection")]
|
||||
if output.frame_pending && recovery::inject("retirement") {
|
||||
tracing::warn!(?node, ?crtc, "injecting DRM retirement failure");
|
||||
retirement_failed = true;
|
||||
return None;
|
||||
}
|
||||
|
||||
Err(error) => {
|
||||
let (feedback, completed, error) = recovery::retirement(output.surface.frame_submitted());
|
||||
retired = completed;
|
||||
if let Some(error) = error {
|
||||
tracing::error!(?node, ?crtc, %error, "failed to retire DRM frame");
|
||||
None
|
||||
retirement_failed = true;
|
||||
}
|
||||
|
||||
feedback
|
||||
});
|
||||
|
||||
if let (Some(mut feedback), DrmEventTime::Monotonic(time)) = (feedback, metadata.time) {
|
||||
@@ -852,6 +875,7 @@ fn open_device(state: &mut Ferese, path: &Path) -> Result<DrmNode, Box<dyn Error
|
||||
.and_then(|device| device.outputs.get_mut(&crtc))
|
||||
&& retired
|
||||
{
|
||||
output.completion.retired();
|
||||
output.frame_pending = false;
|
||||
output.power.presented();
|
||||
if let DrmEventTime::Monotonic(timestamp) = metadata.time {
|
||||
@@ -886,6 +910,11 @@ fn open_device(state: &mut Ferese, path: &Path) -> Result<DrmNode, Box<dyn Error
|
||||
state.refresh_idle_inhibition();
|
||||
}
|
||||
|
||||
if retirement_failed {
|
||||
recovery::reconcile_device(state, node);
|
||||
return;
|
||||
}
|
||||
|
||||
apply_output_power(state);
|
||||
|
||||
state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence);
|
||||
@@ -1168,6 +1197,10 @@ pub(crate) fn system_resumed(state: &mut Ferese) {
|
||||
lid::request_refresh(state, true);
|
||||
}
|
||||
|
||||
pub(crate) fn reconcile_after_monitor_recovery(state: &mut Ferese) {
|
||||
lid::request_reconcile(state);
|
||||
}
|
||||
|
||||
fn internal_connector(interface: connector::Interface) -> bool {
|
||||
matches!(
|
||||
interface,
|
||||
@@ -1268,19 +1301,19 @@ fn render_output(
|
||||
primary.sync.wait()?;
|
||||
}
|
||||
|
||||
if output.mirror_source.is_some() {
|
||||
capture::capture_mirror(state, &mut device.renderer, &scene_output, &frame, &mut output)?;
|
||||
} else {
|
||||
capture::capture_output(
|
||||
state,
|
||||
&mut device.renderer,
|
||||
&mut output.capture_texture,
|
||||
&output.output,
|
||||
&frame,
|
||||
)?;
|
||||
}
|
||||
|
||||
if result.is_empty {
|
||||
if output.mirror_source.is_some() {
|
||||
capture::capture_mirror(state, &mut device.renderer, &scene_output, &frame, &mut output);
|
||||
} else {
|
||||
capture::capture_output(
|
||||
state,
|
||||
&mut device.renderer,
|
||||
&mut output.capture_texture,
|
||||
&output.output,
|
||||
&frame,
|
||||
);
|
||||
}
|
||||
|
||||
output
|
||||
.render_metrics
|
||||
.record_no_damage(render_started.elapsed(), missed_deadlines);
|
||||
@@ -1327,16 +1360,35 @@ fn render_output(
|
||||
};
|
||||
let visibility = result.states.clone();
|
||||
drop(result);
|
||||
output.surface.queue_frame(presentation)?;
|
||||
if output.mirror_source.is_none() {
|
||||
state.display_presentation.queued(&output.output, &visibility);
|
||||
}
|
||||
output.primary_commit = primary_commit;
|
||||
output
|
||||
.render_metrics
|
||||
.record_frame(render_started.elapsed(), &damage, missed_deadlines, effects);
|
||||
output.frame_pending = true;
|
||||
output.lock_frame_pending = state.session_lock.active();
|
||||
|
||||
capture::after_queue(output.surface.queue_frame(presentation), || {
|
||||
if output.mirror_source.is_none() {
|
||||
state.display_presentation.queued(&output.output, &visibility);
|
||||
}
|
||||
output.primary_commit = primary_commit;
|
||||
output
|
||||
.render_metrics
|
||||
.record_frame(render_started.elapsed(), &damage, missed_deadlines, effects);
|
||||
output.frame_pending = true;
|
||||
if !recovery::arm_completion(&state.loop_handle, node, crtc, &mut output) {
|
||||
recovery::reconcile_device(state, node);
|
||||
}
|
||||
|
||||
output.lock_frame_pending = state.session_lock.active();
|
||||
|
||||
if output.mirror_source.is_some() {
|
||||
capture::capture_mirror(state, &mut device.renderer, &scene_output, &frame, &mut output);
|
||||
} else {
|
||||
capture::capture_output(
|
||||
state,
|
||||
&mut device.renderer,
|
||||
&mut output.capture_texture,
|
||||
&output.output,
|
||||
&frame,
|
||||
);
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(true)
|
||||
})()
|
||||
};
|
||||
@@ -2042,6 +2094,8 @@ fn create_direct_output(
|
||||
capture_texture: None,
|
||||
render_metrics,
|
||||
frame_pending: false,
|
||||
completion: Default::default(),
|
||||
completion_timer: None,
|
||||
animation_fallback: AnimationFallback::default(),
|
||||
cursor_visible: false,
|
||||
power: OutputPower::default(),
|
||||
|
||||
@@ -3,6 +3,7 @@ use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesRenderer, GlesTarget, GlesTexture};
|
||||
use smithay::backend::renderer::{Bind, Offscreen, Texture};
|
||||
use smithay::output::Output;
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::Ferese;
|
||||
use crate::render::{capture_output_elements, redraw_output};
|
||||
@@ -14,7 +15,7 @@ pub(crate) fn capture_output(
|
||||
texture: &mut Option<GlesTexture>,
|
||||
output: &Output,
|
||||
scene: &FrameScene,
|
||||
) -> Result<(), GlesError> {
|
||||
) {
|
||||
for include_cursor in [true, false] {
|
||||
if !state.has_pending_screencopy(output, include_cursor) {
|
||||
continue;
|
||||
@@ -23,11 +24,13 @@ pub(crate) fn capture_output(
|
||||
// Sample the same scene into an independent target: the displayed
|
||||
// primary buffer may omit both a scanned-out client and hardware cursor.
|
||||
let elements = capture_output_elements(state, renderer, output, include_cursor, scene);
|
||||
let target = render_capture(renderer, texture, output, &elements)?;
|
||||
let Some(target) = capture_request(state, output, include_cursor, || {
|
||||
render_capture(renderer, texture, output, &elements)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
state.process_screencopies(renderer, &target, output, include_cursor);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn capture_mirror(
|
||||
@@ -36,7 +39,7 @@ pub(super) fn capture_mirror(
|
||||
source: &Output,
|
||||
scene: &FrameScene,
|
||||
output: &mut super::DirectOutput,
|
||||
) -> Result<(), GlesError> {
|
||||
) {
|
||||
for include_cursor in [true, false] {
|
||||
if !state.has_pending_screencopy(&output.output, include_cursor) {
|
||||
continue;
|
||||
@@ -44,17 +47,46 @@ pub(super) fn capture_mirror(
|
||||
let elements = capture_output_elements(state, renderer, source, include_cursor, scene);
|
||||
// Never sample mirror_texture: it contains the unfiltered display.
|
||||
let canvas = output.mirror_canvas.as_ref().expect("mirror display composed");
|
||||
super::mirror::compose(renderer, &mut output.mirror_capture_texture, canvas, source, &elements)?;
|
||||
let Some(()) = capture_request(state, &output.output, include_cursor, || {
|
||||
super::mirror::compose(renderer, &mut output.mirror_capture_texture, canvas, source, &elements)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
let element = super::mirror::fitted_texture(
|
||||
output.mirror_capture_texture.as_ref().unwrap().clone(),
|
||||
&output.output,
|
||||
smithay::backend::renderer::element::Id::new(),
|
||||
Default::default(),
|
||||
);
|
||||
let target = render_capture(renderer, &mut output.capture_texture, &output.output, &[element])?;
|
||||
let Some(target) = capture_request(state, &output.output, include_cursor, || {
|
||||
render_capture(renderer, &mut output.capture_texture, &output.output, &[element])
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
state.process_screencopies(renderer, &target, &output.output, include_cursor);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn capture_request<T, E: Display>(
|
||||
state: &mut Ferese,
|
||||
output: &Output,
|
||||
include_cursor: bool,
|
||||
render: impl FnOnce() -> Result<T, E>,
|
||||
) -> Option<T> {
|
||||
match render() {
|
||||
Ok(target) => Some(target),
|
||||
Err(error) => {
|
||||
tracing::warn!(?output, include_cursor, %error, "screencopy render failed");
|
||||
state.fail_screencopies(output, include_cursor);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn after_queue<T, E>(queued: Result<T, E>, capture: impl FnOnce()) -> Result<T, E> {
|
||||
let queued = queued?;
|
||||
capture();
|
||||
Ok(queued)
|
||||
}
|
||||
|
||||
pub(super) fn render_capture<'a, E: RenderElement<GlesRenderer>>(
|
||||
@@ -178,3 +210,93 @@ mod tests {
|
||||
assert_eq!(capture.as_ref().unwrap().size(), (48, 16).into());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod failure_tests {
|
||||
use std::cell::Cell;
|
||||
|
||||
use calloop::channel::channel;
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Buffer, Rectangle};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn capture_allocation_failure_fails_one_request_after_display_submission() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"backends::direct::capture::failure_tests::capture_allocation_failure_fails_one_request_after_display_submission",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut events = calloop::EventLoop::try_new().unwrap();
|
||||
let mut state = crate::startup_tests::state(&mut events);
|
||||
let output = Output::new(
|
||||
"capture-failure".into(),
|
||||
smithay::output::PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: smithay::output::Subpixel::Unknown,
|
||||
make: "test".into(),
|
||||
model: "test".into(),
|
||||
},
|
||||
);
|
||||
let (sender, receiver) = channel();
|
||||
state
|
||||
.pending_screencopies
|
||||
.push(crate::handlers::screencopy::PendingScreencopy::owned(
|
||||
41,
|
||||
0,
|
||||
sender,
|
||||
output.clone(),
|
||||
Rectangle::<i32, Buffer>::from_size((16, 16).into()),
|
||||
));
|
||||
let other_output = Output::new(
|
||||
"other-capture".into(),
|
||||
smithay::output::PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: smithay::output::Subpixel::Unknown,
|
||||
make: "test".into(),
|
||||
model: "test".into(),
|
||||
},
|
||||
);
|
||||
let (other_sender, other_receiver) = channel();
|
||||
state
|
||||
.pending_screencopies
|
||||
.push(crate::handlers::screencopy::PendingScreencopy::owned(
|
||||
42,
|
||||
0,
|
||||
other_sender,
|
||||
other_output.clone(),
|
||||
Rectangle::<i32, Buffer>::from_size((16, 16).into()),
|
||||
));
|
||||
|
||||
let submitted = Cell::new(false);
|
||||
let queued = {
|
||||
submitted.set(true);
|
||||
Ok::<(), ()>(())
|
||||
};
|
||||
after_queue(queued, || {
|
||||
assert!(submitted.get(), "capture must run after the display queue succeeds");
|
||||
assert!(
|
||||
capture_request(&mut state, &output, false, || Err::<(), _>(
|
||||
"injected allocation failure"
|
||||
))
|
||||
.is_none()
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(submitted.get(), "capture failure must not undo queued display work");
|
||||
assert_eq!(state.pending_screencopies.len(), 1);
|
||||
assert_eq!(state.pending_screencopies[0].output, other_output);
|
||||
let outcome = receiver.try_recv().unwrap();
|
||||
assert_eq!(outcome.request, 41);
|
||||
assert_eq!(outcome.part, 0);
|
||||
assert!(outcome.result.is_err());
|
||||
assert!(receiver.try_recv().is_err(), "request should fail exactly once");
|
||||
assert!(
|
||||
other_receiver.try_recv().is_err(),
|
||||
"other output's request must stay pending"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub(super) struct Probe {
|
||||
serial: u64,
|
||||
revision: u64,
|
||||
reactivate: bool,
|
||||
wake: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -33,12 +34,13 @@ impl LidState {
|
||||
changed
|
||||
}
|
||||
|
||||
fn begin(&mut self, reactivate: bool) -> Probe {
|
||||
fn begin(&mut self, reactivate: bool, wake: bool) -> Probe {
|
||||
self.serial = self.serial.wrapping_add(1);
|
||||
let probe = Probe {
|
||||
serial: self.serial,
|
||||
revision: self.revision,
|
||||
reactivate: reactivate || self.pending.is_some_and(|pending| pending.reactivate),
|
||||
wake: wake || self.pending.is_some_and(|pending| pending.wake),
|
||||
};
|
||||
self.pending = Some(probe);
|
||||
probe
|
||||
@@ -269,6 +271,15 @@ impl Reader {
|
||||
}
|
||||
|
||||
pub(super) fn request_refresh(state: &mut Ferese, reactivate: bool) {
|
||||
request(state, reactivate, true);
|
||||
}
|
||||
|
||||
/// Listener recovery reconciles hardware without resetting idle activity.
|
||||
pub(super) fn request_reconcile(state: &mut Ferese) {
|
||||
request(state, false, false);
|
||||
}
|
||||
|
||||
fn request(state: &mut Ferese, reactivate: bool, wake: bool) {
|
||||
let Some(backend) = state.direct_backend.as_mut() else {
|
||||
return;
|
||||
};
|
||||
@@ -289,7 +300,7 @@ pub(super) fn request_refresh(state: &mut Ferese, reactivate: bool) {
|
||||
}
|
||||
backend.lid_reader.suspend_requested = false;
|
||||
backend.lid_reader.lease.lock().unwrap().suspend = false;
|
||||
let probe = backend.lid.begin(reactivate);
|
||||
let probe = backend.lid.begin(reactivate, wake);
|
||||
let sent = backend.lid_reader.requests.send(probe).is_ok();
|
||||
let timer = state
|
||||
.loop_handle
|
||||
@@ -329,10 +340,13 @@ fn complete_refresh(state: &mut Ferese, probe: Probe, value: Option<bool>) {
|
||||
// Waking a locked session may request redraws. Hold them until the fresh
|
||||
// device/connector snapshot has been reconciled.
|
||||
backend.lid_reader.suspend_requested = false;
|
||||
backend.topology.reconciling = true;
|
||||
state.reset_animation_clock();
|
||||
state.lock_input_activity();
|
||||
state.direct_backend.as_mut().unwrap().topology.reconciling = false;
|
||||
if probe.wake {
|
||||
backend.topology.reconciling = true;
|
||||
state.reset_animation_clock();
|
||||
state.lock_input_activity();
|
||||
state.direct_backend.as_mut().unwrap().topology.reconciling = false;
|
||||
}
|
||||
|
||||
reconcile_outputs(state, reactivate);
|
||||
}
|
||||
|
||||
@@ -431,7 +445,7 @@ mod tests {
|
||||
fn newer_input_wins_over_a_delayed_resume_read() {
|
||||
let mut lid = LidState::default();
|
||||
lid.observe(true);
|
||||
let probe = lid.begin(true);
|
||||
let probe = lid.begin(true, true);
|
||||
lid.observe(false);
|
||||
assert_eq!(lid.complete(probe, Some(true)), Some(true));
|
||||
assert!(!lid.closed());
|
||||
@@ -440,11 +454,11 @@ mod tests {
|
||||
#[test]
|
||||
fn failed_reads_preserve_known_state_and_unknown_is_not_an_open_observation() {
|
||||
let mut lid = LidState::default();
|
||||
let probe = lid.begin(false);
|
||||
let probe = lid.begin(false, true);
|
||||
lid.complete(probe, None);
|
||||
assert_eq!(lid.value, None);
|
||||
lid.observe(true);
|
||||
let probe = lid.begin(true);
|
||||
let probe = lid.begin(true, true);
|
||||
lid.complete(probe, None);
|
||||
assert_eq!(lid.value, Some(true));
|
||||
}
|
||||
@@ -452,21 +466,39 @@ mod tests {
|
||||
#[test]
|
||||
fn pause_and_newer_resume_invalidate_old_replies() {
|
||||
let mut lid = LidState::default();
|
||||
let old = lid.begin(true);
|
||||
let old = lid.begin(true, true);
|
||||
lid.pause();
|
||||
assert_eq!(lid.complete(old, Some(true)), None);
|
||||
let current = lid.begin(true);
|
||||
let current = lid.begin(true, true);
|
||||
assert_eq!(lid.complete(old, Some(true)), None);
|
||||
assert!(lid.pending());
|
||||
assert_eq!(lid.complete(current, Some(false)), Some(true));
|
||||
assert!(!lid.pending());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listener_recovery_does_not_request_idle_wake_or_reactivation() {
|
||||
let mut lid = LidState::default();
|
||||
let recovery = lid.begin(false, false);
|
||||
assert!(!recovery.wake);
|
||||
assert_eq!(lid.complete(recovery, Some(false)), Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listener_recovery_preserves_a_pending_real_wake() {
|
||||
let mut lid = LidState::default();
|
||||
let resume = lid.begin(true, true);
|
||||
let recovery = lid.begin(false, false);
|
||||
assert!(recovery.wake);
|
||||
assert_eq!(lid.complete(resume, Some(false)), None);
|
||||
assert_eq!(lid.complete(recovery, Some(false)), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_wake_cannot_cancel_pending_session_reactivation() {
|
||||
let mut lid = LidState::default();
|
||||
lid.begin(true);
|
||||
let probe = lid.begin(false);
|
||||
lid.begin(true, true);
|
||||
let probe = lid.begin(false, true);
|
||||
assert_eq!(lid.complete(probe, Some(false)), Some(true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
//! Uncertain KMS completion requires device reconciliation, not buffer reuse.
|
||||
use super::*;
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,14 @@ pub(super) fn cancel_output_timers(
|
||||
handle: &smithay::reexports::calloop::LoopHandle<'static, Ferese>,
|
||||
output: &mut DirectOutput,
|
||||
) {
|
||||
for token in [output.render_timer.take(), output.callback_timer.take()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
output.completion.retired();
|
||||
for token in [
|
||||
output.render_timer.take(),
|
||||
output.callback_timer.take(),
|
||||
output.completion_timer.take(),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
handle.remove(token);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user