use std::collections::{HashMap, HashSet}; use std::error::Error; use std::io; use std::os::fd::AsFd; use std::path::Path; use std::time::{Duration, Instant}; pub(crate) mod capture; mod context; mod crtc_assignment; mod diagnostics; pub(crate) mod feedback; pub(crate) mod gamma; mod lid; mod mirror; mod output_power; mod planes; mod recovery; mod redraw; mod scheduling; mod session_device; mod topology; mod transaction; use diagnostics::{Diagnostics, PrimaryOutcome}; use output_power::{OutputPower, Power}; use redraw::{AnimationFallback, SchedulingMetrics}; use scheduling::{cancel_output_timers, monotonic_now, request_frame, arm_frame, record_frame_schedule}; use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}; use smithay::backend::allocator::{Buffer, Fourcc}; use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneElement}; use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter; use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, NodeType}; use smithay::backend::egl::EGLDisplay; use smithay::backend::input::InputEvent; use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface}; use smithay::backend::renderer::element::{RenderElement, UnderlyingStorage}; use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; use smithay::backend::renderer::utils::CommitCounter; use smithay::backend::renderer::{ImportDma, ImportMemWl, Renderer}; use smithay::backend::session::libseat::LibSeatSession; use smithay::backend::session::{Event as SessionEvent, Session}; use smithay::backend::udev::{UdevBackend, UdevEvent, all_gpus, primary_gpu}; use smithay::desktop::layer_map_for_output; use smithay::desktop::utils::OutputPresentationFeedback; use smithay::output::{Mode as OutputMode, Output, OutputModeSource, PhysicalProperties, Scale, Subpixel}; use smithay::reexports::calloop::timer::{TimeoutAction, Timer}; use smithay::reexports::calloop::{EventLoop, RegistrationToken}; use smithay::reexports::drm::control::{ Device as ControlDevice, Mode as DrmMode, ModeTypeFlags, connector, crtc, property, }; use smithay::reexports::input::{Device as LibinputDevice, Libinput}; use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind; use smithay::reexports::wayland_server::backend::GlobalId; use smithay::utils::{Monotonic, Time, Transform}; use smithay::wayland::dmabuf::DmabufFeedbackBuilder; use smithay::wayland::presentation::Refresh; use topology::{Topology, reconcile_outputs}; use crate::Ferese; use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform}; use crate::metrics::RenderMetrics; use crate::monitor_identity::{Identity, IdentityRegistry}; use crate::output_policy::{DesiredOutputConfiguration, ManualOverride, Monitor}; use crate::render::{frame_effect_metrics, sampled_output_elements}; pub struct DirectBackendState { pub session: LibSeatSession, pub active: bool, lid: lid::LidState, lid_reader: lid::Reader, topology: Topology, identities: IdentityRegistry, pub(crate) manual_outputs: ManualOverride, desired_outputs: DesiredOutputConfiguration, monitors: Vec, output_error: Option, applied_outputs: DesiredOutputConfiguration, revert_outputs: Option, confirmation: Option<(DesiredOutputConfiguration, ManualOverride)>, confirmation_timer: Option, confirmation_lid: bool, pub(crate) low_power: bool, devices: HashMap, input_devices: Vec, presentation: HashMap<(DrmNode, crtc::Handle), PresentationClock>, pub(crate) connected_outputs: Vec, battery_timer: Option, power_retry: Option, animation_timer: Option, animation_deadline: Option, scheduling_metrics: SchedulingMetrics, } impl DirectBackendState { pub(crate) fn physical_outputs(&self) -> impl Iterator { self.devices .values() .flat_map(|device| device.outputs.values().map(|output| &output.output)) } pub(crate) fn output_configuration_error(&self) -> Option<&str> { self.output_error.as_deref() } pub(crate) fn confirmation_pending(&self) -> bool { self.confirmation.is_some() } pub(crate) fn reconciling(&self) -> bool { self.topology.reconciling } fn can_render(&self) -> bool { self.active && !self.topology.reconciling && !self.lid.pending() } pub(crate) fn process_dmabuf_imports( &mut self, imports: &mut crate::dmabuf_imports::ImportManager, ) { if !self.active || self.topology.reconciling { return; } for device in self .devices .values_mut() .filter(|device| device.drm.is_active()) { imports.process( device.dmabuf_global, &mut device.renderer, Some(device.render_node), ); } } pub(crate) fn scene_output(&self, state: &Ferese, output: &Output) -> Output { let source = self .devices .values() .flat_map(|device| device.outputs.values()) .find(|candidate| &candidate.output == output) .and_then(|candidate| candidate.mirror_source.as_ref()); source .and_then(|key| self.monitors.iter().find(|monitor| &monitor.key == key)) .and_then(|monitor| state.output_by_identity(&monitor.identity)) .cloned() .unwrap_or_else(|| output.clone()) } pub(crate) fn dump_scheduling_metrics(&self) { self.scheduling_metrics.dump(); } pub(crate) fn capture_resize_snapshot( &mut self, window: &smithay::desktop::Window, geometry: smithay::utils::Rectangle, output: &Output, remaining: usize, ) -> Result, smithay::backend::renderer::gles::GlesError> { let Some(device) = self.devices.values_mut().find(|device| { device .outputs .values() .any(|candidate| &candidate.output == output) }) else { return Ok(None); }; crate::render::capture_resize_snapshot( &mut device.renderer, window, geometry, output.current_scale().fractional_scale(), remaining, ) } pub(crate) fn clear_stream_capture_targets(&mut self) { for device in self.devices.values_mut() { crate::render::clear_stream_capture_targets(&mut device.renderer); } } pub(crate) fn capture_renderer(&mut self, output: &Output) -> Option<&mut GlesRenderer> { self.devices .values_mut() .find(|device| { device .outputs .values() .any(|candidate| &candidate.output == output) }) .map(|device| &mut device.renderer) } pub(crate) fn capture_window_buffer( &mut self, window: &smithay::desktop::Window, geometry: smithay::utils::Rectangle, output: &Output, ) -> Result { let device = self .devices .values_mut() .find(|device| { device .outputs .values() .any(|candidate| &candidate.output == output) }) .ok_or("Window output is unavailable")?; crate::render::capture_window_buffer( &mut device.renderer, window, geometry, output.current_scale().fractional_scale(), ) } pub(crate) fn capture_window_frame( &mut self, state: &Ferese, window: &smithay::desktop::Window, output: &Output, cursor: Option>, scale: f64, ) -> Result { let device = self .devices .values_mut() .find(|device| { device .outputs .values() .any(|candidate| &candidate.output == output) }) .ok_or("Window output is unavailable")?; crate::render::capture_window_frame( &mut device.renderer, window, window.geometry(), scale, cursor.map(|rect| (state, rect)), ) } } #[derive(Clone, Debug, PartialEq)] pub(crate) struct ConnectedOutputInfo { pub connector: String, pub connected: bool, pub identity: String, pub internal: bool, pub requested_enabled: bool, pub mirror_source: Option, pub enabled: bool, pub profile: Option, pub requested_profile: Option, pub auto_refresh: bool, pub physical_size: Option<(u32, u32)>, pub current_mode: Option, pub available_modes: Vec, pub scale: f64, pub transform: OutputTransform, pub configured_position: Option<[i32; 2]>, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct ConnectedModeInfo { pub width: u16, pub height: u16, pub refresh_millihertz: i32, pub preferred: bool, } #[derive(Default)] struct PresentationClock { last_presentation: Option, presented_frames: u64, refresh_interval: Option, missed_deadlines: u64, } type OutputCompositor = DrmCompositor< GbmAllocator, GbmFramebufferExporter, OutputPresentationFeedback, DrmDeviceFd, >; struct DirectDevice { notifier: RegistrationToken, dmabuf_global: smithay::wayland::dmabuf::DmabufGlobal, connected_outputs: Vec, drm: DrmDevice, gbm: GbmDevice, renderer: GlesRenderer, render_node: DrmNode, outputs: HashMap, // Last: release libseat's original FD after renderer/DRM resources. _session_device: session_device::SessionDevice, } struct DirectOutput { gamma: gamma::State, internal: bool, connector: connector::Handle, mode: DrmMode, settings: OutputSettings, output: Output, global: Option, identity: String, mirror_source: Option, mirror_texture: Option, mirror_capture_texture: [Option; 2], mirror_canvas: Option, mirror_id: smithay::backend::renderer::element::Id, mirror_commit: CommitCounter, surface: OutputCompositor, primary_commit: Option, capture_texture: [Option; 2], render_metrics: RenderMetrics, diagnostics: Diagnostics, dmabuf_feedback: feedback::OutputFeedback, frame_pending: bool, completion: recovery::Completion, completion_timer: Option, animation_fallback: AnimationFallback, cursor_visible: bool, scheduler: crate::frame_scheduler::FrameScheduler, render_timer: Option, power: OutputPower, callback_timer: Option, lock_frame_pending: bool, } #[derive(Clone)] struct OutputSelection { connector: connector::Info, crtc: crtc::Handle, mode: DrmMode, settings: OutputSettings, identity: String, mirror_source: Option, } struct OutputScan { selections: Vec, connected_outputs: Vec, } pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<(), Box> { let (session, notifier) = LibSeatSession::new()?; let seat_name = session.seat(); let udev_backend = UdevBackend::new(&seat_name)?; let primary_path = primary_gpu(&seat_name)? .or_else(|| { udev_backend .device_list() .next() .map(|(_, path)| path.to_owned()) }) .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no DRM device found"))?; let mut libinput_context = Libinput::new_with_udev::>(session.clone().into()); libinput_context .udev_assign_seat(&seat_name) .map_err(|()| io::Error::other(format!("failed to assign libinput seat {seat_name}")))?; let libinput_backend = LibinputInputBackend::new(libinput_context.clone()); let session_active = session.is_active(); if !session_active { libinput_context.suspend(); } let lid_reader = lid::Reader::start(state)?; state.direct_backend = Some(DirectBackendState { session, active: session_active, lid: lid::LidState::default(), lid_reader, topology: Topology::default(), identities: IdentityRegistry::default(), manual_outputs: ManualOverride::default(), desired_outputs: DesiredOutputConfiguration::default(), monitors: Vec::new(), output_error: None, applied_outputs: DesiredOutputConfiguration::default(), revert_outputs: None, confirmation: None, confirmation_timer: None, confirmation_lid: false, low_power: super::power::low_power(false, super::power::battery_percent()), devices: HashMap::new(), input_devices: Vec::new(), presentation: HashMap::new(), connected_outputs: Vec::new(), battery_timer: None, power_retry: None, animation_timer: None, animation_deadline: None, scheduling_metrics: SchedulingMetrics::new(), }); state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend") .topology .observe_devices(&[( primary_path.clone(), topology::device_identity(&primary_path), )]); lid::request_refresh(state, false); event_loop .handle() .insert_source(libinput_backend, |event, _, state| match event { InputEvent::DeviceAdded { mut device } => { configure_libinput_device(state, &mut device); if let Some(backend) = state.direct_backend.as_mut() { backend.input_devices.push(device); } } InputEvent::DeviceRemoved { device } => { state.cancel_window_interaction(); if let Some(backend) = state.direct_backend.as_mut() { backend .input_devices .retain(|candidate| candidate != &device); } } event => state.process_input_event(event), })?; event_loop .handle() .insert_source(notifier, move |event, _, state| match event { SessionEvent::PauseSession => { state.cancel_window_interaction(); state.set_binding_mode("default"); let handle = state.loop_handle.clone(); if let Some(backend) = state.direct_backend.as_mut() { backend.active = false; backend.lid.pause(); backend.lid_reader.set_active(false); if let Some(token) = backend.lid_reader.deadline.take() { handle.remove(token); } backend.topology.dirty = true; backend.topology.coalescer.clear(); if let Some(token) = backend.topology.settle_timer.take() { handle.remove(token); } if let Some(token) = backend.topology.retry_timer.take() { handle.remove(token); } if let Some(token) = backend.battery_timer.take() { handle.remove(token); } if let Some(token) = backend.power_retry.take() { handle.remove(token); } for clock in backend.presentation.values_mut() { clock.reset_timing(); } for device in backend.devices.values_mut() { for output in device.outputs.values_mut() { output.gamma.suspend(); cancel_output_timers(&handle, output); output.surface.reset_buffers(); output.dmabuf_feedback.invalidate(); output.primary_commit = None; output.capture_texture = Default::default(); output.frame_pending = false; output.power.suspended(); } device.drm.pause(); } } state.display_presentation.clear(); state.refresh_idle_inhibition(); libinput_context.suspend(); tracing::info!("direct session paused"); } SessionEvent::ActivateSession => { if libinput_context.resume().is_err() { tracing::error!("failed to resume libinput"); } if let Some(backend) = state.direct_backend.as_mut() { backend.active = true; backend.lid_reader.set_active(true); } let seat = state.seat.clone(); state.idle_notifier_state.notify_activity(&seat); lid::request_refresh(state, true); tracing::info!("direct session reacquired; refreshing external state"); } })?; event_loop .handle() .insert_source(udev_backend, |event, _, state| { if let UdevEvent::Removed { device_id } = event && let Some(node) = direct_node_for_device(state, device_id) && let Some(backend) = state.direct_backend.as_mut() { // A remove/re-add may reuse the same dev_t. Retire the old fd // even if fresh enumeration already sees its replacement. backend.topology.invalidated.insert(node); } topology::hotplug(state); })?; tracing::info!(seat = %seat_name, "initialized direct session input and device discovery"); Ok(()) } fn configure_libinput_device(state: &Ferese, device: &mut LibinputDevice) { let settings = state.input_settings.touchpad; let touchpad = device.config_tap_finger_count() > 0 || device.config_dwt_is_available(); let mouse = state.input_settings.mouse; if touchpad { if device.config_accel_is_available() && let Err(error) = device.config_accel_set_speed(settings.accel_speed) { tracing::warn!(device = %device.name(), ?error, "failed to configure touchpad acceleration"); } use smithay::reexports::input::SendEventsMode; let mode = if settings.disable_while_mouse_connected { SendEventsMode::DISABLED_ON_EXTERNAL_MOUSE } else { SendEventsMode::ENABLED }; if device .config_send_events_modes() .contains(SendEventsMode::DISABLED_ON_EXTERNAL_MOUSE) && let Err(error) = device.config_send_events_set_mode(mode) { tracing::warn!(device = %device.name(), ?error, "failed to configure touchpad external-mouse policy"); } } if !touchpad && device.has_capability(smithay::reexports::input::DeviceCapability::Pointer) { if device.config_accel_is_available() { if let Err(error) = device.config_accel_set_speed(mouse.accel_speed) { tracing::warn!(device = %device.name(), ?error, "failed to configure mouse acceleration"); } use smithay::reexports::input::AccelProfile; let profile = match mouse.accel_profile { Some(crate::config::AccelerationProfile::Adaptive) => Some(AccelProfile::Adaptive), Some(crate::config::AccelerationProfile::Flat) => Some(AccelProfile::Flat), None => device.config_accel_default_profile(), }; if let Some(profile) = profile.filter(|profile| device.config_accel_profiles().contains(profile)) && let Err(error) = device.config_accel_set_profile(profile) { tracing::warn!(device = %device.name(), ?error, "failed to configure mouse acceleration profile"); } } if device.config_left_handed_is_available() && let Err(error) = device.config_left_handed_set(mouse.left_handed) { tracing::warn!(device = %device.name(), ?error, "failed to configure mouse handedness"); } if device.config_middle_emulation_is_available() && let Err(error) = device.config_middle_emulation_set_enabled(mouse.middle_emulation) { tracing::warn!(device = %device.name(), ?error, "failed to configure middle button emulation"); } } if device.config_tap_finger_count() > 0 && let Err(error) = device.config_tap_set_enabled(settings.tap) { tracing::warn!(device = %device.name(), ?error, "failed to configure tap-to-click"); } if device.config_scroll_has_natural_scroll() && let Err(error) = device.config_scroll_set_natural_scroll_enabled(if touchpad { settings.natural_scroll } else { mouse.natural_scroll }) { tracing::warn!(device = %device.name(), ?error, "failed to configure natural scrolling"); } if device.config_dwt_is_available() && let Err(error) = device.config_dwt_set_enabled(settings.disable_while_typing) { tracing::warn!( device = %device.name(), ?error, "failed to configure disable-while-typing" ); } } pub(crate) fn reload_input_devices(state: &mut Ferese) { let Some(backend) = state.direct_backend.as_ref() else { return; }; // Device clones refer to the same live libinput objects. let mut devices = backend.input_devices.clone(); for device in &mut devices { configure_libinput_device(state, device); } } fn automatic_refresh_active(backend: &DirectBackendState) -> bool { backend.active && backend.devices.values().any(|device| { device .outputs .values() .any(|output| output.settings.auto_refresh) }) } fn sync_battery_timer(state: &mut Ferese) { let Some(backend) = state.direct_backend.as_mut() else { return; }; if !automatic_refresh_active(backend) { if let Some(token) = backend.battery_timer.take() { state.loop_handle.remove(token); } return; } if backend.battery_timer.is_some() { return; } match state.loop_handle.insert_source( Timer::from_duration(Duration::from_secs(5)), |_, _, state| { if let Some(backend) = state.direct_backend.as_mut() { backend.battery_timer = None; } update_power_policy(state); sync_battery_timer(state); TimeoutAction::Drop }, ) { Ok(token) => backend.battery_timer = Some(token), Err(error) => tracing::warn!(%error, "could not arm automatic refresh battery timer"), } } fn update_power_policy(state: &mut Ferese) { if !state .direct_backend .as_ref() .is_some_and(automatic_refresh_active) { return; } let Some(backend) = state.direct_backend.as_mut() else { return; }; // Keep low_power as the applied policy while paused. A transition sampled // during inactivity must still be detected and applied on activation. let battery = if backend.active { super::power::battery_percent() } else { None }; if let Some(next) = super::power::policy_change(backend.active, backend.low_power, battery) { backend.low_power = next; reload_outputs(state, false); } } pub(crate) fn reload_outputs(state: &mut Ferese, layout_dirty: bool) { if let Some(backend) = state.direct_backend.as_mut() { backend.topology.layout_dirty |= layout_dirty; } reconcile_outputs(state, false); } pub(crate) fn set_output_profile(state: &mut Ferese, name: &str) -> Result<(), String> { let backend = state .direct_backend .as_mut() .ok_or("monitor profiles require the direct DRM backend")?; if !backend.active || backend.lid.pending() { return Err("output session is inactive or refreshing lid state".into()); } if name != "auto" && !state .output_profiles .iter() .any(|profile| profile.name == name) { return Err(format!("unknown output profile {name:?}")); } let previous = backend.manual_outputs.clone(); let known_good = backend.applied_outputs.clone(); backend.manual_outputs.profile = (name != "auto").then(|| name.to_owned()); backend.manual_outputs.internal = None; backend.manual_outputs.layout = None; let desired = crate::output_policy::select_profile( &backend.monitors, backend.lid.closed(), &state.output_profiles, &backend.manual_outputs, ); if name != "auto" && desired.profile.as_deref() != Some(name) { backend.manual_outputs = previous; return Err("profile does not match the connected monitors/lid".into()); } if let Err(error) = validate_live_outputs(state, &state.output_profiles) { state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend") .manual_outputs = previous; return Err(error); } reconcile_outputs(state, false); if let Some(error) = state .direct_backend .as_ref() .expect("DRM callbacks require an installed direct backend") .output_error .clone() { state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend") .manual_outputs = previous; return Err(error); } if name != "auto" && state .direct_backend .as_ref() .expect("DRM callbacks require an installed direct backend") .desired_outputs .profile .as_deref() != Some(name) { state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend") .manual_outputs = previous; return Err("profile does not match the connected monitors/lid".into()); } arm_output_confirmation(state, known_good, previous); state.notify_monitor_state(); Ok(()) } pub(crate) fn set_output_layout( state: &mut Ferese, layout: crate::config::OutputLayout, ) -> Result<(), String> { let backend = state .direct_backend .as_mut() .ok_or("display modes require the direct DRM backend")?; if !backend.active || backend.lid.pending() { return Err("output session is inactive or refreshing lid state".into()); } let usable = backend .monitors .iter() .filter(|monitor| monitor.usable) .collect::>(); let available = match layout { crate::config::OutputLayout::InternalOnly => usable.iter().any(|monitor| monitor.internal), crate::config::OutputLayout::ExternalOnly => usable.iter().any(|monitor| !monitor.internal), crate::config::OutputLayout::Extend | crate::config::OutputLayout::Mirror => { usable.len() >= 2 } }; if !available { return Err("this display mode requires monitors that are not connected".into()); } let previous = backend.manual_outputs.clone(); let known_good = backend.applied_outputs.clone(); backend.manual_outputs.profile = None; backend.manual_outputs.internal = None; backend.manual_outputs.layout = Some(layout); if let Err(error) = validate_live_outputs(state, &state.output_profiles) { state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend") .manual_outputs = previous; return Err(error); } reconcile_outputs(state, false); if let Some(error) = state .direct_backend .as_ref() .expect("DRM callbacks require an installed direct backend") .output_error .clone() { state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend") .manual_outputs = previous; return Err(error); } arm_output_confirmation(state, known_good, previous); state.notify_monitor_state(); Ok(()) } pub(crate) fn set_internal_output(state: &mut Ferese, enabled: bool) -> Result<(), String> { let backend = state .direct_backend .as_mut() .ok_or("internal panel control requires the direct DRM backend")?; if !backend.active || backend.lid.pending() { return Err("output session is inactive or refreshing lid state".into()); } if !enabled && !backend .monitors .iter() .any(|monitor| !monitor.internal && monitor.usable) { return Err("cannot disable the last usable output".into()); } let previous = backend.manual_outputs.clone(); let known_good = backend.applied_outputs.clone(); backend.manual_outputs.internal = Some(enabled); backend.manual_outputs.layout = None; reconcile_outputs(state, false); if let Some(error) = state .direct_backend .as_ref() .expect("DRM callbacks require an installed direct backend") .output_error .clone() { state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend") .manual_outputs = previous; return Err(error); } arm_output_confirmation(state, known_good, previous); state.notify_monitor_state(); Ok(()) } fn confirmation_baseline(pending: Option, previous: T) -> T { pending.unwrap_or(previous) } fn arm_output_confirmation( state: &mut Ferese, previous: DesiredOutputConfiguration, manual: ManualOverride, ) { let backend = state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend"); if let Some(token) = backend.confirmation_timer.take() { state.loop_handle.remove(token); } let timeout = backend .desired_outputs .profile .as_ref() .and_then(|name| { state .output_profiles .iter() .find(|profile| &profile.name == name) }) .map_or(15, |profile| profile.confirm_timeout); let baseline = confirmation_baseline(backend.confirmation.take(), (previous, manual)); if timeout == 0 || baseline.0.outputs.is_empty() { return; } backend.confirmation = Some(baseline); backend.confirmation_lid = backend.lid.closed(); match state.loop_handle.insert_source( Timer::from_duration(Duration::from_secs(timeout.into())), |_, _, state| { if let Some(backend) = state.direct_backend.as_mut() { backend.confirmation_timer = None; } let _ = confirm_output_configuration(state, false); TimeoutAction::Drop }, ) { Ok(token) => { state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend") .confirmation_timer = Some(token) } Err(error) => { tracing::warn!(%error, "could not arm monitor confirmation; reverting"); let _ = confirm_output_configuration(state, false); } } } pub(crate) fn confirm_output_configuration( state: &mut Ferese, confirm: bool, ) -> Result<(), String> { let backend = state .direct_backend .as_mut() .ok_or("direct backend unavailable")?; if let Some(token) = backend.confirmation_timer.take() { state.loop_handle.remove(token); } if let Some((previous, manual)) = backend.confirmation.take() && !confirm { backend.revert_outputs = Some(crate::output_policy::restore_configuration( &previous, &backend.monitors, )); backend.manual_outputs = manual; reconcile_outputs(state, false); if let Some(error) = state .direct_backend .as_ref() .expect("DRM callbacks require an installed direct backend") .output_error .clone() { return Err(error); } } state.notify_monitor_state(); Ok(()) } pub(crate) fn validate_live_outputs( state: &Ferese, profiles: &[OutputProfile], ) -> Result<(), String> { let Some(backend) = state.direct_backend.as_ref() else { return Ok(()); }; if !backend.can_render() || backend.devices.is_empty() { // Syntax/config validation still runs; live probing waits for ownership. return Ok(()); } for profile in profiles { let mut matched = HashSet::new(); for settings in &profile.outputs { let matches = backend .monitors .iter() .filter(|monitor| monitor.matches(&settings.matcher)) .collect::>(); if matches.len() > 1 || matches.iter().any(|monitor| !matched.insert(&monitor.key)) { return Err(format!( "profile {:?} contains ambiguous or overlapping monitor selectors", profile.name )); } } } let mut desired = crate::output_policy::select_profile( &backend.monitors, backend.lid.closed(), profiles, &backend.manual_outputs, ); resolve_output_positions(backend, &mut desired)?; let usable = validate_output_configuration(backend, &desired)?; if usable == 0 { return Err("display configuration would leave no usable output".into()); } Ok(()) } fn open_device(state: &mut Ferese, path: &Path) -> Result> { let session = state .direct_backend .as_ref() .expect("direct backend state is initialized before the DRM device") .session .clone(); let node = DrmNode::from_path(path)?; let session_device = session_device::SessionDevice::open(session, path)?; let fd = DrmDeviceFd::new(session_device.device_fd()?); let (drm, notifier) = DrmDevice::new(fd.clone(), true)?; let gbm = GbmDevice::new(fd)?; // SAFETY: GBM owns a valid DRM descriptor for the lifetime of the EGL display. let egl_display = unsafe { EGLDisplay::new(gbm.clone())? }; let egl_context = context::try_new(&egl_display)?; // SAFETY: the new context is not current on another thread and remains renderer-owned. let renderer = unsafe { GlesRenderer::new(egl_context)? }; state.shm_state.update_formats(renderer.shm_formats()); let dmabuf_formats = renderer.dmabuf_formats(); let display_handle = state.display_handle.clone(); // Mesa's Wayland EGL path needs the main device, not just a v3 format // list, to choose a hardware render node. Without feedback, nested EGL // clients can fall back to llvmpipe despite a GPU-backed DRM compositor. let render_node = node .node_with_type(NodeType::Render) .and_then(Result::ok) .unwrap_or(node); let feedback = DmabufFeedbackBuilder::new(render_node.dev_id(), dmabuf_formats).build()?; let registration = state .loop_handle .insert_source(notifier, move |event, metadata, state| match event { DrmEvent::VBlank(crtc) => { #[cfg(feature = "drm-fault-injection")] if state .direct_backend .as_ref() .and_then(|backend| backend.devices.get(&node)) .and_then(|device| device.outputs.get(&crtc)) .is_some_and(|output| output.frame_pending) && recovery::inject("missing-completion") { tracing::warn!(?node, ?crtc, "injecting missing DRM completion"); return; } if let Some(metadata) = metadata { let mut retired = false; let mut retirement_failed = false; let feedback = state .direct_backend .as_mut() .and_then(|backend| backend.devices.get_mut(&node)) .and_then(|device| device.outputs.get_mut(&crtc)) .and_then(|output| { #[cfg(feature = "drm-fault-injection")] if output.frame_pending && recovery::inject("retirement") { tracing::warn!(?node, ?crtc, "injecting DRM retirement failure"); retirement_failed = true; output.diagnostics.retirement(false, true); output.diagnostics.report_if_due(); return None; } let (feedback, completed, error) = recovery::retirement(output.surface.frame_submitted()); retired = completed; output.diagnostics.retirement(completed, error.is_some()); output.diagnostics.report_if_due(); if let Some(error) = error { tracing::error!(?node, ?crtc, %error, "failed to retire DRM frame"); retirement_failed = true; } feedback }); if let (Some(mut feedback), DrmEventTime::Monotonic(time)) = (feedback, metadata.time) { feedback.presented( Time::::from(time), Refresh::fixed(refresh_duration(state, node, crtc)), u64::from(metadata.sequence), Kind::Vsync | Kind::HwClock | Kind::HwCompletion, ); } let lock_output = state .direct_backend .as_mut() .and_then(|backend| backend.devices.get_mut(&node)) .and_then(|device| device.outputs.get_mut(&crtc)) .and_then(|output| { if retired && std::mem::take(&mut output.lock_frame_pending) { Some(output.output.clone()) } else { None } }); if let Some(output) = lock_output { state.lock_frame_presented(&output); } if let Some(output) = state .direct_backend .as_mut() .and_then(|backend| backend.devices.get_mut(&node)) .and_then(|device| device.outputs.get_mut(&crtc)) && retired { output.completion.retired(); output.frame_pending = false; if output.power.needs_wake() { output.gamma.retry_probe(); state.gamma_dirty = true; } output.power.presented(); if let DrmEventTime::Monotonic(timestamp) = metadata.time { let now = monotonic_now(); if timestamp <= now + refresh_duration_from_mode(output.mode) { if let Some(plan) = output.scheduler.presented(timestamp) && std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some() { tracing::debug!(target: "ferese::render", output = %output.output.name(), target_us = plan.present_at.as_micros(), actual_us = timestamp.as_micros(), request_to_present_us = timestamp.saturating_sub(plan.requested_at).as_micros(), missed_deadlines = output.scheduler.missed_deadlines, "DRM presentation schedule"); } } else { output.scheduler.retire_unknown(); } } else { output.scheduler.retire_unknown(); } } if retired { if let Some(output) = state .direct_backend .as_ref() .and_then(|backend| backend.devices.get(&node)) .and_then(|device| device.outputs.get(&crtc)) { state.display_presentation.presented(&output.output); } state.refresh_idle_inhibition(); } if retirement_failed { recovery::reconcile_device(state, node); return; } apply_output_power(state); state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence); tracing::trace!(?node, ?crtc, sequence = metadata.sequence, "page flip"); let active = state .direct_backend .as_ref() .is_some_and(|backend| backend.can_render()); if active && retired { schedule_frame_callbacks(state, node, crtc, monotonic_now()); let identity = state .direct_backend .as_ref() .and_then(|backend| backend.devices.get(&node)) .and_then(|device| device.outputs.get(&crtc)) .map(|output| output.output.clone()); if identity.is_some_and(|output| state.output_has_animations(&output)) { request_frame(state, node, crtc); } else { arm_frame(state, node, crtc); } arm_animation_timer(state); } } } DrmEvent::Error(error) => { tracing::error!(?node, %error, "DRM event error"); } })?; let dmabuf_global = state .dmabuf_state .create_global_with_default_feedback::(&display_handle, &feedback); state.dmabuf_imports.register(dmabuf_global); state.dmabuf_feedback.register(dmabuf_global, feedback); state .direct_backend .as_mut() .expect("direct backend state remains initialized") .devices .insert( node, DirectDevice { _session_device: session_device, drm, gbm, renderer, render_node, outputs: HashMap::new(), notifier: registration, dmabuf_global, connected_outputs: Vec::new(), }, ); tracing::info!(?node, ?path, "initialized DRM/GBM device"); Ok(node) } pub(crate) fn sleep_locked_outputs(state: &mut Ferese) { if !state.session_lock.active() || !state.session_lock.sleeping { return; } if let Some(backend) = state.direct_backend.as_mut() { for device in backend.devices.values_mut() { for output in device.outputs.values_mut() { output.power.request(Power::Off); } } } apply_output_power(state); } // Apply runtime requests without changing output globals or workspace ownership. fn apply_output_power(state: &mut Ferese) { let Some(backend) = state .direct_backend .as_mut() .filter(|backend| backend.can_render()) else { return; }; for device in backend .devices .values_mut() .filter(|device| device.drm.is_active()) { for output in device.outputs.values_mut() { if !output.power.needs_power_off() || output.frame_pending { continue; } match output.surface.clear() { Ok(()) => { cancel_output_timers(&state.loop_handle, output); output.power.powered_off(); state.display_presentation.remove_output(&output.output); } Err(error) => { tracing::warn!(%error, output = %output.output.name(), "could not apply output power-off request") } } } } let retry = backend.active && backend .devices .values() .filter(|device| device.drm.is_active()) .any(|device| { device .outputs .values() .any(|output| output.power.needs_power_off() && !output.frame_pending) }); if !retry { if let Some(token) = backend.power_retry.take() { state.loop_handle.remove(token); } } else if backend.power_retry.is_none() { match state.loop_handle.insert_source( Timer::from_duration(Duration::from_secs(1)), |_, _, state| { if let Some(backend) = state.direct_backend.as_mut() { backend.power_retry = None; } apply_output_power(state); TimeoutAction::Drop }, ) { Ok(token) => backend.power_retry = Some(token), Err(error) => tracing::warn!(%error, "could not arm failed power-off retry"), } } state.refresh_idle_inhibition(); } pub(crate) fn wake_locked_outputs(state: &mut Ferese) { state.reset_animation_clock(); if let Some(backend) = state.direct_backend.as_mut() { for device in backend.devices.values_mut() { for output in device.outputs.values_mut() { if output.power.needs_wake() { output.surface.reset_buffer_ages(); } output.power.request(Power::On); } } } apply_output_power(state); render_all(state); } #[track_caller] pub fn render_all(state: &mut Ferese) { state.image_capture.invalidate_workspaces(); for output in state.space.outputs() { capture::changes::content_changed(output); } redraw(state, None, std::panic::Location::caller()); } #[track_caller] pub(crate) fn render_on(state: &mut Ferese, outputs: &[Output]) { for output in outputs { capture::changes::content_changed(output); } redraw(state, Some(outputs), std::panic::Location::caller()); } /// Consumer demand schedules a frame without invalidating retained scene pixels. #[track_caller] pub(crate) fn schedule_capture(state: &mut Ferese, outputs: &[Output]) { redraw(state, Some(outputs), std::panic::Location::caller()); } #[track_caller] pub(crate) fn render_cursor(state: &mut Ferese) { // Include nested outputs and both sides of an output crossing. This does not // invalidate the content revision used by cursor-hidden captures. for output in state.space.outputs() { capture::changes::cursor_changed(output); } let outputs = cursor_affected_outputs(state); redraw(state, Some(&outputs), std::panic::Location::caller()); } fn cursor_affected_outputs(state: &Ferese) -> Vec { state .direct_backend .as_ref() .into_iter() .flat_map(|backend| backend.devices.values()) .flat_map(|device| device.outputs.values()) .filter(|output| output.cursor_visible || redraw::cursor_on_output(state, &output.output)) .map(|output| output.output.clone()) .collect() } pub(crate) fn cursor_animation_visible(state: &Ferese) -> bool { if let Some(backend) = state.direct_backend.as_ref() { backend.can_render() && backend.devices.values().any(|device| { device.outputs.values().any(|output| { output.power.can_render() && redraw::cursor_on_output(state, &output.output) }) }) } else { state .space .outputs() .any(|output| redraw::cursor_on_output(state, output)) } } #[track_caller] pub(crate) fn render_surface( state: &mut Ferese, surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface, ) { use smithay::input::pointer::CursorImageStatus; use smithay::wayland::compositor::get_parent; let mut root = surface.clone(); while let Some(parent) = get_parent(&root) { root = parent; } if matches!(&state.cursor_status, CursorImageStatus::Surface(cursor) if cursor == &root) { render_cursor(state); return; } let capture_root = state .popups .find_popup(&root) .and_then(|popup| smithay::desktop::find_popup_root_surface(&popup).ok()) .unwrap_or_else(|| root.clone()); if let Some(workspace) = state .windows .id_for_surface(&capture_root) .and_then(|id| state.workspaces.workspace_for_window(id)) { state.image_capture.workspace_changed(workspace); } let outputs = state.surface_outputs(&root); render_on(state, &outputs); } fn redraw( state: &mut Ferese, selected: Option<&[Output]>, source: &'static std::panic::Location<'static>, ) { if let Some(transition) = state.desktop_transition.as_mut() { transition.redraw.extend( selected .map(|outputs| outputs.to_vec()) .unwrap_or_else(|| state.space.outputs().cloned().collect()), ); return; } if state .direct_backend .as_ref() .is_none_or(|backend| !backend.can_render()) { return; } let outputs = state .direct_backend .as_ref() .expect("DRM callbacks require an installed direct backend") .devices .iter() .flat_map(|(node, device)| { device .outputs .iter() .map(|(crtc, output)| (*node, *crtc, output.output.clone())) }) .collect::>(); // Include animations before advancing, so their final settled frame is // drawn too. Advancing shared state must not leave another output stale. let affected = outputs .iter() .map(|(_, _, output)| { let scene = state .direct_backend .as_ref() .expect("DRM callbacks require an installed direct backend") .scene_output(state, output); selected.is_none_or(|selected| selected.contains(output) || selected.contains(&scene)) || state.output_has_animations(&scene) || state.output_has_pending_visual_changes(&scene) }) .collect::>(); state.advance_animations(Instant::now()); let mut requested = 0; for ((node, crtc, output), affected) in outputs.iter().zip(affected) { if affected || state.output_has_animations(output) { request_frame(state, *node, *crtc); requested += 1; } } state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend") .scheduling_metrics .redraw(source, selected.is_none(), requested, outputs.len()); arm_animation_timer(state); } pub fn switch_vt(state: &mut Ferese, vt: i32) { let Some(backend) = state.direct_backend.as_mut() else { return; }; if let Err(error) = backend.session.change_vt(vt) { tracing::error!(vt, %error, "failed to switch virtual terminal"); } } pub(crate) fn set_lid_closed(state: &mut Ferese, closed: bool) { let Some(backend) = state.direct_backend.as_mut() else { return; }; // Even an unchanged switch observation supersedes an in-flight DBus read. if backend.lid.observe(closed) { tracing::info!(closed, "laptop lid state changed"); reconcile_outputs(state, false); } } pub(crate) fn system_resumed(state: &mut Ferese) { if state .direct_backend .as_ref() .is_some_and(|backend| backend.active) { let seat = state.seat.clone(); state.idle_notifier_state.notify_activity(&seat); } lid::request_refresh(state, true); } pub(crate) fn reconcile_after_monitor_recovery(state: &mut Ferese) { lid::request_reconcile(state); } fn internal_connector(interface: connector::Interface) -> bool { matches!( interface, connector::Interface::EmbeddedDisplayPort | connector::Interface::LVDS | connector::Interface::DSI ) } #[cfg(test)] fn lid_hides_panel(closed: bool, external_available: bool) -> bool { closed && external_available } // Return the sampled scene's activity so dispatch does not rescan the windows. fn render_output( state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: crate::frame_scheduler::FramePlan, ) -> bool { if state.session_lock.active() && state.session_lock.sleeping { sleep_locked_outputs(state); let asleep = state .direct_backend .as_ref() .and_then(|backend| backend.devices.get(&node)) .and_then(|device| device.outputs.get(&crtc)) .is_none_or(|output| !output.power.can_render() || output.frame_pending); if asleep { return false; } } let missed_deadlines = state .direct_backend .as_ref() .and_then(|backend| backend.devices.get(&node)) .and_then(|device| device.outputs.get(&crtc)) .map_or(0, |output| output.scheduler.missed_deadlines); let scene_output = state.direct_backend.as_ref().map(|backend| { let physical = &backend.devices[&node].outputs[&crtc].output; backend.scene_output(state, physical) }); let Some(mut device) = state .direct_backend .as_mut() .and_then(|backend| backend.devices.remove(&node)) else { return false; }; let Some(mut output) = device.outputs.remove(&crtc) else { restore_device(state, node, device); return false; }; if !device.drm.is_active() || !output.power.can_render() || output.frame_pending { device.outputs.insert(crtc, output); restore_device(state, node, device); return false; } let render_started = Instant::now(); let now = monotonic_now(); let horizon = output .scheduler .forecast_time(now, plan) .map_or(Duration::ZERO, |target| target.saturating_sub(now)); let scene_output = scene_output.unwrap_or_else(|| output.output.clone()); let frame = state.sample_frame(&scene_output, horizon); let rendered = { (|| -> Result> { let elements = if output.mirror_source.is_some() { mirror::elements( state, &mut device.renderer, &scene_output, &frame, &mut output, )? } else { sampled_output_elements(state, &mut device.renderer, &output.output, true, &frame) }; let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale()); let fullscreen = state .output_has_fullscreen_for_frame(&output.output, frame.overview.is_presenting()); let locked = state.session_lock.active(); let flags = planes::frame_flags(fullscreen, frame.animating, locked); output .diagnostics .attempt(fullscreen, frame.animating, locked); let result = output .surface .render_frame( &mut device.renderer, &elements, if output.mirror_source.is_some() { [0.0, 0.0, 0.0, 1.0] } else { [0.035, 0.04, 0.055, 1.0] }, flags, ) .inspect_err(|_| output.diagnostics.render_failed())?; let primary_format = match &result.primary_element { PrimaryPlaneElement::Swapchain(primary) => Some(primary.buffer().format()), PrimaryPlaneElement::Element(element) => { match element.underlying_storage(&mut device.renderer) { Some(UnderlyingStorage::Wayland(buffer)) => { smithay::wayland::dmabuf::get_dmabuf(buffer) .ok() .map(|buffer| buffer.format()) } _ => None, } } }; let feedback_candidates = feedback::candidates( &mut device.renderer, &elements, &output.output, flags.contains(FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT), ); if result.needs_sync() && let PrimaryPlaneElement::Swapchain(primary) = &result.primary_element { primary .sync .wait() .inspect_err(|_| output.diagnostics.sync_failed())?; } if result.is_empty { output.diagnostics.no_damage(); let capture_started = output.diagnostics.capture_start(|| { [true, false] .into_iter() .any(|cursor| state.has_pending_screencopy(&output.output, cursor)) }); output.dmabuf_feedback.observed_frame( primary_format, &result.states, feedback_candidates, ); if output.mirror_source.is_some() { capture::capture_mirror( state, &mut device.renderer, &scene_output, &frame, &mut output, ); } else { capture::capture_output( state, &mut device.renderer, &mut output.capture_texture, &output.output, &frame, ); } output.diagnostics.capture_finished(capture_started); output .render_metrics .record_no_damage(render_started.elapsed(), missed_deadlines); return Ok(false); } // Count GPU composition damage; direct scanout and cursor-only // updates do not redraw the primary swapchain. let (damage, primary_commit) = match &result.primary_element { PrimaryPlaneElement::Swapchain(primary) => { let damage = primary.damage.damage_since(output.primary_commit); let damage = damage.map_or_else( || { vec![smithay::utils::Rectangle::from_size( output .output .current_mode() .expect("a rendered output must have a current mode") .size, )] }, |damage| { damage .iter() .map(|rect| { smithay::utils::Rectangle::new( (rect.loc.x, rect.loc.y).into(), (rect.size.w, rect.size.h).into(), ) }) .collect() }, ); (damage, Some(primary.damage.current_commit())) } PrimaryPlaneElement::Element(_) => (Vec::new(), None), }; let presentation = if output.mirror_source.is_some() { OutputPresentationFeedback::new(&output.output) } else { crate::presentation::take_output_feedback( state, &output.output, &result.states, Kind::Vsync | Kind::HwClock | Kind::HwCompletion, ) }; let primary_outcome = PrimaryOutcome::classify( matches!(result.primary_element, PrimaryPlaneElement::Element(_)), !damage.is_empty(), ); let cursor_plane = result.cursor_element.is_some(); let visibility = result.states.clone(); drop(result); let queued = output.surface.queue_frame(presentation); output.diagnostics.queue_result( &queued, primary_outcome, cursor_plane, flags.contains(FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT), ); capture::after_queue(queued, || { output.dmabuf_feedback.observed_frame( primary_format, &visibility, feedback_candidates, ); if output.mirror_source.is_none() { state .display_presentation .queued(&output.output, &visibility); } output.primary_commit = primary_commit; output.render_metrics.record_frame( render_started.elapsed(), &damage, missed_deadlines, effects, ); output.frame_pending = true; if !recovery::arm_completion(&state.loop_handle, node, crtc, &mut output) { recovery::reconcile_device(state, node); } output.lock_frame_pending = state.session_lock.active(); let capture_started = output.diagnostics.capture_start(|| { [true, false] .into_iter() .any(|cursor| state.has_pending_screencopy(&output.output, cursor)) }); if output.mirror_source.is_some() { capture::capture_mirror( state, &mut device.renderer, &scene_output, &frame, &mut output, ); } else { capture::capture_output( state, &mut device.renderer, &mut output.capture_texture, &output.output, &frame, ); } output.diagnostics.capture_finished(capture_started); })?; Ok(true) })() }; output.diagnostics.report_if_due(); // Keep the footprint of the last successful scene, including queued // cursor planes. Requests do not read the pointer while a grab holds it. if rendered.is_ok() { output.cursor_visible = redraw::cursor_on_output(state, &output.output); } let submitted = rendered.as_ref().is_ok_and(|submitted| *submitted); record_frame_schedule(&mut output, plan, render_started.elapsed(), submitted); if submitted && let Some(token) = output.callback_timer.take() { state.loop_handle.remove(token); } match rendered { Ok(true) => tracing::trace!(?node, ?crtc, "queued DRM frame"), Ok(false) => (), Err(error) => tracing::error!(?node, ?crtc, %error, "failed to render DRM frame"), } device.outputs.insert(crtc, output); restore_device(state, node, device); if !submitted && scene_output == state .direct_backend .as_ref() .expect("DRM callbacks require an installed direct backend") .devices[&node] .outputs[&crtc] .output { schedule_frame_callbacks(state, node, crtc, plan.present_at); } frame.animating } fn restore_device(state: &mut Ferese, node: DrmNode, device: DirectDevice) { if let Some(backend) = state.direct_backend.as_mut() { backend.devices.insert(node, device); } } fn direct_node_for_device(state: &Ferese, device_id: libc::dev_t) -> Option { state .direct_backend .as_ref()? .devices .keys() .copied() .find(|node| node.dev_id() == device_id) } fn rescan_device(state: &mut Ferese, node: DrmNode) -> Result { let Some(backend) = state .direct_backend .as_mut() .filter(|backend| backend.active) else { return Err(()); }; let Some(mut device) = backend.devices.remove(&node) else { return Err(()); }; let scan = select_outputs( &device, &backend.monitors, &backend.desired_outputs, backend.low_power, ); let result = scan .map_err(|error| error.to_string()) .and_then(|scan| transaction::apply_device(state, node, &mut device, scan)); if let Err(error) = &result { tracing::warn!(?node, %error, "output transaction failed; restored known-good outputs where possible"); state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend") .output_error = Some(error.clone()); transaction::update_applied_info(&mut device); } restore_device(state, node, device); Ok(result.is_ok()) } fn refresh_connected_info(state: &mut Ferese) { let backend = state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend"); for device in backend.devices.values_mut() { if let Ok(scan) = select_outputs( device, &backend.monitors, &backend.desired_outputs, backend.low_power, ) { device.connected_outputs = scan.connected_outputs; } transaction::update_applied_info(device); for info in &mut device.connected_outputs { info.profile = backend.applied_outputs.profile.clone(); } } let mut inventory = backend .devices .values() .flat_map(|device| device.connected_outputs.clone()) .collect::>(); for previous in &backend.connected_outputs { if !inventory .iter() .any(|output| output.identity == previous.identity) { let mut missing = previous.clone(); missing.connected = false; missing.enabled = false; missing.requested_enabled = false; missing.current_mode = None; inventory.push(missing); } } let identities = inventory .iter() .map(|output| output.identity.clone()) .collect::>(); backend.connected_outputs = inventory; for identity in identities { state.ensure_output_identity(&identity); } } fn remember_applied_configuration(state: &mut Ferese) { let backend = state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend"); if backend.topology.reconciling { // Applied positions are read only after the coherent logical publish. backend.topology.remember_applied = true; return; } let mut applied = backend.desired_outputs.clone(); for desired in &mut applied.outputs { let monitor = backend .monitors .iter() .find(|monitor| monitor.key == desired.key); let output = monitor.and_then(|monitor| { backend .devices .values() .flat_map(|device| device.outputs.values()) .find(|output| output.identity == monitor.identity) }); desired.settings.enabled = output.is_some(); if let Some(output) = output { desired.settings = output.settings.clone(); let (width, height) = output.mode.size(); desired.settings.mode = Some(OutputModeRequest { width, height, refresh_millihertz: Some(OutputMode::from(output.mode).refresh as u32), }); let position = output.output.current_location(); desired.settings.position = Some([position.x, position.y]); desired.mirror_source = output.mirror_source.clone(); } } backend.applied_outputs = applied; } fn validate_desired_outputs(state: &Ferese) -> Result<(), String> { let backend = state .direct_backend .as_ref() .ok_or("direct backend unavailable")?; validate_output_configuration(backend, &backend.desired_outputs).map(|_| ()) } fn validate_output_configuration( backend: &DirectBackendState, desired: &DesiredOutputConfiguration, ) -> Result { let scans = backend .devices .values() .map(|device| { select_outputs(device, &backend.monitors, desired, backend.low_power) .map(|scan| (device, scan)) .map_err(|error| error.to_string()) }) .collect::, _>>()?; let selected = scans .iter() .flat_map(|(device, scan)| { let device_key = drm_device_key(&device.drm); scan.selections .iter() .map(move |selection| format!("{device_key}/{}", selection.connector)) }) .collect::>(); validate_output_plan(desired, &selected, || { for (device, scan) in &scans { transaction::validate_device(device, scan).map_err(|error| error.to_string())?; } Ok(()) })?; Ok(selected.len()) } /// Keep plan completeness separate from the hardware probe so a partial scan /// cannot be accepted merely because the driver accepts its selected subset. fn validate_output_plan( desired: &DesiredOutputConfiguration, selected: &[String], test_hardware: impl FnOnce() -> Result<(), String>, ) -> Result<(), String> { validate_selected_outputs(&desired.outputs, selected)?; test_hardware() } fn validate_selected_outputs<'a>( outputs: impl IntoIterator, selected: &[String], ) -> Result<(), String> { for output in outputs.into_iter().filter(|output| output.settings.enabled) { if !selected.contains(&output.key) { return Err(format!( "enabled output {} has no usable CRTC assignment", output.key )); } } Ok(()) } fn remove_device(state: &mut Ferese, node: DrmNode) { let Some(device) = state .direct_backend .as_mut() .and_then(|backend| backend.devices.remove(&node)) else { return; }; state.loop_handle.remove(device.notifier); state.dmabuf_imports.remove(device.dmabuf_global); state.dmabuf_feedback.remove(device.dmabuf_global); state .dmabuf_state .disable_global::(&state.display_handle, &device.dmabuf_global); let context = device.renderer.context_id().erased(); state.wallpaper.forget_context(&context); state.render.forget_context(&context); state.capture_render.forget_context(&context); for (crtc, mut output) in device.outputs { cancel_output_timers(&state.loop_handle, &mut output); if let Some(global) = output.global.take() { state.display_handle.disable_global::(global); } state.unregister_output(&output.output); if let Some(backend) = state.direct_backend.as_mut() { backend.presentation.remove(&(node, crtc)); } } tracing::info!(?node, "removed DRM device"); } fn refresh_duration(state: &Ferese, node: DrmNode, crtc: crtc::Handle) -> Duration { state .direct_backend .as_ref() .and_then(|backend| backend.devices.get(&node)) .and_then(|device| device.outputs.get(&crtc)) .and_then(|output| output.output.current_mode()) .filter(|mode| mode.refresh > 0) .map(|mode| Duration::from_nanos(1_000_000_000_000_u64 / mode.refresh as u64)) .unwrap_or_else(|| Duration::from_millis(16)) } fn refresh_duration_from_mode(mode: DrmMode) -> Duration { Duration::from_nanos(1_000_000_000_000 / OutputMode::from(mode).refresh.max(1) as u64) } fn animation_fallbacks(state: &mut Ferese) -> Vec<(DrmNode, crtc::Handle, Instant)> { let now = Instant::now(); let outputs = state .direct_backend .as_ref() .into_iter() .flat_map(|backend| backend.devices.iter()) .flat_map(|(node, device)| { device.outputs.iter().map(move |(crtc, output)| { (*node, *crtc, output.output.clone(), device.drm.is_active()) }) }) .collect::>(); let mut fallbacks = Vec::new(); for (node, crtc, identity, device_active) in outputs { let enabled = device_active && !state.session_lock.sleeping && state .direct_backend .as_ref() .is_some_and(|backend| backend.can_render()); let scene = state .direct_backend .as_ref() .expect("DRM callbacks require an installed direct backend") .scene_output(state, &identity); let animating = enabled && state.output_has_animations(&scene); let backend = state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend"); let output = backend .devices .get_mut(&node) .expect("the selected DRM device remains registered during this operation") .outputs .get_mut(&crtc) .expect("the selected DRM device remains registered during this operation"); // Preserve recovery of a requested final frame if another output // advances this animation to rest before the watchdog is dispatched. let active = enabled && (animating || (output.animation_fallback.waiting_for_frame() && output.scheduler.has_pending_request())); let chained = output.render_timer.is_some() || output.frame_pending; if active && chained { backend.scheduling_metrics.chained_animation_observations += 1; } if let Some(deadline) = output.animation_fallback.deadline( now, refresh_duration_from_mode(output.mode), active && output.power.can_render(), chained, ) { fallbacks.push((node, crtc, deadline)); } } fallbacks } // Pending buffers are events with deadlines, not visual animations. Recompute // from current output identities and waits so commit, replacement and teardown // cancel stale wakeups, including mirrored and sleeping outputs. fn resize_deadlines(state: &Ferese) -> Vec<(Output, Instant)> { let Some(backend) = state .direct_backend .as_ref() .filter(|backend| backend.can_render()) else { return Vec::new(); }; if state.session_lock.sleeping { return Vec::new(); } backend .devices .values() .filter(|device| device.drm.is_active()) .flat_map(|device| device.outputs.values()) .filter(|output| output.power.can_render()) .filter_map(|output| { let scene = backend.scene_output(state, &output.output); Some(( output.output.clone(), state.start_time + state.output_resize_deadline(&scene)?, )) }) .collect() } fn arm_animation_timer(state: &mut Ferese) { let deadline = animation_fallbacks(state) .iter() .map(|(_, _, deadline)| *deadline) .chain( resize_deadlines(state) .into_iter() .map(|(_, deadline)| deadline), ) .min(); let Some(backend) = state.direct_backend.as_mut() else { return; }; if backend.animation_deadline == deadline { return; } if let Some(token) = backend.animation_timer.take() { state.loop_handle.remove(token); backend.scheduling_metrics.fallback_cancels += 1; } backend.animation_deadline = None; let Some(deadline) = deadline else { return; }; match state .loop_handle .insert_source(Timer::from_deadline(deadline), |_, _, state| { if let Some(backend) = state.direct_backend.as_mut() { backend.animation_timer = None; backend.animation_deadline = None; backend.scheduling_metrics.fallback_wakes += 1; } let now = Instant::now(); let expired_outputs = resize_deadlines(state) .into_iter() .filter_map(|(output, deadline)| (deadline <= now).then_some(output)) .collect::>(); if !expired_outputs.is_empty() { // Retire waits before rearming, even if a DRM submission is // in flight. redraw advances the shared clock and requests the // released outputs plus any final animation frames elsewhere. render_on(state, &expired_outputs); } // Revalidate identities, power state, activity and normal frame chains // at dispatch. Disconnected/replaced outputs never get a stale request. for (node, crtc, deadline) in animation_fallbacks(state) { if deadline <= Instant::now() { state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend") .scheduling_metrics .fallback_recoveries += 1; state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend") .devices .get_mut(&node) .expect( "the selected DRM device must remain registered during this operation", ) .outputs .get_mut(&crtc) .expect("the selected CRTC must remain registered during this operation") .animation_fallback = AnimationFallback::default(); request_frame(state, node, crtc); } } arm_animation_timer(state); TimeoutAction::Drop }) { Ok(token) => { let backend = state .direct_backend .as_mut() .expect("DRM callbacks require an installed direct backend"); backend.animation_timer = Some(token); backend.animation_deadline = Some(deadline); backend.scheduling_metrics.fallback_arms += 1; } Err(error) => tracing::warn!(%error, "could not schedule DRM animation fallback"), } } fn schedule_frame_callbacks( state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, target: Duration, ) { if state.session_lock.sleeping { return; } let now = Instant::now(); let timestamp = monotonic_now(); let Some(output) = state .direct_backend .as_ref() .filter(|backend| backend.can_render()) .and_then(|backend| backend.devices.get(&node)) .filter(|device| device.drm.is_active()) .and_then(|device| device.outputs.get(&crtc)) else { return; }; if output.mirror_source.is_some() || output.callback_timer.is_some() || output.frame_pending { return; } let identity = output.output.clone(); let Some(callback_at) = output.scheduler.callback_deadline(timestamp, target) else { return; }; if callback_at <= timestamp { deliver_frame_callbacks(state, node, crtc, &identity); return; } let deadline = now + callback_at.saturating_sub(timestamp); match state .loop_handle .insert_source(Timer::from_deadline(deadline), move |_, _, state| { deliver_frame_callbacks(state, node, crtc, &identity); TimeoutAction::Drop }) { Ok(token) => { if let Some(output) = state .direct_backend .as_mut() .and_then(|backend| backend.devices.get_mut(&node)) .and_then(|device| device.outputs.get_mut(&crtc)) { output.callback_timer = Some(token); } } Err(error) => tracing::warn!(%error, "could not schedule DRM frame callbacks"), } } fn deliver_frame_callbacks( state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identity: &Output, ) { let Some(backend) = state.direct_backend.as_mut() else { return; }; let Some(device) = backend.devices.get_mut(&node) else { return; }; let Some(output) = device .outputs .get_mut(&crtc) .filter(|output| &output.output == identity) else { return; }; output.callback_timer = None; if !backend.active || backend.topology.reconciling || backend.lid.pending() || !device.drm.is_active() || !output.power.can_render() || state.session_lock.sleeping || output.frame_pending { return; } let timestamp = monotonic_now(); let Some(next) = output.scheduler.callback_deadline(timestamp, timestamp) else { return; }; if next > timestamp { schedule_frame_callbacks(state, node, crtc, next); return; } output.scheduler.callback_sent(timestamp); send_frame_callbacks(state, identity); } fn send_frame_callbacks(state: &mut Ferese, output: &Output) { let eligible = state.callback_outputs(); if state.session_lock.active() { state.lock_frame_callbacks(output); return; } let visible_windows = state.space.elements().filter(|window| { state .windows .ids() .get(*window) .is_some_and(|id| state.window_belongs_to_output(*id, output)) }); for window in visible_windows { window.send_frame(output, state.start_time.elapsed(), None, |surface, _| { eligible.get(&surface.into()).cloned() }); } let layers = layer_map_for_output(output) .layers() .cloned() .collect::>(); for layer in &layers { layer.send_frame(output, state.start_time.elapsed(), None, |surface, _| { eligible.get(&surface.into()).cloned() }); } state.send_cursor_frame(output); state.space.refresh(); state.popups.cleanup(); layer_map_for_output(output).cleanup(); if let Err(error) = state.display_handle.flush_clients() { tracing::debug!(%error, "failed to flush clients after DRM frame"); } } fn select_outputs( device: &DirectDevice, monitors: &[Monitor], desired: &DesiredOutputConfiguration, low_power: bool, ) -> io::Result { let drm = &device.drm; let resources = drm.resource_handles()?; let connected = resources .connectors() .iter() .map(|handle| drm.get_connector(*handle, true)) .collect::>>()? .into_iter() .filter(|connector| connector.state() == connector::State::Connected) .collect::>(); let device_key = drm_device_key(drm); let mut selections = Vec::new(); let mut connected_outputs = Vec::new(); let mut candidates = Vec::new(); let mut requests = Vec::new(); for connector in connected { let key = format!("{device_key}/{}", connector); let monitor = monitors .iter() .find(|monitor| monitor.key == key) .ok_or_else(|| io::Error::other("connector topology changed during reconciliation"))?; let desired_output = desired .outputs .iter() .find(|output| output.key == key) .ok_or_else(|| io::Error::other("connector missing from desired configuration"))?; let identity = monitor.identity.clone(); let settings = desired_output.settings.clone(); let selected_mode = settings .enabled .then(|| { let normal = select_mode(&connector, settings.mode)?; if settings.auto_refresh && low_power { let modes = connector .modes() .iter() .map(|mode| { let (width, height) = mode.size(); (width, height, OutputMode::from(*mode).refresh) }) .collect::>(); super::power::low_refresh_mode(normal.size(), &modes) .map(|index| connector.modes()[index]) .or(Some(normal)) } else { Some(normal) } }) .flatten(); if settings.enabled && selected_mode.is_none() && settings.mode.is_some() { return Err(io::Error::other(format!( "requested mode for {} is unavailable", connector ))); } if let Some(mode) = selected_mode { let size = output_transform(settings.transform).transform_size(OutputMode::from(mode).size); let width = (size.w as f64 / settings.scale).round(); let height = (size.h as f64 / settings.scale).round(); if width < 1.0 || height < 1.0 || width > i32::MAX as f64 || height > i32::MAX as f64 { return Err(io::Error::other(format!( "scale for {} creates invalid logical geometry", connector ))); } if let Some([x, y]) = settings.position && (x.checked_add(width as i32).is_none() || y.checked_add(height as i32).is_none()) { return Err(io::Error::other(format!( "position for {} overflows logical geometry", connector ))); } } connected_outputs.push(ConnectedOutputInfo { connector: connector.to_string(), connected: true, identity: identity.clone(), internal: monitor.internal, requested_enabled: settings.enabled, mirror_source: desired_output.mirror_source.clone(), enabled: settings.enabled, profile: desired.profile.clone(), requested_profile: desired.profile.clone(), auto_refresh: settings.auto_refresh, physical_size: connector.size(), current_mode: selected_mode.map(connected_mode_info), available_modes: connector .modes() .iter() .copied() .map(connected_mode_info) .collect(), scale: settings.scale, transform: settings.transform, configured_position: settings.position, }); if !settings.enabled { tracing::info!(output = %connector, "output disabled by active profile"); continue; } let Some(mode) = selected_mode else { continue; }; let applied = device .outputs .iter() .find(|(_, output)| output.connector == connector.handle()) .map(|(crtc, _)| *crtc); let kernel = connector .current_encoder() .and_then(|handle| drm.get_encoder(handle).ok()) .and_then(|encoder| encoder.crtc()); let mut compatible = Vec::new(); for handle in connector.encoders() { if let Ok(encoder) = drm.get_encoder(*handle) { for crtc in resources.filter_crtcs(encoder.possible_crtcs()) { if !compatible.contains(&crtc) { compatible.push(crtc); } } } } requests.push(crtc_assignment::Candidates { applied, kernel, compatible, }); candidates.push(( connector, mode, settings, identity, desired_output.mirror_source.clone(), )); } for ((connector, mode, settings, identity, mirror_source), crtc) in candidates .into_iter() .zip(crtc_assignment::assign(&requests)) { if let Some(crtc) = crtc { selections.push(OutputSelection { connector, crtc, mode, settings, identity, mirror_source, }); } } let prefix = format!("{device_key}/"); let selected = selections .iter() .map(|selection| format!("{prefix}{}", selection.connector)) .collect::>(); // Also guard the rescan immediately before applying a device transaction: // hotplug can invalidate the earlier configuration-wide validation. validate_selected_outputs( desired .outputs .iter() .filter(|output| output.key.starts_with(&prefix)), &selected, ) .map_err(io::Error::other)?; selections.sort_by_key(|selection| selection.settings.position.is_none()); Ok(OutputScan { selections, connected_outputs, }) } fn connected_mode_info(mode: DrmMode) -> ConnectedModeInfo { let (width, height) = mode.size(); ConnectedModeInfo { width, height, refresh_millihertz: OutputMode::from(mode).refresh, preferred: mode.mode_type().contains(ModeTypeFlags::PREFERRED), } } fn select_mode( connector: &connector::Info, requested: Option, ) -> Option { let fallback = || { connector .modes() .iter() .find(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED)) .or_else(|| connector.modes().first()) .copied() }; let Some(requested) = requested else { return fallback(); }; let matching = connector .modes() .iter() .filter(|mode| mode.size() == (requested.width, requested.height)); if let Some(refresh) = requested.refresh_millihertz { matching .min_by_key(|mode| { OutputMode::from(**mode) .refresh .unsigned_abs() .abs_diff(refresh) }) .filter(|mode| { OutputMode::from(**mode) .refresh .unsigned_abs() .abs_diff(refresh) <= 1_000 }) .copied() } else { matching .max_by_key(|mode| OutputMode::from(**mode).refresh) .copied() } } fn create_direct_output( state: &mut Ferese, drm: &mut DrmDevice, gbm: &GbmDevice, renderer: &GlesRenderer, selection: OutputSelection, publish: bool, ) -> Result> { let OutputSelection { connector, crtc, mode, settings, identity, mirror_source, } = selection; let drm_surface = drm.create_surface(crtc, mode, &[connector.handle()])?; let allocator = GbmAllocator::new( gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT, ); let node = DrmNode::from_file(gbm.as_fd())?; let render_node = node .node_with_type(NodeType::Render) .and_then(Result::ok) .unwrap_or(node); let mut surface = DrmCompositor::new( OutputModeSource::Static { size: OutputMode::from(mode).size, scale: settings.scale.into(), transform: output_transform(settings.transform), }, drm_surface, None, allocator, GbmFramebufferExporter::new(gbm.clone(), Some(render_node).into()), [Fourcc::Argb8888, Fourcc::Abgr8888], renderer.dmabuf_formats(), drm.cursor_size(), Some(gbm.clone()), )?; // Publish only after DRM/GBM creation succeeds; failed activation must not // leave a phantom output/workspace that could receive evacuated windows. let (output, global) = create_output( state, &connector, mode, identity.clone(), &settings, publish && mirror_source.is_none(), ); surface.set_output_mode_source((&output).into()); let render_metrics = RenderMetrics::from_environment(output.name()); let diagnostics = Diagnostics::from_environment(output.name()); let dmabuf_feedback = feedback::OutputFeedback::new( render_node.dev_id(), node.dev_id(), renderer.dmabuf_formats(), surface.surface().plane_info().formats.clone(), ); tracing::info!(?crtc, connector = %connector, "initialized DRM output"); Ok(DirectOutput { gamma: Default::default(), internal: internal_connector(connector.interface()), connector: connector.handle(), mode, settings: settings.clone(), output, global, identity, mirror_source, mirror_texture: None, mirror_capture_texture: Default::default(), mirror_canvas: None, mirror_id: smithay::backend::renderer::element::Id::new(), mirror_commit: CommitCounter::default(), surface, primary_commit: None, capture_texture: Default::default(), render_metrics, diagnostics, dmabuf_feedback, frame_pending: false, completion: Default::default(), completion_timer: None, animation_fallback: AnimationFallback::default(), cursor_visible: false, power: OutputPower::default(), scheduler: crate::frame_scheduler::FrameScheduler::new(refresh_duration_from_mode(mode)), render_timer: None, callback_timer: None, lock_frame_pending: false, }) } fn create_output( state: &mut Ferese, connector: &connector::Info, mode: DrmMode, identity: String, settings: &OutputSettings, publish: bool, ) -> (Output, Option) { let name = connector.to_string(); let physical_size = connector.size().unwrap_or((0, 0)); let edid_identity = identity .strip_prefix("edid:") .map(|value| value.split(':').collect::>()); let output = Output::new( name, PhysicalProperties { serial_number: String::new(), size: (physical_size.0 as i32, physical_size.1 as i32).into(), subpixel: Subpixel::from(connector.subpixel()), make: edid_identity .as_ref() .and_then(|parts| parts.first()) .copied() .unwrap_or("Unknown") .into(), model: edid_identity .as_ref() .and_then(|parts| parts.get(1)) .copied() .unwrap_or("Unknown") .into(), }, ); let output_mode = OutputMode::from(mode); let global = publish.then(|| output.create_global::(&state.display_handle)); for mode in connector.modes().iter().copied().map(OutputMode::from) { output.add_mode(mode); } output.set_preferred(output_mode); let position = settings.position.unwrap_or_else(|| { let x = state .space .outputs() .filter_map(|output| state.space.output_geometry(output)) .map(|geometry| geometry.loc.x + geometry.size.w) .max() .unwrap_or(0); [x, 0] }); output.change_current_state( Some(output_mode), Some(output_transform(settings.transform)), Some(Scale::Fractional(settings.scale)), Some((position[0], position[1]).into()), ); if publish { state.space.map_output(&output, (position[0], position[1])); state.register_output(&output, identity); } (output, global) } fn output_transform(transform: OutputTransform) -> Transform { match transform { OutputTransform::Normal => Transform::Normal, OutputTransform::Rotate90 => Transform::_90, OutputTransform::Rotate180 => Transform::_180, OutputTransform::Rotate270 => Transform::_270, OutputTransform::Flipped => Transform::Flipped, OutputTransform::Flipped90 => Transform::Flipped90, OutputTransform::Flipped180 => Transform::Flipped180, OutputTransform::Flipped270 => Transform::Flipped270, } } fn connector_edid(drm: &DrmDevice, connector: &connector::Info) -> Option> { drm.get_properties(connector.handle()) .ok() .and_then(|properties| { properties.iter().find_map(|(handle, raw)| { let info = drm.get_property(*handle).ok()?; if info.name().to_bytes() != b"EDID" { return None; } match info.value_type().convert_value(*raw) { property::Value::Blob(blob) if blob != 0 => drm.get_property_blob(blob).ok(), _ => None, } }) }) } fn drm_device_key(drm: &DrmDevice) -> String { let node = DrmNode::from_file(drm.as_fd()).expect("DRM descriptor has a device identity"); let path = node .dev_path() .and_then(|path| { path.file_name() .map(|name| Path::new("/sys/class/drm").join(name).join("device")) }) .and_then(|path| std::fs::canonicalize(path).ok()); path.map_or_else( || format!("gpu-{}", node.dev_id()), |path| path.to_string_lossy().into_owned(), ) } fn resolve_output_positions( backend: &DirectBackendState, desired: &mut DesiredOutputConfiguration, ) -> Result<(), String> { let mut placements = Vec::new(); for device in backend.devices.values() { let scan = select_outputs(device, &backend.monitors, desired, backend.low_power) .map_err(|error| error.to_string())?; for selection in scan.selections { let size = output_transform(selection.settings.transform) .transform_size(OutputMode::from(selection.mode).size); let key = format!("{}/{}", drm_device_key(&device.drm), selection.connector); let previous = device .outputs .values() .find(|output| output.identity == selection.identity) .filter(|output| output.mirror_source.is_none()) .map(|output| { let position = output.output.current_location(); [position.x, position.y] }); placements.push(crate::output_policy::Placement { key, size: ( (size.w as f64 / selection.settings.scale).round() as i32, (size.h as f64 / selection.settings.scale).round() as i32, ), previous, }); } } crate::output_policy::arrange_positions(desired, &placements) } fn refresh_monitor_policy(state: &mut Ferese) -> Result<(), String> { let backend = state .direct_backend .as_mut() .ok_or("direct backend unavailable")?; let mut discovered = Vec::new(); for device in backend.devices.values() { let drm = &device.drm; let key = drm_device_key(drm); for handle in drm .resource_handles() .map_err(|error| error.to_string())? .connectors() { let connector = drm .get_connector(*handle, true) .map_err(|error| error.to_string())?; if connector.state() != connector::State::Connected { continue; } let edid = connector_edid(drm, &connector); discovered.push((format!("{key}/{}", connector), connector, edid)); } } discovered.sort_by(|a, b| a.0.cmp(&b.0)); let identities = backend.identities.resolve( &discovered .iter() .map(|(key, _, edid)| (key.clone(), edid.as_deref().and_then(Identity::from_edid))) .collect::>(), ); backend.monitors = discovered .into_iter() .zip(identities) .map(|((key, connector, edid), identity)| { let mut aliases = vec![format!("drm:{connector}")]; if let Some(edid) = edid { aliases.push(format!("drm-edid:{:016x}", stable_hash(&edid))); } Monitor { key, identity, connector: connector.to_string(), aliases, internal: internal_connector(connector.interface()), usable: !connector.modes().is_empty(), } }) .collect(); if backend.confirmation.is_some() { let previous_keys = backend .applied_outputs .outputs .iter() .map(|output| &output.key) .collect::>(); let current_keys = backend .monitors .iter() .map(|monitor| &monitor.key) .collect::>(); if previous_keys != current_keys || backend.confirmation_lid != backend.lid.closed() { backend.confirmation = None; if let Some(token) = backend.confirmation_timer.take() { state.loop_handle.remove(token); } } } let previous_override = ( backend.manual_outputs.profile.clone(), backend.manual_outputs.internal, backend.manual_outputs.layout, ); backend.manual_outputs.observe( &backend.monitors, backend.lid.closed(), &state.output_profiles, ); if previous_override != ( backend.manual_outputs.profile.clone(), backend.manual_outputs.internal, backend.manual_outputs.layout, ) { backend.confirmation = None; if let Some(token) = backend.confirmation_timer.take() { state.loop_handle.remove(token); } } let mut desired = crate::output_policy::select_profile( &backend.monitors, backend.lid.closed(), &state.output_profiles, &backend.manual_outputs, ); resolve_output_positions(backend, &mut desired)?; tracing::debug!(profile = ?desired.profile, changed = ?crate::output_policy::changed_outputs(&backend.desired_outputs, &desired), "selected monitor configuration"); if let Some(previous) = backend.revert_outputs.take() { desired = crate::output_policy::restore_configuration(&previous, &backend.monitors); resolve_output_positions(backend, &mut desired)?; } backend.desired_outputs = desired; Ok(()) } fn stable_hash(bytes: &[u8]) -> u64 { const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325; const FNV_PRIME: u64 = 0x0000_0100_0000_01b3; bytes.iter().fold(FNV_OFFSET, |hash, byte| { (hash ^ u64::from(*byte)).wrapping_mul(FNV_PRIME) }) } impl DirectBackendState { pub fn record_presentation( &mut self, node: DrmNode, crtc: crtc::Handle, time: DrmEventTime, sequence: u32, ) -> Option { let missed = self .devices .get(&node) .and_then(|device| device.outputs.get(&crtc)) .map_or(0, |output| output.scheduler.missed_deadlines); self.presentation.get_mut(&(node, crtc)).and_then(|clock| { clock.missed_deadlines = missed; clock.record(time, sequence) }) } } impl PresentationClock { fn reset_timing(&mut self) { self.last_presentation = None; } fn set_refresh(&mut self, refresh_millihertz: i32) { if refresh_millihertz > 0 { self.refresh_interval = Some(Duration::from_nanos( 1_000_000_000_000_u64 / refresh_millihertz as u64, )); } } fn record(&mut self, time: DrmEventTime, sequence: u32) -> Option { let DrmEventTime::Monotonic(time) = time else { tracing::warn!( sequence, "DRM driver reported a realtime page-flip timestamp" ); return None; }; let delta = self .last_presentation .map(|previous| time.saturating_sub(previous)); self.last_presentation = Some(time); self.presented_frames = self.presented_frames.saturating_add(1); delta } } #[cfg(test)] mod tests { use std::time::SystemTime; use super::*; fn three_monitor_plan(layout: crate::config::OutputLayout) -> DesiredOutputConfiguration { let monitors = ["A", "B", "C"].map(|key| Monitor { key: key.into(), connector: key.into(), identity: key.into(), aliases: vec![], internal: key == "A", usable: true, }); let profile = OutputProfile { name: "three".into(), outputs: monitors .iter() .map(|monitor| crate::output_policy::defaults(monitor.key.clone())) .collect(), layout, confirm_timeout: 15, lid_policy: crate::config::LidPolicy::Ignore, lid_closed: None, mirror_source: None, }; crate::output_policy::select_profile( &monitors, false, &[profile], &ManualOverride::default(), ) } #[test] fn complete_live_plan_still_requires_hardware_success() { let desired = three_monitor_plan(crate::config::OutputLayout::Extend); let selected = vec!["A".into(), "B".into(), "C".into()]; assert!(validate_output_plan(&desired, &selected, || Ok(())).is_ok()); assert_eq!( validate_output_plan( &desired, &selected, || Err("atomic TEST_ONLY failed".into()) ), Err("atomic TEST_ONLY failed".into()) ); } #[test] fn requiredness_does_not_allow_a_connected_enabled_output_to_be_omitted() { let mut desired = three_monitor_plan(crate::config::OutputLayout::Extend); desired.outputs[2].settings.required = false; let error = validate_output_plan(&desired, &["A".into(), "B".into()], || Ok(())).unwrap_err(); assert!(error.contains("C")); desired.outputs[2].settings.enabled = false; assert!(validate_output_plan(&desired, &["A".into(), "B".into()], || Ok(())).is_ok()); } #[test] fn disconnected_monitors_affect_profile_matching_and_do_not_need_assignments() { let connected = ["A", "B"].map(|key| Monitor { key: key.into(), connector: key.into(), identity: key.into(), aliases: vec![], internal: key == "A", usable: true, }); let mut profile = OutputProfile { name: "dock".into(), outputs: ["A", "B", "C"] .map(|key| crate::output_policy::defaults(key.into())) .into(), layout: crate::config::OutputLayout::Extend, confirm_timeout: 15, lid_policy: crate::config::LidPolicy::Ignore, lid_closed: None, mirror_source: None, }; for required in [true, false] { profile.outputs[2].required = required; let desired = crate::output_policy::select_profile( &connected, false, &[profile.clone()], &ManualOverride::default(), ); assert_eq!( desired.profile.as_deref(), if required { None } else { Some("dock") } ); assert_eq!(desired.outputs.len(), 2); assert!(validate_output_plan(&desired, &["A".into(), "B".into()], || Ok(())).is_ok()); } } #[test] fn device_scans_cannot_satisfy_another_gpus_missing_output() { let mut desired = three_monitor_plan(crate::config::OutputLayout::Extend); desired.outputs[0].key = "gpu-one/A".into(); desired.outputs[1].key = "gpu-two/B".into(); desired.outputs[2].key = "gpu-two/C".into(); let selected = vec!["gpu-one/A".into(), "gpu-two/B".into(), "gpu-one/C".into()]; assert!(validate_output_plan(&desired, &selected, || Ok(())).is_err()); // A device-local rescan checks only its own enabled outputs. assert!( validate_selected_outputs( desired .outputs .iter() .filter(|output| output.key.starts_with("gpu-one/")), &selected ) .is_ok() ); } #[test] fn impossible_assignment_reaches_live_validation_as_an_error() { let desired = three_monitor_plan(crate::config::OutputLayout::Extend); let crtc = std::num::NonZeroU32::new(1).unwrap().into(); let requests = (0..3) .map(|_| crtc_assignment::Candidates { applied: None, kernel: None, compatible: vec![crtc], }) .collect::>(); let selected = desired .outputs .iter() .zip(crtc_assignment::assign(&requests)) .filter(|(_, assignment)| assignment.is_some()) .map(|(output, _)| output.key.clone()) .collect::>(); assert!(validate_output_plan(&desired, &selected, || Ok(())).is_err()); } #[test] fn feasible_three_monitor_assignment_passes_live_plan_validation() { let desired = three_monitor_plan(crate::config::OutputLayout::Extend); let requests = [&[1, 2][..], &[1, 3][..], &[1, 3][..]].map(|compatible| crtc_assignment::Candidates { applied: None, kernel: None, compatible: compatible .iter() .map(|value| std::num::NonZeroU32::new(*value).unwrap().into()) .collect(), }); let selected = desired .outputs .iter() .zip(crtc_assignment::assign(&requests)) .filter(|(_, assignment)| assignment.is_some()) .map(|(output, _)| output.key.clone()) .collect::>(); assert_eq!(selected.len(), 3); assert!(validate_output_plan(&desired, &selected, || Ok(())).is_ok()); } #[test] fn live_plan_rejects_partial_extend_and_mirror_before_hardware_validation() { for layout in [ crate::config::OutputLayout::Extend, crate::config::OutputLayout::Mirror, ] { let desired = three_monitor_plan(layout); for omitted in ["A", "B", "C"] { let selected = ["A", "B", "C"] .into_iter() .filter(|key| *key != omitted) .map(String::from) .collect::>(); let probed = std::cell::Cell::new(false); let result = validate_output_plan(&desired, &selected, || { probed.set(true); Ok(()) // A driver can accept the incomplete two-output request. }); assert!( result.is_err(), "accepted a partial {layout:?} configuration without {omitted}" ); assert!(!probed.get()); } } } #[test] fn repeated_unconfirmed_changes_keep_original_rollback_and_manual_state() { let a = ("confirmed A", "automatic"); let b = confirmation_baseline(None, a); let c = confirmation_baseline(Some(b), ("unconfirmed B", "profile B")); assert_eq!(c, a); let d = confirmation_baseline(Some(c), ("unconfirmed C", "profile C")); assert_eq!(d, a); assert_eq!( confirmation_baseline(None, ("confirmed C", "profile C")), ("confirmed C", "profile C") ); } #[test] fn lid_only_hides_panels_with_a_usable_external_output() { assert!(lid_hides_panel(true, true)); assert!(!lid_hides_panel(false, true)); assert!(!lid_hides_panel(true, false)); assert!(!lid_hides_panel(false, false)); } #[test] fn panel_detection_uses_connector_type_not_monitor_name() { assert!(internal_connector( connector::Interface::EmbeddedDisplayPort )); assert!(internal_connector(connector::Interface::LVDS)); assert!(internal_connector(connector::Interface::DSI)); assert!(!internal_connector(connector::Interface::HDMIA)); assert!(!internal_connector(connector::Interface::DisplayPort)); } #[test] fn connector_identity_hash_is_stable() { assert_eq!(stable_hash(b"hello"), 0xa430_d846_80aa_bd0b); } #[test] fn presentation_clock_uses_measured_monotonic_deltas() { let mut clock = PresentationClock::default(); clock.set_refresh(60_000); assert_eq!( clock.record(DrmEventTime::Monotonic(Duration::from_millis(100)), 1), None ); assert_eq!( clock.record(DrmEventTime::Monotonic(Duration::from_millis(116)), 2), Some(Duration::from_millis(16)) ); assert_eq!(clock.presented_frames, 2); assert_eq!(clock.missed_deadlines, 0); } #[test] fn presentation_clock_rejects_realtime_timestamps() { let mut clock = PresentationClock::default(); assert_eq!( clock.record(DrmEventTime::Realtime(SystemTime::now()), 1), None ); assert_eq!(clock.presented_frames, 0); } #[test] fn presentation_clock_does_not_count_idle_gaps_as_missed_deadlines() { let mut clock = PresentationClock::default(); clock.set_refresh(60_000); clock.record(DrmEventTime::Monotonic(Duration::ZERO), 1); clock.record(DrmEventTime::Monotonic(Duration::from_millis(50)), 2); assert_eq!(clock.missed_deadlines, 0); } #[test] fn presentation_clock_excludes_inactive_session_time() { let mut clock = PresentationClock::default(); clock.set_refresh(60_000); clock.record(DrmEventTime::Monotonic(Duration::from_millis(100)), 1); clock.reset_timing(); assert_eq!( clock.record(DrmEventTime::Monotonic(Duration::from_secs(10)), 2), None ); assert_eq!(clock.presented_frames, 2); assert_eq!(clock.missed_deadlines, 0); } }