srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/udev/drm.rs
blob: fd06619d147c21bc5ce076f7353cbbd7bd088ef8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
use super::*;

fn mode_refresh_mhz(mode: &DrmMode) -> i32 {
    let vrefresh = mode.vrefresh();
    if vrefresh > 0 {
        vrefresh as i32 * 1000
    } else {
        60_000
    }
}

/// Brings one connector up: allocates its scanout buffers, sets the mode,
/// and creates the `wl_output` global. Shared by startup and hotplug so a
/// monitor plugged in later is set up exactly like one present at boot.
pub(crate) fn bring_up_head(
    card: &Card,
    dh: &DisplayHandle,
    probe: &ConnectorProbe,
    crtc: crtc::Handle,
    x_offset: i32,
) -> PlatformResult<(UdevHead, crate::state::OutputEntry)> {
    let (width, height) = probe.mode.size();
    let (width, height) = (width as i32, height as i32);

    let buffers = [make_drm_buffer(card, width, height)?, make_drm_buffer(card, width, height)?];
    card.set_crtc(crtc, Some(buffers[0].fb), (0, 0), &[probe.connector], Some(probe.mode)).map_err(err)?;

    // Named after the real connector (eDP-1, HDMI-A-1, ...) so clients and
    // the user can tell monitors apart; `wl_output.name` is what a bar's
    // per-monitor config keys off.
    //
    // Physical size in millimeters comes straight from EDID via the
    // connector, not the hardcoded (0, 0) this used to be - some clients
    // compute their own effective DPI from it (independently of the
    // compositor's own scale factor, which srdwm always reports as 1), so
    // reporting "no physical size at all" was live, wrong data reaching
    // every client, not just an unfilled-in placeholder.
    let (phys_w, phys_h) = probe.info.size().unwrap_or((0, 0));
    let physical_mm = (phys_w as i32, phys_h as i32);
    let output = Output::new(
        probe.name.clone(),
        PhysicalProperties { size: physical_mm.into(), subpixel: Subpixel::Unknown, make: "srdwm".into(), model: "drm".into() },
    );
    let mode = OutputMode { size: (width, height).into(), refresh: mode_refresh_mhz(&probe.mode) };
    output.change_current_state(Some(mode), Some(Transform::Normal), None, Some((x_offset, 0).into()));
    output.set_preferred(mode);
    let global = output.create_global::<CompState>(dh);

    let location: Point<i32, Logical> = (x_offset, 0).into();
    let head = UdevHead {
        crtc,
        connector: probe.connector,
        output: output.clone(),
        global,
        damage_tracker: OutputDamageTracker::from_output(&output),
        buffers,
        front: 0,
        flip_pending: false,
        ages: [0, 0],
        location,
        size: (width, height),
    };
    Ok((head, crate::state::OutputEntry { output, location }))
}

/// A connected connector and the mode we intend to drive it at. CRTC
/// assignment is deliberately separate ([`pick_crtc`]) so a hotplug re-probe
/// can leave surviving heads on the CRTCs they already hold.
pub(crate) struct ConnectorProbe {
    pub(crate) connector: connector::Handle,
    pub(crate) info: connector::Info,
    pub(crate) mode: DrmMode,
    /// Connector name as the kernel reports it (`eDP-1`, `HDMI-A-1`, ...).
    pub(crate) name: String,
}

/// Every connector currently reporting `Connected`, with its preferred mode.
///
/// Forces a fresh probe (`get_connector(.., true)`) rather than trusting
/// cached state - on a hotplug the cached status is exactly what has gone
/// stale.
pub(crate) fn probe_connected(card: &Card) -> PlatformResult<Vec<ConnectorProbe>> {
    let res = card.resource_handles().map_err(err)?;
    let mut probes = Vec::new();
    for handle in res.connectors() {
        let Ok(info) = card.get_connector(*handle, true) else { continue };
        if info.state() != connector::State::Connected {
            continue;
        }
        let name = format!("{:?}-{}", info.interface(), info.interface_id());
        // Prefer the mode the display advertises as PREFERRED (its native
        // resolution) rather than whatever happens to be listed first --
        // the list order is not guaranteed, and picking wrong means running
        // a monitor at the wrong resolution.
        let Some(&mode) = info
            .modes()
            .iter()
            .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))
            .or_else(|| info.modes().first())
        else {
            log::warn!("udev: connector {name} is connected but reports no modes; skipping");
            continue;
        };
        probes.push(ConnectorProbe { connector: *handle, info, mode, name });
    }
    Ok(probes)
}

/// Picks a CRTC for `probe` that is not in `used`.
///
/// CRTCs are a finite hardware resource and cannot be shared, so a machine
/// with more connected monitors than CRTCs drives as many as the hardware
/// allows and logs the rest rather than failing outright.
pub(crate) fn pick_crtc(card: &Card, probe: &ConnectorProbe, used: &[crtc::Handle]) -> Option<crtc::Handle> {
    let res = card.resource_handles().ok()?;
    // Prefer the CRTC already driving this connector, else any free one the
    // encoder can reach, else anything free at all.
    probe
        .info
        .current_encoder()
        .and_then(|enc| card.get_encoder(enc).ok())
        .map(|enc| res.filter_crtcs(enc.possible_crtcs()))
        .unwrap_or_default()
        .into_iter()
        .chain(res.crtcs().iter().copied())
        .find(|c| !used.contains(c))
}

fn make_drm_buffer(card: &Card, width: i32, height: i32) -> PlatformResult<DrmBuffer> {
    let dumb = card.create_dumb_buffer((width as u32, height as u32), DrmFourcc::Xrgb8888, 32).map_err(err)?;
    let fb = card.add_framebuffer(&dumb, 24, 32).map_err(err)?;
    let format = FormatCode::try_from(DrmFourcc::Xrgb8888).map_err(|_| PlatformError::Other("udev: unsupported pixel format".into()))?;
    let image = Image::new(format, width as usize, height as usize, true).map_err(|_| PlatformError::Other("udev: failed to allocate render buffer".into()))?;
    Ok(DrmBuffer { dumb, fb, image })
}