Skip to content

fix(drm-extras): re-emit Connected event when modes change on already-connected connector#1923

Merged
PolyMeilex merged 3 commits intoSmithay:masterfrom
coleleavitt:fix/connector-scanner-edid-race-upstream
Feb 15, 2026
Merged

fix(drm-extras): re-emit Connected event when modes change on already-connected connector#1923
PolyMeilex merged 3 commits intoSmithay:masterfrom
coleleavitt:fix/connector-scanner-edid-race-upstream

Conversation

@coleleavitt
Copy link
Contributor

@coleleavitt coleleavitt commented Feb 7, 2026

Fixes #1922

Problem

ConnectorScanner treats (Connected, Connected) as a no-op, only tracking state transitions. When a connector's mode list changes while remaining Connected (e.g., EDID becomes available after initial probe returned empty modes), no event is emitted.

This causes USB-C dock monitors with DP MST/alt-mode to get stuck in a permanent "connected but never activated" state when the kernel reports the connector as Connected before EDID data is ready.

Fix

Add a new Changed event variant to both ConnectorScanEvent and DrmScanEvent. When a connector stays Connected but its mode list differs from the previous scan, emit Changed instead of silently ignoring it:

// ConnectorScanEvent
pub enum ConnectorScanEvent {
    Connected(connector::Info),
    Disconnected(connector::Info),
    Changed(connector::Info),  // NEW: mode list changed while staying connected
}

// DrmScanEvent
pub enum DrmScanEvent {
    Connected { connector, crtc },
    Disconnected { connector, crtc },
    Changed { connector, crtc },  // NEW
}

The scanner logic:

(State::Connected, State::Connected) => {
    if old.modes() != conn.modes() {
        changed.push(conn);
    }
}

This is better than re-emitting Connected (original approach) because:

Context

Discovered while debugging intermittent monitor activation failures on a ThinkPad P16 Gen 3 with USB-C Dock Gen 2 (DP alt-mode + MST). Verified against kernel DRM source (drm_probe_helper.c lines 598-653) — drm_helper_probe_detect() sets connector status before EDID/mode enumeration, so userspace can observe Connected with empty modes.

Related:

…-connected connector

ConnectorScanner previously treated (Connected, Connected) as a no-op,
only tracking state transitions between Connected/Disconnected/Unknown.
This meant that a connector whose mode list changed while remaining
Connected would not emit any event.

This is problematic for USB-C docks with DP MST/alt-mode where the
kernel may report a connector as Connected before EDID data is
available. The initial scan sees an empty mode list and the compositor
skips activation. When a later rescan finds modes populated, no
Connected event is emitted because the state is still Connected.

Now compare the old and new mode lists on (Connected, Connected)
transitions and re-emit a Connected event when they differ. This
allows compositors to activate outputs that initially had no modes
without requiring custom retry logic.
@PolyMeilex
Copy link
Member

PolyMeilex commented Feb 7, 2026

Hi!

  1. Have you debugged this to check if what LLM states about lazy mode loading aligns with reality?
  2. If so, wouldn't it be better to not emit the first connected event (the one that is not yet ready), rather than send duplicated connected event?
  3. Or if it turns out that the modes can change at any time and not only on startup, perhaps we need a new Changed/Updated event?

@coleleavitt
Copy link
Contributor Author

Hi! Thanks for the thoughtful review.

1. Verification:

The initial investigation was LLM-assisted, but I've verified the mechanism against kernel DRM source and have a hardware reproducer (ThinkPad P16 Gen 3 + USB-C Dock Gen 2, DP alt-mode + MST hub).

The relevant kernel codepath is drm_helper_probe_single_connector_modes() in drm_probe_helper.c:

  • Lines 598-606: drm_helper_probe_detect() sets connector->status to connected before EDID/mode enumeration
  • Line 649: drm_helper_probe_get_modes() reads EDID after status — can return 0 for DP MST when link training is still in-progress
  • Lines 651-653: Kernel falls back to drm_add_modes_noedid(1024x768) when count == 0 && connected

So what smithay sees via DRM_IOCTL_MODE_GETCONNECTOR: connector reports Connected with either zero modes or just the 1024x768 fallback. A subsequent probe (triggered by kernel hotplug event as EDID arrives) returns the real mode list. With the current (Connected, Connected) => {} arm, that second probe is silently ignored.

On my hardware, this manifests as intermittent monitor activation failure — niri's pick_mode() returns None on the first probe, logs warn!("error connecting connector: no mode"), and never retries because smithay never re-emits the event.

2. Suppressing the first empty-modes Connected:

I considered this, but modes().is_empty() on a Connected connector isn't a reliable "not ready yet" signal:

  • The kernel already provides 1024x768 fallbacks for no-EDID connectors (lines 651-653 above), so modes() may not actually be empty — just wrong
  • Suppressing would break legitimate no-EDID connectors (VGA without EDID, some KVM switches)
  • It would also break the switchbox case from Smithay doesn't play nice when EDID drops due to video switchbox changing inputs #1859, where suppressing the initial Connected means the output never activates at all

3. New Changed event — I think this is the right approach.

The current PR overloads Connected for something semantically different. A new variant like ConnectorScanEvent::Changed(connector::Info) would:

Happy to revise the PR with a Changed variant. Should it fire on any connector::Info change (property changes, encoder reassignment, etc.), or specifically just mode-list changes?

…ent variant

Per maintainer review, introduce ConnectorScanEvent::Changed and
DrmScanEvent::Changed to semantically distinguish mode-list updates on
already-connected connectors from initial connection events.

This gives compositors explicit control over how to handle EDID races
and mode changes (e.g. video switchbox input change per Smithay#1859) without
overloading Connected semantics.

Changes:
- Add Changed(connector::Info) to ConnectorScanEvent
- Add Changed { connector, crtc } to DrmScanEvent
- Add changed field to ConnectorScanResult and DrmScanResult
- Simplify IntoIterator impls to Vec-based approach for 3-way chain
- Update anvil and simple.rs examples with Changed arm
- Fix comment in connector_scanner.rs: 'Re-emit Connected' -> 'Emit Changed'
  to match the actual behavior after the Changed variant was introduced.
- Add explanatory comment on the Changed arm in anvil/src/udev.rs per
  reviewer request, since anvil serves as reference for downstream compositors.
@coleleavitt coleleavitt force-pushed the fix/connector-scanner-edid-race-upstream branch from f08f79e to a9e0555 Compare February 8, 2026 18:03
@coleleavitt
Copy link
Contributor Author

@PolyMeilex

Copy link
Member

@PolyMeilex PolyMeilex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems good now

@PolyMeilex PolyMeilex merged commit 62fab5a into Smithay:master Feb 15, 2026
13 checks passed
@coleleavitt coleleavitt deleted the fix/connector-scanner-edid-race-upstream branch February 22, 2026 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ConnectorScanner does not re-emit Connected event when modes change on already-connected connector

2 participants