fix compositor edge cases, process handing and other cool improvements

This commit is contained in:
Samuel Onoja committed 2026-10-04 17:11:40 +01:00
1 parent 5706a71941
commit 405c74af9f
38 files changed
+3391 -875

No files matched your search

+82 -28
View File
@@ -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(),
+130 -8
View File
@@ -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"
);
}
}
+45 -13
View File
@@ -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));
}
}
+165
View File
@@ -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"
);
}
}
+8 -3
View File
@@ -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);
}