Skip to main content

drmkit_scene_sources/
cursor.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Scanning a cursor out as an ordinary layer.
5//!
6//! The usual path drives a cursor plane directly. This one puts the sprite in
7//! a dumb buffer and lets the scene place it like anything else, which is what
8//! hardware with no cursor-type plane needs -- rockchip's VOP2 among them,
9//! where the pointer has to ride an overlay and compositing it into the canvas
10//! does not work. Reserve the plane up front and put the layer's destination
11//! at `pointer - hotspot`.
12
13use std::time::Instant;
14
15/// `DRM_FORMAT_ARGB8888`. The one format a cursor sprite is in, and the one a
16/// dumb buffer can always be allocated as.
17const ARGB8888: u32 = drmkit_fmt::fourcc::ARGB8888;
18
19use drmkit_core::Device;
20use drmkit_cursor::Cursor;
21use drmkit_dumb::{Buffer, Config, DumbError, MapAccess, Mapping};
22use drmkit_scene::{
23    AcquiredBuffer, BindingModel, DamageRect, LayerBufferSource, SourceError, SourceFormat,
24};
25
26/// A cursor presented as a scene layer.
27///
28/// Static cursors are painted once. An animated one repaints when the frame
29/// under the clock changes, and says so as full-frame damage.
30#[derive(Debug)]
31pub struct CursorSource {
32    cursor: Cursor,
33    buffer: Buffer,
34    format: SourceFormat,
35    /// The hotspot of the frame currently painted.
36    ///
37    /// Per frame rather than per cursor: an animated cursor may move its
38    /// hotspot between frames, and a caller positioning by a stale one makes
39    /// the pointer drift as the animation plays.
40    hotspot: (u32, u32),
41    /// When the animation started. Set on the first acquire, so a cursor
42    /// created long before it is shown does not begin mid-cycle.
43    started: Option<Instant>,
44    /// Which frame is in the buffer, so an unchanged one is not repainted.
45    painted: Option<usize>,
46    /// Reported on the next acquire, then cleared.
47    pending_damage: Vec<DamageRect>,
48}
49
50impl CursorSource {
51    /// Wrap a cursor that is already loaded.
52    ///
53    /// The buffer is sized to the largest frame, so an animation whose frames
54    /// differ in size still has room for all of them, and the first frame is
55    /// painted immediately.
56    ///
57    /// # Errors
58    ///
59    /// [`DumbError`] if the buffer cannot be allocated or mapped.
60    pub fn new(device: &Device, cursor: Cursor) -> Result<Self, DumbError> {
61        let width = cursor.frames().iter().map(|frame| frame.width).max();
62        let height = cursor.frames().iter().map(|frame| frame.height).max();
63        // A cursor with no frames cannot be built -- `Cursor` rejects it -- so
64        // these are always present; the fallback keeps the arithmetic total
65        // rather than relying on that from another crate.
66        let (width, height) = (width.unwrap_or(1).max(1), height.unwrap_or(1).max(1));
67
68        let buffer = Buffer::create(
69            device,
70            &Config {
71                width,
72                height,
73                fourcc: ARGB8888,
74                ..Config::default()
75            },
76        )?;
77
78        let mut source = Self {
79            cursor,
80            buffer,
81            format: SourceFormat {
82                fourcc: ARGB8888,
83                // A dumb buffer is linear by construction.
84                modifier: 0,
85                width,
86                height,
87            },
88            hotspot: (0, 0),
89            started: None,
90            painted: None,
91            pending_damage: Vec::new(),
92        };
93        source.paint(0);
94        Ok(source)
95    }
96
97    /// Build a static cursor from raw premultiplied `0xAARRGGBB` pixels.
98    ///
99    /// The themeless fallback: what a caller uses when there is no cursor
100    /// theme on the system, which is most embedded images.
101    ///
102    /// # Errors
103    ///
104    /// [`SourceError::Failed`] with `EINVAL` if a dimension is zero or oversize
105    /// or the pixel count does not match the dimensions, or whatever the
106    /// allocation failed with.
107    pub fn from_argb(
108        device: &Device,
109        pixels: &[u32],
110        width: u32,
111        height: u32,
112        xhot: u32,
113        yhot: u32,
114    ) -> Result<Self, SourceError> {
115        let cursor = Cursor::from_argb(pixels, width, height, xhot, yhot)
116            .map_err(|_| SourceError::Failed(rustix::io::Errno::INVAL))?;
117        Self::new(device, cursor).map_err(|error| SourceError::Failed(errno(&error)))
118    }
119
120    /// Where the pointer actually points, in buffer pixels.
121    ///
122    /// The caller puts the layer's destination at `pointer - hotspot` so the
123    /// active point tracks the pointer rather than the sprite's corner.
124    #[must_use]
125    pub const fn hotspot(&self) -> (u32, u32) {
126        self.hotspot
127    }
128
129    /// The buffer's size, which is the largest frame's.
130    #[must_use]
131    pub const fn size(&self) -> (u32, u32) {
132        (self.format.width, self.format.height)
133    }
134
135    /// The cursor being shown.
136    #[must_use]
137    pub const fn cursor(&self) -> &Cursor {
138        &self.cursor
139    }
140
141    /// Paint frame `index` into the buffer, clearing what was there.
142    ///
143    /// The clear is what makes a smaller frame following a larger one correct:
144    /// without it the previous frame's edges stay lit around the new one.
145    fn paint(&mut self, index: usize) {
146        let frame = self
147            .cursor
148            .frames()
149            .get(index)
150            .unwrap_or_else(|| self.cursor.first());
151        let (xhot, yhot) = (frame.xhot, frame.yhot);
152        let pixels = frame.pixels.clone();
153        let (width, height) = (frame.width, frame.height);
154
155        blit(&mut self.buffer, &pixels, width, height, self.format.width);
156
157        self.hotspot = (xhot, yhot);
158        self.painted = Some(index);
159    }
160
161    /// Which frame the clock is on, and whether it differs from the painted one.
162    fn due(&mut self) -> Option<usize> {
163        if !self.cursor.is_animated() {
164            return None;
165        }
166        let started = *self.started.get_or_insert_with(Instant::now);
167        let index = self.cursor.frame_index_at(started.elapsed());
168        (self.painted != Some(index)).then_some(index)
169    }
170}
171
172/// Copy `pixels` into `buffer`, clearing the rest of it.
173fn blit(buffer: &mut Buffer, pixels: &[u32], width: u32, height: u32, buffer_width: u32) {
174    let mut mapping = buffer.map(MapAccess::Write);
175
176    // Every row, not just the frame's: a smaller frame following a larger one
177    // would otherwise keep the previous frame's edges lit around it.
178    for row in 0..mapping.height() {
179        let Some(bytes) = mapping.row_mut(row) else {
180            break;
181        };
182        bytes.fill(0);
183        if row >= height {
184            continue;
185        }
186        // `row_mut` addresses by the buffer's stride, which is what keeps this
187        // correct on a driver that pads its rows -- writing at `width * 4`
188        // shears the sprite on every one that does.
189        let columns = width.min(buffer_width) as usize;
190        let source = &pixels[row as usize * width as usize..][..columns];
191        for (column, pixel) in source.iter().enumerate() {
192            let at = column * 4;
193            bytes[at..at + 4].copy_from_slice(&pixel.to_le_bytes());
194        }
195    }
196}
197
198/// A dumb-buffer failure as an errno.
199///
200/// The kernel's own answer where there is one. Flattening these to a single
201/// value would turn a driver refusing a size into "out of memory", which sends
202/// whoever reads the log looking in the wrong place.
203pub(crate) fn errno(error: &DumbError) -> rustix::io::Errno {
204    match error {
205        DumbError::Io(errno) => *errno,
206        DumbError::InvalidConfig { .. } | DumbError::UnsupportedFormat { .. } => {
207            rustix::io::Errno::INVAL
208        }
209        DumbError::TooLarge { .. } => rustix::io::Errno::OVERFLOW,
210        _ => rustix::io::Errno::INVAL,
211    }
212}
213
214impl LayerBufferSource for CursorSource {
215    fn acquire(&mut self) -> Result<AcquiredBuffer, SourceError> {
216        if let Some(index) = self.due() {
217            self.paint(index);
218            // Full-frame: the paint cleared the buffer, so everything in it
219            // changed even where the new frame is transparent.
220            self.pending_damage.clear();
221        }
222
223        let fb_id = self
224            .buffer
225            .fb_id()
226            .ok_or(SourceError::Failed(rustix::io::Errno::INVAL))?;
227        Ok(AcquiredBuffer {
228            fb_id,
229            token: 0,
230            acquire_fence: None,
231            damage: std::mem::take(&mut self.pending_damage),
232        })
233    }
234
235    fn release(&mut self, _acquired: AcquiredBuffer) {
236        // One buffer, owned here and handed out again next acquire.
237    }
238
239    fn binding_model(&self) -> BindingModel {
240        BindingModel::SceneSubmitsFbId
241    }
242
243    fn format(&self) -> SourceFormat {
244        self.format
245    }
246
247    fn map(&mut self, access: MapAccess) -> Result<Mapping<'_>, SourceError> {
248        Ok(self.buffer.map(access))
249    }
250
251    fn on_session_paused(&mut self) {
252        // Nothing outstanding: acquire runs from the scene's commit path,
253        // which does not run while paused.
254    }
255
256    fn on_session_resumed(&mut self, device: &Device) -> Result<(), SourceError> {
257        let (width, height) = (self.format.width, self.format.height);
258        // The old descriptor is dead: drop the GEM handle and framebuffer id
259        // without issuing ioctls against it, since the kernel reclaimed both
260        // when the descriptor closed.
261        self.buffer.forget();
262        self.buffer = Buffer::create(
263            device,
264            &Config {
265                width,
266                height,
267                fourcc: self.format.fourcc,
268                ..Config::default()
269            },
270        )
271        .map_err(|error| SourceError::Failed(errno(&error)))?;
272
273        // The buffer is new and empty, so whatever was painted is gone.
274        let index = self.painted.unwrap_or(0);
275        self.painted = None;
276        self.paint(index);
277        Ok(())
278    }
279}