Skip to main content

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/// How far to move the sprite up and left inside its buffer so the plane need
29/// not start above or left of the CRTC (drm-cxx#384).
30///
31/// Some drivers -- the SA8155P's -- refuse a negative `CRTC_X`/`CRTC_Y`, and
32/// the usual fix, trimming the source, is refused by others: i915 and amdgpu
33/// take no source offset on a cursor plane. Moving the pixels works on all of
34/// them. The plane goes to the edge and the sprite is drawn shifted by what
35/// would have hung off it.
36///
37/// At most the hotspot, so the hotspot never leaves the buffer: a pointer
38/// that is itself off the top or left of the CRTC keeps a negative position,
39/// as before.
40#[must_use]
41pub fn edge_shift(pointer: (i32, i32), hotspot: (u32, u32)) -> (u32, u32) {
42    let (x, y) = plane_origin(pointer, hotspot);
43    let shift = |origin: i32, hotspot: u32| {
44        u32::try_from(-i64::from(origin)).map_or(0, |overhang| overhang.min(hotspot))
45    };
46    (shift(x, hotspot.0), shift(y, hotspot.1))
47}
48
49/// What to write to the plane's `rotation` property.
50///
51/// The requested rotation when the plane advertises it, and `ROTATE_0`
52/// otherwise -- because in that case the pixels were already turned in
53/// software, and asking the hardware to turn them again would turn them twice.
54///
55/// Written every commit rather than when it changes: a plane that went dark
56/// during a hide, or a descriptor replaced across a session resume, has lost
57/// whatever the kernel knew, and re-arming is cheaper than tracking that.
58#[must_use]
59pub fn hardware_rotation(requested: Rotation, supported_mask: u64) -> u64 {
60    let want = requested.drm_mask();
61    if supported_mask & want != 0 {
62        want
63    } else {
64        Rotation::None.drm_mask()
65    }
66}
67
68/// The state a cursor plane is put into for one frame.
69///
70/// Distinct from [`crate::sprite::Placement`], which says where the sprite
71/// landed inside the buffer. This says where the buffer lands on the screen.
72#[derive(Debug, Clone, Copy)]
73#[allow(
74    clippy::exhaustive_structs,
75    reason = "built by callers, with no default to fill a new field from"
76)]
77pub struct PlaneState {
78    /// The plane being written.
79    pub plane_id: u32,
80    /// The CRTC it feeds.
81    pub crtc_id: u32,
82    /// The framebuffer to scan out.
83    pub fb_id: u32,
84    /// Buffer dimensions. Source and destination match: a cursor plane is not
85    /// asked to scale.
86    pub width: u32,
87    /// As [`PlaneState::width`].
88    pub height: u32,
89    /// Where the pointer is on the CRTC.
90    pub pointer_x: i32,
91    /// As [`PlaneState::pointer_x`].
92    pub pointer_y: i32,
93    /// The hotspot in buffer coordinates, from
94    /// [`compose`](crate::sprite::compose).
95    ///
96    /// `None` before a cursor has been drawn, which is a real state: the
97    /// buffer exists so the size can be probed, but nothing is in it yet.
98    pub hotspot: Option<(u32, u32)>,
99    /// How the sprite is turned.
100    pub rotation: Rotation,
101    /// The rotations the plane can do in hardware, as a `DRM_MODE_ROTATE_*`
102    /// mask. Zero when it has no `rotation` property.
103    ///
104    /// Supplied rather than read back from the property, because `rotation` is
105    /// a bitmask and a property store reports ranges. Guessing here would make
106    /// every rotation look supported and turn the sprite twice: once in
107    /// software, once by the hardware.
108    pub rotation_mask: u64,
109}
110
111/// Write `placement` into `request`.
112///
113/// Every property, every commit. Drivers accept partial writes, but a plane
114/// that went dark during a hide needs all of it re-armed to come back, and
115/// writing the lot is cheaper than tracking which fields a given transition
116/// dirtied.
117///
118/// `rotation`, `HOTSPOT_X` and `HOTSPOT_Y` are optional and skipped when the
119/// plane does not carry them. The hotspot properties are a hint for
120/// virtualized drivers, which need to know where inside the sprite the pointer
121/// actually is; before anything has been drawn they are left alone rather than
122/// clobbered with a stale zero.
123///
124/// Returns how many properties were written.
125///
126/// # Errors
127///
128/// [`CursorError::MissingProperty`] if the plane lacks one of the properties
129/// every plane is required to have. That is a broken driver rather than
130/// anything a caller did.
131pub fn stage(
132    request: &mut AtomicRequest,
133    props: &PropertyStore,
134    placement: &PlaneState,
135) -> Result<usize, CursorError> {
136    let (x, y) = plane_origin(
137        (placement.pointer_x, placement.pointer_y),
138        placement.hotspot.unwrap_or((0, 0)),
139    );
140
141    let mut written = 0;
142    let mut put = |name: &str, value: u64| -> Result<(), CursorError> {
143        let id = props
144            .property_id(placement.plane_id, name)
145            .map_err(|_| CursorError::MissingProperty)?;
146        request
147            .add_property(placement.plane_id, id, value)
148            .map_err(|_| CursorError::MissingProperty)?;
149        written += 1;
150        Ok(())
151    };
152
153    put("FB_ID", u64::from(placement.fb_id))?;
154    put("CRTC_ID", u64::from(placement.crtc_id))?;
155    // Signed, carried in the unsigned slot as a 64-bit two's complement: the
156    // kernel range-checks the value as an `i64`, so a 32-bit one reads as a
157    // large positive number and the commit is refused.
158    put("CRTC_X", i64::from(x).cast_unsigned())?;
159    put("CRTC_Y", i64::from(y).cast_unsigned())?;
160    put("CRTC_W", u64::from(placement.width))?;
161    put("CRTC_H", u64::from(placement.height))?;
162    put("SRC_X", 0)?;
163    put("SRC_Y", 0)?;
164    // The source rectangle is 16.16 fixed point; the CRTC rectangle is plain
165    // pixels. Sending either in the other's units is a cursor that fills the
166    // screen or vanishes.
167    put("SRC_W", u64::from(placement.width) << 16)?;
168    put("SRC_H", u64::from(placement.height) << 16)?;
169
170    if let Ok(id) = props.property_id(placement.plane_id, "rotation") {
171        let value = hardware_rotation(placement.rotation, placement.rotation_mask);
172        if request.add_property(placement.plane_id, id, value).is_err() {
173            return Err(CursorError::MissingProperty);
174        }
175        written += 1;
176    }
177
178    if let Some((hotspot_x, hotspot_y)) = placement.hotspot {
179        for (name, value) in [("HOTSPOT_X", hotspot_x), ("HOTSPOT_Y", hotspot_y)] {
180            if let Ok(id) = props.property_id(placement.plane_id, name) {
181                if request
182                    .add_property(placement.plane_id, id, u64::from(value))
183                    .is_err()
184                {
185                    return Err(CursorError::MissingProperty);
186                }
187                written += 1;
188            }
189        }
190    }
191
192    Ok(written)
193}