Skip to main content

denise_drm/
mode.rs

1//! Choosing which output to drive and at what mode.
2//!
3//! Like [`crate::swapchain`], this knows nothing about DRM. It works on plain data
4//! copied out of the connector list, so the policy can be tested exhaustively on a
5//! machine with no display hardware.
6//!
7//! That separation is not ceremony. Mode selection is where a headless box picks
8//! 640×480 because it took `modes[0]`, or where a kiosk with a DSI panel and an
9//! HDMI debug monitor plugged in comes up on the wrong one. Both are trivial to
10//! test as pure functions and close to impossible to debug in a rack.
11
12use core::fmt;
13
14/// The physical connector type, as far as picking an output cares.
15///
16/// Mirrors DRM's connector interface list, collapsed to the distinctions that
17/// change the decision.
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20pub enum ConnectorKind {
21    /// MIPI DSI — the ribbon-cable panel on a Pi.
22    Dsi,
23    /// Parallel DPI.
24    Dpi,
25    /// Embedded DisplayPort, as in a laptop lid.
26    Edp,
27    /// LVDS, as in older industrial panels.
28    Lvds,
29    /// HDMI, either connector letter.
30    Hdmi,
31    /// External DisplayPort.
32    DisplayPort,
33    /// DVI, any flavour.
34    Dvi,
35    /// Analogue VGA.
36    Vga,
37    /// Composite, S-Video and similar analogue TV outputs.
38    Composite,
39    /// A virtual output, as offered by vkms or a virtualised GPU.
40    Virtual,
41    /// Anything not worth distinguishing.
42    Other,
43}
44
45impl ConnectorKind {
46    /// Returns `true` if this is a panel physically built into the product.
47    #[inline]
48    pub const fn is_internal_panel(self) -> bool {
49        matches!(
50            self,
51            ConnectorKind::Dsi | ConnectorKind::Dpi | ConnectorKind::Edp | ConnectorKind::Lvds
52        )
53    }
54
55    /// Ranking for [`OutputPreference::Auto`]; lower sorts first.
56    ///
57    /// Internal panels win. On a shipped kiosk the panel *is* the product, and an
58    /// HDMI cable someone plugged in to look at a log should not move the UI off
59    /// it. Anyone who wants the other behaviour asks for it by kind or by id.
60    const fn rank(self) -> u8 {
61        match self {
62            ConnectorKind::Dsi | ConnectorKind::Dpi => 0,
63            ConnectorKind::Edp | ConnectorKind::Lvds => 1,
64            ConnectorKind::Hdmi | ConnectorKind::DisplayPort => 2,
65            ConnectorKind::Dvi => 3,
66            ConnectorKind::Vga => 4,
67            ConnectorKind::Composite => 5,
68            ConnectorKind::Other => 6,
69            // Last resort: on a machine with a real output as well, a virtual one
70            // is almost never what was wanted.
71            ConnectorKind::Virtual => 7,
72        }
73    }
74}
75
76/// One mode a connector reports.
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub struct ModeInfo {
79    /// Horizontal resolution in pixels.
80    pub width: u16,
81    /// Vertical resolution in pixels.
82    pub height: u16,
83    /// Vertical refresh in whole Hz.
84    pub vrefresh: u32,
85    /// Set if the driver flagged this as the connector's preferred mode, which for
86    /// a fixed panel means its native resolution.
87    pub preferred: bool,
88}
89
90impl ModeInfo {
91    /// Pixel count.
92    #[inline]
93    pub const fn area(&self) -> u64 {
94        self.width as u64 * self.height as u64
95    }
96}
97
98impl fmt::Display for ModeInfo {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        write!(f, "{}x{}@{}", self.width, self.height, self.vrefresh)
101    }
102}
103
104/// One connector, reduced to what selection needs.
105#[derive(Clone, Debug)]
106pub struct ConnectorInfo {
107    /// The DRM connector id, for reporting and for [`OutputPreference::Id`].
108    pub id: u32,
109    /// Physical connector type.
110    pub kind: ConnectorKind,
111    /// Whether something is plugged in and responding.
112    pub connected: bool,
113    /// Modes the connector reports, in driver order.
114    pub modes: Vec<ModeInfo>,
115}
116
117impl ConnectorInfo {
118    /// Returns `true` if this connector could actually be driven.
119    #[inline]
120    pub fn is_usable(&self) -> bool {
121        self.connected && !self.modes.is_empty()
122    }
123
124    /// The largest mode, breaking ties on refresh rate.
125    fn largest(&self) -> Option<usize> {
126        self.modes
127            .iter()
128            .enumerate()
129            .max_by_key(|(_, m)| (m.area(), m.vrefresh))
130            .map(|(i, _)| i)
131    }
132
133    /// The driver's preferred mode, if it flagged one.
134    fn preferred(&self) -> Option<usize> {
135        self.modes.iter().position(|m| m.preferred)
136    }
137}
138
139/// Which output to drive.
140#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
141pub enum OutputPreference {
142    /// Internal panel first, then external digital, then analogue.
143    #[default]
144    Auto,
145    /// The first usable connector of this type.
146    Kind(ConnectorKind),
147    /// One specific DRM connector id.
148    Id(u32),
149}
150
151/// Which mode to set on it.
152#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
153pub enum ModePreference {
154    /// Whatever the driver flagged as preferred — a fixed panel's native
155    /// resolution. Correct for essentially every embedded display.
156    #[default]
157    Preferred,
158    /// A specific resolution, at any refresh rate.
159    Exact {
160        /// Horizontal resolution.
161        width: u16,
162        /// Vertical resolution.
163        height: u16,
164    },
165    /// A specific resolution at a specific refresh rate.
166    ExactRefresh {
167        /// Horizontal resolution.
168        width: u16,
169        /// Vertical resolution.
170        height: u16,
171        /// Vertical refresh in whole Hz.
172        vrefresh: u32,
173    },
174    /// The highest pixel count on offer.
175    Largest,
176}
177
178/// What [`select`] settled on.
179#[derive(Clone, Copy, Debug, PartialEq, Eq)]
180pub struct Selection {
181    /// Index into the connector slice that was passed in.
182    pub connector: usize,
183    /// Index into that connector's `modes`.
184    pub mode: usize,
185    /// Set when the requested mode was unavailable and a fallback was used.
186    ///
187    /// Not an error — coming up at the wrong resolution beats not coming up — but
188    /// the caller should log it, because it means a configured resolution was
189    /// silently ignored.
190    pub fell_back: bool,
191}
192
193/// Why no output could be chosen.
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195#[non_exhaustive]
196pub enum SelectionError {
197    /// The device reported no connectors at all.
198    NoConnectors,
199
200    /// Connectors exist but none has a display attached and modes to offer.
201    NothingConnected,
202
203    /// A specific connector was asked for and is not usable.
204    RequestedIdUnavailable(u32),
205
206    /// A connector kind was asked for and none is usable.
207    RequestedKindUnavailable(ConnectorKind),
208}
209
210impl core::fmt::Display for SelectionError {
211    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
212        match self {
213            Self::NoConnectors => f.write_str("the device has no connectors"),
214            Self::NothingConnected => f.write_str("no connector has a display attached"),
215            Self::RequestedIdUnavailable(id) => {
216                write!(f, "connector {id} was requested but is not connected")
217            }
218            Self::RequestedKindUnavailable(kind) => write!(f, "no usable {kind:?} connector"),
219        }
220    }
221}
222
223impl core::error::Error for SelectionError {}
224
225/// Picks an output and a mode.
226///
227/// Mode selection always succeeds once a connector is chosen: an unavailable
228/// requested resolution falls back to preferred, then to largest, rather than
229/// refusing to start. A kiosk that comes up at the wrong resolution can be fixed
230/// remotely; one that does not come up cannot.
231pub fn select(
232    connectors: &[ConnectorInfo],
233    output: OutputPreference,
234    mode: ModePreference,
235) -> Result<Selection, SelectionError> {
236    if connectors.is_empty() {
237        return Err(SelectionError::NoConnectors);
238    }
239
240    let index = select_connector(connectors, output)?;
241    let (mode, fell_back) = select_mode(&connectors[index], mode);
242
243    Ok(Selection {
244        connector: index,
245        mode,
246        fell_back,
247    })
248}
249
250/// Picks the output alone.
251pub fn select_connector(
252    connectors: &[ConnectorInfo],
253    preference: OutputPreference,
254) -> Result<usize, SelectionError> {
255    let usable = |i: &usize| connectors[*i].is_usable();
256
257    match preference {
258        OutputPreference::Id(id) => (0..connectors.len())
259            .filter(usable)
260            .find(|&i| connectors[i].id == id)
261            .ok_or(SelectionError::RequestedIdUnavailable(id)),
262
263        OutputPreference::Kind(kind) => (0..connectors.len())
264            .filter(usable)
265            .find(|&i| connectors[i].kind == kind)
266            .ok_or(SelectionError::RequestedKindUnavailable(kind)),
267
268        OutputPreference::Auto => (0..connectors.len())
269            .filter(usable)
270            // Rank first, then prefer the bigger panel, then keep driver order.
271            .min_by_key(|&i| {
272                let c = &connectors[i];
273                let area = c
274                    .preferred()
275                    .or_else(|| c.largest())
276                    .map(|m| c.modes[m].area())
277                    .unwrap_or(0);
278                (c.kind.rank(), core::cmp::Reverse(area), i)
279            })
280            .ok_or(SelectionError::NothingConnected),
281    }
282}
283
284/// Picks the mode alone. Returns the index and whether a fallback was taken.
285pub fn select_mode(connector: &ConnectorInfo, preference: ModePreference) -> (usize, bool) {
286    let exact = |width: u16, height: u16, vrefresh: Option<u32>| {
287        connector
288            .modes
289            .iter()
290            .enumerate()
291            // Among equal resolutions take the fastest, so a panel offering 60 and
292            // 50 Hz does not land on 50 by accident of driver order.
293            .filter(|(_, m)| {
294                m.width == width && m.height == height && vrefresh.is_none_or(|hz| m.vrefresh == hz)
295            })
296            .max_by_key(|(_, m)| m.vrefresh)
297            .map(|(i, _)| i)
298    };
299
300    let requested = match preference {
301        ModePreference::Preferred => connector.preferred(),
302        ModePreference::Exact { width, height } => exact(width, height, None),
303        ModePreference::ExactRefresh {
304            width,
305            height,
306            vrefresh,
307        } => exact(width, height, Some(vrefresh)).or_else(|| exact(width, height, None)),
308        ModePreference::Largest => connector.largest(),
309    };
310
311    match requested {
312        Some(i) => (i, false),
313        // Never index blindly into `modes[0]`. Drivers conventionally put the
314        // preferred mode first, and conventions are not guarantees.
315        None => (
316            connector
317                .preferred()
318                .or_else(|| connector.largest())
319                .unwrap_or(0),
320            true,
321        ),
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    fn mode(width: u16, height: u16, vrefresh: u32, preferred: bool) -> ModeInfo {
330        ModeInfo {
331            width,
332            height,
333            vrefresh,
334            preferred,
335        }
336    }
337
338    fn connector(id: u32, kind: ConnectorKind, modes: Vec<ModeInfo>) -> ConnectorInfo {
339        ConnectorInfo {
340            id,
341            kind,
342            connected: true,
343            modes,
344        }
345    }
346
347    fn disconnected(id: u32, kind: ConnectorKind) -> ConnectorInfo {
348        ConnectorInfo {
349            id,
350            kind,
351            connected: false,
352            modes: vec![mode(1920, 1080, 60, true)],
353        }
354    }
355
356    /// A Pi with the official touchscreen on DSI and a monitor on HDMI.
357    fn pi_with_debug_monitor() -> Vec<ConnectorInfo> {
358        vec![
359            connector(
360                32,
361                ConnectorKind::Hdmi,
362                vec![mode(1920, 1080, 60, true), mode(1280, 720, 60, false)],
363            ),
364            connector(45, ConnectorKind::Dsi, vec![mode(800, 480, 60, true)]),
365        ]
366    }
367
368    #[test]
369    fn no_connectors_is_an_error() {
370        assert_eq!(
371            select(&[], OutputPreference::Auto, ModePreference::Preferred),
372            Err(SelectionError::NoConnectors)
373        );
374    }
375
376    #[test]
377    fn nothing_plugged_in_is_an_error() {
378        let connectors = vec![
379            disconnected(32, ConnectorKind::Hdmi),
380            disconnected(45, ConnectorKind::Dsi),
381        ];
382        assert_eq!(
383            select_connector(&connectors, OutputPreference::Auto),
384            Err(SelectionError::NothingConnected)
385        );
386    }
387
388    #[test]
389    fn a_connector_with_no_modes_is_not_usable() {
390        // Connected but mode-less happens with a live cable and a sleeping display.
391        let connectors = vec![connector(32, ConnectorKind::Hdmi, vec![])];
392        assert_eq!(
393            select_connector(&connectors, OutputPreference::Auto),
394            Err(SelectionError::NothingConnected)
395        );
396    }
397
398    #[test]
399    fn the_built_in_panel_wins_over_a_debug_monitor() {
400        // The scenario this policy exists for: the kiosk must not migrate to HDMI
401        // because somebody plugged a monitor in, even though HDMI is bigger and
402        // comes first in driver order.
403        let connectors = pi_with_debug_monitor();
404        let i = select_connector(&connectors, OutputPreference::Auto).expect("a connector");
405        assert_eq!(connectors[i].kind, ConnectorKind::Dsi);
406    }
407
408    #[test]
409    fn hdmi_is_used_when_there_is_no_panel() {
410        let connectors = vec![connector(
411            32,
412            ConnectorKind::Hdmi,
413            vec![mode(1920, 1080, 60, true)],
414        )];
415        let i = select_connector(&connectors, OutputPreference::Auto).expect("a connector");
416        assert_eq!(connectors[i].kind, ConnectorKind::Hdmi);
417    }
418
419    #[test]
420    fn a_virtual_output_loses_to_anything_real() {
421        let connectors = vec![
422            connector(1, ConnectorKind::Virtual, vec![mode(1024, 768, 60, true)]),
423            connector(2, ConnectorKind::Vga, vec![mode(640, 480, 60, true)]),
424        ];
425        let i = select_connector(&connectors, OutputPreference::Auto).expect("a connector");
426        assert_eq!(connectors[i].kind, ConnectorKind::Vga);
427    }
428
429    #[test]
430    fn equal_rank_prefers_the_larger_display() {
431        let connectors = vec![
432            connector(1, ConnectorKind::Hdmi, vec![mode(1280, 720, 60, true)]),
433            connector(
434                2,
435                ConnectorKind::DisplayPort,
436                vec![mode(2560, 1440, 60, true)],
437            ),
438        ];
439        let i = select_connector(&connectors, OutputPreference::Auto).expect("a connector");
440        assert_eq!(connectors[i].id, 2);
441    }
442
443    #[test]
444    fn an_explicit_id_overrides_the_ranking() {
445        let connectors = pi_with_debug_monitor();
446        let i = select_connector(&connectors, OutputPreference::Id(32)).expect("a connector");
447        assert_eq!(connectors[i].kind, ConnectorKind::Hdmi);
448    }
449
450    #[test]
451    fn an_explicit_id_that_is_absent_is_an_error_not_a_fallback() {
452        // Silently driving a different display than the one configured is worse
453        // than refusing: the operator would never find out.
454        let connectors = pi_with_debug_monitor();
455        assert_eq!(
456            select_connector(&connectors, OutputPreference::Id(99)),
457            Err(SelectionError::RequestedIdUnavailable(99))
458        );
459    }
460
461    #[test]
462    fn an_explicit_kind_that_is_absent_is_an_error() {
463        let connectors = pi_with_debug_monitor();
464        assert_eq!(
465            select_connector(&connectors, OutputPreference::Kind(ConnectorKind::Vga)),
466            Err(SelectionError::RequestedKindUnavailable(ConnectorKind::Vga))
467        );
468    }
469
470    #[test]
471    fn a_disconnected_requested_id_is_rejected() {
472        let connectors = vec![disconnected(32, ConnectorKind::Hdmi)];
473        assert_eq!(
474            select_connector(&connectors, OutputPreference::Id(32)),
475            Err(SelectionError::RequestedIdUnavailable(32))
476        );
477    }
478
479    #[test]
480    fn preferred_mode_is_taken_even_when_it_is_not_first() {
481        let c = connector(
482            1,
483            ConnectorKind::Dsi,
484            vec![
485                mode(640, 480, 60, false),
486                mode(800, 480, 60, true),
487                mode(1024, 600, 60, false),
488            ],
489        );
490        let (i, fell_back) = select_mode(&c, ModePreference::Preferred);
491        assert_eq!(i, 1);
492        assert!(!fell_back);
493    }
494
495    #[test]
496    fn no_preferred_flag_falls_back_to_the_largest_not_the_first() {
497        // The 640x480 bug: taking modes[0] because the driver happened to list it.
498        let c = connector(
499            1,
500            ConnectorKind::Hdmi,
501            vec![
502                mode(640, 480, 60, false),
503                mode(1920, 1080, 60, false),
504                mode(1280, 720, 60, false),
505            ],
506        );
507        let (i, fell_back) = select_mode(&c, ModePreference::Preferred);
508        assert_eq!(c.modes[i], mode(1920, 1080, 60, false));
509        assert!(fell_back);
510    }
511
512    #[test]
513    fn exact_mode_is_found() {
514        let c = connector(
515            1,
516            ConnectorKind::Hdmi,
517            vec![mode(1920, 1080, 60, true), mode(1280, 720, 60, false)],
518        );
519        let (i, fell_back) = select_mode(
520            &c,
521            ModePreference::Exact {
522                width: 1280,
523                height: 720,
524            },
525        );
526        assert_eq!(c.modes[i], mode(1280, 720, 60, false));
527        assert!(!fell_back);
528    }
529
530    #[test]
531    fn exact_mode_takes_the_fastest_refresh_available() {
532        let c = connector(
533            1,
534            ConnectorKind::Hdmi,
535            vec![mode(1920, 1080, 50, false), mode(1920, 1080, 60, false)],
536        );
537        let (i, _) = select_mode(
538            &c,
539            ModePreference::Exact {
540                width: 1920,
541                height: 1080,
542            },
543        );
544        assert_eq!(c.modes[i].vrefresh, 60);
545    }
546
547    #[test]
548    fn an_unavailable_exact_mode_falls_back_and_says_so() {
549        let c = connector(1, ConnectorKind::Dsi, vec![mode(800, 480, 60, true)]);
550        let (i, fell_back) = select_mode(
551            &c,
552            ModePreference::Exact {
553                width: 1920,
554                height: 1080,
555            },
556        );
557        assert_eq!(c.modes[i], mode(800, 480, 60, true));
558        assert!(
559            fell_back,
560            "a silently ignored configured mode must be flagged"
561        );
562    }
563
564    #[test]
565    fn exact_refresh_relaxes_to_the_same_resolution_before_giving_up() {
566        let c = connector(
567            1,
568            ConnectorKind::Hdmi,
569            vec![mode(1920, 1080, 60, false), mode(1280, 720, 60, true)],
570        );
571        let (i, fell_back) = select_mode(
572            &c,
573            ModePreference::ExactRefresh {
574                width: 1920,
575                height: 1080,
576                vrefresh: 144,
577            },
578        );
579        assert_eq!(c.modes[i], mode(1920, 1080, 60, false));
580        assert!(
581            !fell_back,
582            "same resolution at another rate is not a fallback"
583        );
584    }
585
586    #[test]
587    fn largest_ignores_the_preferred_flag() {
588        let c = connector(
589            1,
590            ConnectorKind::Hdmi,
591            vec![mode(1280, 720, 60, true), mode(1920, 1080, 60, false)],
592        );
593        let (i, _) = select_mode(&c, ModePreference::Largest);
594        assert_eq!(c.modes[i], mode(1920, 1080, 60, false));
595    }
596
597    #[test]
598    fn selection_never_returns_an_out_of_range_index() {
599        // Whatever the preference and however odd the mode list, the indices handed
600        // back get used to index straight into DRM's arrays.
601        let connectors = pi_with_debug_monitor();
602        let prefs = [
603            ModePreference::Preferred,
604            ModePreference::Largest,
605            ModePreference::Exact {
606                width: 3840,
607                height: 2160,
608            },
609            ModePreference::ExactRefresh {
610                width: 800,
611                height: 480,
612                vrefresh: 75,
613            },
614        ];
615        for pref in prefs {
616            let s = select(&connectors, OutputPreference::Auto, pref).expect("a selection");
617            assert!(s.connector < connectors.len());
618            assert!(s.mode < connectors[s.connector].modes.len());
619        }
620    }
621}