Skip to main content

denise_drm/
device.rs

1//! Opening a DRM device and reading what it can drive.
2
3use std::cell::Cell;
4use std::fs::OpenOptions;
5use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
6use std::path::{Path, PathBuf};
7
8use drm::Device as BasicDevice;
9use drm::control::{Device as ControlDevice, Mode, ModeTypeFlags, connector, crtc};
10
11use crate::error::DrmError;
12use crate::mode::{ConnectorInfo, ConnectorKind, ModeInfo};
13
14/// Where card nodes live.
15const DRI_DIR: &str = "/dev/dri";
16
17/// An open DRM device.
18///
19/// Releases DRM master on drop, so a crash or a clean exit both hand the console
20/// back rather than leaving a black screen that needs a reboot.
21#[derive(Debug)]
22pub struct Card {
23    fd: OwnedFd,
24    path: Option<PathBuf>,
25    mastered: Cell<bool>,
26}
27
28impl AsFd for Card {
29    fn as_fd(&self) -> BorrowedFd<'_> {
30        self.fd.as_fd()
31    }
32}
33
34impl BasicDevice for Card {}
35impl ControlDevice for Card {}
36
37impl Card {
38    /// Opens a specific card node, such as `/dev/dri/card0`.
39    pub fn open(path: impl AsRef<Path>) -> Result<Self, DrmError> {
40        let path = path.as_ref();
41        // std opens with O_CLOEXEC already, which matters: a leaked DRM fd in a
42        // child process keeps the device busy for the next run.
43        let file = OpenOptions::new()
44            .read(true)
45            .write(true)
46            .open(path)
47            .map_err(|source| DrmError::Open {
48                path: path.to_path_buf(),
49                source,
50            })?;
51
52        Ok(Self {
53            fd: OwnedFd::from(file),
54            path: Some(path.to_path_buf()),
55            mastered: Cell::new(false),
56        })
57    }
58
59    /// Opens the first card node that actually has a display output.
60    ///
61    /// Nodes are tried in name order and skipped unless they report at least one
62    /// connector, which filters out render-only devices.
63    pub fn open_first() -> Result<Self, DrmError> {
64        let dir = std::fs::read_dir(DRI_DIR).map_err(|source| DrmError::Open {
65            path: PathBuf::from(DRI_DIR),
66            source,
67        })?;
68
69        let mut cards: Vec<PathBuf> = dir
70            .filter_map(Result::ok)
71            .map(|entry| entry.path())
72            .filter(|path| {
73                path.file_name()
74                    .and_then(|name| name.to_str())
75                    .is_some_and(|name| name.starts_with("card"))
76            })
77            .collect();
78        cards.sort();
79
80        if cards.is_empty() {
81            return Err(DrmError::NoDevice);
82        }
83
84        let mut last_error = None;
85        for path in cards {
86            match Card::open(&path) {
87                Ok(card) if card.has_display_output() => return Ok(card),
88                Ok(_) => {}
89                Err(err) => last_error = Some(err),
90            }
91        }
92
93        Err(last_error.unwrap_or(DrmError::NoDisplayCapableDevice))
94    }
95
96    /// Adopts a descriptor opened by somebody else.
97    ///
98    /// This is how to coexist with `libseat` or a systemd unit that holds the
99    /// device rights, rather than demanding root.
100    pub fn from_fd(fd: OwnedFd) -> Self {
101        Self {
102            fd,
103            path: None,
104            mastered: Cell::new(false),
105        }
106    }
107
108    /// The node this card was opened from, if it was opened by path.
109    pub fn path(&self) -> Option<&Path> {
110        self.path.as_deref()
111    }
112
113    /// Returns `true` if this device has connectors, rather than being a render
114    /// node.
115    pub fn has_display_output(&self) -> bool {
116        self.resource_handles()
117            .map(|res| !res.connectors().is_empty())
118            .unwrap_or(false)
119    }
120
121    /// Takes the DRM master lock, which is required to set a mode.
122    ///
123    /// Fails if a compositor or another instance already holds it. See the crate
124    /// documentation for the ways to have the right to it.
125    pub fn become_master(&self) -> Result<(), DrmError> {
126        self.acquire_master_lock().map_err(DrmError::NotMaster)?;
127        self.mastered.set(true);
128        Ok(())
129    }
130
131    /// Gives the master lock back, restoring whatever had the console before.
132    pub fn release_master(&self) {
133        if self.mastered.replace(false) {
134            let _ = self.release_master_lock();
135        }
136    }
137
138    /// Returns `true` if this process currently holds DRM master.
139    pub fn is_master(&self) -> bool {
140        self.mastered.get()
141    }
142
143    /// Reads every connector, as handles and as plain data for [`crate::mode`].
144    ///
145    /// The two vectors are index-aligned: a [`crate::Selection::connector`] indexes
146    /// both.
147    pub fn connectors(&self) -> Result<(Vec<connector::Handle>, Vec<ConnectorInfo>), DrmError> {
148        let resources = self.resource_handles().map_err(DrmError::Resources)?;
149        let mut handles = Vec::with_capacity(resources.connectors().len());
150        let mut infos = Vec::with_capacity(resources.connectors().len());
151
152        for &handle in resources.connectors() {
153            // Force a probe: a display powered on after boot reads as disconnected
154            // from the cached state, and a kiosk that has to be rebooted because
155            // the screen was switched on second is not a kiosk.
156            let info = self
157                .get_connector(handle, true)
158                .map_err(DrmError::Resources)?;
159
160            handles.push(handle);
161            infos.push(ConnectorInfo {
162                id: u32::from(handle),
163                kind: connector_kind(info.interface()),
164                connected: info.state() == connector::State::Connected,
165                modes: info.modes().iter().map(mode_info).collect(),
166            });
167        }
168
169        Ok((handles, infos))
170    }
171
172    /// Finds a CRTC that can drive `connector`.
173    ///
174    /// Prefers the one already routed to it, so taking over from the console does
175    /// not reshuffle the pipeline for no reason.
176    pub fn crtc_for(&self, connector: connector::Handle) -> Result<crtc::Handle, DrmError> {
177        let resources = self.resource_handles().map_err(DrmError::Resources)?;
178        let info = self
179            .get_connector(connector, false)
180            .map_err(DrmError::Resources)?;
181
182        if let Some(encoder) = info.current_encoder()
183            && let Ok(encoder) = self.get_encoder(encoder)
184            && let Some(crtc) = encoder.crtc()
185        {
186            return Ok(crtc);
187        }
188
189        for &handle in info.encoders() {
190            let Ok(encoder) = self.get_encoder(handle) else {
191                continue;
192            };
193            if let Some(&crtc) = resources.filter_crtcs(encoder.possible_crtcs()).first() {
194                return Ok(crtc);
195            }
196        }
197
198        Err(DrmError::NoCrtc {
199            connector: u32::from(connector),
200        })
201    }
202}
203
204impl Drop for Card {
205    fn drop(&mut self) {
206        self.release_master();
207    }
208}
209
210/// Copies the parts of a DRM mode that selection cares about.
211fn mode_info(mode: &Mode) -> ModeInfo {
212    let (width, height) = mode.size();
213    ModeInfo {
214        width,
215        height,
216        vrefresh: mode.vrefresh(),
217        preferred: mode.mode_type().contains(ModeTypeFlags::PREFERRED),
218    }
219}
220
221/// Collapses DRM's connector interface list to the distinctions that matter.
222fn connector_kind(interface: connector::Interface) -> ConnectorKind {
223    use connector::Interface;
224
225    match interface {
226        Interface::DSI => ConnectorKind::Dsi,
227        Interface::DPI => ConnectorKind::Dpi,
228        Interface::EmbeddedDisplayPort => ConnectorKind::Edp,
229        Interface::LVDS => ConnectorKind::Lvds,
230        Interface::HDMIA | Interface::HDMIB => ConnectorKind::Hdmi,
231        Interface::DisplayPort => ConnectorKind::DisplayPort,
232        Interface::DVII | Interface::DVID | Interface::DVIA => ConnectorKind::Dvi,
233        Interface::VGA => ConnectorKind::Vga,
234        Interface::Composite | Interface::SVideo | Interface::Component | Interface::TV => {
235            ConnectorKind::Composite
236        }
237        Interface::Virtual => ConnectorKind::Virtual,
238        _ => ConnectorKind::Other,
239    }
240}