Skip to main content

drmkit_cursor/
plane.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Choosing the plane a cursor is drawn on.
5//!
6//! This decides everything downstream: whether the cursor is committed
7//! atomically with the rest of the frame, whether it can be rotated in
8//! hardware, and how big its buffer may be. It is a policy over what the
9//! registry says, with no device in it -- the one thing that genuinely needs
10//! to ask the kernel, whether a plane is already bound elsewhere, is a
11//! predicate the caller supplies.
12//!
13//! The reference enumerates the registry and calls `drmModeGetPlane` itself,
14//! which makes the policy untestable without hardware. Splitting the question
15//! out is what lets every case below be a test.
16
17use drmkit_planes::{PlaneCapabilities, PlaneRegistry, PlaneType};
18
19use crate::CursorError;
20
21/// How the cursor reaches the screen.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[non_exhaustive]
24pub enum PlanePath {
25    /// A dedicated cursor plane, driven atomically.
26    ///
27    /// The best case: per-CRTC hardware with no z-ordering to negotiate.
28    AtomicCursor,
29    /// An overlay plane, driven atomically.
30    ///
31    /// For hardware with no cursor plane, or whose cursor plane can take none
32    /// of [`CURSOR_FORMATS`]. Costs an overlay the compositor might have wanted.
33    AtomicOverlay,
34    /// `drmModeSetCursor`, the legacy path.
35    ///
36    /// No atomic commit, so the cursor cannot be coalesced with a frame and
37    /// cannot be rotated. Last resort.
38    Legacy,
39}
40
41/// The plane a cursor will use.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43#[non_exhaustive]
44pub struct SelectedPlane {
45    /// The plane's KMS id, or `0` on the legacy path, which addresses the CRTC
46    /// rather than a plane.
47    pub plane_id: u32,
48    /// How it will be driven.
49    pub path: PlanePath,
50    /// The plane's maximum cursor width, when it advertises one.
51    pub cursor_max_w: u32,
52    /// The plane's maximum cursor height, when it advertises one.
53    pub cursor_max_h: u32,
54    /// The `FourCC` the cursor buffer is allocated and drawn in.
55    ///
56    /// Not always `ARGB8888`. Tegra's cursor plane advertises `RGBA8888` and
57    /// nothing else, and a plane that differs only in channel order is
58    /// perfectly able to carry a cursor -- see [`CURSOR_FORMATS`].
59    pub fourcc: u32,
60    /// On the legacy path, the CRTC's cursor plane the legacy ioctls drive
61    /// when [`RendererConfig::prefer_legacy`](crate::RendererConfig::prefer_legacy)
62    /// chose them over it; `0` otherwise.
63    pub legacy_plane_id: u32,
64}
65
66/// The formats a cursor can be drawn in, best first.
67///
68/// All four are 32-bit with alpha and differ only in channel order, so the
69/// renderer reaches any of them by permuting the pixel it already has. There
70/// is no `XRGB8888` here on purpose: a cursor without an alpha channel is a
71/// rectangle.
72///
73/// `ARGB8888` leads because it is what the shadow is already in and what the
74/// legacy path requires; the rest are taken only when a plane offers nothing
75/// better.
76///
77/// The same list, in the same order, as the reference's `k_cursor_formats`:
78/// Tegra's cursor plane, `RGBA8888` only, carries a cursor rather than an
79/// overlay being spent on one.
80pub const CURSOR_FORMATS: [u32; 4] = [
81    drmkit_fmt::fourcc::ARGB8888,
82    drmkit_fmt::fourcc::RGBA8888,
83    drmkit_fmt::fourcc::ABGR8888,
84    drmkit_fmt::fourcc::BGRA8888,
85];
86
87/// The format this plane would carry a cursor in, if it can carry one.
88///
89/// [`CURSOR_FORMATS`] in order, so a plane that takes `ARGB8888` is drawn in
90/// what the shadow already holds and nothing is permuted. Linear, since the
91/// cursor is a dumb buffer.
92fn usable(plane: &PlaneCapabilities) -> Option<u32> {
93    CURSOR_FORMATS
94        .into_iter()
95        .find(|fourcc| plane.scans_out_linear(*fourcc))
96}
97
98/// Pick a plane for the cursor on `crtc_index`.
99///
100/// `forced` bypasses the search: the named plane is validated and taken, or
101/// the call fails. That is a caller overriding policy, and silently ignoring
102/// an override is worse than refusing it.
103///
104/// `bound_elsewhere` answers whether a plane is currently driving some other
105/// CRTC. Overlays are shared on many parts, and taking one that another output
106/// is using costs that output a layer; the search prefers a free one but will
107/// take a busy one rather than fall back to legacy.
108///
109/// `prefer_legacy` takes the legacy path over a cursor plane that can carry
110/// `ARGB8888`, which is the plane the legacy ioctls then drive. It needs
111/// `allow_legacy`, and a forced plane overrides it.
112///
113/// # Errors
114///
115/// - [`CursorError::NoPlane`] when nothing on this CRTC can carry a cursor and
116///   `allow_legacy` is false.
117/// - [`CursorError::PlaneUnusable`] when `forced` names a plane that does not
118///   exist, cannot feed this CRTC, or can take none of [`CURSOR_FORMATS`]
119///   linearly.
120pub fn select_plane(
121    registry: &PlaneRegistry,
122    crtc_index: u32,
123    forced: Option<u32>,
124    allow_legacy: bool,
125    prefer_legacy: bool,
126    bound_elsewhere: &dyn Fn(u32) -> bool,
127) -> Result<SelectedPlane, CursorError> {
128    if let Some(plane_id) = forced {
129        let plane = registry
130            .by_id(plane_id)
131            .filter(|plane| plane.compatible_with_crtc(crtc_index))
132            .ok_or(CursorError::PlaneUnusable)?;
133        let fourcc = usable(plane).ok_or(CursorError::PlaneUnusable)?;
134        return Ok(selected(plane, fourcc));
135    }
136
137    // A cursor plane first. It is per-CRTC hardware built for this, with no
138    // z-order to negotiate and no overlay taken away from anyone.
139    if let Some((plane, fourcc)) = registry
140        .for_crtc(crtc_index)
141        .filter(|plane| plane.plane_type == PlaneType::Cursor)
142        .find_map(|plane| usable(plane).map(|fourcc| (plane, fourcc)))
143    {
144        // The legacy ioctls drive this same plane, as an async update that
145        // does not wait for vblank, so a moving pointer does not contend with
146        // the caller's flips (drm-cxx `0925491`). They hand the kernel an
147        // ARGB8888 buffer, so only a plane that takes one.
148        if prefer_legacy && allow_legacy && plane.scans_out_linear(drmkit_fmt::fourcc::ARGB8888) {
149            return Ok(SelectedPlane {
150                legacy_plane_id: plane.id,
151                ..legacy()
152            });
153        }
154        return Ok(selected(plane, fourcc));
155    }
156
157    // Otherwise an overlay, preferring one no other CRTC is using. Some parts
158    // expose only a handful, and a compositor wants the rest for layers.
159    let mut fallback: Option<(&PlaneCapabilities, u32)> = None;
160    for plane in registry.for_crtc(crtc_index) {
161        if plane.plane_type != PlaneType::Overlay {
162            continue;
163        }
164        let Some(fourcc) = usable(plane) else {
165            continue;
166        };
167        if !bound_elsewhere(plane.id) {
168            return Ok(selected(plane, fourcc));
169        }
170        fallback.get_or_insert((plane, fourcc));
171    }
172    if let Some((plane, fourcc)) = fallback {
173        return Ok(selected(plane, fourcc));
174    }
175
176    if allow_legacy {
177        return Ok(legacy());
178    }
179    Err(CursorError::NoPlane)
180}
181
182/// The legacy path, which addresses the CRTC rather than a plane.
183const fn legacy() -> SelectedPlane {
184    SelectedPlane {
185        plane_id: 0,
186        path: PlanePath::Legacy,
187        cursor_max_w: 0,
188        cursor_max_h: 0,
189        // `drmModeSetCursor` has no format argument: the kernel reads the
190        // buffer as ARGB8888 and there is nothing to negotiate.
191        fourcc: drmkit_fmt::fourcc::ARGB8888,
192        legacy_plane_id: 0,
193    }
194}
195
196/// Describe a chosen plane.
197fn selected(plane: &PlaneCapabilities, fourcc: u32) -> SelectedPlane {
198    let path = if plane.plane_type == PlaneType::Cursor {
199        PlanePath::AtomicCursor
200    } else {
201        PlanePath::AtomicOverlay
202    };
203    SelectedPlane {
204        plane_id: plane.id,
205        path,
206        // Only a cursor plane advertises a maximum; an overlay is bounded by
207        // the mode, and reporting a zero here says "no limit of its own"
208        // rather than "zero by zero".
209        cursor_max_w: plane.cursor_max_w,
210        cursor_max_h: plane.cursor_max_h,
211        fourcc,
212        legacy_plane_id: 0,
213    }
214}