atagen

monolith

public
Star0
Watch0

Commit cac2b35598c9

tarball

atagen <boss@atagen.co>2026-10-19 07:51 UTC

3 files changed+112-6
drm: handle monitor unplug, replug and rescans properly

The `GlobalId` from `Output::create_global` was dropped, so clients kept phantom wl_outputs and saw duplicates after a replug. Layer surfaces and lock surfaces stayed on the dead `Output`. `DrmScanEvent::Changed` (the EDID race) was ignored, and a connector whose `connect` failed was never retried because the scanner had already recorded it. Nothing rescanned on VT return, and a udev change during a paused session modeset on a revoked fd.
Modified file: crates/monolith/src/drm.rs+93 additions−6 deletions
@@ -35,10 +35,11 @@ use smithay::reexports::drm::Device as _;
3535 use smithay::reexports::drm::control::{self, ModeTypeFlags, connector, crtc};
3636 use smithay::reexports::input::Libinput;
3737 use smithay::reexports::rustix::fs::OFlags;
38+use smithay::reexports::wayland_server::backend::GlobalId;
3839 use smithay::utils::{DeviceFd, Logical, Point, Transform};
3940 use smithay::wayland::drm_syncobj::{DrmSyncobjState, supports_syncobj_eventfd};
4041 use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
41-use std::collections::HashMap;
42+use std::collections::{HashMap, HashSet};
4243 use std::path::{Path, PathBuf};
4344 use std::time::Duration;
4445
@@ -47,6 +48,10 @@ use crate::frame::{CLEAR, FrameCtx};
4748 /// mouse does not override the refresh rate a game is pacing.
4849 const CURSOR_FRAME_NS: u64 = 16_666_667;
4950
51+/// How long a disabled wl_output stays bound before it is removed, so
52+/// clients that were mid-bind are not killed by the removal.
53+const GLOBAL_REMOVAL_DELAY: Duration = Duration::from_secs(5);
54+
5055 type Allocator = GbmAllocator<DrmDeviceFd>;
5156 type Exporter = GbmFramebufferExporter<DrmDeviceFd>;
5257 type Manager = DrmOutputManager<Allocator, Exporter, (), DrmDeviceFd>;
@@ -64,11 +69,15 @@ pub struct Drm {
6469 nvidia: bool,
6570 frame_flags: FrameFlags,
6671 surfaces: HashMap<crtc::Handle, Surface>,
72+ /// Connected connectors the scanner has already reported but that are
73+ /// not lit, retried on the next scan.
74+ failed: HashSet<connector::Handle>,
6775 pub devices: Devices,
6876 }
6977
7078 struct Surface {
7179 output: Output,
80+ global: GlobalId,
7281 id: OutputId,
7382 drm: Out,
7483 /// A frame is on its way to the screen and its vblank has not landed.
@@ -214,6 +223,7 @@ pub fn init(
214223 s.redraw = true;
215224 }
216225 }
226+ scan(state);
217227 state.dirty.redraw = true;
218228 }
219229 })?;
@@ -227,7 +237,7 @@ pub fn init(
227237 && state
228238 .backend
229239 .drm()
230- .is_some_and(|d| d.node.dev_id() == device_id)
240+ .is_some_and(|d| d.node.dev_id() == device_id && d.session.is_active())
231241 {
232242 scan(state);
233243 }
@@ -249,6 +259,7 @@ pub fn init(
249259 FrameFlags::DEFAULT
250260 },
251261 surfaces: HashMap::new(),
262+ failed: HashSet::new(),
252263 devices: Devices::default(),
253264 }));
254265 scan(state);
@@ -267,21 +278,66 @@ fn scan(state: &mut State) {
267278 return;
268279 }
269280 };
281+ let retry = std::mem::take(&mut drm.failed);
270282 for event in events {
271283 match event {
272284 DrmScanEvent::Connected {
273285 connector,
274286 crtc: Some(crtc),
275287 } => connect(state, connector, crtc),
288+ DrmScanEvent::Connected {
289+ connector,
290+ crtc: None,
291+ } => {
292+ tracing::warn!(handle = ?connector.handle(), "no free crtc for the connector");
293+ if let Some(drm) = state.backend.drm_mut() {
294+ drm.failed.insert(connector.handle());
295+ }
296+ }
276297 DrmScanEvent::Disconnected {
277298 crtc: Some(crtc), ..
278299 } => disconnect(state, crtc),
300+ DrmScanEvent::Changed {
301+ connector,
302+ crtc: Some(crtc),
303+ } => {
304+ disconnect(state, crtc);
305+ connect(state, connector, crtc);
306+ }
279307 _ => {}
280308 }
281309 }
310+ for handle in retry {
311+ retry_connector(state, handle);
312+ }
282313 state.apply_layout();
283314 }
284315
316+/// Lights a connector that failed on an earlier scan, which the scanner
317+/// will not report again until it changes.
318+fn retry_connector(state: &mut State, handle: connector::Handle) {
319+ let Some(drm) = state.backend.drm_mut() else {
320+ return;
321+ };
322+ if drm.failed.contains(&handle) {
323+ return;
324+ }
325+ let Some(info) = drm.scanner.connectors().get(&handle) else {
326+ return;
327+ };
328+ if info.state() != connector::State::Connected {
329+ return;
330+ }
331+ let info = info.clone();
332+ let Some(crtc) = drm.scanner.crtc_for_connector(&handle) else {
333+ drm.failed.insert(handle);
334+ return;
335+ };
336+ if !drm.surfaces.contains_key(&crtc) {
337+ connect(state, info, crtc);
338+ }
339+}
340+
285341 fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
286342 let name = format!(
287343 "{}-{}",
@@ -312,6 +368,9 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
312368 .copied()
313369 }) else {
314370 tracing::warn!(name, "connector has no modes");
371+ if let Some(drm) = state.backend.drm_mut() {
372+ drm.failed.insert(connector.handle());
373+ }
315374 return;
316375 };
317376 let transform = transform(config.transform);
@@ -352,7 +411,6 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
352411 Some(position),
353412 );
354413 output.set_preferred(wl_mode);
355- output.create_global::<State>(&core.display);
356414 let mut planes = drm.manager.device().planes(&crtc).ok();
357415 if drm.nvidia
358416 && let Some(p) = planes.as_mut()
@@ -372,6 +430,7 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
372430 Ok(out) => out,
373431 Err(e) => {
374432 tracing::warn!(name, "could not light up the output: {e}");
433+ drm.failed.insert(connector.handle());
375434 return;
376435 }
377436 };
@@ -379,6 +438,7 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
379438 out.with_compositor(|c| c.vrr_supported(connector.handle())),
380439 Ok(VrrSupport::Supported)
381440 );
441+ let global = output.create_global::<State>(&core.display);
382442 space.map_output(&output, position);
383443 let (w, h) = mode.size();
384444 let size = transform.transform_size(
@@ -406,6 +466,7 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
406466 crtc,
407467 Surface {
408468 output,
469+ global,
409470 id,
410471 drm: out,
411472 queued: false,
@@ -540,8 +601,27 @@ fn disconnect(state: &mut State, crtc: crtc::Handle) {
540601 drm.handle.remove(token);
541602 }
542603 tracing::info!(name = surface.output.name(), "output disconnected");
604+ state
605+ .core
606+ .display
607+ .disable_global::<State>(surface.global.clone());
608+ let global = surface.global.clone();
609+ let removal = Timer::from_duration(GLOBAL_REMOVAL_DELAY);
610+ let armed = drm.handle.insert_source(removal, move |_, _, state| {
611+ state.core.display.remove_global::<State>(global.clone());
612+ TimeoutAction::Drop
613+ });
614+ if let Err(e) = armed {
615+ tracing::warn!("could not defer removing the wl_output: {e}");
616+ state
617+ .core
618+ .display
619+ .remove_global::<State>(surface.global.clone());
620+ }
543621 state.wm.space.unmap_output(&surface.output);
544622 state.wm.layout.remove_output(surface.id);
623+ state.close_layers(&surface.output);
624+ state.services.lock.forget(&surface.output);
545625 }
546626
547627 impl Drm {
@@ -769,9 +849,16 @@ fn draw(state: &mut State, crtc: crtc::Handle, target: u64) {
769849 let wait = (surface.refresh_ns.max(1_000_000) << backoff).min(1_000_000_000);
770850 let wait = Duration::from_nanos(wait);
771851 let preemptible = surface.failures == 0;
772- arm(&drm.handle, surface, crtc, wait, preemptible, move |state| {
773- schedule(state, crtc);
774- });
852+ arm(
853+ &drm.handle,
854+ surface,
855+ crtc,
856+ wait,
857+ preemptible,
858+ move |state| {
859+ schedule(state, crtc);
860+ },
861+ );
775862 }
776863
777864 /// Renders every output that has something new and no frame in flight.
Modified file: crates/monolith/src/layers.rs+13 additions
@@ -114,6 +114,19 @@ impl State {
114114 true
115115 }
116116
117+ /// Closes and unmaps every layer surface on an output that was unplugged.
118+ pub fn close_layers(&mut self, output: &Output) {
119+ let mut map = layer_map_for_output(output);
120+ let layers: Vec<LayerSurface> = map.layers().cloned().collect();
121+ for layer in layers {
122+ layer.layer_surface().send_close();
123+ map.unmap_layer(&layer);
124+ if self.wm.layer_focus.as_ref() == Some(layer.wl_surface()) {
125+ self.wm.layer_focus = None;
126+ }
127+ }
128+ }
129+
117130 /// Keeps tiled windows out of the space bars reserve.
118131 pub fn reserve_layers(&mut self) {
119132 let outputs: Vec<OutputId> = self.wm.layout.outputs().map(|(o, _)| o).collect();
Modified file: crates/monolith/src/lock.rs+6 additions
@@ -39,6 +39,12 @@ impl Lock {
3939 .map(|(_, s)| s)
4040 }
4141
42+ /// Drops what belonged to an output that was unplugged.
43+ pub fn forget(&mut self, output: &Output) {
44+ self.surfaces.retain(|(o, _)| o != output);
45+ self.blanked.remove(&output.name());
46+ }
47+
4248 /// Records a frame shown on `output`, which counts towards confirming a
4349 /// pending lock.
4450 pub fn shown(&mut self, output: &Output) {