use std::io; use std::os::unix::io::{AsFd, BorrowedFd}; use std::sync::atomic::Ordering; use std::sync::Arc; use drm::control::{connector, crtc, framebuffer, plane, Device as ControlDevice, Mode}; use drm::Device as BasicDevice; use libc::dev_t; pub(super) mod atomic; #[cfg(feature = "backend_gbm")] pub(super) mod gbm; pub(super) mod legacy; use super::{ device::PlaneClaimStorage, error::Error, plane_type, DrmDeviceFd, PlaneClaim, PlaneInfo, PlaneType, Planes, }; use crate::utils::DevPath; use crate::utils::{Buffer, Physical, Point, Rectangle, Transform}; use atomic::AtomicDrmSurface; use legacy::LegacyDrmSurface; /// An open crtc + plane combination that can be used for scan-out #[derive(Debug)] pub struct DrmSurface { // This field is only read when 'backend_session' is enabled #[allow(dead_code)] pub(super) dev_id: dev_t, pub(super) crtc: crtc::Handle, pub(super) planes: Planes, pub(super) internal: Arc, pub(super) plane_claim_storage: PlaneClaimStorage, pub(super) primary_plane: (PlaneInfo, PlaneClaim), } #[derive(Debug)] struct PlaneDamageInner { drm: DrmDeviceFd, blob: Option>, } impl Drop for PlaneDamageInner { fn drop(&mut self) { // There is nothing we can do if that fails if let Some(drm::control::property::Value::Blob(id)) = self.blob.take() { let _ = self.drm.destroy_property_blob(id); } } } #[derive(Debug)] /// Helper for `FB_DAMAGE_CLIPS` pub struct PlaneDamageClips { inner: Arc, } impl PlaneDamageClips { /// Returns the underlying blob pub fn blob(&self) -> drm::control::property::Value<'_> { self.inner.blob.unwrap() } } impl PlaneDamageClips { /// Initialize damage clips for a a plane #[profiling::function] pub fn from_damage( device: &DrmDeviceFd, src: Rectangle, dst: Rectangle, damage: impl IntoIterator>, ) -> io::Result> { let scale = src.size / dst.size.to_logical(1).to_buffer(1, Transform::Normal).to_f64(); let mut rects = damage .into_iter() .map(|rect| { let mut rect = rect .to_f64() .to_logical(1f64) .to_buffer( 1f64, Transform::Normal, &src.size.to_logical(1f64, Transform::Normal), ) .upscale(scale); rect.loc += src.loc; let rect = rect.to_i32_up(); drm_ffi::drm_mode_rect { x1: rect.loc.x, y1: rect.loc.y, x2: rect.loc.x.saturating_add(rect.size.w), y2: rect.loc.y.saturating_add(rect.size.h), } }) .collect::>(); if rects.is_empty() { return Ok(None); } let data = unsafe { std::slice::from_raw_parts_mut( rects.as_mut_ptr() as *mut u8, std::mem::size_of::() * rects.len(), ) }; let blob = drm_ffi::mode::create_property_blob(device.as_fd(), data)?; Ok(Some(PlaneDamageClips { inner: Arc::new(PlaneDamageInner { drm: device.clone(), blob: Some(drm::control::property::Value::Blob(blob.blob_id as u64)), }), })) } } impl Clone for PlaneDamageClips { fn clone(&self) -> Self { Self { inner: self.inner.clone(), } } } /// State of a single plane #[derive(Debug, Clone)] pub struct PlaneState<'a> { /// Handle of the plane pub handle: plane::Handle, /// Configuration for that plane /// /// Can be `None` if nothing is attached pub config: Option>, } /// Configuration for a single plane #[derive(Debug, Clone)] pub struct PlaneConfig<'a> { /// Source [`Rectangle`] of the attached framebuffer pub src: Rectangle, /// Destination [`Rectangle`] on the CRTC pub dst: Rectangle, /// Transform for the attached framebuffer pub transform: Transform, /// Alpha value for the plane pub alpha: f32, /// Damage clips of the attached framebuffer pub damage_clips: Option>, /// Framebuffer handle pub fb: framebuffer::Handle, /// Optional fence pub fence: Option>, } /// VRR support state #[derive(Debug, Clone, Copy, PartialEq)] pub enum VrrSupport { /// VRR is generally not supported NotSupported, /// VRR is supported, but changing it requires a modeset RequiresModeset, /// VRR is supported and can be changed without a modeset Supported, } #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum DrmSurfaceInternal { Atomic(AtomicDrmSurface), Legacy(LegacyDrmSurface), } impl AsFd for DrmSurface { fn as_fd(&self) -> BorrowedFd<'_> { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.fd.as_fd(), DrmSurfaceInternal::Legacy(surf) => surf.fd.as_fd(), } } } impl BasicDevice for DrmSurface {} impl ControlDevice for DrmSurface {} impl DrmSurface { /// Returns the underlying [`DrmDeviceFd`] pub fn device_fd(&self) -> &DrmDeviceFd { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.device_fd(), DrmSurfaceInternal::Legacy(surf) => surf.device_fd(), } } /// Returns whether this surface is using the legacy api pub fn is_legacy(&self) -> bool { matches!(&*self.internal, DrmSurfaceInternal::Legacy(_)) } /// Returns the underlying [`crtc`](drm::control::crtc) of this surface pub fn crtc(&self) -> crtc::Handle { self.crtc } /// Returns the underlying primary [`plane`](drm::control::plane) of this surface pub fn plane(&self) -> plane::Handle { self.primary_plane.0.handle } /// Returns the [`PlaneInfo`] of the underlying primary [`plane`](drm::control::plane) of this surface pub fn plane_info(&self) -> &PlaneInfo { &self.primary_plane.0 } /// Currently used [`connector`](drm::control::connector)s of this surface pub fn current_connectors(&self) -> impl IntoIterator { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.current_connectors(), DrmSurfaceInternal::Legacy(surf) => surf.current_connectors(), } } /// Returns the pending [`connector`](drm::control::connector)s /// used after the next [`commit`](DrmSurface::commit) of this surface pub fn pending_connectors(&self) -> impl IntoIterator { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.pending_connectors(), DrmSurfaceInternal::Legacy(surf) => surf.pending_connectors(), } } /// Tries to add a new [`connector`](drm::control::connector) /// to be used after the next commit. /// /// **Warning**: You need to make sure, that the connector is not used with another surface /// or was properly removed via `remove_connector` + `commit` before adding it to another surface. /// Behavior if failing to do so is undefined, but might result in rendering errors or the connector /// getting removed from the other surface without updating it's internal state. /// /// Fails if the `connector` is not compatible with the underlying [`crtc`](drm::control::crtc) /// (e.g. no suitable [`encoder`](drm::control::encoder) may be found) /// or is not compatible with the currently pending /// [`Mode`](drm::control::Mode). pub fn add_connector(&self, connector: connector::Handle) -> Result<(), Error> { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.add_connector(connector), DrmSurfaceInternal::Legacy(surf) => surf.add_connector(connector), } } /// Tries to mark a [`connector`](drm::control::connector) /// for removal on the next commit. pub fn remove_connector(&self, connector: connector::Handle) -> Result<(), Error> { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.remove_connector(connector), DrmSurfaceInternal::Legacy(surf) => surf.remove_connector(connector), } } /// Tries to replace the current connector set with the newly provided one on the next commit. /// /// Fails if one new `connector` is not compatible with the underlying [`crtc`](drm::control::crtc) /// (e.g. no suitable [`encoder`](drm::control::encoder) may be found) /// or is not compatible with the currently pending /// [`Mode`](drm::control::Mode). pub fn set_connectors(&self, connectors: &[connector::Handle]) -> Result<(), Error> { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.set_connectors(connectors), DrmSurfaceInternal::Legacy(surf) => surf.set_connectors(connectors), } } /// Returns the currently active [`Mode`](drm::control::Mode) /// of the underlying [`crtc`](drm::control::crtc) pub fn current_mode(&self) -> Mode { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.current_mode(), DrmSurfaceInternal::Legacy(surf) => surf.current_mode(), } } /// Returns the currently pending [`Mode`](drm::control::Mode) /// to be used after the next commit. pub fn pending_mode(&self) -> Mode { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.pending_mode(), DrmSurfaceInternal::Legacy(surf) => surf.pending_mode(), } } /// Tries to set a new [`Mode`](drm::control::Mode) /// to be used after the next commit. /// /// Fails if the mode is not compatible with the underlying /// [`crtc`](drm::control::crtc) or any of the /// pending [`connector`](drm::control::connector)s. pub fn use_mode(&self, mode: Mode) -> Result<(), Error> { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.use_mode(mode), DrmSurfaceInternal::Legacy(surf) => surf.use_mode(mode), } } /// Returns if Variable Refresh Rate is advertised as supported by the given connector. /// /// Note: This will always return [`VrrSupport::NotSupported`] if the underlying /// implementation is using the legacy DRM api. pub fn vrr_supported(&self, conn: connector::Handle) -> Result { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.vrr_supported(conn), DrmSurfaceInternal::Legacy(_) => Ok(VrrSupport::NotSupported), } } /// Returns whether the next frame state would set Variable Refresh Rate as enabled. /// /// Note: This will always return `false` if the underlying implementation is using the /// legacy DRM api. pub fn vrr_enabled(&self) -> bool { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.vrr_enabled(), DrmSurfaceInternal::Legacy(_) => false, } } /// Tries to set Variable Refresh Rate (VRR) for the next frame. /// /// Doing so might cause [`DrmSurface::commit_pending`] to return `true`. /// Check [`DrmSurface::vrr_supported`], which indicates if VRR can be /// used without a modeset on the attached connectors. pub fn use_vrr(&self, vrr: bool) -> Result<(), Error> { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.use_vrr(vrr), DrmSurfaceInternal::Legacy(_) => Err(Error::UnknownProperty { handle: self.crtc.into(), name: "VRR_ENABLED", }), } } /// Disables the given plane. /// /// Errors if the plane is not supported by this crtc or if the underlying /// implementation does not support the use of planes. pub fn clear_plane(&self, plane: plane::Handle) -> Result<(), Error> { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.clear_plane(plane), DrmSurfaceInternal::Legacy(_) => Err(Error::NonPrimaryPlane(plane)), } } /// Returns true whenever any state changes are pending to be commited /// /// The following functions may trigger a pending commit: /// - [`add_connector`](DrmSurface::add_connector) /// - [`remove_connector`](DrmSurface::remove_connector) /// - [`use_mode`](DrmSurface::use_mode) pub fn commit_pending(&self) -> bool { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.commit_pending(), DrmSurfaceInternal::Legacy(surf) => surf.commit_pending(), } } /// Test a state given a set of framebuffers. /// /// *Note*: This will always return `Ok` for legacy devices if `allow_modeset = false`. /// The legacy drm api has no way to test a buffer without triggering a modeset. #[profiling::function] pub fn test_state<'a>( &self, planes: impl IntoIterator>, allow_modeset: bool, ) -> Result<(), Error> { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.test_state(planes, allow_modeset), DrmSurfaceInternal::Legacy(surf) => { let fb = ensure_legacy_planes(self, planes)?; if allow_modeset { surf.test_buffer(fb, &self.pending_mode()) } else { // Legacy can not test a buffer without triggering a modeset, so we can // only assume it works and hope for the best. A later call to commit or // page_flip will show the correct result Ok(()) } } } } /// Commit the pending state rendering a given set of framebuffers. /// /// *Note*: This will trigger a full modeset on the underlying device, /// potentially causing some flickering. Check before performing this /// operation if a commit really is necessary using [`commit_pending`](DrmSurface::commit_pending). /// /// This operation is not necessarily blocking until the crtc is in the desired state, /// but will trigger a `vblank` event once done. /// Make sure to have the device registered in your event loop prior to invoking this, to not miss /// any generated event. #[profiling::function] pub fn commit<'a>( &self, planes: impl IntoIterator>, event: bool, ) -> Result<(), Error> { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.commit(planes, event), DrmSurfaceInternal::Legacy(surf) => { let fb = ensure_legacy_planes(self, planes)?; surf.commit(fb, event) } } } /// Page-flip the underlying [`crtc`](drm::control::crtc) /// to a new given set of [`framebuffer`]s. /// /// This will not cause the crtc to modeset. /// /// This operation is not blocking and will produce a `vblank` event once swapping is done. /// Make sure to have the device registered in your event loop to not miss the event. #[profiling::function] pub fn page_flip<'a>( &self, planes: impl IntoIterator>, event: bool, ) -> Result<(), Error> { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.page_flip(planes, event), DrmSurfaceInternal::Legacy(surf) => { let fb = ensure_legacy_planes(self, planes)?; surf.page_flip(fb, event) } } } /// Returns a set of available planes for this surface pub fn planes(&self) -> &Planes { &self.planes } /// Claim a plane so that it won't be used by a different crtc /// /// Returns `None` if the plane could not be claimed pub fn claim_plane(&self, plane: plane::Handle) -> Option { // Validate that we are called with an plane that belongs to us if self.planes.primary.iter().any(|p| p.handle == plane) || self.planes.cursor.iter().any(|p| p.handle == plane) || self.planes.overlay.iter().any(|p| p.handle == plane) { self.plane_claim_storage.claim(plane, self.crtc) } else { None } } /// Re-evaluates the current state of the crtc. /// /// It is recommended to call this function after this used [`Session`](crate::backend::session::Session) /// gets re-activated / VT switched to. /// /// Usually you do not need to call this in other circumstances, but if /// the state of the crtc is modified elsewhere, you may call this function /// to reset it's internal state. pub fn reset_state(&self) -> Result<(), Error> { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.reset_state::(None), DrmSurfaceInternal::Legacy(surf) => surf.reset_state::(None), } } /// Returns if the underlying device is currently paused or not. pub fn is_active(&self) -> bool { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.active.load(Ordering::SeqCst), DrmSurfaceInternal::Legacy(surf) => surf.active.load(Ordering::SeqCst), } } #[cfg(feature = "backend_gbm")] pub(super) fn span(&self) -> &tracing::Span { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => &surf.span, DrmSurfaceInternal::Legacy(surf) => &surf.span, } } /// Clear the surface, setting DPMS state to off and disabling all planes. /// /// The surface will be re-enabled on the next [`page_flip`][Self::page_flip] or /// [`commit`][Self::commit]. pub fn clear(&self) -> Result<(), Error> { match &*self.internal { DrmSurfaceInternal::Atomic(surf) => surf.clear(), DrmSurfaceInternal::Legacy(surf) => surf.clear(), } } } fn ensure_legacy_planes<'a>( dev: &(impl ControlDevice + DevPath), planes: impl IntoIterator>, ) -> Result { let state = planes.into_iter().next().ok_or(Error::NoPlane)?; if plane_type(dev, state.handle)? != PlaneType::Primary { return Err(Error::NonPrimaryPlane(state.handle)); } let Some(config) = state.config else { // we need a config on the primary plane return Err(Error::NoFramebuffer(state.handle)); }; if config.dst.loc != Point::default() { // legacy does not support crtc position (technically we could do it, // but the position can only be changed by commit, not by page-flip, // so we just not allow it) return Err(Error::UnsupportedPlaneConfiguration(state.handle)); } if config.src.loc != Point::default() || config .src .size .to_logical(1.0, Transform::Normal) .to_physical(1.0) != config.dst.size.to_f64() { // legacy does not support crop nor scale return Err(Error::UnsupportedPlaneConfiguration(state.handle)); } if config.transform != Transform::Normal { // legacy does not support transform return Err(Error::UnsupportedPlaneConfiguration(state.handle)); } Ok(config.fb) }