Watch
1
0
Fork
You've already forked smithay
0

drm_scanner: release CRTC reservations for vanished connectors

`SimpleCrtcMapper` keeps a `connector -> crtc` reservation map and, on each `map()` call, releases the reservation of any connector it is told about that is no longer connected:

```rust
for connector in connectors
    .clone()
    .filter(|conn| conn.state() != connector::State::Connected)
{
    self.crtcs.remove(&connector.handle());
}
```

This only releases connectors that are still present in the resource list and reported as disconnected. A connector whose handle disappears from the resource list entirely is never passed to `map()` again, so its reservation is never released and leaks for the lifetime of the mapper.

This happens routinely with DP-MST: the sink connectors behind an MST hub are dynamically created and destroyed, so unplugging a USB-C/Thunderbolt dock — or suspending and resuming while docked, where the connectors are torn down and recreated under fresh handles while the compositor isn't scanning — makes the old handles vanish rather than transition to `Disconnected`. Each such event leaks one reservation per vanished connector. Because a leaked reservation still counts as "taken", the reservations accumulate across cycles until every CRTC is reserved by a ghost handle. At that point `pick_next_available_for_connector` can no longer find a free CRTC for a genuinely connected connector, so `crtc_for_connector` returns `None` for it and the compositor silently never brings up that output. The only recovery is recreating the `DrmScanner`.

This was observed in niri as external monitors on a dock going missing after a number of suspend/resume cycles, getting progressively more likely over time, and only recoverable by restarting the compositor.

Fix it by releasing the reservation of every connector that is not currently connected, including connectors that are no longer present at all. At the start of `map()`, collect the set of currently connected connector handles and retain only reservations whose handle is in that set. The previously handled case (a connector present but reported as disconnected) is still covered, since such a connector is excluded from the connected set.
This commit is contained in:
Sven Over 2026-06-21 00:47:03 +01:00 • committed by Victoria Brekenfeld
commit e76f1af141
2 changed files with 19 additions and 6 deletions

View file

@ -202,6 +202,15 @@ default implementations, which result in skipping the new functionality. As such
- `lower_element()`: lowers an element to the bottom of the stack, respecting its z-index group.
- `relocate_element()`: moves an element to a new location in the space without changing the stacking order.
### Bugfixes
`SimpleCrtcMapper` (in `smithay-drm-extras`) now releases the CRTC reservation of any connector that
is no longer connected, including connectors that have disappeared from the resource list entirely
rather than being reported as disconnected. Previously such connectors (for example DP-MST sink
connectors when a dock is unplugged, or across suspend/resume) leaked their CRTC reservation
indefinitely, which could accumulate until a newly connected output could no longer be assigned a
CRTC.
## 0.7.0
### Breaking changes

View file

@ -1,4 +1,4 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use drm::control::{Device as ControlDevice, connector, crtc};
@ -86,12 +86,16 @@ impl super::CrtcMapper for SimpleCrtcMapper {
drm: &impl ControlDevice,
connectors: impl Iterator<Item = &'a connector::Info> + Clone,
) {
for connector in connectors
// Release CRTC reservations for every connector that is not currently connected. This
// covers connectors that are present but disconnected, as well as connectors that have
// vanished from the resource list entirely (e.g. DP-MST sink connectors when a dock is
// unplugged or the system is suspended).
let connected: HashSet<connector::Handle> = connectors
.clone()
.filter(|conn| conn.state() != connector::State::Connected)
{
self.crtcs.remove(&connector.handle());
}
.filter(|conn| conn.state() == connector::State::Connected)
.map(|conn| conn.handle())
.collect();
self.crtcs.retain(|handle, _| connected.contains(handle));
let mut needs_crtc: Vec<&connector::Info> = connectors
.filter(|conn| conn.state() == connector::State::Connected)