Skip to main content

ferroui_linuxframebuffer/output/
drm_bindings.rs

1//! A card, its resources, connectors, encoders and modes as values (the
2//! port of `Output/DrmBindings.cs`).
3
4use super::drm::lib_drm::{drmModeConnector, drmModeCrtc, drmModeEncoder, drmModeModeInfo, DrmModeType};
5use super::drm::{DrmModeConnection, DrmModeSubPixel};
6use crate::DrmConnectorType;
7use ferroui_base::{PixelSize, Size};
8use std::collections::HashMap;
9use std::ffi::CStr;
10use std::sync::atomic::{AtomicI32, Ordering};
11
12static KNOWN_CONNECTOR_TYPES: [&str; 17] = [
13    "None",
14    "VGA",
15    "DVI-I",
16    "DVI-D",
17    "DVI-A",
18    "Composite",
19    "S-Video",
20    "LVDS",
21    "Component",
22    "DIN",
23    "DisplayPort",
24    "HDMI-A",
25    "HDMI-B",
26    "TV",
27    "eDP",
28    "Virtual",
29    "DSI",
30];
31
32/// A connector of a card, as it was when the resources were read.
33#[derive(Clone, Debug)]
34pub struct DrmConnector {
35    connection: DrmModeConnection,
36    id: u32,
37    name: String,
38    size_mm: Size,
39    sub_pixel: DrmModeSubPixel,
40    encoder_id: u32,
41    encoder_ids: Vec<u32>,
42    modes: Vec<DrmModeInfo>,
43    connector_type: DrmConnectorType,
44    connector_type_id: u32,
45}
46
47impl DrmConnector {
48    /// The connector the library describes.
49    ///
50    /// # Safety
51    /// `conn` is a connector of the library: its arrays of modes and
52    /// encoders hold the numbers of elements its counts say.
53    pub(crate) unsafe fn new(conn: &drmModeConnector) -> Self {
54        let mut encoder_ids = Vec::new();
55        for c in 0..conn.count_encoders.max(0) as usize {
56            // SAFETY: the array has `count_encoders` elements (the contract of the function).
57            encoder_ids.push(unsafe { *conn.encoders.add(c) });
58        }
59        let mut modes = Vec::new();
60        for c in 0..conn.count_modes.max(0) as usize {
61            // SAFETY: the array has `count_modes` elements (the contract of the function).
62            modes.push(DrmModeInfo::new(unsafe { &*conn.modes.add(c) }));
63        }
64        let name = if conn.connector_type as usize > KNOWN_CONNECTOR_TYPES.len() - 1 {
65            format!("Unknown({})-{}", conn.connector_type, conn.connector_type_id)
66        } else {
67            format!("{}-{}", KNOWN_CONNECTOR_TYPES[conn.connector_type as usize], conn.connector_type_id)
68        };
69        Self {
70            connection: conn.connection,
71            id: conn.connector_id,
72            size_mm: Size::new(conn.mm_width as f64, conn.mm_height as f64),
73            sub_pixel: conn.subpixel,
74            encoder_ids,
75            encoder_id: conn.encoder_id,
76            modes,
77            connector_type: DrmConnectorType(conn.connector_type),
78            connector_type_id: conn.connector_type_id,
79            name,
80        }
81    }
82
83    pub fn connection(&self) -> DrmModeConnection {
84        self.connection
85    }
86
87    pub fn id(&self) -> u32 {
88        self.id
89    }
90
91    pub fn name(&self) -> &str {
92        &self.name
93    }
94
95    pub fn size_mm(&self) -> Size {
96        self.size_mm
97    }
98
99    pub fn sub_pixel(&self) -> DrmModeSubPixel {
100        self.sub_pixel
101    }
102
103    pub(crate) fn encoder_id(&self) -> u32 {
104        self.encoder_id
105    }
106
107    pub(crate) fn encoder_ids(&self) -> &[u32] {
108        &self.encoder_ids
109    }
110
111    pub fn modes(&self) -> &[DrmModeInfo] {
112        &self.modes
113    }
114
115    pub fn connector_type(&self) -> DrmConnectorType {
116        self.connector_type
117    }
118
119    pub fn connector_type_id(&self) -> u32 {
120        self.connector_type_id
121    }
122}
123
124/// A display mode of a connector.
125#[derive(Clone, Debug)]
126pub struct DrmModeInfo {
127    pub(crate) mode: drmModeModeInfo,
128    name: Option<String>,
129}
130
131impl DrmModeInfo {
132    pub(crate) fn new(info: &drmModeModeInfo) -> Self {
133        // The name ends at its first zero, or with the array.
134        let bytes: Vec<u8> = info.name.iter().map(|&c| c as u8).collect();
135        let name = match CStr::from_bytes_until_nul(&bytes) {
136            Ok(name) => name.to_string_lossy().into_owned(),
137            Err(_) => String::from_utf8_lossy(&bytes).into_owned(),
138        };
139        Self { mode: *info, name: Some(name) }
140    }
141
142    pub fn resolution(&self) -> PixelSize {
143        PixelSize::new(self.mode.hdisplay as i32, self.mode.vdisplay as i32)
144    }
145
146    pub fn is_preferred(&self) -> bool {
147        self.mode.type_.has_all_flags(DrmModeType::DRM_MODE_TYPE_PREFERRED)
148    }
149
150    pub fn name(&self) -> Option<&str> {
151        self.name.as_deref()
152    }
153}
154
155/// An encoder of a card and the CRTCs it can drive.
156#[derive(Clone, Debug)]
157pub(crate) struct DrmEncoder {
158    encoder: drmModeEncoder,
159    possible_crtcs: Vec<drmModeCrtc>,
160}
161
162impl DrmEncoder {
163    pub fn new(encoder: drmModeEncoder, crtcs: &[drmModeCrtc]) -> Self {
164        let mut possible_crtcs = Vec::new();
165        for (c, crtc) in crtcs.iter().enumerate() {
166            // A card has at most 32 CRTCs: the bits of `possible_crtcs`.
167            let bit = 1u32.checked_shl(c as u32).unwrap_or(0);
168            if (encoder.possible_crtcs & bit) != 0 {
169                possible_crtcs.push(*crtc);
170            }
171        }
172        Self { encoder, possible_crtcs }
173    }
174
175    pub fn encoder(&self) -> drmModeEncoder {
176        self.encoder
177    }
178
179    pub fn possible_crtcs(&self) -> &[drmModeCrtc] {
180        &self.possible_crtcs
181    }
182}
183
184/// The connectors and encoders of a card.
185#[derive(Clone, Debug, Default)]
186pub struct DrmResources {
187    connectors: Vec<DrmConnector>,
188    encoders: HashMap<u32, DrmEncoder>,
189}
190
191impl DrmResources {
192    /// Reads the resources of the card `fd`; with `connectors_force_probe`
193    /// the connectors are probed (`drmModeGetConnector`, which may take
194    /// long), else their current state is read.
195    #[cfg(target_os = "linux")]
196    pub fn new(fd: i32, connectors_force_probe: bool) -> std::io::Result<Self> {
197        use super::drm::lib_drm::LibDrm;
198        use std::io;
199
200        let lib = LibDrm::get()?;
201        // SAFETY: a call with a descriptor; the result is null or resources the caller frees.
202        let res = unsafe { lib.drm_mode_get_resources(fd) };
203        if res.is_null() {
204            return Err(io::Error::new(
205                io::Error::last_os_error().kind(),
206                format!("drmModeGetResources failed: {}", io::Error::last_os_error()),
207            ));
208        }
209
210        let read = || -> io::Result<Self> {
211            // SAFETY: `res` is the resources of the card, alive until they are freed below; its
212            // arrays have the numbers of elements its counts say.
213            let res = unsafe { &*res };
214            let mut this = Self::default();
215            let mut crtcs = Vec::with_capacity(res.count_crtcs.max(0) as usize);
216            for c in 0..res.count_crtcs.max(0) as usize {
217                // SAFETY: see above; the result is null or a CRTC the caller frees.
218                let crtc = unsafe { lib.drm_mode_get_crtc(fd, *res.crtcs.add(c)) };
219                if crtc.is_null() {
220                    return Err(io::Error::other("drmModeGetCrtc failed"));
221                }
222                // SAFETY: a CRTC of the library, read and freed once.
223                unsafe {
224                    crtcs.push(*crtc);
225                    lib.drm_mode_free_crtc(crtc);
226                }
227            }
228
229            for c in 0..res.count_encoders.max(0) as usize {
230                // SAFETY: as for the CRTCs.
231                let id = unsafe { *res.encoders.add(c) };
232                // SAFETY: as for the CRTCs.
233                let enc = unsafe { lib.drm_mode_get_encoder(fd, id) };
234                if enc.is_null() {
235                    return Err(io::Error::other("drmModeGetEncoder failed"));
236                }
237                // SAFETY: an encoder of the library, read and freed once.
238                unsafe {
239                    this.encoders.insert(id, DrmEncoder::new(*enc, &crtcs));
240                    lib.drm_mode_free_encoder(enc);
241                }
242            }
243
244            for c in 0..res.count_connectors.max(0) as usize {
245                // SAFETY: as for the CRTCs.
246                let id = unsafe { *res.connectors.add(c) };
247                // SAFETY: as for the CRTCs.
248                let conn = unsafe {
249                    if connectors_force_probe {
250                        lib.drm_mode_get_connector(fd, id)
251                    } else {
252                        lib.drm_mode_get_connector_current(fd, id)
253                    }
254                };
255                if conn.is_null() {
256                    return Err(io::Error::other("drmModeGetConnector failed"));
257                }
258                // SAFETY: a connector of the library, read and freed once.
259                unsafe {
260                    this.connectors.push(DrmConnector::new(&*conn));
261                    lib.drm_mode_free_connector(conn);
262                }
263            }
264            Ok(this)
265        };
266        let result = read();
267        // SAFETY: the resources the call above returned, freed once.
268        unsafe { lib.drm_mode_free_resources(res) };
269        result
270    }
271
272    /// The resources made of values, for the tests of the selection of the
273    /// connector, the mode and the CRTC without a card.
274    #[cfg(test)]
275    pub(crate) fn from_parts(connectors: Vec<DrmConnector>, encoders: HashMap<u32, DrmEncoder>) -> Self {
276        Self { connectors, encoders }
277    }
278
279    pub fn connectors(&self) -> &[DrmConnector] {
280        &self.connectors
281    }
282
283    pub(crate) fn encoders(&self) -> &HashMap<u32, DrmEncoder> {
284        &self.encoders
285    }
286
287    /// Writes the connectors and their modes to the standard output.
288    /// (Internal in the reference, where nothing calls it; public here for
289    /// the diagnostics of the smoke example.)
290    pub fn dump(&self) {
291        fn print(off: usize, s: &str) {
292            println!("{}{s}", "    ".repeat(off));
293        }
294        print(0, "Connectors");
295        for conn in &self.connectors {
296            print(1, &format!("{}:", conn.name()));
297            print(2, &format!("Id: {}", conn.id()));
298            print(2, &format!("Size: {} mm", conn.size_mm()));
299            print(2, &format!("Encoder id: {}", conn.encoder_id()));
300            print(2, "Modes");
301            for m in conn.modes() {
302                print(3, &format!("{} {}", m.name().unwrap_or_default(), if m.is_preferred() { "PREFERRED" } else { "" }));
303            }
304        }
305    }
306}
307
308/// A card of the DRM subsystem, open for reading and writing.
309#[derive(Debug)]
310pub struct DrmCard {
311    fd: AtomicI32,
312}
313
314impl DrmCard {
315    /// Opens the card `path`, or, without one, the first card of
316    /// `/dev/dri` (by its number) that has CRTCs, encoders and connectors.
317    #[cfg(target_os = "linux")]
318    pub fn new(path: Option<&str>) -> std::io::Result<Self> {
319        use super::drm::lib_drm::LibDrm;
320        use std::collections::BTreeMap;
321        use std::ffi::CString;
322        use std::io;
323
324        let open = |file: &str| -> i32 {
325            let Ok(file) = CString::new(file) else {
326                return -1;
327            };
328            // SAFETY: a valid C string.
329            unsafe { libc::open(file.as_ptr(), libc::O_RDWR, 0) }
330        };
331
332        match path {
333            None => {
334                let lib = LibDrm::get()?;
335                let mut files = BTreeMap::new();
336                if let Ok(entries) = std::fs::read_dir("/dev/dri/") {
337                    for entry in entries.flatten() {
338                        let file_name = entry.file_name().to_string_lossy().into_owned();
339                        let Some(number) = file_name.strip_prefix("card") else {
340                            continue;
341                        };
342                        // Digits only (`NumberStyles.None`).
343                        if number.is_empty() || !number.bytes().all(|b| b.is_ascii_digit()) {
344                            continue;
345                        }
346                        if let Ok(card_number) = number.parse::<u32>() {
347                            files.insert(card_number, entry.path().to_string_lossy().into_owned());
348                        }
349                    }
350                }
351
352                for file in files.values() {
353                    let fd = open(file);
354                    if fd == -1 {
355                        continue;
356                    }
357
358                    // SAFETY: a call with a descriptor; the result is null or resources that
359                    // are read here and freed once.
360                    unsafe {
361                        let resources = lib.drm_mode_get_resources(fd);
362                        if !resources.is_null()
363                            && (*resources).count_crtcs > 0
364                            && (*resources).count_encoders > 0
365                            && (*resources).count_connectors > 0
366                        {
367                            lib.drm_mode_free_resources(resources);
368                            return Ok(Self { fd: AtomicI32::new(fd) });
369                        }
370
371                        if !resources.is_null() {
372                            lib.drm_mode_free_resources(resources);
373                        }
374                        libc::close(fd);
375                    }
376                }
377
378                Err(io::Error::new(io::ErrorKind::NotFound, "Couldn't open /dev/dri/card[0-9]+"))
379            }
380            Some(path) => {
381                let fd = open(path);
382                if fd == -1 {
383                    let error = io::Error::last_os_error();
384                    return Err(io::Error::new(error.kind(), format!("Couldn't open {path}: {error}")));
385                }
386                Ok(Self { fd: AtomicI32::new(fd) })
387            }
388        }
389    }
390
391    /// The card of a descriptor that is already open; the card closes it.
392    pub fn from_fd(fd: i32) -> Self {
393        Self { fd: AtomicI32::new(fd) }
394    }
395
396    /// The descriptor of the card, or -1 once it is disposed.
397    pub fn fd(&self) -> i32 {
398        self.fd.load(Ordering::Acquire)
399    }
400
401    #[cfg(target_os = "linux")]
402    pub fn get_resources(&self, connectors_force_probe: bool) -> std::io::Result<DrmResources> {
403        DrmResources::new(self.fd(), connectors_force_probe)
404    }
405
406    /// Closes the card.
407    pub fn dispose(&self) {
408        let fd = self.fd.swap(-1, Ordering::AcqRel);
409        if fd == -1 {
410            return;
411        }
412        // SAFETY: the descriptor the card owns, closed once.
413        unsafe {
414            libc::close(fd);
415        }
416    }
417}
418
419impl Drop for DrmCard {
420    fn drop(&mut self) {
421        self.dispose();
422    }
423}
424
425#[cfg(test)]
426pub(crate) mod tests {
427    // Not from the reference, which has no tests for this file.
428    use super::*;
429
430    /// A mode of `width` by `height`, preferred or not, named as the
431    /// kernel names modes.
432    pub(crate) fn mode(width: u16, height: u16, preferred: bool) -> drmModeModeInfo {
433        let mut info = drmModeModeInfo { hdisplay: width, vdisplay: height, ..Default::default() };
434        if preferred {
435            info.type_ = DrmModeType::DRM_MODE_TYPE_PREFERRED | DrmModeType::DRM_MODE_TYPE_DRIVER;
436        }
437        for (target, source) in info.name.iter_mut().zip(format!("{width}x{height}").bytes()) {
438            *target = source as _;
439        }
440        info
441    }
442
443    /// A connector of the library, with its arrays.
444    pub(crate) fn connector(
445        id: u32,
446        connector_type: u32,
447        connector_type_id: u32,
448        connection: DrmModeConnection,
449        encoder_id: u32,
450        encoders: &[u32],
451        modes: &[drmModeModeInfo],
452    ) -> DrmConnector {
453        let mut encoders = encoders.to_vec();
454        let mut modes = modes.to_vec();
455        let raw = drmModeConnector {
456            connector_id: id,
457            encoder_id,
458            connector_type,
459            connector_type_id,
460            connection,
461            mm_width: 300,
462            mm_height: 200,
463            subpixel: DrmModeSubPixel::DRM_MODE_SUBPIXEL_UNKNOWN,
464            count_modes: modes.len() as i32,
465            modes: modes.as_mut_ptr(),
466            count_props: 0,
467            props: std::ptr::null_mut(),
468            prop_values: std::ptr::null_mut(),
469            count_encoders: encoders.len() as i32,
470            encoders: encoders.as_mut_ptr(),
471        };
472        // SAFETY: the arrays are the two vectors, with the counts of their lengths.
473        unsafe { DrmConnector::new(&raw) }
474    }
475
476    #[test]
477    fn a_connector_is_named_after_its_type_and_reads_its_arrays() {
478        let conn = connector(
479            31,
480            15,
481            1,
482            DrmModeConnection::DRM_MODE_CONNECTED,
483            40,
484            &[40, 41],
485            &[mode(1024, 768, true), mode(800, 600, false)],
486        );
487        assert_eq!(conn.name(), "Virtual-1");
488        assert_eq!(conn.id(), 31);
489        assert_eq!(conn.connector_type(), DrmConnectorType::VIRTUAL);
490        assert_eq!(conn.connector_type_id(), 1);
491        assert_eq!(conn.connection(), DrmModeConnection::DRM_MODE_CONNECTED);
492        assert_eq!(conn.size_mm(), Size::new(300.0, 200.0));
493        assert_eq!(conn.encoder_id(), 40);
494        assert_eq!(conn.encoder_ids(), [40, 41]);
495        assert_eq!(conn.modes().len(), 2);
496        assert_eq!(conn.modes()[0].name(), Some("1024x768"));
497        assert_eq!(conn.modes()[0].resolution(), PixelSize::new(1024, 768));
498        assert!(conn.modes()[0].is_preferred());
499        assert!(!conn.modes()[1].is_preferred());
500
501        let hdmi = connector(32, 11, 2, DrmModeConnection::DRM_MODE_DISCONNECTED, 0, &[], &[]);
502        assert_eq!(hdmi.name(), "HDMI-A-2");
503        assert!(hdmi.modes().is_empty());
504        // Types the table does not know (a writeback connector is 18).
505        let unknown = connector(33, 18, 1, DrmModeConnection::DRM_MODE_UNKNOWNCONNECTION, 0, &[], &[]);
506        assert_eq!(unknown.name(), "Unknown(18)-1");
507        assert_eq!(unknown.connector_type(), DrmConnectorType(18));
508    }
509
510    #[test]
511    fn a_name_of_32_characters_without_its_zero_is_read_whole() {
512        let mut info = drmModeModeInfo::default();
513        info.name = [b'a' as _; 32];
514        assert_eq!(DrmModeInfo::new(&info).name().map(str::len), Some(32));
515    }
516
517    #[test]
518    fn an_encoder_lists_the_crtcs_of_its_bits() {
519        let crtcs: Vec<drmModeCrtc> =
520            (0..3).map(|c| drmModeCrtc { crtc_id: 100 + c, ..Default::default() }).collect();
521        let encoder = DrmEncoder::new(drmModeEncoder { encoder_id: 7, possible_crtcs: 0b101, ..Default::default() }, &crtcs);
522        assert_eq!(encoder.encoder().encoder_id, 7);
523        let ids: Vec<u32> = encoder.possible_crtcs().iter().map(|crtc| crtc.crtc_id).collect();
524        assert_eq!(ids, [100, 102]);
525    }
526
527    #[test]
528    fn a_card_closes_its_descriptor_once() {
529        let mut fds = [0; 2];
530        // SAFETY: an array of two descriptors.
531        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
532        let card = DrmCard::from_fd(fds[0]);
533        assert_eq!(card.fd(), fds[0]);
534        card.dispose();
535        assert_eq!(card.fd(), -1);
536        card.dispose();
537        drop(card);
538        // SAFETY: the other end, which the card did not own.
539        unsafe { libc::close(fds[1]) };
540    }
541}