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}