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 centre 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}