Reconcile DRM topology before resuming presentation

This commit is contained in:
Samuel Onoja committed 2026-10-02 09:33:19 +01:00
1 parent 73da6c68d2
commit 0420e59cbe
3 files changed
+398 -147

No files matched your search

+97 -144
View File
@@ -10,6 +10,7 @@ mod output_power;
mod planes; mod planes;
mod redraw; mod redraw;
mod scheduling; mod scheduling;
mod topology;
use output_power::{OutputPower, Power}; use output_power::{OutputPower, Power};
use redraw::{AnimationFallback, SchedulingMetrics}; use redraw::{AnimationFallback, SchedulingMetrics};
use scheduling::*; use scheduling::*;
@@ -26,7 +27,7 @@ use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::{ImportDma, ImportMemWl, Renderer}; use smithay::backend::renderer::{ImportDma, ImportMemWl, Renderer};
use smithay::backend::session::libseat::LibSeatSession; use smithay::backend::session::libseat::LibSeatSession;
use smithay::backend::session::{Event as SessionEvent, Session}; use smithay::backend::session::{Event as SessionEvent, Session};
use smithay::backend::udev::{UdevBackend, UdevEvent, primary_gpu}; use smithay::backend::udev::{UdevBackend, UdevEvent, all_gpus, primary_gpu};
use smithay::desktop::layer_map_for_output; use smithay::desktop::layer_map_for_output;
use smithay::desktop::utils::OutputPresentationFeedback; use smithay::desktop::utils::OutputPresentationFeedback;
use smithay::output::{Mode as OutputMode, Output, OutputModeSource, PhysicalProperties, Scale, Subpixel}; use smithay::output::{Mode as OutputMode, Output, OutputModeSource, PhysicalProperties, Scale, Subpixel};
@@ -42,6 +43,7 @@ use smithay::reexports::wayland_server::backend::GlobalId;
use smithay::utils::{DeviceFd, Monotonic, Time, Transform}; use smithay::utils::{DeviceFd, Monotonic, Time, Transform};
use smithay::wayland::dmabuf::DmabufFeedbackBuilder; use smithay::wayland::dmabuf::DmabufFeedbackBuilder;
use smithay::wayland::presentation::Refresh; use smithay::wayland::presentation::Refresh;
use topology::{Topology, reconcile_outputs};
use crate::Ferese; use crate::Ferese;
use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform}; use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform};
@@ -52,6 +54,7 @@ pub struct DirectBackendState {
pub session: LibSeatSession, pub session: LibSeatSession,
pub active: bool, pub active: bool,
lid_closed: bool, lid_closed: bool,
topology: Topology,
pub(crate) low_power: bool, pub(crate) low_power: bool,
devices: HashMap<DrmNode, DirectDevice>, devices: HashMap<DrmNode, DirectDevice>,
input_devices: Vec<LibinputDevice>, input_devices: Vec<LibinputDevice>,
@@ -65,6 +68,10 @@ pub struct DirectBackendState {
} }
impl DirectBackendState { impl DirectBackendState {
fn can_render(&self) -> bool {
self.active && !self.topology.reconciling
}
pub(crate) fn dump_scheduling_metrics(&self) { pub(crate) fn dump_scheduling_metrics(&self) {
self.scheduling_metrics.dump(); self.scheduling_metrics.dump();
} }
@@ -171,6 +178,9 @@ type OutputCompositor = DrmCompositor<
>; >;
struct DirectDevice { struct DirectDevice {
notifier: RegistrationToken,
dmabuf_global: smithay::wayland::dmabuf::DmabufGlobal,
connected_outputs: Vec<ConnectedOutputInfo>,
drm: DrmDevice, drm: DrmDevice,
gbm: GbmDevice<DrmDeviceFd>, gbm: GbmDevice<DrmDeviceFd>,
renderer: GlesRenderer, renderer: GlesRenderer,
@@ -233,6 +243,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
session, session,
active: session_active, active: session_active,
lid_closed: initial_lid_closed(), lid_closed: initial_lid_closed(),
topology: Topology::default(),
low_power: super::power::low_power(false, super::power::battery_percent()), low_power: super::power::low_power(false, super::power::battery_percent()),
devices: HashMap::new(), devices: HashMap::new(),
input_devices: Vec::new(), input_devices: Vec::new(),
@@ -245,7 +256,14 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
scheduling_metrics: SchedulingMetrics::new(), scheduling_metrics: SchedulingMetrics::new(),
}); });
open_primary_device(event_loop, state, &primary_path)?; state
.direct_backend
.as_mut()
.unwrap()
.topology
.devices
.insert(primary_path, Default::default());
reconcile_outputs(state, false);
sync_battery_timer(state); sync_battery_timer(state);
event_loop event_loop
@@ -273,6 +291,10 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
let handle = state.loop_handle.clone(); let handle = state.loop_handle.clone();
if let Some(backend) = state.direct_backend.as_mut() { if let Some(backend) = state.direct_backend.as_mut() {
backend.active = false; backend.active = false;
backend.topology.dirty = true;
if let Some(token) = backend.topology.retry_timer.take() {
handle.remove(token);
}
if let Some(token) = backend.battery_timer.take() { if let Some(token) = backend.battery_timer.take() {
handle.remove(token); handle.remove(token);
} }
@@ -310,23 +332,11 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
if let Some(backend) = state.direct_backend.as_mut() { if let Some(backend) = state.direct_backend.as_mut() {
backend.active = true; backend.active = true;
for device in backend.devices.values_mut() {
if let Err(error) = device.drm.activate(true) {
tracing::error!(%error, "failed to reactivate DRM device");
continue;
}
for output in device.outputs.values_mut() {
// A suspended page flip may never arrive. Clear the old
// pending frame before accepting a fresh submission.
if let Err(error) = output.surface.clear().and_then(|()| output.surface.reset_state()) {
tracing::error!(%error, "failed to reset DRM output after activation");
}
}
}
} }
update_power_policy(state); let seat = state.seat.clone();
state.idle_notifier_state.notify_activity(&seat);
reconcile_outputs(state, true);
sync_battery_timer(state); sync_battery_timer(state);
state.reset_animation_clock(); state.reset_animation_clock();
tracing::info!("direct session activated"); tracing::info!("direct session activated");
@@ -334,29 +344,18 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
render_all(state); render_all(state);
} }
})?; })?;
event_loop event_loop.handle().insert_source(udev_backend, |event, _, state| {
.handle() if let UdevEvent::Removed { device_id } = event
.insert_source(udev_backend, |event, _, state| match event { && let Some(node) = direct_node_for_device(state, device_id)
UdevEvent::Added { device_id, path } => { && let Some(backend) = state.direct_backend.as_mut()
if let Some(node) = direct_node_for_device(state, device_id) { {
rescan_device(state, node); // A remove/re-add may reuse the same dev_t. Retire the old fd
} else { // even if fresh enumeration already sees its replacement.
tracing::info!(?device_id, ?path, "additional DRM device discovered"); backend.topology.invalidated.insert(node);
} }
}
UdevEvent::Changed { device_id } => { reconcile_outputs(state, false);
if let Some(node) = direct_node_for_device(state, device_id) { })?;
rescan_device(state, node);
}
}
UdevEvent::Removed { device_id } => {
if let Some(node) = direct_node_for_device(state, device_id) {
remove_device(state, node);
}
}
})?;
tracing::info!(seat = %seat_name, "initialized direct session input and device discovery"); tracing::info!(seat = %seat_name, "initialized direct session input and device discovery");
Ok(()) Ok(())
@@ -455,27 +454,17 @@ fn update_power_policy(state: &mut Ferese) {
} }
pub(crate) fn reload_outputs(state: &mut Ferese) { pub(crate) fn reload_outputs(state: &mut Ferese) {
if let Some(backend) = state.direct_backend.as_mut() { reconcile_outputs(state, false);
let enabled = state
.output_profiles
.iter()
.any(|profile| profile.outputs.iter().any(|output| output.auto_refresh));
backend.low_power = enabled && super::power::low_power(backend.low_power, super::power::battery_percent());
}
let nodes = state
.direct_backend
.as_ref()
.map(|backend| backend.devices.keys().copied().collect::<Vec<_>>())
.unwrap_or_default();
for node in nodes {
rescan_device(state, node);
}
} }
pub(crate) fn validate_live_outputs(state: &Ferese, profiles: &[OutputProfile]) -> Result<(), String> { pub(crate) fn validate_live_outputs(state: &Ferese, profiles: &[OutputProfile]) -> Result<(), String> {
let Some(backend) = state.direct_backend.as_ref() else { let Some(backend) = state.direct_backend.as_ref() else {
return Ok(()); return Ok(());
}; };
if !backend.can_render() || backend.devices.is_empty() {
// Syntax/config validation still runs; live probing waits for ownership.
return Ok(());
}
let mut usable = 0; let mut usable = 0;
for device in backend.devices.values() { for device in backend.devices.values() {
let mut scan = select_outputs(&device.drm, profiles, backend.low_power).map_err(|e| e.to_string())?; let mut scan = select_outputs(&device.drm, profiles, backend.low_power).map_err(|e| e.to_string())?;
@@ -488,11 +477,7 @@ pub(crate) fn validate_live_outputs(state: &Ferese, profiles: &[OutputProfile])
Ok(()) Ok(())
} }
fn open_primary_device( fn open_device(state: &mut Ferese, path: &Path) -> Result<DrmNode, Box<dyn Error>> {
event_loop: &mut EventLoop<Ferese>,
state: &mut Ferese,
path: &Path,
) -> Result<(), Box<dyn Error>> {
let mut session = state let mut session = state
.direct_backend .direct_backend
.as_ref() .as_ref()
@@ -502,7 +487,7 @@ fn open_primary_device(
let node = DrmNode::from_path(path)?; let node = DrmNode::from_path(path)?;
let fd = session.open(path, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK)?; let fd = session.open(path, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK)?;
let fd = DrmDeviceFd::new(DeviceFd::from(fd)); let fd = DrmDeviceFd::new(DeviceFd::from(fd));
let (mut drm, notifier) = DrmDevice::new(fd.clone(), true)?; let (drm, notifier) = DrmDevice::new(fd.clone(), true)?;
let gbm = GbmDevice::new(fd)?; let gbm = GbmDevice::new(fd)?;
// SAFETY: GBM owns a valid DRM descriptor for the lifetime of the EGL display. // SAFETY: GBM owns a valid DRM descriptor for the lifetime of the EGL display.
let egl_display = unsafe { EGLDisplay::new(gbm.clone())? }; let egl_display = unsafe { EGLDisplay::new(gbm.clone())? };
@@ -520,49 +505,11 @@ fn open_primary_device(
.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()?;
state let dmabuf_global = state
.dmabuf_state .dmabuf_state
.create_global_with_default_feedback::<Ferese>(&display_handle, &feedback); .create_global_with_default_feedback::<Ferese>(&display_handle, &feedback);
let mut scan = select_outputs( let registration = state
&drm, .loop_handle
&state.output_profiles,
state.direct_backend.as_ref().is_some_and(|backend| backend.low_power),
)?;
apply_lid_policy(
&mut scan,
state.direct_backend.as_ref().is_some_and(|backend| backend.lid_closed),
);
if scan.selections.is_empty() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"no connected enabled DRM output with a usable CRTC",
)
.into());
}
state
.direct_backend
.as_mut()
.expect("direct backend state remains initialized")
.connected_outputs = scan.connected_outputs;
let mut outputs = HashMap::new();
for selection in scan.selections {
let crtc = selection.crtc;
let mode = selection.mode;
let output = create_direct_output(state, &mut drm, &gbm, &renderer, selection)?;
state
.direct_backend
.as_mut()
.expect("direct backend state remains initialized")
.presentation
.entry((node, crtc))
.or_default()
.set_refresh(OutputMode::from(mode).refresh);
outputs.insert(crtc, output);
}
event_loop
.handle()
.insert_source(notifier, move |event, metadata, state| match event { .insert_source(notifier, move |event, metadata, state| match event {
DrmEvent::VBlank(crtc) => { DrmEvent::VBlank(crtc) => {
if let Some(metadata) = metadata { if let Some(metadata) = metadata {
@@ -655,7 +602,10 @@ fn open_primary_device(
state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence); state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence);
tracing::trace!(?node, ?crtc, sequence = metadata.sequence, "page flip"); tracing::trace!(?node, ?crtc, sequence = metadata.sequence, "page flip");
let active = state.direct_backend.as_ref().is_some_and(|backend| backend.active); let active = state
.direct_backend
.as_ref()
.is_some_and(|backend| backend.can_render());
if active && retired { if active && retired {
schedule_frame_callbacks(state, node, crtc, monotonic_now()); schedule_frame_callbacks(state, node, crtc, monotonic_now());
let identity = state let identity = state
@@ -692,13 +642,14 @@ fn open_primary_device(
gbm, gbm,
renderer, renderer,
render_node, render_node,
outputs, outputs: HashMap::new(),
notifier: registration,
dmabuf_global,
connected_outputs: Vec::new(),
}, },
); );
state.relayout(); tracing::info!(?node, ?path, "initialized DRM/GBM device");
render_all(state); Ok(node)
tracing::info!(?node, ?path, "initialized DRM/GBM device outputs");
Ok(())
} }
pub(crate) fn sleep_locked_outputs(state: &mut Ferese) { pub(crate) fn sleep_locked_outputs(state: &mut Ferese) {
@@ -718,7 +669,7 @@ pub(crate) fn sleep_locked_outputs(state: &mut Ferese) {
// Apply runtime requests without changing output globals or workspace ownership. // Apply runtime requests without changing output globals or workspace ownership.
fn apply_output_power(state: &mut Ferese) { fn apply_output_power(state: &mut Ferese) {
let Some(backend) = state.direct_backend.as_mut() else { let Some(backend) = state.direct_backend.as_mut().filter(|backend| backend.can_render()) else {
return; return;
}; };
@@ -850,7 +801,11 @@ pub(crate) fn render_surface(
} }
fn redraw(state: &mut Ferese, selected: Option<&[Output]>, source: &'static std::panic::Location<'static>) { fn redraw(state: &mut Ferese, selected: Option<&[Output]>, source: &'static std::panic::Location<'static>) {
if state.direct_backend.as_ref().is_none_or(|backend| !backend.active) { if state
.direct_backend
.as_ref()
.is_none_or(|backend| !backend.can_render())
{
return; return;
} }
@@ -914,10 +869,7 @@ pub(crate) fn set_lid_closed(state: &mut Ferese, closed: bool) {
} }
backend.lid_closed = closed; backend.lid_closed = closed;
tracing::info!(closed, "laptop lid state changed"); tracing::info!(closed, "laptop lid state changed");
let nodes = backend.devices.keys().copied().collect::<Vec<_>>(); reconcile_outputs(state, false);
for node in nodes {
rescan_device(state, node);
}
} }
fn initial_lid_closed() -> bool { fn initial_lid_closed() -> bool {
@@ -1145,13 +1097,16 @@ fn direct_node_for_device(state: &Ferese, device_id: libc::dev_t) -> Option<DrmN
.find(|node| node.dev_id() == device_id) .find(|node| node.dev_id() == device_id)
} }
fn rescan_device(state: &mut Ferese, node: DrmNode) { fn rescan_device(state: &mut Ferese, node: DrmNode) -> Result<bool, ()> {
if state.direct_backend.as_ref().is_none_or(|backend| !backend.active) {
return Err(());
}
let Some(mut device) = state let Some(mut device) = state
.direct_backend .direct_backend
.as_mut() .as_mut()
.and_then(|backend| backend.devices.remove(&node)) .and_then(|backend| backend.devices.remove(&node))
else { else {
return; return Err(());
}; };
let mut scan = match select_outputs( let mut scan = match select_outputs(
@@ -1163,15 +1118,14 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
Err(error) => { Err(error) => {
tracing::error!(?node, %error, "failed to scan DRM connectors"); tracing::error!(?node, %error, "failed to scan DRM connectors");
restore_device(state, node, device); restore_device(state, node, device);
return; return Err(());
} }
}; };
let lid_closed = state.direct_backend.as_ref().is_some_and(|backend| backend.lid_closed); let lid_closed = state.direct_backend.as_ref().is_some_and(|backend| backend.lid_closed);
apply_lid_policy(&mut scan, lid_closed); apply_lid_policy(&mut scan, lid_closed);
if let Some(backend) = state.direct_backend.as_mut() { device.connected_outputs = scan.connected_outputs;
backend.connected_outputs = scan.connected_outputs; let mut complete = true;
}
let mut selections = scan let mut selections = scan
.selections .selections
@@ -1253,6 +1207,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
} }
Err(error) => { Err(error) => {
complete = false;
tracing::warn!(?crtc, %error, "display change failed; retaining current output") tracing::warn!(?crtc, %error, "display change failed; retaining current output")
} }
} }
@@ -1310,27 +1265,27 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
device.outputs.insert(crtc, output); device.outputs.insert(crtc, output);
} }
Err(error) => tracing::error!(?node, ?crtc, %error, "failed to add DRM output"), Err(error) => {
complete = false;
tracing::error!(?node, ?crtc, %error, "failed to add DRM output");
}
} }
} }
for crtc in deferred_removals { for crtc in deferred_removals {
let keep_internal = device.outputs.get(&crtc).is_some_and(|output| output.internal) let keep_internal = device.outputs.get(&crtc).is_some_and(|output| output.internal)
&& lid_closed && lid_closed
&& state.direct_backend.as_ref().is_some_and(|backend| { && device.outputs.get(&crtc).is_some_and(|output| {
device.outputs.get(&crtc).is_some_and(|output| { device
backend .connected_outputs
.connected_outputs .iter()
.iter() .any(|info| info.connector == output.output.name())
.any(|info| info.connector == output.output.name())
})
}) })
&& !device.outputs.values().any(|output| !output.internal); && !device.outputs.values().any(|output| !output.internal);
if keep_internal { if keep_internal {
tracing::warn!(?crtc, "keeping laptop panel enabled: external output activation failed"); tracing::warn!(?crtc, "keeping laptop panel enabled: external output activation failed");
if let Some(output) = device.outputs.get(&crtc) if let Some(output) = device.outputs.get(&crtc)
&& let Some(backend) = state.direct_backend.as_mut() && let Some(info) = device
&& let Some(info) = backend
.connected_outputs .connected_outputs
.iter_mut() .iter_mut()
.find(|info| info.connector == output.output.name()) .find(|info| info.connector == output.output.name())
@@ -1352,9 +1307,9 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
} }
} }
if let Some(backend) = state.direct_backend.as_mut() { {
for output in device.outputs.values() { for output in device.outputs.values() {
if let Some(info) = backend if let Some(info) = device
.connected_outputs .connected_outputs
.iter_mut() .iter_mut()
.find(|info| info.connector == output.output.name()) .find(|info| info.connector == output.output.name())
@@ -1370,11 +1325,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
} }
restore_device(state, node, device); restore_device(state, node, device);
sync_battery_timer(state); Ok(complete)
update_power_policy(state);
state.restore_output_focus();
state.relayout();
render_all(state);
} }
fn remove_device(state: &mut Ferese, node: DrmNode) { fn remove_device(state: &mut Ferese, node: DrmNode) {
@@ -1386,6 +1337,10 @@ fn remove_device(state: &mut Ferese, node: DrmNode) {
return; return;
}; };
state.loop_handle.remove(device.notifier);
state
.dmabuf_state
.disable_global::<Ferese>(&state.display_handle, &device.dmabuf_global);
let context = device.renderer.context_id().erased(); let context = device.renderer.context_id().erased();
state.wallpaper.forget_context(&context); state.wallpaper.forget_context(&context);
state.render.forget_context(&context); state.render.forget_context(&context);
@@ -1398,12 +1353,6 @@ fn remove_device(state: &mut Ferese, node: DrmNode) {
} }
} }
if let Some(backend) = state.direct_backend.as_mut() {
backend.connected_outputs.clear();
}
sync_battery_timer(state);
arm_animation_timer(state);
tracing::info!(?node, "removed DRM device"); tracing::info!(?node, "removed DRM device");
} }
@@ -1441,7 +1390,10 @@ fn animation_fallbacks(state: &mut Ferese) -> Vec<(DrmNode, crtc::Handle, Instan
for (node, crtc, identity, device_active) in outputs { for (node, crtc, identity, device_active) in outputs {
let enabled = device_active let enabled = device_active
&& !state.session_lock.sleeping && !state.session_lock.sleeping
&& state.direct_backend.as_ref().is_some_and(|backend| backend.active); && state
.direct_backend
.as_ref()
.is_some_and(|backend| backend.can_render());
let animating = enabled && state.output_has_animations(&identity); let animating = enabled && state.output_has_animations(&identity);
let backend = state.direct_backend.as_mut().unwrap(); let backend = state.direct_backend.as_mut().unwrap();
let output = backend.devices.get_mut(&node).unwrap().outputs.get_mut(&crtc).unwrap(); let output = backend.devices.get_mut(&node).unwrap().outputs.get_mut(&crtc).unwrap();
@@ -1546,7 +1498,7 @@ fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handl
let Some(output) = state let Some(output) = state
.direct_backend .direct_backend
.as_ref() .as_ref()
.filter(|backend| backend.active) .filter(|backend| backend.can_render())
.and_then(|backend| backend.devices.get(&node)) .and_then(|backend| backend.devices.get(&node))
.filter(|device| device.drm.is_active()) .filter(|device| device.drm.is_active())
.and_then(|device| device.outputs.get(&crtc)) .and_then(|device| device.outputs.get(&crtc))
@@ -1609,6 +1561,7 @@ fn deliver_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle
output.callback_timer = None; output.callback_timer = None;
if !backend.active if !backend.active
|| backend.topology.reconciling
|| !device.drm.is_active() || !device.drm.is_active()
|| !output.power.can_render() || !output.power.can_render()
|| state.session_lock.sleeping || state.session_lock.sleeping
+3 -3
View File
@@ -31,7 +31,7 @@ pub(super) fn request_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handl
let Some(output) = state let Some(output) = state
.direct_backend .direct_backend
.as_mut() .as_mut()
.filter(|backend| backend.active) .filter(|backend| backend.can_render())
.and_then(|backend| backend.devices.get_mut(&node)) .and_then(|backend| backend.devices.get_mut(&node))
.filter(|device| device.drm.is_active()) .filter(|device| device.drm.is_active())
.and_then(|device| device.outputs.get_mut(&crtc)) .and_then(|device| device.outputs.get_mut(&crtc))
@@ -55,7 +55,7 @@ pub(super) fn arm_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
let Some(output) = state let Some(output) = state
.direct_backend .direct_backend
.as_mut() .as_mut()
.filter(|backend| backend.active) .filter(|backend| backend.can_render())
.and_then(|backend| backend.devices.get_mut(&node)) .and_then(|backend| backend.devices.get_mut(&node))
.filter(|device| device.drm.is_active()) .filter(|device| device.drm.is_active())
.and_then(|device| device.outputs.get_mut(&crtc)) .and_then(|device| device.outputs.get_mut(&crtc))
@@ -104,7 +104,7 @@ fn dispatch_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identit
let Some(output) = state let Some(output) = state
.direct_backend .direct_backend
.as_mut() .as_mut()
.filter(|backend| backend.active) .filter(|backend| backend.can_render())
.and_then(|backend| backend.devices.get_mut(&node)) .and_then(|backend| backend.devices.get_mut(&node))
.filter(|device| device.drm.is_active()) .filter(|device| device.drm.is_active())
.and_then(|device| device.outputs.get_mut(&crtc)) .and_then(|device| device.outputs.get_mut(&crtc))
+298
View File
@@ -0,0 +1,298 @@
//! Desired device identities survive temporary loss of their DRM resources.
use std::path::PathBuf;
use super::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Availability {
Ready(DrmNode),
Degraded(DrmNode),
Missing,
Unavailable,
}
pub(super) struct DeviceState {
pub availability: Availability,
failures: u32,
retry_at: Option<Instant>,
}
impl Default for DeviceState {
fn default() -> Self {
Self {
availability: Availability::Missing,
failures: 0,
retry_at: None,
}
}
}
impl DeviceState {
pub fn failed(&mut self, now: Instant) {
self.availability = Availability::Unavailable;
self.failures = self.failures.saturating_add(1);
self.retry_at = Some(now + Duration::from_secs(1u64 << self.failures.saturating_sub(1).min(5)));
}
pub fn ready(&mut self, node: DrmNode) {
self.availability = Availability::Ready(node);
self.failures = 0;
self.retry_at = None;
}
pub fn missing(&mut self) {
*self = Self::default();
}
}
#[derive(Default)]
pub(super) struct Topology {
pub devices: HashMap<PathBuf, DeviceState>,
pub invalidated: HashSet<DrmNode>,
pub reconciling: bool,
pub dirty: bool,
pub retry_timer: Option<RegistrationToken>,
}
impl Topology {
pub fn request(&mut self, active: bool) -> bool {
self.dirty = true;
active && !self.reconciling
}
pub fn retry_deadline(&self, active: bool) -> Option<Instant> {
active
.then(|| self.devices.values().filter_map(|device| device.retry_at).min())
.flatten()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inactive_changes_coalesce_and_reconciliation_cannot_reenter() {
let mut topology = Topology::default();
assert!(!topology.request(false));
assert!(!topology.request(false));
assert!(topology.dirty);
assert!(topology.request(true));
topology.reconciling = true;
assert!(!topology.request(true));
}
#[test]
fn unavailable_devices_retry_independently_with_capped_backoff() {
let now = Instant::now();
let mut topology = Topology::default();
topology.devices.insert("/dev/dri/card0".into(), DeviceState::default());
let healthy = DrmNode::from_dev_id(libc::makedev(226, 1)).unwrap();
let mut second = DeviceState::default();
second.ready(healthy);
topology.devices.insert("/dev/dri/card1".into(), second);
let device = topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap();
device.failed(now);
assert_eq!(device.retry_at, Some(now + Duration::from_secs(1)));
for _ in 0..20 {
device.failed(now);
}
assert_eq!(device.retry_at, Some(now + Duration::from_secs(32)));
assert_eq!(
topology.devices[Path::new("/dev/dri/card1")].availability,
Availability::Ready(healthy)
);
assert!(topology.retry_deadline(false).is_none());
assert_eq!(topology.retry_deadline(true), Some(now + Duration::from_secs(32)));
topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap().missing();
assert!(topology.retry_deadline(true).is_none());
}
}
/// The only entry point for applying the current output configuration to DRM.
pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
let Some(backend) = state.direct_backend.as_mut() else {
return;
};
if !backend.topology.request(backend.active) {
return;
}
backend.topology.reconciling = true;
backend.topology.dirty = false;
if let Some(token) = backend.topology.retry_timer.take() {
state.loop_handle.remove(token);
}
let paths = backend.topology.devices.keys().cloned().collect::<Vec<_>>();
let invalidated = std::mem::take(&mut backend.topology.invalidated);
let observed = all_gpus(backend.session.seat());
let automatic = state
.output_profiles
.iter()
.any(|profile| profile.outputs.iter().any(|output| output.auto_refresh));
backend.low_power =
automatic && super::super::power::low_power(backend.low_power, super::super::power::battery_percent());
for node in invalidated {
remove_device(state, node);
}
for path in paths {
let existing = DrmNode::from_path(&path)
.ok()
.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 state.direct_backend.as_ref().unwrap().topology.devices[&path].availability {
Availability::Ready(node) | Availability::Degraded(node) => Some(node),
_ => existing,
};
let found = observed.as_ref().map(|paths| paths.contains(&path));
if !matches!(found, Ok(true)) {
if let Some(node) = previous {
remove_device(state, node);
}
let entry = state
.direct_backend
.as_mut()
.unwrap()
.topology
.devices
.get_mut(&path)
.unwrap();
match found {
Ok(false) => entry.missing(),
Err(error) => {
tracing::warn!(%error, ?path, "could not enumerate DRM topology");
entry.failed(Instant::now());
}
Ok(true) => unreachable!(),
}
continue;
}
if let Some(node) = previous.filter(|node| Some(*node) != existing) {
remove_device(state, node);
}
let node = match existing {
Some(node) => node,
None => match open_device(state, &path) {
Ok(node) => node,
Err(error) => {
tracing::warn!(%error, ?path, "DRM device unavailable; will retry");
state
.direct_backend
.as_mut()
.unwrap()
.topology
.devices
.get_mut(&path)
.unwrap()
.failed(Instant::now());
continue;
}
},
};
let mut failed_outputs = Vec::new();
let activation = if reactivate && existing.is_some() {
let device = state.direct_backend.as_mut().unwrap().devices.get_mut(&node).unwrap();
device.drm.activate(true).map(|()| {
for (crtc, output) in &mut device.outputs {
if let Err(error) = output.surface.clear().and_then(|()| output.surface.reset_state()) {
tracing::warn!(%error, ?node, ?crtc, "output reset failed; recreating output");
failed_outputs.push(*crtc);
}
}
})
} else {
Ok(())
};
if let Err(error) = activation {
tracing::warn!(%error, ?node, "DRM reactivation failed; retiring unavailable outputs");
remove_device(state, node);
state
.direct_backend
.as_mut()
.unwrap()
.topology
.devices
.get_mut(&path)
.unwrap()
.failed(Instant::now());
continue;
}
for crtc in failed_outputs {
let mut output = state
.direct_backend
.as_mut()
.unwrap()
.devices
.get_mut(&node)
.unwrap()
.outputs
.remove(&crtc)
.unwrap();
cancel_output_timers(&state.loop_handle, &mut output);
state.display_handle.disable_global::<Ferese>(output.global);
state.unregister_output(&output.output);
state
.direct_backend
.as_mut()
.unwrap()
.presentation
.remove(&(node, crtc));
}
let complete = rescan_device(state, node);
if complete.is_err() {
remove_device(state, node);
}
let entry = state
.direct_backend
.as_mut()
.unwrap()
.topology
.devices
.get_mut(&path)
.unwrap();
if complete == Ok(true) {
entry.ready(node);
} else {
entry.failed(Instant::now());
// A partial connector failure must not take down working outputs.
if complete == Ok(false) {
entry.availability = Availability::Degraded(node);
}
}
}
let backend = state.direct_backend.as_mut().unwrap();
backend.connected_outputs = backend
.devices
.values()
.flat_map(|device| device.connected_outputs.clone())
.collect();
backend.topology.reconciling = false;
let retry = backend.topology.retry_deadline(backend.active);
if let Some(deadline) = retry {
match state
.loop_handle
.insert_source(Timer::from_deadline(deadline), |_, _, state| {
if let Some(backend) = state.direct_backend.as_mut() {
backend.topology.retry_timer = None;
}
reconcile_outputs(state, false);
TimeoutAction::Drop
}) {
Ok(token) => state.direct_backend.as_mut().unwrap().topology.retry_timer = Some(token),
Err(error) => tracing::warn!(%error, "could not arm DRM recovery deadline"),
}
}
sync_battery_timer(state);
state.restore_output_focus();
state.relayout();
render_all(state);
}