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}