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}; mod capture; mod output_power; mod planes; mod redraw; mod scheduling; use output_power::{OutputPower, Power}; use redraw::{AnimationFallback, SchedulingMetrics}; use scheduling::*; use smithay::backend::allocator::Fourcc; use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}; use smithay::backend::drm::compositor::{DrmCompositor, PrimaryPlaneElement}; use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter; use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, NodeType}; use smithay::backend::egl::{EGLContext, EGLDisplay}; use smithay::backend::input::InputEvent; use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface}; 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, 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::rustix::fs::OFlags; use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind; use smithay::reexports::wayland_server::backend::GlobalId; use smithay::utils::{DeviceFd, Monotonic, Time, Transform}; use smithay::wayland::dmabuf::DmabufFeedbackBuilder; use smithay::wayland::presentation::Refresh; use crate::Ferese; use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform}; use crate::metrics::RenderMetrics; use crate::render::{frame_effect_metrics, sampled_output_elements}; pub struct DirectBackendState { pub session: LibSeatSession, pub active: bool, lid_closed: 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 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 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>, ) -> 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(), output.current_scale().fractional_scale(), cursor.map(|rect| (state, rect)), ) } } #[derive(Clone, Debug)] pub(crate) struct ConnectedOutputInfo { pub connector: String, pub identity: String, pub enabled: bool, pub 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)] 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 { drm: DrmDevice, gbm: GbmDevice, renderer: GlesRenderer, render_node: DrmNode, outputs: HashMap, } struct DirectOutput { internal: bool, connector: connector::Handle, mode: DrmMode, settings: OutputSettings, output: Output, global: GlobalId, surface: OutputCompositor, primary_commit: Option, capture_texture: Option, render_metrics: RenderMetrics, frame_pending: bool, animation_fallback: AnimationFallback, cursor_visible: bool, scheduler: crate::frame_scheduler::FrameScheduler, render_timer: Option, power: OutputPower, callback_timer: Option, lock_frame_pending: bool, } struct OutputSelection { connector: connector::Info, crtc: crtc::Handle, mode: DrmMode, settings: OutputSettings, } 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(); } state.direct_backend = Some(DirectBackendState { session, active: session_active, lid_closed: initial_lid_closed(), 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(), }); open_primary_device(event_loop, state, &primary_path)?; sync_battery_timer(state); 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 } => { 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 => { let handle = state.loop_handle.clone(); if let Some(backend) = state.direct_backend.as_mut() { backend.active = false; if let Some(token) = backend.battery_timer.take() { handle.remove(token); } if let Some(token) = backend.power_retry.take() { handle.remove(token); } backend .presentation .values_mut() .for_each(PresentationClock::reset_timing); backend.devices.values_mut().for_each(|device| { device.drm.pause(); device.outputs.values_mut().for_each(|output| { cancel_output_timers(&handle, output); output.surface.reset_buffers(); output.primary_commit = None; output.capture_texture = None; output.frame_pending = false; output.power.suspended(); }); }); } 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; for device in backend.devices.values_mut() { if let Err(error) = device.drm.activate(true) { tracing::error!(%error, "failed to reactivate DRM device"); continue; } for output in device.outputs.values_mut() { // A suspended page flip may never arrive. Clear the old // pending frame before accepting a fresh submission. if let Err(error) = output.surface.clear().and_then(|()| output.surface.reset_state()) { tracing::error!(%error, "failed to reset DRM output after activation"); } } } } update_power_policy(state); sync_battery_timer(state); state.reset_animation_clock(); tracing::info!("direct session activated"); state.lock_input_activity(); render_all(state); } })?; event_loop .handle() .insert_source(udev_backend, |event, _, state| match event { UdevEvent::Added { device_id, path } => { if let Some(node) = direct_node_for_device(state, device_id) { rescan_device(state, node); } else { tracing::info!(?device_id, ?path, "additional DRM device discovered"); } } UdevEvent::Changed { device_id } => { 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"); Ok(()) } fn configure_libinput_device(state: &Ferese, device: &mut LibinputDevice) { let settings = state.input_settings.touchpad; 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(settings.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); } } pub(crate) fn reload_outputs(state: &mut Ferese) { if let Some(backend) = state.direct_backend.as_mut() { 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::>()) .unwrap_or_default(); for node in nodes { rescan_device(state, node); } } pub(crate) fn validate_live_outputs(state: &Ferese, profiles: &[OutputProfile]) -> Result<(), String> { let Some(backend) = state.direct_backend.as_ref() else { return Ok(()); }; let mut usable = 0; for device in backend.devices.values() { let mut scan = select_outputs(&device.drm, profiles, backend.low_power).map_err(|e| e.to_string())?; apply_lid_policy(&mut scan, backend.lid_closed); usable += scan.selections.len(); } if usable == 0 { return Err("display configuration would leave no usable output".into()); } Ok(()) } fn open_primary_device( event_loop: &mut EventLoop, state: &mut Ferese, path: &Path, ) -> Result<(), Box> { let mut 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 fd = session.open(path, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK)?; let fd = DrmDeviceFd::new(DeviceFd::from(fd)); let (mut 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 = EGLContext::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()?; state .dmabuf_state .create_global_with_default_feedback::(&display_handle, &feedback); let mut scan = select_outputs( &drm, &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 { DrmEvent::VBlank(crtc) => { if let Some(metadata) = metadata { let mut retired = 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| match output.surface.frame_submitted() { Ok(feedback) => { retired = feedback.is_some(); feedback } Err(error) => { tracing::error!(?node, ?crtc, %error, "failed to retire DRM frame"); None } }); 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.frame_pending = false; 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(); } 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.active); 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"); } })?; state .direct_backend .as_mut() .expect("direct backend state remains initialized") .devices .insert( node, DirectDevice { drm, gbm, renderer, render_node, outputs, }, ); state.relayout(); render_all(state); tracing::info!(?node, ?path, "initialized DRM/GBM device outputs"); Ok(()) } 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() 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) { redraw(state, None, std::panic::Location::caller()); } #[track_caller] pub(crate) fn render_on(state: &mut Ferese, outputs: &[Output]) { redraw(state, Some(outputs), std::panic::Location::caller()); } #[track_caller] pub(crate) fn render_cursor(state: &mut Ferese) { 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() } #[track_caller] pub(crate) fn render_window(state: &mut Ferese, window: ferese_layout::WindowId) { let outputs = state .space .outputs() .filter(|output| state.window_belongs_to_output(window, output)) .cloned() .collect::>(); render_on(state, &outputs); } #[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 outputs = state.surface_outputs(&root); render_on(state, &outputs); } 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) { return; } let outputs = state .direct_backend .as_ref() .unwrap() .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)| { selected.is_none_or(|selected| selected.contains(output)) || state.output_has_animations(output) || state.output_has_pending_visual_changes(output) }) .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().unwrap().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; }; if backend.lid_closed == closed { return; } backend.lid_closed = closed; tracing::info!(closed, "laptop lid state changed"); let nodes = backend.devices.keys().copied().collect::>(); for node in nodes { rescan_device(state, node); } } fn initial_lid_closed() -> bool { std::fs::read_dir("/proc/acpi/button/lid") .ok() .into_iter() .flatten() .filter_map(Result::ok) .filter_map(|entry| std::fs::read_to_string(entry.path().join("state")).ok()) .any(|state| state.split_whitespace().last() == Some("closed")) } fn internal_connector(interface: connector::Interface) -> bool { matches!( interface, connector::Interface::EmbeddedDisplayPort | connector::Interface::LVDS | connector::Interface::DSI ) } fn apply_lid_policy(scan: &mut OutputScan, closed: bool) { let external_available = scan .selections .iter() .any(|selection| !internal_connector(selection.connector.interface())); if !lid_hides_panel(closed, external_available) { return; } let internal = scan .selections .iter() .filter(|selection| internal_connector(selection.connector.interface())) .map(|selection| selection.connector.to_string()) .collect::>(); scan.selections .retain(|selection| !internal_connector(selection.connector.interface())); for output in &mut scan.connected_outputs { if internal.contains(&output.connector) { output.enabled = false; output.current_mode = None; } } } 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 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 frame = state.sample_frame(&output.output, horizon); let rendered = { (|| -> Result> { state.process_dmabuf_imports(&mut device.renderer, Some(device.render_node)); let elements = sampled_output_elements(state, &mut device.renderer, &output.output, true, &frame); let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale()); let flags = planes::frame_flags( state.output_has_fullscreen_for_frame(&output.output, frame.overview.is_presenting()), frame.animating, state.session_lock.active(), ); let result = output .surface .render_frame(&mut device.renderer, &elements, [0.035, 0.04, 0.055, 1.0], flags)?; if result.needs_sync() && let PrimaryPlaneElement::Swapchain(primary) = &result.primary_element { primary.sync.wait()?; } capture::capture_output( state, &mut device.renderer, &mut output.capture_texture, &output.output, &frame, )?; if result.is_empty { 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().unwrap().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 = crate::presentation::take_output_feedback( state, &output.output, &result.states, Kind::Vsync | Kind::HwClock | Kind::HwCompletion, ); let visibility = result.states.clone(); drop(result); output.surface.queue_frame(presentation)?; 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; output.lock_frame_pending = state.session_lock.active(); Ok(true) })() }; // 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 { 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) { let Some(mut device) = state .direct_backend .as_mut() .and_then(|backend| backend.devices.remove(&node)) else { return; }; let mut scan = match select_outputs( &device.drm, &state.output_profiles, state.direct_backend.as_ref().is_some_and(|backend| backend.low_power), ) { Ok(scan) => scan, Err(error) => { tracing::error!(?node, %error, "failed to scan DRM connectors"); restore_device(state, node, device); return; } }; let lid_closed = state.direct_backend.as_ref().is_some_and(|backend| backend.lid_closed); apply_lid_policy(&mut scan, lid_closed); if let Some(backend) = state.direct_backend.as_mut() { backend.connected_outputs = scan.connected_outputs; } let mut selections = scan .selections .into_iter() .map(|selection| (selection.crtc, selection)) .collect::>(); let existing = device.outputs.keys().copied().collect::>(); let mut deferred_removals = Vec::new(); for crtc in existing { if let Some(selection) = selections.get(&crtc) && let Some(output) = device.outputs.get_mut(&crtc) && selection.connector.handle() == output.connector && (selection.mode != output.mode || selection.settings != output.settings) { // Keep the wl_output/global and workspace ownership alive. A mode // change is tested by DRM before updating client-visible geometry. let result = if selection.mode != output.mode { output.surface.use_mode(selection.mode) } else { Ok(()) }; match result { Ok(()) => { let old_geometry = state.space.output_geometry(&output.output); output.mode = selection.mode; cancel_output_timers(&state.loop_handle, output); output.settings = selection.settings.clone(); let position = selection.settings.position.unwrap_or_else(|| { state .space .output_geometry(&output.output) .map(|g| [g.loc.x, g.loc.y]) .unwrap_or([0, 0]) }); output.output.change_current_state( Some(OutputMode::from(output.mode)), Some(output_transform(output.settings.transform)), Some(Scale::Fractional(output.settings.scale)), Some((position[0], position[1]).into()), ); state.space.map_output(&output.output, (position[0], position[1])); if let Some(id) = state.output_id(&output.output) && let Some(g) = state.space.output_geometry(&output.output) { state.output_workspaces.update_geometry( id, ferese_core::OutputGeometry::new(g.loc.x, g.loc.y, g.size.w, g.size.h), ); if let Some(old) = old_geometry { state.reposition_output_floats( id, ferese_layout::Rect::new( old.loc.x as f64, old.loc.y as f64, old.size.w as f64, old.size.h as f64, ), ferese_layout::Rect::new( g.loc.x as f64, g.loc.y as f64, g.size.w as f64, g.size.h as f64, ), ); } } output.surface.reset_buffer_ages(); output.primary_commit = None; output.capture_texture = None; if let Some(backend) = state.direct_backend.as_mut() { let clock = backend.presentation.entry((node, crtc)).or_default(); clock.set_refresh(OutputMode::from(output.mode).refresh); clock.reset_timing(); } } Err(error) => { tracing::warn!(?crtc, %error, "display change failed; retaining current output") } } selections.remove(&crtc); continue; } let unchanged = device.outputs.get(&crtc).is_some_and(|output| { selections.get(&crtc).is_some_and(|selection| { selection.connector.handle() == output.connector && selection.mode == output.mode && selection.settings == output.settings }) }); if unchanged { selections.remove(&crtc); continue; } // Add replacements on free CRTCs before removing vanished outputs so // workspace evacuation always has a real destination. Reused CRTCs // still need their old surface released first. if !selections.contains_key(&crtc) { deferred_removals.push(crtc); continue; } if let Some(mut output) = device.outputs.remove(&crtc) { cancel_output_timers(&state.loop_handle, &mut output); state.display_handle.disable_global::(output.global); state.unregister_output(&output.output); if let Some(backend) = state.direct_backend.as_mut() { backend.presentation.remove(&(node, crtc)); } tracing::info!(?node, ?crtc, "removed DRM output"); } } for (_, selection) in selections { let crtc = selection.crtc; let mode = selection.mode; match create_direct_output(state, &mut device.drm, &device.gbm, &device.renderer, selection) { Ok(output) => { state .direct_backend .as_mut() .expect("direct backend state remains initialized") .presentation .entry((node, crtc)) .or_default() .set_refresh(OutputMode::from(mode).refresh); device.outputs.insert(crtc, output); } Err(error) => tracing::error!(?node, ?crtc, %error, "failed to add DRM output"), } } for crtc in deferred_removals { let keep_internal = device.outputs.get(&crtc).is_some_and(|output| output.internal) && lid_closed && state.direct_backend.as_ref().is_some_and(|backend| { device.outputs.get(&crtc).is_some_and(|output| { backend .connected_outputs .iter() .any(|info| info.connector == output.output.name()) }) }) && !device.outputs.values().any(|output| !output.internal); if keep_internal { tracing::warn!(?crtc, "keeping laptop panel enabled: external output activation failed"); if let Some(output) = device.outputs.get(&crtc) && let Some(backend) = state.direct_backend.as_mut() && let Some(info) = backend .connected_outputs .iter_mut() .find(|info| info.connector == output.output.name()) { info.enabled = true; info.current_mode = Some(connected_mode_info(output.mode)); } continue; } if let Some(mut output) = device.outputs.remove(&crtc) { cancel_output_timers(&state.loop_handle, &mut output); state.display_handle.disable_global::(output.global); state.unregister_output(&output.output); if let Some(backend) = state.direct_backend.as_mut() { backend.presentation.remove(&(node, crtc)); } } } if let Some(backend) = state.direct_backend.as_mut() { for output in device.outputs.values() { if let Some(info) = backend .connected_outputs .iter_mut() .find(|info| info.connector == output.output.name()) { info.enabled = true; info.current_mode = Some(connected_mode_info(output.mode)); info.auto_refresh = output.settings.auto_refresh; info.scale = output.settings.scale; info.transform = output.settings.transform; info.configured_position = output.settings.position; } } } restore_device(state, node, device); sync_battery_timer(state); update_power_policy(state); state.restore_output_focus(); state.relayout(); render_all(state); } 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; }; let context = device.renderer.context_id().erased(); state.wallpaper.forget_context(&context); state.render.forget_context(&context); for (crtc, mut output) in device.outputs { cancel_output_timers(&state.loop_handle, &mut output); state.display_handle.disable_global::(output.global); state.unregister_output(&output.output); if let Some(backend) = state.direct_backend.as_mut() { backend.presentation.remove(&(node, crtc)); } } 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"); } 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.active); let animating = enabled && state.output_has_animations(&identity); let backend = state.direct_backend.as_mut().unwrap(); let output = backend.devices.get_mut(&node).unwrap().outputs.get_mut(&crtc).unwrap(); // 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 } fn arm_animation_timer(state: &mut Ferese) { let deadline = animation_fallbacks(state) .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; } // 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() .unwrap() .scheduling_metrics .fallback_recoveries += 1; state .direct_backend .as_mut() .unwrap() .devices .get_mut(&node) .unwrap() .outputs .get_mut(&crtc) .unwrap() .animation_fallback = AnimationFallback::default(); request_frame(state, node, crtc); } } arm_animation_timer(state); TimeoutAction::Drop }) { Ok(token) => { let backend = state.direct_backend.as_mut().unwrap(); 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.active) .and_then(|backend| backend.devices.get(&node)) .filter(|device| device.drm.is_active()) .and_then(|device| device.outputs.get(&crtc)) else { return; }; if 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 || !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; } state .space .elements() .filter(|window| { state .windows .ids() .get(*window) .is_some_and(|id| state.window_belongs_to_output(*id, output)) }) .for_each(|window| { 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::>(); layers.iter().for_each(|layer| { 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(drm: &DrmDevice, profiles: &[OutputProfile], low_power: bool) -> io::Result { 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 identities = connected .iter() .map(|connector| connector_identity(drm, connector)) .collect::>(); let active_profile = profiles.iter().find(|profile| { profile.outputs.iter().all(|settings| { connected .iter() .zip(&identities) .any(|(connector, identity)| output_matches(settings, connector, identity)) }) }); if let Some(profile) = active_profile { tracing::info!(profile = %profile.name, "selected output profile"); } let mut selections = Vec::new(); let mut connected_outputs = Vec::new(); let mut used_crtcs = HashSet::new(); for (connector, identity) in connected.into_iter().zip(identities) { let settings = active_profile .and_then(|profile| { profile .outputs .iter() .find(|settings| output_matches(settings, &connector, &identity)) }) .cloned() .unwrap_or_else(|| default_output_settings(connector.to_string())); 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(); connected_outputs.push(ConnectedOutputInfo { connector: connector.to_string(), identity: identity.clone(), enabled: settings.enabled, profile: active_profile.map(|profile| profile.name.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 current_crtc = connector .current_encoder() .and_then(|handle| drm.get_encoder(handle).ok()) .and_then(|encoder| encoder.crtc()) .filter(|crtc| !used_crtcs.contains(crtc)); let compatible_crtc = connector.encoders().iter().find_map(|handle| { let encoder = drm.get_encoder(*handle).ok()?; resources .filter_crtcs(encoder.possible_crtcs()) .into_iter() .find(|crtc| !used_crtcs.contains(crtc)) }); if let Some(crtc) = current_crtc.or(compatible_crtc) { used_crtcs.insert(crtc); selections.push(OutputSelection { connector, crtc, mode, settings, }); } } 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 output_matches(settings: &OutputSettings, connector: &connector::Info, identity: &str) -> bool { settings.matcher == connector.to_string() || settings.matcher == identity } fn default_output_settings(matcher: String) -> OutputSettings { OutputSettings { auto_refresh: false, matcher, enabled: true, mode: None, scale: 1.0, transform: OutputTransform::Normal, position: None, } } 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)); let selected = 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() }; if selected.is_none() { tracing::warn!( output = %connector, width = requested.width, height = requested.height, refresh_millihertz = ?requested.refresh_millihertz, "requested output mode is unavailable; using preferred mode" ); } selected.or_else(fallback) } fn create_direct_output( state: &mut Ferese, drm: &mut DrmDevice, gbm: &GbmDevice, renderer: &GlesRenderer, selection: OutputSelection, ) -> Result> { let OutputSelection { connector, crtc, mode, settings, } = selection; let identity = connector_identity(drm, &connector); 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)), [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, &settings); surface.set_output_mode_source((&output).into()); let render_metrics = RenderMetrics::from_environment(output.name()); tracing::info!(?crtc, connector = %connector, "initialized DRM output"); Ok(DirectOutput { internal: internal_connector(connector.interface()), connector: connector.handle(), mode, settings: settings.clone(), output, global, surface, primary_commit: None, capture_texture: None, render_metrics, frame_pending: false, 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, ) -> (Output, GlobalId) { let name = connector.to_string(); let physical_size = connector.size().unwrap_or((0, 0)); let output = Output::new( name, PhysicalProperties { size: (physical_size.0 as i32, physical_size.1 as i32).into(), subpixel: Subpixel::from(connector.subpixel()), make: "Unknown".into(), model: "Unknown".into(), }, ); let output_mode = OutputMode::from(mode); let global = 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()), ); 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_identity(drm: &DrmDevice, connector: &connector::Info) -> String { let edid = 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, } }) }); match edid { Some(edid) if !edid.is_empty() => format!("drm-edid:{:016x}", stable_hash(&edid)), _ => format!("drm:{connector}"), } } 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::*; #[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); } }