Skip to main content

drmkit_display/
modes.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! What each connector is, and what modes it will take.
5//!
6//! Nothing here needs a rendering backend, a GBM device or DRM master, so it
7//! can answer `--list-modes` before anything is brought up, and can answer it
8//! again on a hotplug.
9
10use drm::control::Device as _;
11use drm::control::connector::{Interface, State};
12
13use drmkit_core::{Device, Result};
14
15use crate::profile::errno;
16use drm::control::Mode;
17
18/// Whether to make the kernel ask the display again.
19///
20/// Not a free choice. A probe is a round trip to the sink over DDC or the
21/// DP aux channel: tens of milliseconds per connector, and on a sink
22/// in standby it can wake the panel. The cached answer costs nothing and is
23/// what the kernel last saw.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25#[non_exhaustive]
26pub enum Probe {
27    /// Ask the display. What a listing wants, and what a hotplug wants: the
28    /// cache is exactly what has just gone stale.
29    #[default]
30    Force,
31    /// Take the kernel's cached answer. What a loop wants, and anything
32    /// running while a display might be asleep.
33    Cached,
34}
35
36/// One connector and the modes it advertises.
37#[derive(Debug, Clone, PartialEq)]
38#[non_exhaustive]
39pub struct ConnectorModes {
40    /// The connector's object id.
41    pub connector_id: u32,
42    /// What kind of connector it is.
43    pub interface: Interface,
44    /// Which one of that kind -- the `-N` in `HDMI-A-1`.
45    pub interface_id: u32,
46    /// Whether a display is attached.
47    pub connected: bool,
48    /// The modes, in the order the kernel lists them: preferred first on
49    /// almost every driver, but that is a convention rather than a promise --
50    /// [`select_preferred_mode`](drmkit_modeset::select_preferred_mode) reads
51    /// the flag instead. [`ModeInfo`](drmkit_modeset::ModeInfo) is the
52    /// extension trait that reads the rest.
53    pub modes: Vec<Mode>,
54}
55
56impl ConnectorModes {
57    /// The conventional name: `HDMI-A-1`, `eDP-1`, `DP-3`.
58    ///
59    /// The same string the kernel puts in `/sys/class/drm/card0-*` and
60    /// `modetest` prints, so it can be matched against either.
61    #[must_use]
62    pub fn name(&self) -> String {
63        format!("{}-{}", self.interface.as_str(), self.interface_id)
64    }
65}
66
67/// Every connector on the device, with its modes.
68///
69/// In resource order, which is the order everything else on the device is
70/// listed in.
71///
72/// A connector that disappears mid-enumeration is skipped rather than failing
73/// the call: hotplug is asynchronous, and a listing that a monitor was
74/// unplugged during should still describe the rest of the machine.
75///
76/// Writeback connectors are not here unless the caller has asked for them.
77/// The kernel hides them from any client that has not set
78/// `DRM_CLIENT_CAP_WRITEBACK_CONNECTORS`, and setting it is a decision about
79/// the whole device rather than about one listing -- it changes what every
80/// other enumeration on that descriptor returns too. Set it and they appear:
81/// measured on amdgpu, `Writeback-1` shows up reporting nothing attached.
82///
83/// # Errors
84///
85/// `drmkit_core::CoreError::Io` if the device's resources cannot be read
86/// at all.
87pub fn query_connector_modes(device: &Device, probe: Probe) -> Result<Vec<ConnectorModes>> {
88    let resources = device.resource_handles().map_err(|error| errno(&error))?;
89
90    let force = probe == Probe::Force;
91    let mut out = Vec::with_capacity(resources.connectors().len());
92    for handle in resources.connectors() {
93        let Ok(connector) = device.get_connector(*handle, force) else {
94            continue;
95        };
96        out.push(ConnectorModes {
97            connector_id: (*handle).into(),
98            interface: connector.interface(),
99            interface_id: connector.interface_id(),
100            connected: connector.state() == State::Connected,
101            modes: connector.modes().to_vec(),
102        });
103    }
104    Ok(out)
105}
106
107#[cfg(test)]
108mod tests {
109    use super::ConnectorModes;
110    use drm::control::connector::Interface;
111
112    /// Every connector type as libdrm names it.
113    ///
114    /// Measured with `drmModeGetConnectorTypeName` on libdrm 2.4.x rather
115    /// than copied from a header, because the point of the table is to agree
116    /// with what the kernel and every other tool print. A name that has
117    /// drifted looks right and matches nothing: no `/sys/class/drm/card0-*`
118    /// directory, no line in `modetest`, no `--output` a user could pass.
119    const LIBDRM: [(Interface, &str); 21] = [
120        (Interface::Unknown, "Unknown"),
121        (Interface::VGA, "VGA"),
122        (Interface::DVII, "DVI-I"),
123        (Interface::DVID, "DVI-D"),
124        (Interface::DVIA, "DVI-A"),
125        (Interface::Composite, "Composite"),
126        (Interface::SVideo, "SVIDEO"),
127        (Interface::LVDS, "LVDS"),
128        (Interface::Component, "Component"),
129        (Interface::NinePinDIN, "DIN"),
130        (Interface::DisplayPort, "DP"),
131        (Interface::HDMIA, "HDMI-A"),
132        (Interface::HDMIB, "HDMI-B"),
133        (Interface::TV, "TV"),
134        (Interface::EmbeddedDisplayPort, "eDP"),
135        (Interface::Virtual, "Virtual"),
136        (Interface::DSI, "DSI"),
137        (Interface::DPI, "DPI"),
138        (Interface::Writeback, "Writeback"),
139        (Interface::SPI, "SPI"),
140        (Interface::USB, "USB"),
141    ];
142
143    fn connector(interface: Interface, interface_id: u32) -> ConnectorModes {
144        ConnectorModes {
145            connector_id: 1,
146            interface,
147            interface_id,
148            connected: false,
149            modes: Vec::new(),
150        }
151    }
152
153    #[test]
154    fn every_connector_type_is_named_the_way_libdrm_names_it() {
155        for (interface, expected) in LIBDRM {
156            assert_eq!(
157                connector(interface, 1).name(),
158                format!("{expected}-1"),
159                "{interface:?}"
160            );
161        }
162    }
163
164    #[test]
165    fn the_name_carries_the_instance() {
166        // Two displays of the same kind differ only by this number, so a name
167        // without it identifies both or neither.
168        assert_eq!(connector(Interface::HDMIA, 1).name(), "HDMI-A-1");
169        assert_eq!(connector(Interface::HDMIA, 2).name(), "HDMI-A-2");
170        assert_eq!(connector(Interface::EmbeddedDisplayPort, 2).name(), "eDP-2");
171    }
172
173    #[test]
174    fn a_type_nothing_knows_about_still_has_a_name() {
175        // The kernel gains connector types; a listing that panicked or
176        // produced an empty name on one it had not heard of would break on a
177        // machine newer than the build.
178        assert_eq!(
179            connector(Interface::from(0xFFFF_u32), 1).name(),
180            "Unknown-1"
181        );
182    }
183}