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}