Recover known GPUs across DRM node renumbering
This commit is contained in:
1 parent
428267a714
commit
bbb1eac189
3 files changed
+130
-46
No files matched your search
@@ -266,7 +266,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.topology
|
.topology
|
||||||
.devices
|
.devices
|
||||||
.insert(primary_path, Default::default());
|
.insert(primary_path.clone(), topology::DeviceState::known(&primary_path));
|
||||||
lid::request_refresh(state, false);
|
lid::request_refresh(state, false);
|
||||||
|
|
||||||
event_loop
|
event_loop
|
||||||
@@ -508,9 +508,6 @@ fn open_device(state: &mut Ferese, path: &Path) -> Result<DrmNode, Box<dyn Error
|
|||||||
.and_then(Result::ok)
|
.and_then(Result::ok)
|
||||||
.unwrap_or(node);
|
.unwrap_or(node);
|
||||||
let feedback = DmabufFeedbackBuilder::new(render_node.dev_id(), dmabuf_formats).build()?;
|
let feedback = DmabufFeedbackBuilder::new(render_node.dev_id(), dmabuf_formats).build()?;
|
||||||
let dmabuf_global = state
|
|
||||||
.dmabuf_state
|
|
||||||
.create_global_with_default_feedback::<Ferese>(&display_handle, &feedback);
|
|
||||||
let registration = state
|
let registration = state
|
||||||
.loop_handle
|
.loop_handle
|
||||||
.insert_source(notifier, move |event, metadata, state| match event {
|
.insert_source(notifier, move |event, metadata, state| match event {
|
||||||
@@ -633,6 +630,9 @@ fn open_device(state: &mut Ferese, path: &Path) -> Result<DrmNode, Box<dyn Error
|
|||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let dmabuf_global = state
|
||||||
|
.dmabuf_state
|
||||||
|
.create_global_with_default_feedback::<Ferese>(&display_handle, &feedback);
|
||||||
state
|
state
|
||||||
.direct_backend
|
.direct_backend
|
||||||
.as_mut()
|
.as_mut()
|
||||||
@@ -1311,6 +1311,12 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) -> Result<bool, ()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
// Connector detection is not proof that its requested mode was applied.
|
||||||
|
for info in &mut device.connected_outputs {
|
||||||
|
info.enabled = false;
|
||||||
|
info.current_mode = None;
|
||||||
|
}
|
||||||
|
|
||||||
for output in device.outputs.values() {
|
for output in device.outputs.values() {
|
||||||
if let Some(info) = device
|
if let Some(info) = device
|
||||||
.connected_outputs
|
.connected_outputs
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub(super) enum Availability {
|
|||||||
|
|
||||||
pub(super) struct DeviceState {
|
pub(super) struct DeviceState {
|
||||||
pub availability: Availability,
|
pub availability: Availability,
|
||||||
|
identity: Option<PathBuf>,
|
||||||
failures: u32,
|
failures: u32,
|
||||||
retry_at: Option<Instant>,
|
retry_at: Option<Instant>,
|
||||||
}
|
}
|
||||||
@@ -21,13 +22,36 @@ impl Default for DeviceState {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
availability: Availability::Missing,
|
availability: Availability::Missing,
|
||||||
|
identity: None,
|
||||||
failures: 0,
|
failures: 0,
|
||||||
retry_at: None,
|
retry_at: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn device_identity(path: &Path) -> Option<PathBuf> {
|
||||||
|
// The parent hardware identity survives cardN renumbering after unplug.
|
||||||
|
std::fs::canonicalize(Path::new("/sys/class/drm").join(path.file_name()?).join("device")).ok()
|
||||||
|
}
|
||||||
|
|
||||||
impl DeviceState {
|
impl DeviceState {
|
||||||
|
pub fn known(path: &Path) -> Self {
|
||||||
|
Self {
|
||||||
|
identity: device_identity(path),
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_path<'a>(&self, original: &Path, observed: &'a [(PathBuf, Option<PathBuf>)]) -> Option<&'a Path> {
|
||||||
|
observed
|
||||||
|
.iter()
|
||||||
|
.find(|(path, identity)| match &self.identity {
|
||||||
|
Some(expected) => identity.as_ref() == Some(expected),
|
||||||
|
None => path == original,
|
||||||
|
})
|
||||||
|
.map(|(path, _)| path.as_path())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn failed(&mut self, now: Instant) {
|
pub fn failed(&mut self, now: Instant) {
|
||||||
self.availability = Availability::Unavailable;
|
self.availability = Availability::Unavailable;
|
||||||
self.failures = self.failures.saturating_add(1);
|
self.failures = self.failures.saturating_add(1);
|
||||||
@@ -41,7 +65,9 @@ impl DeviceState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn missing(&mut self) {
|
pub fn missing(&mut self) {
|
||||||
*self = Self::default();
|
self.availability = Availability::Missing;
|
||||||
|
self.failures = 0;
|
||||||
|
self.retry_at = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +108,31 @@ mod tests {
|
|||||||
assert!(!topology.request(true));
|
assert!(!topology.request(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replug_matches_hardware_even_when_card_numbers_change() {
|
||||||
|
let original = Path::new("/dev/dri/card0");
|
||||||
|
let identity = PathBuf::from("/sys/devices/pci0000:00/0000:00:02.0");
|
||||||
|
let mut slot = DeviceState {
|
||||||
|
identity: Some(identity.clone()),
|
||||||
|
..DeviceState::default()
|
||||||
|
};
|
||||||
|
slot.failed(Instant::now());
|
||||||
|
slot.missing();
|
||||||
|
let observed = vec![
|
||||||
|
(
|
||||||
|
PathBuf::from("/dev/dri/card0"),
|
||||||
|
Some(PathBuf::from("/sys/devices/other-gpu")),
|
||||||
|
),
|
||||||
|
(PathBuf::from("/dev/dri/card2"), Some(identity)),
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
slot.current_path(original, &observed),
|
||||||
|
Some(Path::new("/dev/dri/card2"))
|
||||||
|
);
|
||||||
|
assert!(slot.current_path(original, &observed[..1]).is_none());
|
||||||
|
assert!(slot.current_path(original, &[]).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unavailable_devices_retry_independently_with_capped_backoff() {
|
fn unavailable_devices_retry_independently_with_capped_backoff() {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
@@ -109,6 +160,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn slot_mut<'a>(state: &'a mut Ferese, path: &Path) -> &'a mut DeviceState {
|
||||||
|
state
|
||||||
|
.direct_backend
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.topology
|
||||||
|
.devices
|
||||||
|
.get_mut(path)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
/// The only entry point for applying the current output configuration to DRM.
|
/// The only entry point for applying the current output configuration to DRM.
|
||||||
pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
||||||
let Some(backend) = state.direct_backend.as_mut() else {
|
let Some(backend) = state.direct_backend.as_mut() else {
|
||||||
@@ -121,14 +183,25 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
|||||||
backend.topology.reconciling = true;
|
backend.topology.reconciling = true;
|
||||||
backend.topology.dirty = false;
|
backend.topology.dirty = false;
|
||||||
if reactivate {
|
if reactivate {
|
||||||
backend.presentation.values_mut().for_each(PresentationClock::reset_timing);
|
backend
|
||||||
|
.presentation
|
||||||
|
.values_mut()
|
||||||
|
.for_each(PresentationClock::reset_timing);
|
||||||
}
|
}
|
||||||
if let Some(token) = backend.topology.retry_timer.take() {
|
if let Some(token) = backend.topology.retry_timer.take() {
|
||||||
state.loop_handle.remove(token);
|
state.loop_handle.remove(token);
|
||||||
}
|
}
|
||||||
let paths = backend.topology.devices.keys().cloned().collect::<Vec<_>>();
|
let paths = backend.topology.devices.keys().cloned().collect::<Vec<_>>();
|
||||||
let invalidated = std::mem::take(&mut backend.topology.invalidated);
|
let invalidated = std::mem::take(&mut backend.topology.invalidated);
|
||||||
let observed = all_gpus(backend.session.seat());
|
let observed = all_gpus(backend.session.seat()).map(|paths| {
|
||||||
|
paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| {
|
||||||
|
let identity = device_identity(&path);
|
||||||
|
(path, identity)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
let automatic = state
|
let automatic = state
|
||||||
.output_profiles
|
.output_profiles
|
||||||
.iter()
|
.iter()
|
||||||
@@ -141,27 +214,24 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for path in paths {
|
for path in paths {
|
||||||
let existing = DrmNode::from_path(&path)
|
let slot = &state.direct_backend.as_ref().unwrap().topology.devices[&path];
|
||||||
|
let current_path = observed
|
||||||
|
.as_ref()
|
||||||
.ok()
|
.ok()
|
||||||
|
.and_then(|devices| slot.current_path(&path, devices));
|
||||||
|
let existing = current_path
|
||||||
|
.and_then(|path| DrmNode::from_path(path).ok())
|
||||||
.filter(|node| state.direct_backend.as_ref().unwrap().devices.contains_key(node));
|
.filter(|node| state.direct_backend.as_ref().unwrap().devices.contains_key(node));
|
||||||
// If the path vanished, its retained identity still identifies the old fd.
|
let previous = match slot.availability {
|
||||||
let previous = match state.direct_backend.as_ref().unwrap().topology.devices[&path].availability {
|
|
||||||
Availability::Ready(node) | Availability::Degraded(node) => Some(node),
|
Availability::Ready(node) | Availability::Degraded(node) => Some(node),
|
||||||
_ => existing,
|
_ => existing,
|
||||||
};
|
};
|
||||||
let found = observed.as_ref().map(|paths| paths.contains(&path));
|
let found = observed.as_ref().map(|_| current_path.is_some());
|
||||||
if !matches!(found, Ok(true)) {
|
if !matches!(found, Ok(true)) {
|
||||||
if let Some(node) = previous {
|
if let Some(node) = previous {
|
||||||
remove_device(state, node);
|
remove_device(state, node);
|
||||||
}
|
}
|
||||||
let entry = state
|
let entry = slot_mut(state, &path);
|
||||||
.direct_backend
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.topology
|
|
||||||
.devices
|
|
||||||
.get_mut(&path)
|
|
||||||
.unwrap();
|
|
||||||
match found {
|
match found {
|
||||||
Ok(false) => entry.missing(),
|
Ok(false) => entry.missing(),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -178,19 +248,11 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
|||||||
}
|
}
|
||||||
let node = match existing {
|
let node = match existing {
|
||||||
Some(node) => node,
|
Some(node) => node,
|
||||||
None => match open_device(state, &path) {
|
None => match open_device(state, current_path.unwrap()) {
|
||||||
Ok(node) => node,
|
Ok(node) => node,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
tracing::warn!(%error, ?path, "DRM device unavailable; will retry");
|
tracing::warn!(%error, ?path, "DRM device unavailable; will retry");
|
||||||
state
|
slot_mut(state, &path).failed(Instant::now());
|
||||||
.direct_backend
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.topology
|
|
||||||
.devices
|
|
||||||
.get_mut(&path)
|
|
||||||
.unwrap()
|
|
||||||
.failed(Instant::now());
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -223,15 +285,7 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
|||||||
if let Err(error) = activation {
|
if let Err(error) = activation {
|
||||||
tracing::warn!(%error, ?node, "DRM reactivation failed; retiring unavailable outputs");
|
tracing::warn!(%error, ?node, "DRM reactivation failed; retiring unavailable outputs");
|
||||||
remove_device(state, node);
|
remove_device(state, node);
|
||||||
state
|
slot_mut(state, &path).failed(Instant::now());
|
||||||
.direct_backend
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.topology
|
|
||||||
.devices
|
|
||||||
.get_mut(&path)
|
|
||||||
.unwrap()
|
|
||||||
.failed(Instant::now());
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,14 +315,7 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
|
|||||||
if complete.is_err() {
|
if complete.is_err() {
|
||||||
remove_device(state, node);
|
remove_device(state, node);
|
||||||
}
|
}
|
||||||
let entry = state
|
let entry = slot_mut(state, &path);
|
||||||
.direct_backend
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.topology
|
|
||||||
.devices
|
|
||||||
.get_mut(&path)
|
|
||||||
.unwrap();
|
|
||||||
if complete == Ok(true) {
|
if complete == Ok(true) {
|
||||||
entry.ready(node);
|
entry.ready(node);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -74,3 +74,34 @@ multiple monitors, display sleep, and live authentication still need session tes
|
|||||||
Include the commit or installed release, reproduction steps, backend (nested or
|
Include the commit or installed release, reproduction steps, backend (nested or
|
||||||
direct), monitor scales/transforms, and relevant logs. Avoid publishing passwords,
|
direct), monitor scales/transforms, and relevant logs. Avoid publishing passwords,
|
||||||
access tokens, or private window content.
|
access tokens, or private window content.
|
||||||
|
|
||||||
|
## Resume recovery
|
||||||
|
|
||||||
|
The direct backend retains known GPU identities while their DRM resources are
|
||||||
|
unavailable. Hotplug, configuration and lid changes reconcile through one path;
|
||||||
|
inactive sessions defer that work. Activation reads the lid asynchronously, then
|
||||||
|
re-enumerates devices and connectors before allowing presentation. A logind
|
||||||
|
system-wake signal follows the same path, including when seat ownership did not
|
||||||
|
change. Newer libinput observations supersede in-flight lid reads. A failed read
|
||||||
|
preserves the last observation; a three-second deadline prevents a stalled bus
|
||||||
|
from blocking recovery indefinitely.
|
||||||
|
|
||||||
|
Device failures retire their output globals and retry with exponential backoff
|
||||||
|
capped at 32 seconds. Working devices remain usable. Known hardware can return
|
||||||
|
under a different `cardN` path. This does not expand support to previously
|
||||||
|
unmanaged secondary GPUs. Connected-output reports mark a mode active only when
|
||||||
|
output creation succeeded. Lock ownership is retained throughout recovery.
|
||||||
|
|
||||||
|
Run the ordering and private D-Bus checks with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo test --locked -p ferese backends::direct::topology
|
||||||
|
cargo test --locked -p ferese backends::direct::lid -- --include-ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
The D-Bus test requires `dbus-daemon` and permission to create private sockets.
|
||||||
|
Hardware validation still needs a direct session: change monitors while on
|
||||||
|
another VT, suspend/resume with a dock disconnected, change the lid while
|
||||||
|
suspended, and unplug/reconnect the managed GPU where supported. Verify current
|
||||||
|
output geometry, idle notifications, recovery after failures and continued lock
|
||||||
|
protection. Nested sessions do not validate DRM reacquisition.
|
||||||
Reference in new issue
Block a user