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 a CRTC --
105/// any, this one included, as the reference asks. Taking an overlay that is
106/// showing something costs whoever put it there a layer, on this output or
107/// another; the search prefers a free one but will take a busy one rather
108/// than fall back to legacy.
109///
110/// `prefer_legacy` takes the legacy path over a cursor plane that can carry
111/// `ARGB8888`, which is the plane the legacy ioctls then drive. It needs
112/// `allow_legacy`, and a forced plane overrides it.
113///
114/// # Errors
115///
116/// - [`CursorError::NoPlane`] when nothing on this CRTC can carry a cursor and
117///   `allow_legacy` is false.
118/// - [`CursorError::PlaneUnusable`] when `forced` names a plane that does not
119///   exist, cannot feed this CRTC, or can take none of [`CURSOR_FORMATS`]
120///   linearly.
121pub fn select_plane(
122    registry: &PlaneRegistry,
123    crtc_index: u32,
124    forced: Option<u32>,
125    allow_legacy: bool,
126    prefer_legacy: bool,
127    bound_elsewhere: &dyn Fn(u32) -> bool,
128) -> Result<SelectedPlane, CursorError> {
129    if let Some(plane_id) = forced {
130        let plane = registry
131            .by_id(plane_id)
132            .filter(|plane| plane.compatible_with_crtc(crtc_index))
133            .ok_or(CursorError::PlaneUnusable)?;
134        let fourcc = usable(plane).ok_or(CursorError::PlaneUnusable)?;
135        return Ok(selected(plane, fourcc));
136    }
137
138    // A cursor plane first. It is per-CRTC hardware built for this, with no
139    // z-order to negotiate and no overlay taken away from anyone.
140    if let Some((plane, fourcc)) = registry
141        .for_crtc(crtc_index)
142        .filter(|plane| plane.plane_type == PlaneType::Cursor)
143        .find_map(|plane| usable(plane).map(|fourcc| (plane, fourcc)))
144    {
145        // The legacy ioctls drive this same plane, as an async update that
146        // does not wait for vblank, so a moving pointer does not contend with
147        // the caller's flips (drm-cxx `0925491`). They hand the kernel an
148        // ARGB8888 buffer, so only a plane that takes one.
149        if prefer_legacy && allow_legacy && plane.scans_out_linear(drmkit_fmt::fourcc::ARGB8888) {
150            return Ok(SelectedPlane {
151                legacy_plane_id: plane.id,
152                ..legacy()
153            });
154        }
155        return Ok(selected(plane, fourcc));
156    }
157
158    // Otherwise an overlay, preferring one no other CRTC is using. Some parts
159    // expose only a handful, and a compositor wants the rest for layers. A
160    // multirect virtual plane is skipped, as the reference skips it
161    // (drm-cxx#387): the driver refuses it unless its parent is armed too,
162    // which the SA8155P reports as "r1 only virt plane".
163    let mut fallback: Option<(&PlaneCapabilities, u32)> = None;
164    for plane in registry.for_crtc(crtc_index) {
165        if plane.plane_type != PlaneType::Overlay || plane.multirect_parent.is_some() {
166            continue;
167        }
168        let Some(fourcc) = usable(plane) else {
169            continue;
170        };
171        if !bound_elsewhere(plane.id) {
172            return Ok(selected(plane, fourcc));
173        }
174        fallback.get_or_insert((plane, fourcc));
175    }
176    if let Some((plane, fourcc)) = fallback {
177        return Ok(selected(plane, fourcc));
178    }
179
180    if allow_legacy {
181        return Ok(legacy());
182    }
183    Err(CursorError::NoPlane)
184}
185
186/// The legacy path, which addresses the CRTC rather than a plane.
187const fn legacy() -> SelectedPlane {
188    SelectedPlane {
189        plane_id: 0,
190        path: PlanePath::Legacy,
191        cursor_max_w: 0,
192        cursor_max_h: 0,
193        // `drmModeSetCursor` has no format argument: the kernel reads the
194        // buffer as ARGB8888 and there is nothing to negotiate.
195        fourcc: drmkit_fmt::fourcc::ARGB8888,
196        legacy_plane_id: 0,
197    }
198}
199
200/// Describe a chosen plane.
201fn selected(plane: &PlaneCapabilities, fourcc: u32) -> SelectedPlane {
202    let path = if plane.plane_type == PlaneType::Cursor {
203        PlanePath::AtomicCursor
204    } else {
205        PlanePath::AtomicOverlay
206    };
207    SelectedPlane {
208        plane_id: plane.id,
209        path,
210        // Only a cursor plane advertises a maximum; an overlay is bounded by
211        // the mode, and reporting a zero here says "no limit of its own"
212        // rather than "zero by zero".
213        cursor_max_w: plane.cursor_max_w,
214        cursor_max_h: plane.cursor_max_h,
215        fourcc,
216        legacy_plane_id: 0,
217    }
218}