diff --git a/compositor/src/backends/direct.rs b/compositor/src/backends/direct.rs index 5b8e748..41b4f55 100644 --- a/compositor/src/backends/direct.rs +++ b/compositor/src/backends/direct.rs @@ -1,20 +1,54 @@ -use std::{error::Error, io}; +use std::{collections::HashMap, error::Error, io, path::Path, time::Duration}; use smithay::{ backend::{ + allocator::gbm::GbmDevice, + drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode}, + egl::{EGLContext, EGLDisplay}, libinput::{LibinputInputBackend, LibinputSessionInterface}, + renderer::gles::GlesRenderer, session::{Event as SessionEvent, Session, libseat::LibSeatSession}, - udev::{UdevBackend, UdevEvent}, + udev::{UdevBackend, UdevEvent, primary_gpu}, }, - reexports::{calloop::EventLoop, input::Libinput}, + reexports::{calloop::EventLoop, input::Libinput, rustix::fs::OFlags}, + utils::DeviceFd, }; use crate::Ferese; +pub struct DirectBackendState { + pub session: LibSeatSession, + pub active: bool, + devices: HashMap, + presentation: PresentationClock, +} + +#[derive(Default)] +struct PresentationClock { + last_presentation: Option, + presented_frames: u64, +} + +struct DirectDevice { + drm: DrmDevice, + #[allow(dead_code)] + gbm: GbmDevice, + #[allow(dead_code)] + renderer: GlesRenderer, +} + 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 @@ -22,8 +56,13 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() .map_err(|()| io::Error::other(format!("failed to assign libinput seat {seat_name}")))?; let libinput_backend = LibinputInputBackend::new(libinput_context.clone()); - state.direct_session = Some(session); - state.session_active = true; + state.direct_backend = Some(DirectBackendState { + session, + active: true, + devices: HashMap::new(), + presentation: PresentationClock::default(), + }); + open_primary_device(event_loop, state, &primary_path)?; event_loop .handle() @@ -34,7 +73,13 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() .handle() .insert_source(notifier, move |event, _, state| match event { SessionEvent::PauseSession => { - state.session_active = false; + if let Some(backend) = state.direct_backend.as_mut() { + backend.active = false; + backend + .devices + .values_mut() + .for_each(|device| device.drm.pause()); + } libinput_context.suspend(); tracing::info!("direct session paused"); } @@ -42,7 +87,14 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() if libinput_context.resume().is_err() { tracing::error!("failed to resume libinput"); } - state.session_active = true; + 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(false) { + tracing::error!(%error, "failed to reactivate DRM device"); + } + } + } tracing::info!("direct session activated"); } })?; @@ -63,3 +115,105 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() tracing::info!(seat = %seat_name, "initialized direct session input and device discovery"); Ok(()) } + +fn open_primary_device( + event_loop: &mut EventLoop, + state: &mut Ferese, + path: &Path, +) -> Result<(), Box> { + let backend = state + .direct_backend + .as_mut() + .expect("direct backend state is initialized before the DRM device"); + let node = DrmNode::from_path(path)?; + let fd = backend.session.open( + path, + OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK, + )?; + let fd = DrmDeviceFd::new(DeviceFd::from(fd)); + let (drm, notifier) = DrmDevice::new(fd.clone(), true)?; + let gbm = GbmDevice::new(fd)?; + // SAFETY: GBM owns a valid DRM descriptor for the lifetime of the EGL display. + let egl_display = unsafe { EGLDisplay::new(gbm.clone())? }; + let egl_context = 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)? }; + + event_loop + .handle() + .insert_source(notifier, move |event, metadata, state| match event { + DrmEvent::VBlank(crtc) => { + if let Some(metadata) = metadata { + state.record_drm_presentation(metadata.time, metadata.sequence); + tracing::trace!(?node, ?crtc, sequence = metadata.sequence, "page flip"); + } + } + DrmEvent::Error(error) => { + tracing::error!(?node, %error, "DRM event error"); + } + })?; + + backend + .devices + .insert(node, DirectDevice { drm, gbm, renderer }); + tracing::info!(?node, ?path, "initialized primary DRM/GBM device"); + Ok(()) +} + +impl DirectBackendState { + pub fn record_presentation(&mut self, time: DrmEventTime, sequence: u32) -> Option { + self.presentation.record(time, sequence) + } +} + +impl PresentationClock { + 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 presentation_clock_uses_measured_monotonic_deltas() { + let mut clock = PresentationClock::default(); + + 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); + } + + #[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); + } +} diff --git a/compositor/src/backends/mod.rs b/compositor/src/backends/mod.rs index 6ab46d0..8a165ef 100644 --- a/compositor/src/backends/mod.rs +++ b/compositor/src/backends/mod.rs @@ -1,4 +1,4 @@ -mod direct; +pub(crate) mod direct; use std::ffi::OsString; diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 07176ca..8f0fd78 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -11,7 +11,7 @@ use ferese_core::{WindowPlacement, WorkspaceSet}; use ferese_layout::{Axis, Direction, GapConfig, LayoutResult, Rect, SizeConstraints, WindowId}; use smithay::{ - backend::session::libseat::LibSeatSession, + backend::drm::DrmEventTime, desktop::{PopupManager, Space, Window, WindowSurfaceType}, input::{Seat, SeatState}, reexports::{ @@ -52,8 +52,7 @@ pub struct Ferese { pub window_geometry: HashMap, pub focused_window: Option, pub intercepted_keys: HashSet, - pub direct_session: Option, - pub session_active: bool, + pub direct_backend: Option, next_window_id: u64, last_animation_tick: Instant, pub popups: PopupManager, @@ -113,8 +112,7 @@ impl Ferese { window_geometry: HashMap::new(), focused_window: None, intercepted_keys: HashSet::new(), - direct_session: None, - session_active: true, + direct_backend: None, next_window_id: 1, last_animation_tick: start_time, popups: PopupManager::default(), @@ -382,6 +380,20 @@ impl Ferese { pub fn advance_animations(&mut self, now: Instant) -> bool { let delta = now.saturating_duration_since(self.last_animation_tick); self.last_animation_tick = now; + self.advance_animations_by(delta) + } + + pub fn record_drm_presentation(&mut self, time: DrmEventTime, sequence: u32) { + let delta = self + .direct_backend + .as_mut() + .and_then(|backend| backend.record_presentation(time, sequence)); + if let Some(delta) = delta { + self.advance_animations_by(delta); + } + } + + fn advance_animations_by(&mut self, delta: std::time::Duration) -> bool { let windows = self .space .elements()