diff --git a/anvil/src/shell/mod.rs b/anvil/src/shell/mod.rs index 4235751233fe..cacf99692560 100644 --- a/anvil/src/shell/mod.rs +++ b/anvil/src/shell/mod.rs @@ -2,6 +2,10 @@ use std::cell::RefCell; #[cfg(feature = "xwayland")] use smithay::xwayland::XWaylandClientData; + +#[cfg(feature = "udev")] +use smithay::wayland::drm_syncobj::DrmSyncobjCachedState; + use smithay::{ backend::renderer::utils::on_commit_buffer_handler, desktop::{ @@ -112,7 +116,17 @@ impl CompositorHandler for AnvilState { fn new_surface(&mut self, surface: &WlSurface) { add_pre_commit_hook::(surface, move |state, _dh, surface| { + #[cfg(feature = "udev")] + let mut acquire_point = None; let maybe_dmabuf = with_states(surface, |surface_data| { + #[cfg(feature = "udev")] + acquire_point.clone_from( + &surface_data + .cached_state + .get::() + .pending() + .acquire_point, + ); surface_data .cached_state .get::() @@ -125,6 +139,21 @@ impl CompositorHandler for AnvilState { }) }); if let Some(dmabuf) = maybe_dmabuf { + #[cfg(feature = "udev")] + if let Some(acquire_point) = acquire_point { + if let Ok((blocker, source)) = acquire_point.generate_blocker() { + let client = surface.client().unwrap(); + let res = state.handle.insert_source(source, move |_, _, data| { + let dh = data.display_handle.clone(); + data.client_compositor_state(&client).blocker_cleared(data, &dh); + Ok(()) + }); + if res.is_ok() { + add_blocker(surface, blocker); + return; + } + } + } if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) { if let Some(client) = surface.client() { let res = state.handle.insert_source(source, move |_, _, data| { diff --git a/anvil/src/udev.rs b/anvil/src/udev.rs index f8fdb82f192e..76f590b26de9 100644 --- a/anvil/src/udev.rs +++ b/anvil/src/udev.rs @@ -85,6 +85,7 @@ use smithay::{ drm_lease::{ DrmLease, DrmLeaseBuilder, DrmLeaseHandler, DrmLeaseRequest, DrmLeaseState, LeaseRejected, }, + drm_syncobj::{supports_syncobj_eventfd, DrmSyncobjHandler, DrmSyncobjState}, }, }; use smithay_drm_extras::{ @@ -124,6 +125,7 @@ pub struct UdevData { pub session: LibSeatSession, dh: DisplayHandle, dmabuf_state: Option<(DmabufState, DmabufGlobal)>, + syncobj_state: Option, primary_gpu: DrmNode, gpus: GpuManager>, backends: HashMap, @@ -247,6 +249,7 @@ pub fn run_udev() { let data = UdevData { dh: display_handle.clone(), dmabuf_state: None, + syncobj_state: None, session, primary_gpu, gpus, @@ -434,6 +437,23 @@ pub fn run_udev() { }); }); + // Expose syncobj protocol if supported by primary GPU + if let Some(primary_node) = state + .backend_data + .primary_gpu + .node_with_type(NodeType::Primary) + .and_then(|x| x.ok()) + { + if let Some(backend) = state.backend_data.backends.get(&primary_node) { + let import_device = backend.drm.device_fd().clone(); + if supports_syncobj_eventfd(&import_device) { + let syncobj_state = + DrmSyncobjState::new::>(&display_handle, import_device); + state.backend_data.syncobj_state = Some(syncobj_state); + } + } + } + event_loop .handle() .insert_source(udev_backend, move |event, _, data| match event { @@ -553,6 +573,13 @@ impl DrmLeaseHandler for AnvilState { delegate_drm_lease!(AnvilState); +impl DrmSyncobjHandler for AnvilState { + fn drm_syncobj_state(&mut self) -> &mut DrmSyncobjState { + self.backend_data.syncobj_state.as_mut().unwrap() + } +} +smithay::delegate_drm_syncobj!(AnvilState); + pub type RenderSurface = GbmBufferedSurface, Option>; pub type GbmDrmCompositor = DrmCompositor< diff --git a/src/backend/allocator/dmabuf.rs b/src/backend/allocator/dmabuf.rs index a28f5917b74f..9e949d7f568e 100644 --- a/src/backend/allocator/dmabuf.rs +++ b/src/backend/allocator/dmabuf.rs @@ -278,7 +278,7 @@ impl Dmabuf { *self.0.node.lock().unwrap() = node.into(); } - /// Create an [`calloop::EventSource`] and [`crate::wayland::compositor::Blocker`] for this [`Dmabuf`]. + /// Create an [`calloop::EventSource`] and [`Blocker`] for this [`Dmabuf`]. /// /// Usually used to block applying surface state on the readiness of an attached dmabuf. #[cfg(feature = "wayland_frontend")] @@ -526,7 +526,7 @@ where } } -/// [`crate::wayland::compositor::Blocker`] implementation for an accompaning [`DmabufSource`] +/// [`Blocker`] implementation for an accompaning [`DmabufSource`] #[cfg(feature = "wayland_frontend")] #[derive(Debug)] pub struct DmabufBlocker(Arc); diff --git a/src/backend/drm/compositor/mod.rs b/src/backend/drm/compositor/mod.rs index 463f5e45aded..8b22b00ee817 100644 --- a/src/backend/drm/compositor/mod.rs +++ b/src/backend/drm/compositor/mod.rs @@ -131,7 +131,7 @@ use std::{ }; use drm::{ - control::{connector, crtc, framebuffer, plane, Mode, PlaneType}, + control::{connector, crtc, framebuffer, plane, Device as _, Mode, PlaneType}, Device, DriverCapability, }; use drm_fourcc::{DrmFormat, DrmFourcc, DrmModifier}; @@ -204,6 +204,22 @@ enum ScanoutBuffer { Cursor(GbmBuffer), } +impl ScanoutBuffer { + fn acquire_point( + &self, + signaled_fence: Option<&Arc>, + ) -> Option<(SyncPoint, Option>)> { + if let Self::Wayland(buffer) = self { + // Assume `DrmSyncobjBlocker` is used, so acquire point has already + // been signaled. Instead of converting with `SyncPoint::from`. + if buffer.acquire_point().is_some() { + return Some((SyncPoint::signaled(), signaled_fence.cloned())); + } + } + None + } +} + impl ScanoutBuffer { #[inline] fn from_underlying_storage(storage: UnderlyingStorage<'_>) -> Option { @@ -1567,6 +1583,7 @@ where supports_fencing: bool, direct_scanout: bool, reset_pending: bool, + signaled_fence: Option>, framebuffer_exporter: F, @@ -1633,6 +1650,24 @@ where cursor_size: Size, gbm: Option>, ) -> FrameResult { + let signaled_fence = match surface.create_syncobj(true) { + Ok(signaled_syncobj) => match surface.syncobj_to_fd(signaled_syncobj, true) { + Ok(signaled_fence) => { + let _ = surface.destroy_syncobj(signaled_syncobj); + Some(Arc::new(signaled_fence)) + } + Err(err) => { + tracing::warn!(?err, "failed to export signaled syncobj"); + let _ = surface.destroy_syncobj(signaled_syncobj); + None + } + }, + Err(err) => { + tracing::warn!(?err, "failed to create signaled syncobj"); + None + } + }; + let span = info_span!( parent: None, "drm_compositor", @@ -1735,6 +1770,7 @@ where primary_is_opaque: is_opaque, direct_scanout: true, reset_pending: true, + signaled_fence, current_frame, pending_frame: None, queued_frame: None, @@ -4095,7 +4131,10 @@ where buffer: element_config.buffer.clone(), damage_clips, plane_claim, - sync: None, + sync: element_config + .buffer + .buffer + .acquire_point(self.signaled_fence.as_ref()), }; let is_compatible = previous_state diff --git a/src/backend/drm/device/fd.rs b/src/backend/drm/device/fd.rs index f59c828d1bbe..e01548a1d9ee 100644 --- a/src/backend/drm/device/fd.rs +++ b/src/backend/drm/device/fd.rs @@ -7,7 +7,7 @@ use tracing::{error, info, warn}; use crate::utils::{DevPath, DeviceFd}; -#[derive(Debug)] +#[derive(Debug, PartialEq)] struct InternalDrmDeviceFd { fd: DeviceFd, privileged: bool, @@ -33,7 +33,7 @@ impl BasicDevice for InternalDrmDeviceFd {} impl ControlDevice for InternalDrmDeviceFd {} /// Ref-counted file descriptor of an open drm device -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct DrmDeviceFd(Arc); impl AsFd for DrmDeviceFd { diff --git a/src/backend/renderer/sync/mod.rs b/src/backend/renderer/sync/mod.rs index 5cf3400b27a5..aad60899e99f 100644 --- a/src/backend/renderer/sync/mod.rs +++ b/src/backend/renderer/sync/mod.rs @@ -58,6 +58,11 @@ impl SyncPoint { } } + /// Returns `true` if `SyncPoint` contains a [`Fence`] + pub fn contains_fence(&self) -> bool { + self.fence.is_some() + } + /// Get a reference to the underlying [`Fence`] if any /// /// Returns `None` if the sync point does not contain a fence diff --git a/src/backend/renderer/utils/wayland.rs b/src/backend/renderer/utils/wayland.rs index 19b30cc2a030..dc8566554b89 100644 --- a/src/backend/renderer/utils/wayland.rs +++ b/src/backend/renderer/utils/wayland.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "backend_drm")] +use crate::wayland::drm_syncobj::{DrmSyncPoint, DrmSyncobjCachedState}; use crate::{ backend::renderer::{buffer_dimensions, buffer_has_alpha, element::RenderElement, ImportAll, Renderer}, utils::{Buffer as BufferCoord, Coordinate, Logical, Physical, Point, Rectangle, Scale, Size, Transform}, @@ -53,12 +55,24 @@ unsafe impl Send for RendererSurfaceState {} unsafe impl Sync for RendererSurfaceState {} #[derive(Debug)] -struct InnerBuffer(WlBuffer); +struct InnerBuffer { + buffer: WlBuffer, + #[cfg(feature = "backend_drm")] + acquire_point: Option, + #[cfg(feature = "backend_drm")] + release_point: Option, +} impl Drop for InnerBuffer { #[inline] fn drop(&mut self) { - self.0.release(); + self.buffer.release(); + #[cfg(feature = "backend_drm")] + if let Some(release_point) = &self.release_point { + if let Err(err) = release_point.signal() { + tracing::error!("Failed to signal syncobj release point: {}", err); + } + } } } @@ -68,12 +82,11 @@ pub struct Buffer { inner: Arc, } -impl From for Buffer { - #[inline] - fn from(buffer: WlBuffer) -> Self { - Buffer { - inner: Arc::new(InnerBuffer(buffer)), - } +impl Buffer { + #[cfg(feature = "backend_drm")] + #[allow(dead_code)] + pub(crate) fn acquire_point(&self) -> Option<&DrmSyncPoint> { + self.inner.acquire_point.as_ref() } } @@ -82,29 +95,34 @@ impl std::ops::Deref for Buffer { #[inline] fn deref(&self) -> &Self::Target { - &self.inner.0 + &self.inner.buffer } } impl PartialEq for Buffer { #[inline] fn eq(&self, other: &WlBuffer) -> bool { - self.inner.0 == *other + self.inner.buffer == *other } } impl PartialEq for &Buffer { #[inline] fn eq(&self, other: &WlBuffer) -> bool { - self.inner.0 == *other + self.inner.buffer == *other } } impl RendererSurfaceState { #[profiling::function] pub(crate) fn update_buffer(&mut self, states: &SurfaceData) { - let mut attrs_state = states.cached_state.get::(); - let attrs = attrs_state.current(); + #[cfg(feature = "backend_drm")] + let mut guard = states.cached_state.get::(); + #[cfg(feature = "backend_drm")] + let syncobj_state = guard.current(); + + let mut guard = states.cached_state.get::(); + let attrs = guard.current(); let new_buffer = matches!(attrs.buffer, Some(BufferAssignment::NewBuffer(_))); match attrs.buffer.take() { @@ -121,7 +139,15 @@ impl RendererSurfaceState { self.buffer_transform = attrs.buffer_transform.into(); if !self.buffer.as_ref().map_or(false, |b| b == buffer) { - self.buffer = Some(Buffer::from(buffer)); + self.buffer = Some(Buffer { + inner: Arc::new(InnerBuffer { + buffer, + #[cfg(feature = "backend_drm")] + acquire_point: syncobj_state.acquire_point.take(), + #[cfg(feature = "backend_drm")] + release_point: syncobj_state.release_point.take(), + }), + }); } self.textures.clear(); diff --git a/src/wayland/compositor/tree.rs b/src/wayland/compositor/tree.rs index 6662bbdc584f..805044f648c1 100644 --- a/src/wayland/compositor/tree.rs +++ b/src/wayland/compositor/tree.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "backend_drm")] +use crate::wayland::drm_syncobj::DrmSyncobjCachedState; use crate::{ utils::{ hook::{Hook, HookId}, @@ -150,9 +152,25 @@ impl PrivateSurfaceData { if let Some(BufferAssignment::NewBuffer(buffer)) = guard.pending().buffer.take() { buffer.release(); }; + drop(guard); + #[cfg(feature = "backend_drm")] + let mut guard = my_data.public_data.cached_state.get::(); + #[cfg(feature = "backend_drm")] + if let Some(release_point) = &guard.pending().release_point { + if let Err(err) = release_point.signal() { + tracing::error!("Failed to signal syncobj release point: {}", err); + } + } + #[cfg(feature = "backend_drm")] + if let Some(release_point) = &guard.current().release_point { + if let Err(err) = release_point.signal() { + tracing::error!("Failed to signal syncobj release point: {}", err); + } + } let hooks = my_data.destruction_hooks.clone(); // don't hold the mutex while the hooks are invoked + #[cfg(feature = "backend_drm")] drop(guard); drop(my_data); for hook in hooks { diff --git a/src/wayland/drm_syncobj/mod.rs b/src/wayland/drm_syncobj/mod.rs new file mode 100644 index 000000000000..46845e91b6d4 --- /dev/null +++ b/src/wayland/drm_syncobj/mod.rs @@ -0,0 +1,398 @@ +//! DRM syncobj protocol +//! +//! This module implement the `linux-drm-syncobj-v1` protocol, used to support +//! explicit sync. +//! +//! Currently, the implementation here assumes acquire fences are already signalled +//! when the surface transaction is ready. Use [`DrmSyncPointBlocker`]. +//! +//! The server should only expose the protocol if [`supports_syncobj_eventfd`] returns +//! `true`. Or it won't be possible to create the blocker. This is similar to other +//! implementations. +//! +//! The release fence is signalled when all references to a +//! [`Buffer`][crate::backend::renderer::utils::Buffer] are dropped. +//! +//! ```no_run +//! # use smithay::delegate_drm_syncobj; +//! # use smithay::wayland::drm_syncobj::*; +//! +//! pub struct State { +//! syncobj_state: Option, +//! } +//! +//! impl DrmSyncobjHandler for State { +//! fn drm_syncobj_state(&mut self) -> &mut DrmSyncobjState { +//! self.syncobj_state.as_mut().unwrap() +//! } +//! } +//! +//! # let mut display = wayland_server::Display::::new().unwrap(); +//! # let display_handle = display.handle(); +//! # let import_device = todo!(); +//! let syncobj_state = if supports_syncobj_eventfd(&import_device) { +//! Some(DrmSyncobjState::new::(&display_handle, import_device)) +//! } else { +//! None +//! }; +//! +//! delegate_drm_syncobj!(State); +//! ``` + +use std::{cell::RefCell, os::unix::io::AsFd}; +use wayland_protocols::wp::linux_drm_syncobj::v1::server::{ + wp_linux_drm_syncobj_manager_v1::{self, WpLinuxDrmSyncobjManagerV1}, + wp_linux_drm_syncobj_surface_v1::{self, WpLinuxDrmSyncobjSurfaceV1}, + wp_linux_drm_syncobj_timeline_v1::{self, WpLinuxDrmSyncobjTimelineV1}, +}; +use wayland_server::{ + protocol::wl_surface::WlSurface, Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, + Resource, Weak, +}; + +use super::{ + compositor::{self, with_states, BufferAssignment, Cacheable, SurfaceAttributes}, + dmabuf::get_dmabuf, +}; +use crate::backend::drm::DrmDeviceFd; + +mod sync_point; +pub use sync_point::*; + +/// Test if DRM device supports `syncobj_eventfd`. +// Similar to test used in Mutter +pub fn supports_syncobj_eventfd(device: &DrmDeviceFd) -> bool { + // Pass device as palceholder for eventfd as well, since `drm_ffi` requires + // a valid fd. + match drm_ffi::syncobj::eventfd(device.as_fd(), 0, 0, device.as_fd(), false) { + Ok(_) => unreachable!(), + Err(err) => err.kind() == std::io::ErrorKind::NotFound, + } +} + +/// Handler trait for DRM syncobj protocol. +pub trait DrmSyncobjHandler { + /// Returns a mutable reference to the [`DrmSyncobjState`] delegate type + fn drm_syncobj_state(&mut self) -> &mut DrmSyncobjState; +} + +/// Data associated with a drm syncobj global +#[allow(missing_debug_implementations)] +pub struct DrmSyncobjGlobalData { + filter: Box Fn(&'c Client) -> bool + Send + Sync>, +} + +/// Pending DRM syncobj sync point state +#[derive(Debug, Default)] +pub struct DrmSyncobjCachedState { + /// Timeline point signaled when buffer is ready to read + pub acquire_point: Option, + /// Timeline point to be signaled when server is done with buffer + pub release_point: Option, +} + +impl Cacheable for DrmSyncobjCachedState { + fn commit(&mut self, _dh: &DisplayHandle) -> Self { + Self { + acquire_point: self.acquire_point.take(), + release_point: self.release_point.take(), + } + } + + fn merge_into(self, into: &mut Self, _dh: &DisplayHandle) { + if self.acquire_point.is_some() && self.release_point.is_some() { + if let Some(release_point) = &into.release_point { + if let Err(err) = release_point.signal() { + tracing::error!("Failed to signal syncobj release point: {}", err); + } + } + into.acquire_point = self.acquire_point; + into.release_point = self.release_point; + } + } +} + +/// Delegate type for a `wp_linux_drm_syncobj_manager_v1` global +#[derive(Debug)] +pub struct DrmSyncobjState { + import_device: DrmDeviceFd, +} + +impl DrmSyncobjState { + /// Create a new `wp_linux_drm_syncobj_manager_v1` global + /// + /// The `import_device` will be used to import the syncobj fds, and wait on them. + pub fn new(display: &DisplayHandle, import_device: DrmDeviceFd) -> Self + where + D: GlobalDispatch, + D: 'static, + { + Self::new_with_filter::(display, import_device, |_| true) + } + + /// Create a new `wp_linuxdrm_syncobj_manager_v1` global with a client filter + /// + /// The `import_device` will be used to import the syncobj fds, and wait on them. + pub fn new_with_filter(display: &DisplayHandle, import_device: DrmDeviceFd, filter: F) -> Self + where + D: GlobalDispatch, + D: 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let _global = display.create_global::( + 1, + DrmSyncobjGlobalData { + filter: Box::new(filter), + }, + ); + + Self { import_device } + } +} + +impl GlobalDispatch for DrmSyncobjState +where + D: Dispatch, +{ + fn bind( + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &DrmSyncobjGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init::<_, _>(resource, ()); + } + + fn can_view(client: Client, global_data: &DrmSyncobjGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +fn commit_hook(_data: &mut D, _dh: &DisplayHandle, surface: &WlSurface) { + compositor::with_states(surface, |states| { + let mut cached = states.cached_state.get::(); + let pending = cached.pending(); + let new_buffer = pending.buffer.as_ref().and_then(|buffer| match buffer { + BufferAssignment::NewBuffer(buffer) => Some(buffer), + _ => None, + }); + if let Some(data) = states + .data_map + .get::>>() + { + if let Some(syncobj_surface) = data.borrow().as_ref() { + let mut cached = states.cached_state.get::(); + let pending = cached.pending(); + if pending.acquire_point.is_some() && new_buffer.is_none() { + syncobj_surface.post_error( + wp_linux_drm_syncobj_surface_v1::Error::NoBuffer as u32, + "acquire point without buffer".to_string(), + ); + } else if pending.acquire_point.is_some() && pending.release_point.is_none() { + syncobj_surface.post_error( + wp_linux_drm_syncobj_surface_v1::Error::NoReleasePoint as u32, + "acquire point without release point".to_string(), + ); + } else if pending.acquire_point.is_none() && pending.release_point.is_some() { + syncobj_surface.post_error( + wp_linux_drm_syncobj_surface_v1::Error::NoAcquirePoint as u32, + "release point without acquire point".to_string(), + ); + } else if let (Some(acquire), Some(release)) = + (pending.acquire_point.as_ref(), pending.release_point.as_ref()) + { + if acquire.timeline == release.timeline && acquire.point <= release.point { + syncobj_surface.post_error( + wp_linux_drm_syncobj_surface_v1::Error::ConflictingPoints as u32, + format!( + "release point '{}' is not greater than acquire point '{}'", + release.point, acquire.point + ), + ); + } + if let Some(buffer) = new_buffer { + if get_dmabuf(buffer).is_err() { + syncobj_surface.post_error( + wp_linux_drm_syncobj_surface_v1::Error::UnsupportedBuffer as u32, + "sync points with non-dmabuf buffer".to_string(), + ); + } + } + } + } + } + }); +} + +impl Dispatch for DrmSyncobjState +where + D: Dispatch, + D: Dispatch, + D: DrmSyncobjHandler, +{ + fn request( + state: &mut D, + _client: &Client, + resource: &WpLinuxDrmSyncobjManagerV1, + request: wp_linux_drm_syncobj_manager_v1::Request, + _data: &(), + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + wp_linux_drm_syncobj_manager_v1::Request::GetSurface { id, surface } => { + let already_exists = with_states(&surface, |states| { + states + .data_map + .get::>>() + .map(|v| v.borrow().is_some()) + .unwrap_or(false) + }); + if already_exists { + resource.post_error( + wp_linux_drm_syncobj_manager_v1::Error::SurfaceExists as u32, + "the surface already has a syncobj_surface object associated".to_string(), + ); + return; + } + let syncobj_surface = data_init.init::<_, _>( + id, + DrmSyncobjSurfaceData { + surface: surface.downgrade(), + }, + ); + with_states(&surface, |states| { + states + .data_map + .insert_if_missing(|| RefCell::new(Some(syncobj_surface))) + }); + compositor::add_pre_commit_hook::(&surface, commit_hook); + } + wp_linux_drm_syncobj_manager_v1::Request::ImportTimeline { id, fd } => { + match DrmTimeline::new(&state.drm_syncobj_state().import_device, fd.as_fd()) { + Ok(timeline) => { + data_init.init::<_, _>(id, DrmSyncobjTimelineData { timeline }); + } + Err(err) => { + resource.post_error( + wp_linux_drm_syncobj_manager_v1::Error::InvalidTimeline as u32, + format!("failed to import syncobj timeline: {}", err), + ); + } + } + } + wp_linux_drm_syncobj_manager_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +/// Data attached to wp_linux_drm_syncobj_surface_v1 objects +#[derive(Debug)] +pub struct DrmSyncobjSurfaceData { + surface: Weak, +} + +impl Dispatch for DrmSyncobjState { + fn request( + _state: &mut D, + _client: &Client, + _resource: &WpLinuxDrmSyncobjSurfaceV1, + request: wp_linux_drm_syncobj_surface_v1::Request, + data: &DrmSyncobjSurfaceData, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + let Ok(surface) = data.surface.upgrade() else { + return; + }; + match request { + wp_linux_drm_syncobj_surface_v1::Request::Destroy => {} + wp_linux_drm_syncobj_surface_v1::Request::SetAcquirePoint { + timeline, + point_hi, + point_lo, + } => { + let sync_point = DrmSyncPoint { + timeline: timeline + .data::() + .unwrap() + .timeline + .clone(), + point: ((point_hi as u64) << 32) + (point_lo as u64), + }; + with_states(&surface, |states| { + let mut cached = states.cached_state.get::(); + let cached_state = cached.pending(); + cached_state.acquire_point = Some(sync_point); + }); + } + wp_linux_drm_syncobj_surface_v1::Request::SetReleasePoint { + timeline, + point_hi, + point_lo, + } => { + let sync_point = DrmSyncPoint { + timeline: timeline + .data::() + .unwrap() + .timeline + .clone(), + point: ((point_hi as u64) << 32) + (point_lo as u64), + }; + with_states(&surface, |states| { + let mut cached = states.cached_state.get::(); + let cached_state = cached.pending(); + cached_state.release_point = Some(sync_point); + }); + } + _ => unreachable!(), + } + } +} + +/// Data attached to wp_linux_drm_syncobj_timeline_v1 objects +#[derive(Debug)] +pub struct DrmSyncobjTimelineData { + timeline: DrmTimeline, +} + +impl Dispatch for DrmSyncobjState { + fn request( + _state: &mut D, + _client: &Client, + _resource: &WpLinuxDrmSyncobjTimelineV1, + request: wp_linux_drm_syncobj_timeline_v1::Request, + _data: &DrmSyncobjTimelineData, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + wp_linux_drm_syncobj_timeline_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +/// Macro to delegate implementation of the drm syncobj protocol to [`DrmSyncobjState`]. +/// +/// You must also implement [`DrmSyncobjHandler`] to use this. +#[macro_export] +macro_rules! delegate_drm_syncobj { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + $crate::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::wayland_protocols::wp::linux_drm_syncobj::v1::server::wp_linux_drm_syncobj_manager_v1::WpLinuxDrmSyncobjManagerV1: $crate::wayland::drm_syncobj::DrmSyncobjGlobalData + ] => $crate::wayland::drm_syncobj::DrmSyncobjState); + $crate::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::wayland_protocols::wp::linux_drm_syncobj::v1::server::wp_linux_drm_syncobj_manager_v1::WpLinuxDrmSyncobjManagerV1: () + ] => $crate::wayland::drm_syncobj::DrmSyncobjState); + $crate::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::wayland_protocols::wp::linux_drm_syncobj::v1::server::wp_linux_drm_syncobj_surface_v1::WpLinuxDrmSyncobjSurfaceV1: $crate::wayland::drm_syncobj::DrmSyncobjSurfaceData + ] => $crate::wayland::drm_syncobj::DrmSyncobjState); + $crate::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + $crate::reexports::wayland_protocols::wp::linux_drm_syncobj::v1::server::wp_linux_drm_syncobj_timeline_v1::WpLinuxDrmSyncobjTimelineV1: $crate::wayland::drm_syncobj::DrmSyncobjTimelineData + ] => $crate::wayland::drm_syncobj::DrmSyncobjState); + } +} diff --git a/src/wayland/drm_syncobj/sync_point.rs b/src/wayland/drm_syncobj/sync_point.rs new file mode 100644 index 000000000000..3c2738d038b2 --- /dev/null +++ b/src/wayland/drm_syncobj/sync_point.rs @@ -0,0 +1,203 @@ +use calloop::generic::Generic; +use calloop::{EventSource, Interest, Mode, Poll, PostAction, Readiness, Token, TokenFactory}; +use drm::control::Device; +use std::{ + io, + os::unix::io::{AsFd, BorrowedFd, OwnedFd}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; + +use crate::backend::drm::DrmDeviceFd; +use crate::backend::renderer::sync::{Fence, Interrupted}; +use crate::wayland::compositor::{Blocker, BlockerState}; + +#[derive(Debug, PartialEq)] +struct DrmTimelineInner { + device: DrmDeviceFd, + syncobj: drm::control::syncobj::Handle, +} + +impl Drop for DrmTimelineInner { + fn drop(&mut self) { + let _ = self.device.destroy_syncobj(self.syncobj); + } +} + +/// DRM timeline syncobj +#[derive(Clone, Debug, PartialEq)] +pub struct DrmTimeline(Arc); + +impl DrmTimeline { + /// Import DRM timeline from file descriptor + pub fn new(device: &DrmDeviceFd, fd: BorrowedFd<'_>) -> io::Result { + Ok(Self(Arc::new(DrmTimelineInner { + device: device.clone(), + syncobj: device.fd_to_syncobj(fd, false)?, + }))) + } + + /// Query the last signalled timeline point + pub fn query_signalled_point(&self) -> io::Result { + let mut points = [0]; + self.0 + .device + .syncobj_timeline_query(&[self.0.syncobj], &mut points, false)?; + Ok(points[0]) + } +} + +/// Point on a DRM timeline syncobj +#[derive(Clone, Debug, PartialEq)] +pub struct DrmSyncPoint { + pub(super) timeline: DrmTimeline, + pub(super) point: u64, +} + +impl DrmSyncPoint { + /// Create an eventfd that will be signaled by the syncpoint + pub fn eventfd(&self) -> io::Result { + let fd = rustix::event::eventfd( + 0, + rustix::event::EventfdFlags::CLOEXEC | rustix::event::EventfdFlags::NONBLOCK, + )?; + self.timeline + .0 + .device + .syncobj_eventfd(self.timeline.0.syncobj, self.point, fd.as_fd(), false)?; + Ok(fd) + } + + /// Signal the sync point. + pub fn signal(&self) -> io::Result<()> { + self.timeline + .0 + .device + .syncobj_timeline_signal(&[self.timeline.0.syncobj], &[self.point]) + } + + /// Wait for sync point. + pub fn wait(&self, timeout_nsec: i64) -> io::Result<()> { + self.timeline.0.device.syncobj_timeline_wait( + &[self.timeline.0.syncobj], + &[self.point], + timeout_nsec, + false, + false, + false, + )?; + Ok(()) + } + + /// Export DRM sync file for sync point. + pub fn export_sync_file(&self) -> io::Result { + let syncobj = self.timeline.0.device.create_syncobj(false)?; + // Wrap in `DrmTimelineInner` to destroy on drop + let syncobj = DrmTimelineInner { + device: self.timeline.0.device.clone(), + syncobj, + }; + syncobj + .device + .syncobj_timeline_transfer(self.timeline.0.syncobj, syncobj.syncobj, self.point, 0)?; + syncobj.device.syncobj_to_fd(syncobj.syncobj, true) + } + + /// Create an [`calloop::EventSource`] and [`Blocker`] for this sync point. + /// + /// This will fail if `drmSyncobjEventfd` isn't supported by the device. See + /// [`supports_syncobj_eventfd`](super::supports_syncobj_eventfd). + pub fn generate_blocker(&self) -> io::Result<(DrmSyncPointBlocker, DrmSyncPointSource)> { + let fd = self.eventfd()?; + let signal = Arc::new(AtomicBool::new(false)); + let blocker = DrmSyncPointBlocker { + signal: signal.clone(), + }; + let source = DrmSyncPointSource { + source: Generic::new(fd, Interest::READ, Mode::Level), + signal, + }; + Ok((blocker, source)) + } +} + +impl Fence for DrmSyncPoint { + fn is_signaled(&self) -> bool { + self.timeline + .query_signalled_point() + .ok() + .map_or(false, |point| point >= self.point) + } + + fn wait(&self) -> Result<(), Interrupted> { + self.wait(i64::MAX).map_err(|_| Interrupted) + } + + fn is_exportable(&self) -> bool { + true + } + + fn export(&self) -> Option { + self.export_sync_file().ok() + } +} + +/// Event source generating an event when a [`DrmSyncPoint`] is signalled.. +#[derive(Debug)] +pub struct DrmSyncPointSource { + source: Generic, + signal: Arc, +} + +impl EventSource for DrmSyncPointSource { + type Event = (); + type Metadata = (); + type Ret = Result<(), std::io::Error>; + type Error = io::Error; + + fn process_events( + &mut self, + readiness: Readiness, + token: Token, + mut callback: C, + ) -> Result + where + C: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, + { + self.signal.store(true, Ordering::SeqCst); + self.source + .process_events(readiness, token, |_, _| Ok(PostAction::Remove))?; + callback((), &mut ())?; + Ok(PostAction::Remove) + } + + fn register(&mut self, poll: &mut Poll, token_factory: &mut TokenFactory) -> calloop::Result<()> { + self.source.register(poll, token_factory) + } + + fn reregister(&mut self, poll: &mut Poll, token_factory: &mut TokenFactory) -> calloop::Result<()> { + self.source.reregister(poll, token_factory) + } + + fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> { + self.source.unregister(poll) + } +} + +/// [`Blocker`] implementation for an accompaning [`DrmSyncPointSource`] +#[derive(Debug)] +pub struct DrmSyncPointBlocker { + signal: Arc, +} + +impl Blocker for DrmSyncPointBlocker { + fn state(&self) -> BlockerState { + if self.signal.load(Ordering::SeqCst) { + BlockerState::Released + } else { + BlockerState::Pending + } + } +} diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs index d7e56c56cae4..ecddf6588a50 100644 --- a/src/wayland/mod.rs +++ b/src/wayland/mod.rs @@ -53,6 +53,8 @@ pub mod cursor_shape; pub mod dmabuf; #[cfg(feature = "backend_drm")] pub mod drm_lease; +#[cfg(feature = "backend_drm")] +pub mod drm_syncobj; pub mod foreign_toplevel_list; pub mod fractional_scale; pub mod idle_inhibit;