Reconcile DRM topology before resuming presentation
This commit is contained in:
1 parent
73da6c68d2
commit
0420e59cbe
3 files changed
+393
-142
No files matched your search
@@ -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() {
|
let seat = state.seat.clone();
|
||||||
// A suspended page flip may never arrive. Clear the old
|
state.idle_notifier_state.notify_activity(&seat);
|
||||||
// pending frame before accepting a fresh submission.
|
reconcile_outputs(state, true);
|
||||||
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);
|
|
||||||
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,28 +344,17 @@ 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");
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user