Skip to main content

drmkit_display/
profile.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! What a DRM node can do, probed once.
5//!
6//! Consolidates the "can this device…" questions a presenter has to answer
7//! before it commits anything.
8//!
9//! # Capabilities, never the name
10//!
11//! [`DriverProfile::name`] is informational. Branching on it is how a display
12//! stack accumulates a table of driver names and a bug for every driver not in
13//! it — including every future one. Every decision here is gated on a
14//! capability the kernel reports, which is what the rest of drmkit already
15//! does.
16
17use drmkit_core::Device;
18
19/// Whether a connected panel might hold its image without a flip.
20///
21/// Telemetry only, and deliberately not authoritative: PSR has no portable KMS
22/// property, so the best available signal is the connector type. Nothing gates
23/// behavior on this — a frame-economy decision that skipped a commit because
24/// it guessed the panel would self-refresh, and guessed wrong, freezes the
25/// display.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27#[non_exhaustive]
28pub enum PanelSelfRefresh {
29    /// No connected connector, or the set could not be read.
30    #[default]
31    Unknown,
32    /// Every connected connector is external, so none can self-refresh.
33    None,
34    /// A connected eDP or DSI panel, which *may* support it.
35    Possible,
36}
37
38/// What `DRM_CAP_PRIME`'s bitmask says.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40#[non_exhaustive]
41pub struct PrimeCaps {
42    /// The device can import a dma-buf.
43    pub can_import: bool,
44    /// The device can export one.
45    pub can_export: bool,
46}
47
48/// `DRM_PRIME_CAP_IMPORT`.
49const PRIME_CAP_IMPORT: u64 = 0x1;
50/// `DRM_PRIME_CAP_EXPORT`.
51const PRIME_CAP_EXPORT: u64 = 0x2;
52
53/// Decode the `DRM_CAP_PRIME` bitmask.
54///
55/// A pure helper so the bit positions can be tested without a device — the
56/// kind of thing that is silently wrong on hardware nobody has.
57#[must_use]
58pub const fn decode_prime_caps(cap: u64) -> PrimeCaps {
59    PrimeCaps {
60        can_import: cap & PRIME_CAP_IMPORT != 0,
61        can_export: cap & PRIME_CAP_EXPORT != 0,
62    }
63}
64
65/// `DRM_MODE_CONNECTOR_eDP`.
66const CONNECTOR_EDP: u32 = 14;
67/// `DRM_MODE_CONNECTOR_DSI`.
68const CONNECTOR_DSI: u32 = 16;
69
70/// Whether a connector type can hold its image without a flip.
71///
72/// Internal panels only. Anything on a cable has to be fed.
73#[must_use]
74pub const fn connector_type_self_refreshes(connector_type: u32) -> bool {
75    matches!(connector_type, CONNECTOR_EDP | CONNECTOR_DSI)
76}
77
78/// A device's capabilities.
79///
80/// One field per `DRM_CAP_*` the presenter consults, rather than a bitflags
81/// type: these are independent questions with independent answers, and a
82/// reader checking one against the kernel's documentation should find the
83/// kernel's name for it.
84#[derive(Debug, Clone, PartialEq, Eq)]
85#[expect(
86    clippy::struct_excessive_bools,
87    reason = "the shape is the kernel's capability list, not a design choice"
88)]
89#[non_exhaustive]
90pub struct DriverProfile {
91    /// The driver's name. Informational — see the module docs.
92    pub name: String,
93    /// `AddFB2` accepts explicit modifiers.
94    pub addfb2_modifiers: bool,
95    /// Asynchronous page flip is supported.
96    pub async_page_flip: bool,
97    /// Vblank timestamps are `CLOCK_MONOTONIC`.
98    pub timestamp_monotonic: bool,
99    /// dma-bufs can be imported.
100    pub prime_import: bool,
101    /// dma-bufs can be exported.
102    pub prime_export: bool,
103    /// Maximum cursor width. 64 where the driver reports nothing, which is the
104    /// figure every KMS driver has historically accepted.
105    pub cursor_width: u64,
106    /// Maximum cursor height, defaulted the same way.
107    pub cursor_height: u64,
108    /// At least one plane advertises `FB_DAMAGE_CLIPS`.
109    pub fb_damage_clips: bool,
110    /// A connected sink reports `vrr_capable`, and a CRTC exposes the
111    /// `VRR_ENABLED` that arms it.
112    ///
113    /// `VRR_ENABLED` alone says nothing: the DRM core attaches it to every
114    /// CRTC of an atomic driver, whether or not anything behind it can vary
115    /// its refresh. The sink's own property is what does.
116    pub vrr_capable: bool,
117    /// Whether a connected panel might self-refresh. Telemetry only.
118    pub psr: PanelSelfRefresh,
119}
120
121impl DriverProfile {
122    /// Probe `device`.
123    ///
124    /// Capabilities the driver does not report are taken as absent rather than
125    /// as an error: `DRM_CAP_*` is additive, and a kernel too old to know a cap
126    /// answers the same way as one whose hardware lacks it.
127    ///
128    /// Turns on universal planes and atomic for `device` first, as the
129    /// reference does: without them the kernel hides primary and cursor
130    /// planes, and `FB_DAMAGE_CLIPS` is only shown to atomic clients, so
131    /// [`fb_damage_clips`](Self::fb_damage_clips) would read false on a device
132    /// that has it. Either being refused leaves the scan to see what it can.
133    /// The per-object scans are best-effort the same way: a resource list that
134    /// cannot be read leaves [`vrr_capable`](Self::vrr_capable) and
135    /// [`psr`](Self::psr) at their defaults rather than failing the probe.
136    ///
137    /// # Errors
138    ///
139    /// [`drmkit_core::CoreError`] if the driver name cannot be read — which
140    /// means this is not a DRM node at all.
141    pub fn probe(device: &Device) -> Result<Self, drmkit_core::CoreError> {
142        use drm::control::Device as _;
143
144        let driver = drm::Device::get_driver(device).map_err(|e| errno(&e))?;
145        let cap = |c: drm::DriverCapability| drm::Device::get_driver_capability(device, c).ok();
146
147        let prime = decode_prime_caps(cap(drm::DriverCapability::Prime).unwrap_or(0));
148
149        // Refusals are not errors: an older kernel, or a node that is not KMS,
150        // answers with whatever it can show.
151        let _ = device.enable_universal_planes();
152        let _ = device.enable_atomic();
153
154        // Per-object capabilities are found by enumeration, not by asking the
155        // device: they are properties of planes and CRTCs, and a device may
156        // have some that carry them and some that do not.
157        let mut fb_damage_clips = false;
158        if let Ok(planes) = device.plane_handles() {
159            for handle in planes {
160                if object_has_property(device, handle.into(), ObjectType::Plane, "FB_DAMAGE_CLIPS")
161                {
162                    fb_damage_clips = true;
163                    break;
164                }
165            }
166        }
167
168        let resources = device.resource_handles().ok();
169        let crtcs = resources.as_ref().map_or(&[][..], |r| r.crtcs());
170        let connectors = resources.as_ref().map_or(&[][..], |r| r.connectors());
171
172        let vrr_armable = crtcs.iter().any(|crtc| {
173            object_has_property(device, (*crtc).into(), ObjectType::Crtc, "VRR_ENABLED")
174        });
175        let mut vrr_sink = false;
176
177        // Unknown unless a connector is actually connected: an unplugged eDP
178        // says nothing about what is on screen, because nothing is.
179        let mut psr = PanelSelfRefresh::Unknown;
180        for handle in connectors {
181            let Ok(info) = device.get_connector(*handle, false) else {
182                continue;
183            };
184            if info.state() != drm::control::connector::State::Connected {
185                continue;
186            }
187            vrr_sink = vrr_sink || connector_reports_vrr(device, (*handle).into());
188            let internal = connector_type_self_refreshes(connector_type_code(info.interface()));
189            psr = if internal {
190                PanelSelfRefresh::Possible
191            } else if psr == PanelSelfRefresh::Unknown {
192                PanelSelfRefresh::None
193            } else {
194                psr
195            };
196        }
197        let vrr_capable = vrr_armable && vrr_sink;
198
199        Ok(Self {
200            name: driver.name().to_string_lossy().into_owned(),
201            addfb2_modifiers: cap(drm::DriverCapability::AddFB2Modifiers).unwrap_or(0) != 0,
202            async_page_flip: cap(drm::DriverCapability::ASyncPageFlip).unwrap_or(0) != 0,
203            timestamp_monotonic: cap(drm::DriverCapability::MonotonicTimestamp).unwrap_or(0) != 0,
204            prime_import: prime.can_import,
205            prime_export: prime.can_export,
206            cursor_width: nonzero_or(cap(drm::DriverCapability::CursorWidth), 64),
207            cursor_height: nonzero_or(cap(drm::DriverCapability::CursorHeight), 64),
208            fb_damage_clips,
209            vrr_capable,
210            psr,
211        })
212    }
213}
214
215use drmkit_core::{CoreError, ObjectType, PropertyStore};
216
217/// An io error as the crate's own error type.
218///
219/// `CoreError::from_io` is private to drmkit-core, and widening it just for
220/// this would export an implementation detail; the errno is what carries the
221/// information either way.
222pub(crate) fn errno(error: &std::io::Error) -> CoreError {
223    CoreError::Io(rustix::io::Errno::from_io_error(error).unwrap_or(rustix::io::Errno::IO))
224}
225
226/// A reported capability, or the fallback when the driver said nothing.
227///
228/// Zero counts as "said nothing": a driver reporting a maximum cursor of zero
229/// would mean no cursor at all, which no KMS driver means.
230pub(crate) const fn nonzero_or(reported: Option<u64>, fallback: u64) -> u64 {
231    match reported {
232        Some(v) if v != 0 => v,
233        _ => fallback,
234    }
235}
236
237/// Whether one KMS object carries a named property.
238fn object_has_property(device: &Device, id: u32, kind: ObjectType, name: &str) -> bool {
239    let mut store = PropertyStore::new();
240    store.cache_properties(device, id, kind).is_ok() && store.property_id(id, name).is_ok()
241}
242
243/// Whether a connector's sink says it can vary its refresh.
244///
245/// `vrr_capable` is the driver's reading of the sink, and absent on a driver
246/// with no variable refresh at all; either absence or zero is a no.
247fn connector_reports_vrr(device: &Device, id: u32) -> bool {
248    let mut store = PropertyStore::new();
249    store
250        .cache_properties(device, id, ObjectType::Connector)
251        .is_ok()
252        && store
253            .property_value(id, "vrr_capable")
254            .is_ok_and(|value| value != 0)
255}
256
257/// The numeric connector type behind the `drm` crate's enum.
258fn connector_type_code(interface: drm::control::connector::Interface) -> u32 {
259    use drm::control::connector::Interface;
260    match interface {
261        Interface::EmbeddedDisplayPort => CONNECTOR_EDP,
262        Interface::DSI => CONNECTOR_DSI,
263        _ => 0,
264    }
265}