drmkit_cursor/stage.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Writing a cursor's position onto its plane.
5
6use drmkit_core::{AtomicRequest, PropertyStore};
7
8use crate::CursorError;
9use crate::sprite::Rotation;
10
11/// Where the plane's top-left corner goes, given where the pointer is.
12///
13/// The hotspot is the pixel that has to land under the pointer -- the tip of
14/// the arrow, the center of a crosshair -- so the plane is offset back by it.
15/// Getting this wrong is invisible in a screenshot and obvious the moment
16/// anyone clicks: the sprite looks right and the click lands somewhere else.
17///
18/// The result is signed and may be negative, which is correct: a cursor near
19/// the top-left of the screen has its buffer hanging off the edge.
20#[must_use]
21pub fn plane_origin(pointer: (i32, i32), hotspot: (u32, u32)) -> (i32, i32) {
22 let back = |coordinate: i32, offset: u32| {
23 i32::try_from(offset).map_or(coordinate, |offset| coordinate.saturating_sub(offset))
24 };
25 (back(pointer.0, hotspot.0), back(pointer.1, hotspot.1))
26}
27
28/// What to write to the plane's `rotation` property.
29///
30/// The requested rotation when the plane advertises it, and `ROTATE_0`
31/// otherwise -- because in that case the pixels were already turned in
32/// software, and asking the hardware to turn them again would turn them twice.
33///
34/// Written every commit rather than when it changes: a plane that went dark
35/// during a hide, or a descriptor replaced across a session resume, has lost
36/// whatever the kernel knew, and re-arming is cheaper than tracking that.
37#[must_use]
38pub fn hardware_rotation(requested: Rotation, supported_mask: u64) -> u64 {
39 let want = requested.drm_mask();
40 if supported_mask & want != 0 {
41 want
42 } else {
43 Rotation::None.drm_mask()
44 }
45}
46
47/// The state a cursor plane is put into for one frame.
48///
49/// Distinct from [`crate::sprite::Placement`], which says where the sprite
50/// landed inside the buffer. This says where the buffer lands on the screen.
51#[derive(Debug, Clone, Copy)]
52#[allow(
53 clippy::exhaustive_structs,
54 reason = "built by callers, with no default to fill a new field from"
55)]
56pub struct PlaneState {
57 /// The plane being written.
58 pub plane_id: u32,
59 /// The CRTC it feeds.
60 pub crtc_id: u32,
61 /// The framebuffer to scan out.
62 pub fb_id: u32,
63 /// Buffer dimensions. Source and destination match: a cursor plane is not
64 /// asked to scale.
65 pub width: u32,
66 /// As [`PlaneState::width`].
67 pub height: u32,
68 /// Where the pointer is on the CRTC.
69 pub pointer_x: i32,
70 /// As [`PlaneState::pointer_x`].
71 pub pointer_y: i32,
72 /// The hotspot in buffer coordinates, from
73 /// [`compose`](crate::sprite::compose).
74 ///
75 /// `None` before a cursor has been drawn, which is a real state: the
76 /// buffer exists so the size can be probed, but nothing is in it yet.
77 pub hotspot: Option<(u32, u32)>,
78 /// How the sprite is turned.
79 pub rotation: Rotation,
80 /// The rotations the plane can do in hardware, as a `DRM_MODE_ROTATE_*`
81 /// mask. Zero when it has no `rotation` property.
82 ///
83 /// Supplied rather than read back from the property, because `rotation` is
84 /// a bitmask and a property store reports ranges. Guessing here would make
85 /// every rotation look supported and turn the sprite twice: once in
86 /// software, once by the hardware.
87 pub rotation_mask: u64,
88}
89
90/// Write `placement` into `request`.
91///
92/// Every property, every commit. Drivers accept partial writes, but a plane
93/// that went dark during a hide needs all of it re-armed to come back, and
94/// writing the lot is cheaper than tracking which fields a given transition
95/// dirtied.
96///
97/// `rotation`, `HOTSPOT_X` and `HOTSPOT_Y` are optional and skipped when the
98/// plane does not carry them. The hotspot properties are a hint for
99/// virtualized drivers, which need to know where inside the sprite the pointer
100/// actually is; before anything has been drawn they are left alone rather than
101/// clobbered with a stale zero.
102///
103/// Returns how many properties were written.
104///
105/// # Errors
106///
107/// [`CursorError::MissingProperty`] if the plane lacks one of the properties
108/// every plane is required to have. That is a broken driver rather than
109/// anything a caller did.
110pub fn stage(
111 request: &mut AtomicRequest,
112 props: &PropertyStore,
113 placement: &PlaneState,
114) -> Result<usize, CursorError> {
115 let (x, y) = plane_origin(
116 (placement.pointer_x, placement.pointer_y),
117 placement.hotspot.unwrap_or((0, 0)),
118 );
119
120 let mut written = 0;
121 let mut put = |name: &str, value: u64| -> Result<(), CursorError> {
122 let id = props
123 .property_id(placement.plane_id, name)
124 .map_err(|_| CursorError::MissingProperty)?;
125 request
126 .add_property(placement.plane_id, id, value)
127 .map_err(|_| CursorError::MissingProperty)?;
128 written += 1;
129 Ok(())
130 };
131
132 put("FB_ID", u64::from(placement.fb_id))?;
133 put("CRTC_ID", u64::from(placement.crtc_id))?;
134 // Signed, carried in an unsigned slot as two's complement -- which is what
135 // the kernel reads back out of it.
136 put("CRTC_X", u64::from(x.cast_unsigned()))?;
137 put("CRTC_Y", u64::from(y.cast_unsigned()))?;
138 put("CRTC_W", u64::from(placement.width))?;
139 put("CRTC_H", u64::from(placement.height))?;
140 put("SRC_X", 0)?;
141 put("SRC_Y", 0)?;
142 // The source rectangle is 16.16 fixed point; the CRTC rectangle is plain
143 // pixels. Sending either in the other's units is a cursor that fills the
144 // screen or vanishes.
145 put("SRC_W", u64::from(placement.width) << 16)?;
146 put("SRC_H", u64::from(placement.height) << 16)?;
147
148 if let Ok(id) = props.property_id(placement.plane_id, "rotation") {
149 let value = hardware_rotation(placement.rotation, placement.rotation_mask);
150 if request.add_property(placement.plane_id, id, value).is_err() {
151 return Err(CursorError::MissingProperty);
152 }
153 written += 1;
154 }
155
156 if let Some((hotspot_x, hotspot_y)) = placement.hotspot {
157 for (name, value) in [("HOTSPOT_X", hotspot_x), ("HOTSPOT_Y", hotspot_y)] {
158 if let Ok(id) = props.property_id(placement.plane_id, name) {
159 if request
160 .add_property(placement.plane_id, id, u64::from(value))
161 .is_err()
162 {
163 return Err(CursorError::MissingProperty);
164 }
165 written += 1;
166 }
167 }
168 }
169
170 Ok(written)
171}