Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 17 additions & 29 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
137 changes: 136 additions & 1 deletion src/backend/tty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RefCell<Config>>,
session: LibSeatSession,
Expand Down Expand Up @@ -147,6 +150,12 @@ pub struct OutputDevice {
pub drm_lease_state: Option<DrmLeaseState>,
non_desktop_connectors: HashSet<(connector::Handle, crtc::Handle)>,
active_leases: Vec<DrmLease>,

// 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<RegistrationToken>,
rescan_retry_count: u8,
}

// A connected, but not necessarily enabled, crtc.
Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -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 {
Expand All @@ -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 }));
Expand All @@ -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));
}
_ => (),
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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()
Expand Down Expand Up @@ -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| {
Expand Down