diff --git a/Cargo.lock b/Cargo.lock index fde5b21ffc..3ba43c9bd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -567,14 +567,14 @@ dependencies = [ [[package]] name = "calloop" -version = "0.14.4" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" dependencies = [ "async-task", "bitflags 2.10.0", "futures-io", - "nix 0.31.1", + "nix", "polling", "rustix 1.1.3", "slab", @@ -599,7 +599,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" dependencies = [ - "calloop 0.14.4", + "calloop 0.14.3", "rustix 1.1.3", "wayland-backend", "wayland-client", @@ -940,7 +940,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1068,7 +1068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1416,7 +1416,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1735,7 +1735,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1985,7 +1985,7 @@ dependencies = [ "cookie-factory", "libc", "libspa-sys", - "nix 0.30.1", + "nix", "nom 8.0.0", "system-deps", ] @@ -2171,7 +2171,7 @@ dependencies = [ "atomic", "bitflags 2.10.0", "bytemuck", - "calloop 0.14.4", + "calloop 0.14.3", "calloop-wayland-source 0.4.1", "clap", "clap_complete", @@ -2270,18 +2270,6 @@ dependencies = [ "libc", ] -[[package]] -name = "nix" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "nom" version = "7.1.3" @@ -2772,7 +2760,7 @@ dependencies = [ "libc", "libspa", "libspa-sys", - "nix 0.30.1", + "nix", "once_cell", "pipewire-sys", "thiserror 2.0.17", @@ -3177,7 +3165,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3402,13 +3390,13 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "smithay" version = "0.7.0" -source = "git+https://github.com/Smithay/smithay.git#ae14fa12f6ee3fc4dd4c766ffb21848f991a6a18" +source = "git+https://github.com/Smithay/smithay.git?rev=9219bf8a9#9219bf8a9b79f528a834c1083630e590a9b1a04e" dependencies = [ "aliasable", "appendlist", "atomic_float", "bitflags 2.10.0", - "calloop 0.14.4", + "calloop 0.14.3", "cc", "cgmath", "cursor-icon", @@ -3477,7 +3465,7 @@ dependencies = [ [[package]] name = "smithay-drm-extras" version = "0.1.0" -source = "git+https://github.com/Smithay/smithay.git#ae14fa12f6ee3fc4dd4c766ffb21848f991a6a18" +source = "git+https://github.com/Smithay/smithay.git?rev=9219bf8a9#9219bf8a9b79f528a834c1083630e590a9b1a04e" dependencies = [ "drm", "libdisplay-info", @@ -3583,7 +3571,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4182,7 +4170,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b19b364633..dcc1366b08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,12 +31,14 @@ tracy-client = { version = "0.18.4", default-features = false } [workspace.dependencies.smithay] # version = "0.4.1" git = "https://github.com/Smithay/smithay.git" +rev = "9219bf8a9" # path = "../smithay" default-features = false [workspace.dependencies.smithay-drm-extras] # version = "0.1.0" git = "https://github.com/Smithay/smithay.git" +rev = "9219bf8a9" # path = "../smithay/smithay-drm-extras" [package] diff --git a/src/backend/tty.rs b/src/backend/tty.rs index 259dde3091..1a061a9b2d 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -77,6 +77,9 @@ const SUPPORTED_COLOR_FORMATS: [Fourcc; 4] = [ Fourcc::Abgr8888, ]; +const MAX_RESCAN_RETRIES: u8 = 3; +const RESCAN_DELAY: Duration = Duration::from_secs(2); + pub struct Tty { config: Rc>, session: LibSeatSession, @@ -147,6 +150,12 @@ pub struct OutputDevice { pub drm_lease_state: Option, non_desktop_connectors: HashSet<(connector::Handle, crtc::Handle)>, active_leases: Vec, + + // Delayed re-scan for connectors that were detected as connected but had no + // modes yet (EDID not read). Bounded to MAX_RESCAN_RETRIES to prevent + // infinite rescheduling. + rescan_timer_token: Option, + rescan_retry_count: u8, } // A connected, but not necessarily enabled, crtc. @@ -905,6 +914,8 @@ impl Tty { drm_lease_state, active_leases: Vec::new(), non_desktop_connectors: HashSet::new(), + rescan_timer_token: None, + rescan_retry_count: 0, }; assert!(self.devices.insert(node, device).is_none()); @@ -956,6 +967,7 @@ impl Tty { let mut added = Vec::new(); let mut removed = Vec::new(); + let mut changed = Vec::new(); for event in scan_result { match event { DrmScanEvent::Connected { @@ -969,7 +981,6 @@ impl Tty { &name.connector, name.format_make_model_serial(), ); - // Assign an id to this crtc. let id = OutputId::next(); added.push((crtc, CrtcInfo { id, name })); @@ -979,6 +990,17 @@ impl Tty { } => { removed.push(crtc); } + // The connector's mode list changed while it stayed connected + // (e.g. EDID arrived after the initial probe returned + // empty/fallback modes, or a video switchbox changed inputs). + DrmScanEvent::Changed { + connector, + crtc: Some(crtc), + } => { + let connector_name = format_connector_name(&connector); + debug!("connector modes changed: {connector_name}, crtc {crtc:?}"); + changed.push((connector, crtc)); + } _ => (), } } @@ -1029,6 +1051,53 @@ impl Tty { device.known_crtcs.insert(crtc, info); } + // Handle connectors whose mode list changed while staying connected. + // If no surface exists yet (EDID race: initial connect had no modes), register + // the crtc in known_crtcs so on_output_config_changed() can connect it. + // If a surface already exists, on_output_config_changed() will re-evaluate + // the mode selection. + for (connector, crtc) in changed { + let device = self.devices.get_mut(&node).unwrap(); + if !device.surfaces.contains_key(&crtc) + && !device + .non_desktop_connectors + .contains(&(connector.handle(), crtc)) + && !device.known_crtcs.contains_key(&crtc) + { + let connector_name = format_connector_name(&connector); + let mut name = make_output_name(&device.drm, connector.handle(), connector_name); + debug!( + "registering changed connector: {} \"{}\"", + &name.connector, + name.format_make_model_serial(), + ); + + // Same dedup guard as the `added` loop: Layout does not support + // duplicate make/model/serial and will panic. + let formatted = name.format_make_model_serial_or_connector(); + for info in self.devices.values().flat_map(|d| d.known_crtcs.values()) { + if info.name.matches(&formatted) { + let connector = mem::take(&mut name.connector); + warn!( + "changed connector {connector} duplicates make/model/serial \ + of existing connector {}, unnaming", + info.name.connector, + ); + name = OutputName { + connector, + make: None, + model: None, + serial: None, + }; + break; + } + } + + let device = self.devices.get_mut(&node).unwrap(); + let id = OutputId::next(); + device.known_crtcs.insert(crtc, CrtcInfo { id, name }); + } + } // If the device was just added or resumed, we need to cleanup any disconnected connectors // and planes. if cleanup { @@ -1075,6 +1144,66 @@ impl Tty { // It will also call refresh_ipc_outputs(), which will catch the disconnected connectors // above. self.on_output_config_changed(niri); + + self.schedule_rescan_if_needed(node, niri); + } + + fn schedule_rescan_if_needed(&mut self, node: DrmNode, niri: &mut Niri) { + let Some(device) = self.devices.get_mut(&node) else { + return; + }; + + if device.rescan_retry_count >= MAX_RESCAN_RETRIES { + debug!( + "reached max rescan retries ({MAX_RESCAN_RETRIES}) for {node}, \ + not scheduling another" + ); + return; + } + + let has_unactivated = device.drm_scanner.crtcs().any(|(conn, crtc)| { + conn.state() == connector::State::Connected + && !device.surfaces.contains_key(&crtc) + && !device + .non_desktop_connectors + .contains(&(conn.handle(), crtc)) + }); + + if !has_unactivated { + device.rescan_retry_count = 0; + if let Some(token) = device.rescan_timer_token.take() { + niri.event_loop.remove(token); + } + return; + } + + if let Some(token) = device.rescan_timer_token.take() { + niri.event_loop.remove(token); + } + + device.rescan_retry_count = device.rescan_retry_count.saturating_add(1); + let attempt = device.rescan_retry_count; + let device_id = node.dev_id(); + + debug!( + "connected connectors not yet activated on {node}; \ + scheduling rescan attempt {attempt}/{MAX_RESCAN_RETRIES} in {RESCAN_DELAY:?}" + ); + + let token = niri + .event_loop + .insert_source(Timer::from_duration(RESCAN_DELAY), move |_, _, state| { + debug!("rescan timer fired for device {device_id} (attempt {attempt})"); + state + .backend + .tty() + .device_changed(device_id, &mut state.niri, false); + TimeoutAction::Drop + }) + .unwrap(); + + let device = self.devices.get_mut(&node).unwrap(); + device.rescan_timer_token = Some(token); } fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) { @@ -1095,6 +1224,10 @@ impl Tty { return; }; + if let Some(token) = device.rescan_timer_token.take() { + niri.event_loop.remove(token); + } + let crtcs: Vec<_> = device .drm_scanner .crtcs() @@ -1501,6 +1634,8 @@ impl Tty { niri.add_output(output.clone(), Some(refresh_interval(mode)), vrr_enabled); + device.rescan_retry_count = 0; + if niri.monitors_active { // Redraw the new monitor. niri.event_loop.insert_idle(move |state| {