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}