diff --git a/compositor/src/backends/direct.rs b/compositor/src/backends/direct.rs index 0188a81..f4d564f 100644 --- a/compositor/src/backends/direct.rs +++ b/compositor/src/backends/direct.rs @@ -10,6 +10,7 @@ mod output_power; mod planes; mod redraw; mod scheduling; +mod topology; use output_power::{OutputPower, Power}; use redraw::{AnimationFallback, SchedulingMetrics}; use scheduling::*; @@ -26,7 +27,7 @@ use smithay::backend::renderer::utils::CommitCounter; use smithay::backend::renderer::{ImportDma, ImportMemWl, Renderer}; use smithay::backend::session::libseat::LibSeatSession; use smithay::backend::session::{Event as SessionEvent, Session}; -use smithay::backend::udev::{UdevBackend, UdevEvent, primary_gpu}; +use smithay::backend::udev::{UdevBackend, UdevEvent, all_gpus, primary_gpu}; use smithay::desktop::layer_map_for_output; use smithay::desktop::utils::OutputPresentationFeedback; use smithay::output::{Mode as OutputMode, Output, OutputModeSource, PhysicalProperties, Scale, Subpixel}; @@ -42,6 +43,7 @@ use smithay::reexports::wayland_server::backend::GlobalId; use smithay::utils::{DeviceFd, Monotonic, Time, Transform}; use smithay::wayland::dmabuf::DmabufFeedbackBuilder; use smithay::wayland::presentation::Refresh; +use topology::{Topology, reconcile_outputs}; use crate::Ferese; use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform}; @@ -52,6 +54,7 @@ pub struct DirectBackendState { pub session: LibSeatSession, pub active: bool, lid_closed: bool, + topology: Topology, pub(crate) low_power: bool, devices: HashMap, input_devices: Vec, @@ -65,6 +68,10 @@ pub struct DirectBackendState { } impl DirectBackendState { + fn can_render(&self) -> bool { + self.active && !self.topology.reconciling + } + pub(crate) fn dump_scheduling_metrics(&self) { self.scheduling_metrics.dump(); } @@ -171,6 +178,9 @@ type OutputCompositor = DrmCompositor< >; struct DirectDevice { + notifier: RegistrationToken, + dmabuf_global: smithay::wayland::dmabuf::DmabufGlobal, + connected_outputs: Vec, drm: DrmDevice, gbm: GbmDevice, renderer: GlesRenderer, @@ -233,6 +243,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() session, active: session_active, lid_closed: initial_lid_closed(), + topology: Topology::default(), low_power: super::power::low_power(false, super::power::battery_percent()), devices: HashMap::new(), input_devices: Vec::new(), @@ -245,7 +256,14 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() scheduling_metrics: SchedulingMetrics::new(), }); - open_primary_device(event_loop, state, &primary_path)?; + state + .direct_backend + .as_mut() + .unwrap() + .topology + .devices + .insert(primary_path, Default::default()); + reconcile_outputs(state, false); sync_battery_timer(state); event_loop @@ -273,6 +291,10 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() let handle = state.loop_handle.clone(); if let Some(backend) = state.direct_backend.as_mut() { backend.active = false; + backend.topology.dirty = true; + if let Some(token) = backend.topology.retry_timer.take() { + handle.remove(token); + } if let Some(token) = backend.battery_timer.take() { handle.remove(token); } @@ -310,23 +332,11 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() if let Some(backend) = state.direct_backend.as_mut() { backend.active = true; - for device in backend.devices.values_mut() { - if let Err(error) = device.drm.activate(true) { - tracing::error!(%error, "failed to reactivate DRM device"); - continue; - } - - for output in device.outputs.values_mut() { - // A suspended page flip may never arrive. Clear the old - // pending frame before accepting a fresh submission. - if let Err(error) = output.surface.clear().and_then(|()| output.surface.reset_state()) { - tracing::error!(%error, "failed to reset DRM output after activation"); - } - } - } } - update_power_policy(state); + let seat = state.seat.clone(); + state.idle_notifier_state.notify_activity(&seat); + reconcile_outputs(state, true); sync_battery_timer(state); state.reset_animation_clock(); tracing::info!("direct session activated"); @@ -334,29 +344,18 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() render_all(state); } })?; - event_loop - .handle() - .insert_source(udev_backend, |event, _, state| match event { - UdevEvent::Added { device_id, path } => { - if let Some(node) = direct_node_for_device(state, device_id) { - rescan_device(state, node); - } else { - tracing::info!(?device_id, ?path, "additional DRM device discovered"); - } - } + event_loop.handle().insert_source(udev_backend, |event, _, state| { + if let UdevEvent::Removed { device_id } = event + && let Some(node) = direct_node_for_device(state, device_id) + && let Some(backend) = state.direct_backend.as_mut() + { + // A remove/re-add may reuse the same dev_t. Retire the old fd + // even if fresh enumeration already sees its replacement. + backend.topology.invalidated.insert(node); + } - UdevEvent::Changed { device_id } => { - if let Some(node) = direct_node_for_device(state, device_id) { - rescan_device(state, node); - } - } - - UdevEvent::Removed { device_id } => { - if let Some(node) = direct_node_for_device(state, device_id) { - remove_device(state, node); - } - } - })?; + reconcile_outputs(state, false); + })?; tracing::info!(seat = %seat_name, "initialized direct session input and device discovery"); Ok(()) @@ -455,27 +454,17 @@ fn update_power_policy(state: &mut Ferese) { } pub(crate) fn reload_outputs(state: &mut Ferese) { - if let Some(backend) = state.direct_backend.as_mut() { - let enabled = state - .output_profiles - .iter() - .any(|profile| profile.outputs.iter().any(|output| output.auto_refresh)); - backend.low_power = enabled && super::power::low_power(backend.low_power, super::power::battery_percent()); - } - let nodes = state - .direct_backend - .as_ref() - .map(|backend| backend.devices.keys().copied().collect::>()) - .unwrap_or_default(); - for node in nodes { - rescan_device(state, node); - } + reconcile_outputs(state, false); } pub(crate) fn validate_live_outputs(state: &Ferese, profiles: &[OutputProfile]) -> Result<(), String> { let Some(backend) = state.direct_backend.as_ref() else { return Ok(()); }; + if !backend.can_render() || backend.devices.is_empty() { + // Syntax/config validation still runs; live probing waits for ownership. + return Ok(()); + } let mut usable = 0; for device in backend.devices.values() { let mut scan = select_outputs(&device.drm, profiles, backend.low_power).map_err(|e| e.to_string())?; @@ -488,11 +477,7 @@ pub(crate) fn validate_live_outputs(state: &Ferese, profiles: &[OutputProfile]) Ok(()) } -fn open_primary_device( - event_loop: &mut EventLoop, - state: &mut Ferese, - path: &Path, -) -> Result<(), Box> { +fn open_device(state: &mut Ferese, path: &Path) -> Result> { let mut session = state .direct_backend .as_ref() @@ -502,7 +487,7 @@ fn open_primary_device( let node = DrmNode::from_path(path)?; let fd = session.open(path, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK)?; let fd = DrmDeviceFd::new(DeviceFd::from(fd)); - let (mut drm, notifier) = DrmDevice::new(fd.clone(), true)?; + let (drm, notifier) = DrmDevice::new(fd.clone(), true)?; let gbm = GbmDevice::new(fd)?; // SAFETY: GBM owns a valid DRM descriptor for the lifetime of the EGL display. let egl_display = unsafe { EGLDisplay::new(gbm.clone())? }; @@ -520,49 +505,11 @@ fn open_primary_device( .and_then(Result::ok) .unwrap_or(node); let feedback = DmabufFeedbackBuilder::new(render_node.dev_id(), dmabuf_formats).build()?; - state + let dmabuf_global = state .dmabuf_state .create_global_with_default_feedback::(&display_handle, &feedback); - let mut scan = select_outputs( - &drm, - &state.output_profiles, - state.direct_backend.as_ref().is_some_and(|backend| backend.low_power), - )?; - apply_lid_policy( - &mut scan, - state.direct_backend.as_ref().is_some_and(|backend| backend.lid_closed), - ); - if scan.selections.is_empty() { - return Err(io::Error::new( - io::ErrorKind::NotFound, - "no connected enabled DRM output with a usable CRTC", - ) - .into()); - } - - state - .direct_backend - .as_mut() - .expect("direct backend state remains initialized") - .connected_outputs = scan.connected_outputs; - let mut outputs = HashMap::new(); - for selection in scan.selections { - let crtc = selection.crtc; - let mode = selection.mode; - let output = create_direct_output(state, &mut drm, &gbm, &renderer, selection)?; - state - .direct_backend - .as_mut() - .expect("direct backend state remains initialized") - .presentation - .entry((node, crtc)) - .or_default() - .set_refresh(OutputMode::from(mode).refresh); - outputs.insert(crtc, output); - } - - event_loop - .handle() + let registration = state + .loop_handle .insert_source(notifier, move |event, metadata, state| match event { DrmEvent::VBlank(crtc) => { if let Some(metadata) = metadata { @@ -655,7 +602,10 @@ fn open_primary_device( state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence); tracing::trace!(?node, ?crtc, sequence = metadata.sequence, "page flip"); - let active = state.direct_backend.as_ref().is_some_and(|backend| backend.active); + let active = state + .direct_backend + .as_ref() + .is_some_and(|backend| backend.can_render()); if active && retired { schedule_frame_callbacks(state, node, crtc, monotonic_now()); let identity = state @@ -692,13 +642,14 @@ fn open_primary_device( gbm, renderer, render_node, - outputs, + outputs: HashMap::new(), + notifier: registration, + dmabuf_global, + connected_outputs: Vec::new(), }, ); - state.relayout(); - render_all(state); - tracing::info!(?node, ?path, "initialized DRM/GBM device outputs"); - Ok(()) + tracing::info!(?node, ?path, "initialized DRM/GBM device"); + Ok(node) } pub(crate) fn sleep_locked_outputs(state: &mut Ferese) { @@ -718,7 +669,7 @@ pub(crate) fn sleep_locked_outputs(state: &mut Ferese) { // Apply runtime requests without changing output globals or workspace ownership. fn apply_output_power(state: &mut Ferese) { - let Some(backend) = state.direct_backend.as_mut() else { + let Some(backend) = state.direct_backend.as_mut().filter(|backend| backend.can_render()) else { return; }; @@ -850,7 +801,11 @@ pub(crate) fn render_surface( } fn redraw(state: &mut Ferese, selected: Option<&[Output]>, source: &'static std::panic::Location<'static>) { - if state.direct_backend.as_ref().is_none_or(|backend| !backend.active) { + if state + .direct_backend + .as_ref() + .is_none_or(|backend| !backend.can_render()) + { return; } @@ -914,10 +869,7 @@ pub(crate) fn set_lid_closed(state: &mut Ferese, closed: bool) { } backend.lid_closed = closed; tracing::info!(closed, "laptop lid state changed"); - let nodes = backend.devices.keys().copied().collect::>(); - for node in nodes { - rescan_device(state, node); - } + reconcile_outputs(state, false); } fn initial_lid_closed() -> bool { @@ -1145,13 +1097,16 @@ fn direct_node_for_device(state: &Ferese, device_id: libc::dev_t) -> Option Result { + if state.direct_backend.as_ref().is_none_or(|backend| !backend.active) { + return Err(()); + } let Some(mut device) = state .direct_backend .as_mut() .and_then(|backend| backend.devices.remove(&node)) else { - return; + return Err(()); }; let mut scan = match select_outputs( @@ -1163,15 +1118,14 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) { Err(error) => { tracing::error!(?node, %error, "failed to scan DRM connectors"); restore_device(state, node, device); - return; + return Err(()); } }; let lid_closed = state.direct_backend.as_ref().is_some_and(|backend| backend.lid_closed); apply_lid_policy(&mut scan, lid_closed); - if let Some(backend) = state.direct_backend.as_mut() { - backend.connected_outputs = scan.connected_outputs; - } + device.connected_outputs = scan.connected_outputs; + let mut complete = true; let mut selections = scan .selections @@ -1253,6 +1207,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) { } Err(error) => { + complete = false; tracing::warn!(?crtc, %error, "display change failed; retaining current output") } } @@ -1310,27 +1265,27 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) { device.outputs.insert(crtc, output); } - Err(error) => tracing::error!(?node, ?crtc, %error, "failed to add DRM output"), + Err(error) => { + complete = false; + tracing::error!(?node, ?crtc, %error, "failed to add DRM output"); + } } } for crtc in deferred_removals { let keep_internal = device.outputs.get(&crtc).is_some_and(|output| output.internal) && lid_closed - && state.direct_backend.as_ref().is_some_and(|backend| { - device.outputs.get(&crtc).is_some_and(|output| { - backend - .connected_outputs - .iter() - .any(|info| info.connector == output.output.name()) - }) + && device.outputs.get(&crtc).is_some_and(|output| { + device + .connected_outputs + .iter() + .any(|info| info.connector == output.output.name()) }) && !device.outputs.values().any(|output| !output.internal); if keep_internal { tracing::warn!(?crtc, "keeping laptop panel enabled: external output activation failed"); if let Some(output) = device.outputs.get(&crtc) - && let Some(backend) = state.direct_backend.as_mut() - && let Some(info) = backend + && let Some(info) = device .connected_outputs .iter_mut() .find(|info| info.connector == output.output.name()) @@ -1352,9 +1307,9 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) { } } - if let Some(backend) = state.direct_backend.as_mut() { + { for output in device.outputs.values() { - if let Some(info) = backend + if let Some(info) = device .connected_outputs .iter_mut() .find(|info| info.connector == output.output.name()) @@ -1370,11 +1325,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) { } restore_device(state, node, device); - sync_battery_timer(state); - update_power_policy(state); - state.restore_output_focus(); - state.relayout(); - render_all(state); + Ok(complete) } fn remove_device(state: &mut Ferese, node: DrmNode) { @@ -1386,6 +1337,10 @@ fn remove_device(state: &mut Ferese, node: DrmNode) { return; }; + state.loop_handle.remove(device.notifier); + state + .dmabuf_state + .disable_global::(&state.display_handle, &device.dmabuf_global); let context = device.renderer.context_id().erased(); state.wallpaper.forget_context(&context); state.render.forget_context(&context); @@ -1398,12 +1353,6 @@ fn remove_device(state: &mut Ferese, node: DrmNode) { } } - if let Some(backend) = state.direct_backend.as_mut() { - backend.connected_outputs.clear(); - } - - sync_battery_timer(state); - arm_animation_timer(state); tracing::info!(?node, "removed DRM device"); } @@ -1441,7 +1390,10 @@ fn animation_fallbacks(state: &mut Ferese) -> Vec<(DrmNode, crtc::Handle, Instan for (node, crtc, identity, device_active) in outputs { let enabled = device_active && !state.session_lock.sleeping - && state.direct_backend.as_ref().is_some_and(|backend| backend.active); + && state + .direct_backend + .as_ref() + .is_some_and(|backend| backend.can_render()); let animating = enabled && state.output_has_animations(&identity); let backend = state.direct_backend.as_mut().unwrap(); let output = backend.devices.get_mut(&node).unwrap().outputs.get_mut(&crtc).unwrap(); @@ -1546,7 +1498,7 @@ fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handl let Some(output) = state .direct_backend .as_ref() - .filter(|backend| backend.active) + .filter(|backend| backend.can_render()) .and_then(|backend| backend.devices.get(&node)) .filter(|device| device.drm.is_active()) .and_then(|device| device.outputs.get(&crtc)) @@ -1609,6 +1561,7 @@ fn deliver_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle output.callback_timer = None; if !backend.active + || backend.topology.reconciling || !device.drm.is_active() || !output.power.can_render() || state.session_lock.sleeping diff --git a/compositor/src/backends/direct/scheduling.rs b/compositor/src/backends/direct/scheduling.rs index e9cfda0..45fbe42 100644 --- a/compositor/src/backends/direct/scheduling.rs +++ b/compositor/src/backends/direct/scheduling.rs @@ -31,7 +31,7 @@ pub(super) fn request_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handl let Some(output) = state .direct_backend .as_mut() - .filter(|backend| backend.active) + .filter(|backend| backend.can_render()) .and_then(|backend| backend.devices.get_mut(&node)) .filter(|device| device.drm.is_active()) .and_then(|device| device.outputs.get_mut(&crtc)) @@ -55,7 +55,7 @@ pub(super) fn arm_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) { let Some(output) = state .direct_backend .as_mut() - .filter(|backend| backend.active) + .filter(|backend| backend.can_render()) .and_then(|backend| backend.devices.get_mut(&node)) .filter(|device| device.drm.is_active()) .and_then(|device| device.outputs.get_mut(&crtc)) @@ -104,7 +104,7 @@ fn dispatch_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identit let Some(output) = state .direct_backend .as_mut() - .filter(|backend| backend.active) + .filter(|backend| backend.can_render()) .and_then(|backend| backend.devices.get_mut(&node)) .filter(|device| device.drm.is_active()) .and_then(|device| device.outputs.get_mut(&crtc)) diff --git a/compositor/src/backends/direct/topology.rs b/compositor/src/backends/direct/topology.rs new file mode 100644 index 0000000..6561e39 --- /dev/null +++ b/compositor/src/backends/direct/topology.rs @@ -0,0 +1,298 @@ +//! Desired device identities survive temporary loss of their DRM resources. +use std::path::PathBuf; + +use super::*; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum Availability { + Ready(DrmNode), + Degraded(DrmNode), + Missing, + Unavailable, +} + +pub(super) struct DeviceState { + pub availability: Availability, + failures: u32, + retry_at: Option, +} + +impl Default for DeviceState { + fn default() -> Self { + Self { + availability: Availability::Missing, + failures: 0, + retry_at: None, + } + } +} + +impl DeviceState { + pub fn failed(&mut self, now: Instant) { + self.availability = Availability::Unavailable; + self.failures = self.failures.saturating_add(1); + self.retry_at = Some(now + Duration::from_secs(1u64 << self.failures.saturating_sub(1).min(5))); + } + + pub fn ready(&mut self, node: DrmNode) { + self.availability = Availability::Ready(node); + self.failures = 0; + self.retry_at = None; + } + + pub fn missing(&mut self) { + *self = Self::default(); + } +} + +#[derive(Default)] +pub(super) struct Topology { + pub devices: HashMap, + pub invalidated: HashSet, + pub reconciling: bool, + pub dirty: bool, + pub retry_timer: Option, +} + +impl Topology { + pub fn request(&mut self, active: bool) -> bool { + self.dirty = true; + active && !self.reconciling + } + + pub fn retry_deadline(&self, active: bool) -> Option { + active + .then(|| self.devices.values().filter_map(|device| device.retry_at).min()) + .flatten() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn inactive_changes_coalesce_and_reconciliation_cannot_reenter() { + let mut topology = Topology::default(); + assert!(!topology.request(false)); + assert!(!topology.request(false)); + assert!(topology.dirty); + assert!(topology.request(true)); + topology.reconciling = true; + assert!(!topology.request(true)); + } + + #[test] + fn unavailable_devices_retry_independently_with_capped_backoff() { + let now = Instant::now(); + let mut topology = Topology::default(); + topology.devices.insert("/dev/dri/card0".into(), DeviceState::default()); + let healthy = DrmNode::from_dev_id(libc::makedev(226, 1)).unwrap(); + let mut second = DeviceState::default(); + second.ready(healthy); + topology.devices.insert("/dev/dri/card1".into(), second); + let device = topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap(); + device.failed(now); + assert_eq!(device.retry_at, Some(now + Duration::from_secs(1))); + for _ in 0..20 { + device.failed(now); + } + assert_eq!(device.retry_at, Some(now + Duration::from_secs(32))); + assert_eq!( + topology.devices[Path::new("/dev/dri/card1")].availability, + Availability::Ready(healthy) + ); + assert!(topology.retry_deadline(false).is_none()); + assert_eq!(topology.retry_deadline(true), Some(now + Duration::from_secs(32))); + topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap().missing(); + assert!(topology.retry_deadline(true).is_none()); + } +} + +/// The only entry point for applying the current output configuration to DRM. +pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) { + let Some(backend) = state.direct_backend.as_mut() else { + return; + }; + if !backend.topology.request(backend.active) { + return; + } + + backend.topology.reconciling = true; + backend.topology.dirty = false; + if let Some(token) = backend.topology.retry_timer.take() { + state.loop_handle.remove(token); + } + let paths = backend.topology.devices.keys().cloned().collect::>(); + let invalidated = std::mem::take(&mut backend.topology.invalidated); + let observed = all_gpus(backend.session.seat()); + let automatic = state + .output_profiles + .iter() + .any(|profile| profile.outputs.iter().any(|output| output.auto_refresh)); + backend.low_power = + automatic && super::super::power::low_power(backend.low_power, super::super::power::battery_percent()); + + for node in invalidated { + remove_device(state, node); + } + + for path in paths { + let existing = DrmNode::from_path(&path) + .ok() + .filter(|node| state.direct_backend.as_ref().unwrap().devices.contains_key(node)); + // If the path vanished, its retained identity still identifies the old fd. + let previous = match state.direct_backend.as_ref().unwrap().topology.devices[&path].availability { + Availability::Ready(node) | Availability::Degraded(node) => Some(node), + _ => existing, + }; + let found = observed.as_ref().map(|paths| paths.contains(&path)); + if !matches!(found, Ok(true)) { + if let Some(node) = previous { + remove_device(state, node); + } + let entry = state + .direct_backend + .as_mut() + .unwrap() + .topology + .devices + .get_mut(&path) + .unwrap(); + match found { + Ok(false) => entry.missing(), + Err(error) => { + tracing::warn!(%error, ?path, "could not enumerate DRM topology"); + entry.failed(Instant::now()); + } + Ok(true) => unreachable!(), + } + continue; + } + + if let Some(node) = previous.filter(|node| Some(*node) != existing) { + remove_device(state, node); + } + let node = match existing { + Some(node) => node, + None => match open_device(state, &path) { + Ok(node) => node, + Err(error) => { + tracing::warn!(%error, ?path, "DRM device unavailable; will retry"); + state + .direct_backend + .as_mut() + .unwrap() + .topology + .devices + .get_mut(&path) + .unwrap() + .failed(Instant::now()); + continue; + } + }, + }; + + let mut failed_outputs = Vec::new(); + let activation = if reactivate && existing.is_some() { + let device = state.direct_backend.as_mut().unwrap().devices.get_mut(&node).unwrap(); + device.drm.activate(true).map(|()| { + for (crtc, output) in &mut device.outputs { + if let Err(error) = output.surface.clear().and_then(|()| output.surface.reset_state()) { + tracing::warn!(%error, ?node, ?crtc, "output reset failed; recreating output"); + failed_outputs.push(*crtc); + } + } + }) + } else { + Ok(()) + }; + + if let Err(error) = activation { + tracing::warn!(%error, ?node, "DRM reactivation failed; retiring unavailable outputs"); + remove_device(state, node); + state + .direct_backend + .as_mut() + .unwrap() + .topology + .devices + .get_mut(&path) + .unwrap() + .failed(Instant::now()); + continue; + } + + for crtc in failed_outputs { + let mut output = state + .direct_backend + .as_mut() + .unwrap() + .devices + .get_mut(&node) + .unwrap() + .outputs + .remove(&crtc) + .unwrap(); + cancel_output_timers(&state.loop_handle, &mut output); + state.display_handle.disable_global::(output.global); + state.unregister_output(&output.output); + state + .direct_backend + .as_mut() + .unwrap() + .presentation + .remove(&(node, crtc)); + } + + let complete = rescan_device(state, node); + if complete.is_err() { + remove_device(state, node); + } + let entry = state + .direct_backend + .as_mut() + .unwrap() + .topology + .devices + .get_mut(&path) + .unwrap(); + if complete == Ok(true) { + entry.ready(node); + } else { + entry.failed(Instant::now()); + // A partial connector failure must not take down working outputs. + if complete == Ok(false) { + entry.availability = Availability::Degraded(node); + } + } + } + + let backend = state.direct_backend.as_mut().unwrap(); + backend.connected_outputs = backend + .devices + .values() + .flat_map(|device| device.connected_outputs.clone()) + .collect(); + backend.topology.reconciling = false; + let retry = backend.topology.retry_deadline(backend.active); + if let Some(deadline) = retry { + match state + .loop_handle + .insert_source(Timer::from_deadline(deadline), |_, _, state| { + if let Some(backend) = state.direct_backend.as_mut() { + backend.topology.retry_timer = None; + } + reconcile_outputs(state, false); + TimeoutAction::Drop + }) { + Ok(token) => state.direct_backend.as_mut().unwrap().topology.retry_timer = Some(token), + Err(error) => tracing::warn!(%error, "could not arm DRM recovery deadline"), + } + } + + sync_battery_timer(state); + state.restore_output_focus(); + state.relayout(); + render_all(state); +}