Files
feresaria/compositor/src/backends/direct.rs
T
Samuel Onoja 3d0c85c70a Wait for DRM presentation before releasing frame callbacks
Cancel callback-only timers when a frame is submitted and defer callback delivery until retirement. Cover a delayed timer and missed presentation deadline in the scheduler tests.

Clean up shader argument borrows and shell region decoding to pass Clippy.
2026-10-01 17:32:17 +01:00

1808 lines
65 KiB
Rust

use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::io;
use std::path::Path;
use std::time::{Duration, Instant};
mod scheduling;
use scheduling::*;
use smithay::backend::allocator::Fourcc;
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface, NodeType};
use smithay::backend::egl::{EGLContext, EGLDisplay};
use smithay::backend::input::InputEvent;
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::{Bind, Frame, 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, 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::{animated_window_elements, cursorless_window_elements, frame_effect_metrics, redraw_output};
pub struct DirectBackendState {
pub session: LibSeatSession,
pub active: bool,
lid_closed: bool,
pub(crate) low_power: bool,
devices: HashMap<DrmNode, DirectDevice>,
input_devices: Vec<LibinputDevice>,
presentation: HashMap<(DrmNode, crtc::Handle), PresentationClock>,
pub(crate) connected_outputs: Vec<ConnectedOutputInfo>,
animation_timer: Option<RegistrationToken>,
animation_active: bool,
}
impl DirectBackendState {
pub(crate) fn capture_resize_snapshot(
&mut self,
window: &smithay::desktop::Window,
geometry: smithay::utils::Rectangle<i32, smithay::utils::Logical>,
output: &Output,
remaining: usize,
) -> Result<Option<crate::render::ResizeSnapshot>, 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<i32, smithay::utils::Logical>,
output: &Output,
) -> Result<crate::handlers::screenshot::CaptureBuffer, String> {
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<smithay::utils::Rectangle<i32, smithay::utils::Logical>>,
) -> Result<crate::handlers::screenshot::CaptureBuffer, String> {
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<String>,
pub auto_refresh: bool,
pub physical_size: Option<(u32, u32)>,
pub current_mode: Option<ConnectedModeInfo>,
pub available_modes: Vec<ConnectedModeInfo>,
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<Duration>,
presented_frames: u64,
refresh_interval: Option<Duration>,
missed_deadlines: u64,
}
struct DirectDevice {
drm: DrmDevice,
gbm: GbmDevice<DrmDeviceFd>,
renderer: GlesRenderer,
outputs: HashMap<crtc::Handle, DirectOutput>,
}
struct DirectOutput {
internal: bool,
connector: connector::Handle,
mode: DrmMode,
settings: OutputSettings,
output: Output,
global: GlobalId,
surface: GbmBufferedSurface<GbmAllocator<DrmDeviceFd>, OutputPresentationFeedback>,
damage_tracker: OutputDamageTracker,
render_metrics: RenderMetrics,
frame_pending: bool,
scheduler: crate::frame_scheduler::FrameScheduler,
render_timer: Option<RegistrationToken>,
power_off: bool,
callback_timer: Option<RegistrationToken>,
lock_frame_pending: bool,
}
struct OutputSelection {
connector: connector::Info,
crtc: crtc::Handle,
mode: DrmMode,
settings: OutputSettings,
}
struct OutputScan {
selections: Vec<OutputSelection>,
connected_outputs: Vec<ConnectedOutputInfo>,
}
pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<(), Box<dyn Error>> {
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::<LibinputSessionInterface<LibSeatSession>>(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(),
animation_timer: None,
animation_active: false,
});
open_primary_device(event_loop, state, &primary_path)?;
event_loop
.handle()
.insert_source(Timer::from_duration(Duration::from_secs(5)), |_, _, state| {
update_power_policy(state);
TimeoutAction::ToDuration(Duration::from_secs(5))
})?;
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;
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.damage_tracker = OutputDamageTracker::from_output(&output.output);
output.frame_pending = false;
output.power_off = false;
});
});
}
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");
}
}
}
update_power_policy(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 update_power_policy(state: &mut Ferese) {
if !state
.output_profiles
.iter()
.any(|profile| profile.outputs.iter().any(|output| output.auto_refresh))
{
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::<Vec<_>>())
.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<Ferese>,
state: &mut Ferese,
path: &Path,
) -> Result<(), Box<dyn Error>> {
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::<Ferese>(&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 = true;
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::<Monotonic>::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;
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();
}
}
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);
}
}
}
}
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,
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;
}
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_off || output.frame_pending {
continue;
}
match output.surface.surface().clear() {
Ok(()) => {
cancel_output_timers(&state.loop_handle, output);
output.power_off = true;
}
Err(error) => {
tracing::warn!(%error, output = %output.output.name(), "could not sleep locked display")
}
}
}
}
}
pub(crate) fn wake_locked_outputs(state: &mut Ferese) {
if let Some(backend) = state.direct_backend.as_mut() {
for device in backend.devices.values_mut() {
for output in device.outputs.values_mut() {
if std::mem::take(&mut output.power_off) {
output.surface.reset_buffer_ages();
}
}
}
}
render_all(state);
}
pub fn render_all(state: &mut Ferese) {
if state.direct_backend.as_ref().is_none_or(|backend| !backend.active) {
return;
}
let animating = state.advance_animations(Instant::now());
render_outputs(state, animating);
}
fn render_outputs(state: &mut Ferese, animating: bool) {
if let Some(backend) = state.direct_backend.as_mut() {
backend.animation_active = animating && !state.session_lock.sleeping;
}
arm_animation_timer(state);
let outputs = state
.direct_backend
.as_ref()
.map(|backend| {
backend
.devices
.iter()
.flat_map(|(node, device)| device.outputs.keys().map(|crtc| (*node, *crtc)))
.collect::<Vec<_>>()
})
.unwrap_or_default();
for (node, crtc) in outputs {
request_frame(state, node, crtc);
}
}
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::<Vec<_>>();
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::<HashSet<_>>();
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
}
fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: crate::frame_scheduler::FramePlan) {
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_off || output.frame_pending);
if asleep {
return;
}
}
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;
};
let Some(mut output) = device.outputs.remove(&crtc) else {
restore_device(state, node, device);
return;
};
if !device.drm.is_active() || output.frame_pending {
device.outputs.insert(crtc, output);
restore_device(state, node, device);
return;
}
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 mut forecast = state.forecast_render(&output.output, horizon);
let rendered = {
let state = &mut *forecast;
(|| -> Result<bool, Box<dyn Error>> {
state.process_dmabuf_imports(&mut device.renderer);
let (mut buffer, age) = output.surface.next_buffer()?;
let elements = animated_window_elements(state, &mut device.renderer, &output.output);
let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
let mut framebuffer = device.renderer.bind(&mut buffer)?;
let result = output.damage_tracker.render_output(
&mut device.renderer,
&mut framebuffer,
usize::from(age),
&elements,
[0.035, 0.04, 0.055, 1.0],
)?;
let cursorless_capture = state.has_pending_screencopy(&output.output, false);
let captured_with_cursor =
state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, true);
if cursorless_capture {
let cursorless_elements = cursorless_window_elements(state, &mut device.renderer, &output.output);
redraw_output(
&mut device.renderer,
&mut framebuffer,
&output.output,
&cursorless_elements,
)?;
state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, false);
redraw_output(&mut device.renderer, &mut framebuffer, &output.output, &elements)?;
} else if captured_with_cursor {
let _ = device
.renderer
.render(
&mut framebuffer,
output.output.current_mode().expect("output has a mode").size,
output.output.current_transform(),
)?
.finish()?;
}
let Some(damage) = result.damage.cloned() else {
output
.render_metrics
.record_no_damage(render_started.elapsed(), missed_deadlines);
return Ok(false);
};
let presentation = crate::presentation::take_output_feedback(
state,
&output.output,
&result.states,
Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
);
output
.surface
.queue_buffer(Some(result.sync), Some(damage.clone()), presentation)?;
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)
})()
};
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() {
forecast.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(&mut forecast, node, device);
drop(forecast);
if !submitted {
schedule_frame_callbacks(state, node, crtc, plan.present_at);
}
}
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<DrmNode> {
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::<HashMap<_, _>>();
let existing = device.outputs.keys().copied().collect::<Vec<_>>();
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.damage_tracker = OutputDamageTracker::from_output(&output.output);
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::<Ferese>(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::<Ferese>(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);
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.resize_snapshots.retain(|_, snapshot| snapshot.context != context);
for (crtc, mut output) in device.outputs {
cancel_output_timers(&state.loop_handle, &mut output);
state.display_handle.disable_global::<Ferese>(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();
}
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_interval(state: &Ferese) -> Duration {
state
.direct_backend
.as_ref()
.into_iter()
.flat_map(|backend| backend.devices.values())
.flat_map(|device| device.outputs.values())
.filter_map(|output| output.output.current_mode())
.filter(|mode| mode.refresh > 0)
.map(|mode| Duration::from_nanos(1_000_000_000_000 / mode.refresh as u64))
.min()
.unwrap_or(Duration::from_millis(16))
}
fn arm_animation_timer(state: &mut Ferese) {
let needed = state
.direct_backend
.as_ref()
.is_some_and(|backend| backend.animation_active && backend.animation_timer.is_none());
if !needed {
return;
}
let deadline = state.animation_fallback_deadline(animation_interval(state));
match state
.loop_handle
.insert_source(Timer::from_deadline(deadline), |_, _, state| {
let active = state
.direct_backend
.as_ref()
.is_some_and(|backend| backend.active && backend.animation_active && !state.session_lock.sleeping);
if active {
let deadline = state.animation_fallback_deadline(animation_interval(state));
if Instant::now() < deadline {
return TimeoutAction::ToInstant(deadline);
}
let animating = state.advance_animations(Instant::now());
if let Some(backend) = state.direct_backend.as_mut() {
backend.animation_active = animating;
}
let outputs = state
.direct_backend
.as_ref()
.into_iter()
.flat_map(|backend| backend.devices.iter())
.flat_map(|(node, device)| {
device
.outputs
.iter()
.map(|(crtc, output)| (*node, *crtc, output.output.clone()))
})
.collect::<Vec<_>>();
for (node, crtc, output) in outputs {
if state.output_has_animations(&output) {
request_frame(state, node, crtc);
}
}
if state
.direct_backend
.as_ref()
.is_some_and(|backend| backend.animation_active)
{
return TimeoutAction::ToInstant(state.animation_fallback_deadline(animation_interval(state)));
}
}
if let Some(backend) = state.direct_backend.as_mut() {
backend.animation_timer = None;
}
TimeoutAction::Drop
}) {
Ok(token) => state.direct_backend.as_mut().unwrap().animation_timer = Some(token),
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() || 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) {
if state.session_lock.active {
state.lock_frame_callbacks(output);
return;
}
state
.space
.elements()
.filter(|window| {
state
.window_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(), Some(Duration::ZERO), |_, _| {
Some(output.clone())
});
});
let layers = layer_map_for_output(output).layers().cloned().collect::<Vec<_>>();
layers.iter().for_each(|layer| {
layer.send_frame(output, state.start_time.elapsed(), Some(Duration::ZERO), |_, _| {
Some(output.clone())
});
});
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<OutputScan> {
let resources = drm.resource_handles()?;
let connected = resources
.connectors()
.iter()
.map(|handle| drm.get_connector(*handle, true))
.collect::<io::Result<Vec<_>>>()?
.into_iter()
.filter(|connector| connector.state() == connector::State::Connected)
.collect::<Vec<_>>();
let identities = connected
.iter()
.map(|connector| connector_identity(drm, connector))
.collect::<Vec<_>>();
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::<Vec<_>>();
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<OutputModeRequest>) -> Option<DrmMode> {
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<DrmDeviceFd>,
renderer: &GlesRenderer,
selection: OutputSelection,
) -> Result<DirectOutput, Box<dyn Error>> {
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 surface = GbmBufferedSurface::new(
drm_surface,
allocator,
&[Fourcc::Argb8888, Fourcc::Abgr8888],
renderer.dmabuf_formats(),
)?;
// 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);
let damage_tracker = OutputDamageTracker::from_output(&output);
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,
damage_tracker,
render_metrics,
frame_pending: false,
power_off: false,
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::<Ferese>(&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<Duration> {
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<Duration> {
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);
}
}