drmkit_cursor/renderer.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! Putting a cursor on a plane and keeping it there.
5//!
6//! This is where the pieces meet: [`select_plane`] picks the plane,
7//! [`probe_size`] settles the buffer size, [`compose`](crate::compose) draws a frame into the
8//! buffer that is not on screen, [`PingPong`] says which that is, and [`stage`]
9//! writes the result onto the plane.
10//!
11//! The staging is deliberately separate from committing. A compositor wants
12//! the cursor in the same atomic request as the rest of its frame -- a cursor
13//! that lands a commit later than the window it belongs to is visibly wrong
14//! when either is moving. [`Renderer::commit`] exists for callers with nothing
15//! to coalesce with.
16
17use std::time::Duration;
18
19use drmkit_core::{AtomicCommitFlags, AtomicRequest, Device, ObjectType, PropertyStore};
20use drmkit_dumb::{Buffer, Config, MapAccess};
21use drmkit_planes::PlaneRegistry;
22
23use crate::CursorError;
24use crate::cursor::Cursor;
25use crate::pingpong::PingPong;
26use crate::plane::{PlanePath, SelectedPlane, select_plane};
27use crate::size::{preferred_size, probe_size};
28use crate::sprite::{self, Placement, Rotation};
29use crate::stage::{PlaneState, edge_shift, stage};
30
31/// How to set a renderer up.
32#[derive(Debug, Clone, Copy)]
33#[allow(
34 clippy::exhaustive_structs,
35 reason = "built by callers with `..Default::default()`, which a new field does not break"
36)]
37pub struct RendererConfig {
38 /// The CRTC the cursor belongs to, and its index in the resource list.
39 ///
40 /// Both, because the id is what gets written to `CRTC_ID` and the index is
41 /// what `possible_crtcs` is a bitmask over. Deriving one from the other
42 /// needs the resource list, which the caller already has.
43 pub crtc_id: u32,
44 /// As [`RendererConfig::crtc_id`].
45 pub crtc_index: u32,
46 /// Cursor size in pixels, or zero to take the driver's preference.
47 pub preferred_size: u32,
48 /// Use this plane rather than choosing one. Zero to choose.
49 pub forced_plane_id: u32,
50 /// Whether falling back to `drmModeSetCursor` is acceptable.
51 pub allow_legacy: bool,
52 /// Drive the CRTC's cursor plane through `drmModeSetCursor` and
53 /// `drmModeMoveCursor` rather than atomic commits.
54 ///
55 /// On an atomic driver the legacy cursor ioctls run as an async plane
56 /// update that does not wait for vblank, so a moving pointer does not
57 /// contend with the caller's scanout commits. The atomic path's blocking
58 /// one-plane commit per move does: on vc4 it cost the scanout two vblanks
59 /// in three while the pointer moved (drm-cxx `0925491`). Only when the
60 /// CRTC has a cursor plane that takes `ARGB8888`; needs `allow_legacy`;
61 /// ignored with `forced_plane_id`. See
62 /// [`Renderer::reserved_plane_id`].
63 pub prefer_legacy: bool,
64 /// Which way up the cursor is drawn.
65 pub rotation: Rotation,
66}
67
68impl Default for RendererConfig {
69 fn default() -> Self {
70 Self {
71 crtc_id: 0,
72 crtc_index: 0,
73 preferred_size: 0,
74 forced_plane_id: 0,
75 // A cursor that cannot be committed with the frame is still better
76 // than none, so the permissive default matches the reference.
77 allow_legacy: true,
78 prefer_legacy: false,
79 rotation: Rotation::None,
80 }
81 }
82}
83
84/// A cursor on a plane.
85///
86/// Borrows the device: the buffers and the framebuffers registered for them
87/// belong to it, and outliving it would leave this holding ids the kernel has
88/// recycled.
89#[derive(Debug)]
90pub struct Renderer<'a> {
91 device: &'a Device,
92 crtc_id: u32,
93 plane: SelectedPlane,
94 rotation_mask: u64,
95 props: PropertyStore,
96 buffers: [Buffer; 2],
97 which: PingPong,
98 /// The loaded cursor, and where its current frame landed in the buffer.
99 cursor: Option<Cursor>,
100 placement: Option<Placement>,
101 /// How far the drawn sprite was moved up and left, from
102 /// [`edge_shift`]. Subtracted from
103 /// `placement`'s hotspot wherever the hotspot is used.
104 shift: (u32, u32),
105 /// Which animation frame is in the buffer, so a tick that does not change
106 /// it does no work.
107 drawn_frame: Option<usize>,
108 /// Where the pointer is.
109 position: (i32, i32),
110 visible: bool,
111 /// Which way up the cursor is drawn.
112 rotation: Rotation,
113 /// The `zpos` the plane is written with, from [`cursor_zpos`]; `None`
114 /// where it has no settable one.
115 zpos: Option<u64>,
116 /// Legacy only: whether the cursor has been handed to the CRTC yet.
117 ///
118 /// The first commit installs it; later ones are cheap moves.
119 installed: bool,
120}
121
122impl Drop for Renderer<'_> {
123 /// Take the cursor off the CRTC, as the reference's destructor does, so
124 /// the next client does not inherit the sprite.
125 ///
126 /// Only once something was installed: a plane the kernel never saw this
127 /// use can refuse being cleared. On the legacy path, freeing the buffers
128 /// does not do it -- the kernel holds its own reference to the cursor's
129 /// framebuffer -- so without this the sprite stays on screen.
130 #[allow(
131 deprecated,
132 reason = "the legacy path installed the cursor with drmModeSetCursor, \
133 so that is what takes it off"
134 )]
135 fn drop(&mut self) {
136 use drm::control::Device as _;
137
138 if !self.installed {
139 return;
140 }
141 if self.plane.path == PlanePath::Legacy {
142 if let Some(crtc) = crtc_handle(self.crtc_id) {
143 let _ = self.device.set_cursor::<LegacyCursor>(crtc, None);
144 }
145 return;
146 }
147 let mut request = AtomicRequest::new();
148 let plane_id = self.plane.plane_id;
149 for name in ["FB_ID", "CRTC_ID"] {
150 if let Ok(property) = self.props.property_id(plane_id, name) {
151 let _ = request.add_property(plane_id, property, 0);
152 }
153 }
154 let _ = request.commit(self.device, AtomicCommitFlags::empty());
155 }
156}
157
158impl<'a> Renderer<'a> {
159 /// Set up a cursor on `config.crtc_id`.
160 ///
161 /// Chooses a plane, settles a buffer size by asking the kernel what it
162 /// will take, and allocates. No cursor is loaded yet.
163 ///
164 /// # Errors
165 ///
166 /// - [`CursorError::NoPlane`] or [`CursorError::PlaneUnusable`] from
167 /// [`select_plane`].
168 /// - [`CursorError::Unsupported`] on the legacy path, if `config.rotation`
169 /// is not [`Rotation::None`] or the CRTC refuses a cursor -- no cursor
170 /// hardware, so composite one instead.
171 /// - [`CursorError::Io`] if a buffer cannot be allocated or the plane's
172 /// properties cannot be read, or, on the legacy path, if the caller is
173 /// not DRM master.
174 pub fn create(
175 device: &'a Device,
176 registry: &PlaneRegistry,
177 config: &RendererConfig,
178 ) -> Result<Self, CursorError> {
179 let forced = (config.forced_plane_id != 0).then_some(config.forced_plane_id);
180 let bound_elsewhere = |plane_id: u32| plane_in_use(device, plane_id);
181 let plane = select_plane(
182 registry,
183 config.crtc_index,
184 forced,
185 config.allow_legacy,
186 config.prefer_legacy,
187 &bound_elsewhere,
188 )?;
189
190 if plane.path == PlanePath::Legacy {
191 return Self::create_legacy(device, config, plane);
192 }
193
194 let mut props = PropertyStore::new();
195 props
196 .cache_properties(device, plane.plane_id, ObjectType::Plane)
197 .map_err(|error| CursorError::Io(error.to_string()))?;
198
199 let rotation_mask = registry
200 .by_id(plane.plane_id)
201 .map_or(0, |caps| caps.rotation_bits);
202 let zpos = registry
203 .by_id(plane.plane_id)
204 .and_then(|caps| cursor_zpos(caps, registry.blend_stage_limit(config.crtc_index)));
205
206 // The driver's own preference, asked of the device.
207 //
208 // This used to read `plane.cursor_max_w`, which nothing ever fills:
209 // `PlaneRegistry::probe` sets it to zero on every plane, so the cap was
210 // always absent and every atomic path silently fell back to 64 -- a
211 // 64x64 cursor on a card advertising 512x512, four times coarser than
212 // the hardware offers, with a doc comment promising the opposite. The
213 // reference asks the device directly at this point, and so does this.
214 //
215 // `DRM_CAP_CURSOR_WIDTH` is a device capability rather than a plane
216 // one, which is why it cannot come from the registry. It is also an
217 // upper bound rather than a list of accepted sizes, so `probe_size`
218 // still walks the ladder down from it and asks the kernel.
219 let cap = cursor_cap(device);
220 let preferred = preferred_size(config.preferred_size, plane.path, cap);
221
222 let mut buffers: Option<[Buffer; 2]> = None;
223 let size = probe_size(preferred, |candidate| {
224 match allocate_pair(device, candidate, plane.fourcc) {
225 Ok(pair) => {
226 // The kernel is asked about the real thing: a request that
227 // arms this plane with this framebuffer at this size.
228 let accepted = pair[0].fb_id().is_some_and(|fb_id| {
229 test_plane(
230 device,
231 &props,
232 &plane,
233 config.crtc_id,
234 fb_id,
235 candidate,
236 zpos,
237 )
238 });
239 if accepted {
240 buffers = Some(pair);
241 }
242 accepted
243 }
244 Err(_) => false,
245 }
246 });
247
248 let buffers = match buffers {
249 Some(pair) => pair,
250 // Nothing was accepted, which is usually not about size at all --
251 // with no mode set every test fails for unrelated reasons. Take
252 // the fallback size and let the first real commit say what is
253 // wrong.
254 None => allocate_pair(device, size, plane.fourcc)?,
255 };
256
257 Ok(Self {
258 device,
259 crtc_id: config.crtc_id,
260 plane,
261 rotation_mask,
262 props,
263 buffers,
264 which: PingPong::new(),
265 cursor: None,
266 placement: None,
267 shift: (0, 0),
268 drawn_frame: None,
269 position: (0, 0),
270 visible: true,
271 rotation: config.rotation,
272 zpos,
273 installed: false,
274 })
275 }
276
277 /// Set up on the legacy `drmModeSetCursor` path.
278 ///
279 /// The call has no `TEST_ONLY`, so the probe is a cursor *disable*: a
280 /// no-op on any controller that has cursor hardware, and a refusal on one
281 /// that does not. i.MX's LCDIF and tilcdc have no cursor at all, and are
282 /// exactly the parts where plane selection fell through to here -- finding
283 /// out now lets the caller composite a cursor instead of getting a
284 /// renderer that dies on its first show.
285 ///
286 /// A permission failure is reported separately, because "not DRM master"
287 /// and "no cursor hardware" want different responses and look identical
288 /// from the errno alone.
289 #[allow(
290 deprecated,
291 reason = "drmModeSetCursor is deprecated in favor of a cursor plane, \
292 which is what every other path here uses. This is the \
293 fallback for controllers that have no such plane."
294 )]
295 fn create_legacy(
296 device: &'a Device,
297 config: &RendererConfig,
298 plane: SelectedPlane,
299 ) -> Result<Self, CursorError> {
300 use drm::control::Device as _;
301
302 if config.rotation != Rotation::None {
303 // drmModeSetCursor has nowhere to put a rotation, and pre-rotating
304 // in software would still leave the hotspot wrong for a caller who
305 // thinks the hardware did it.
306 return Err(CursorError::Unsupported);
307 }
308
309 let crtc = crtc_handle(config.crtc_id).ok_or(CursorError::Unsupported)?;
310 if let Err(error) = device.set_cursor::<LegacyCursor>(crtc, None) {
311 // EACCES and EPERM mean "not DRM master", which is a different
312 // thing to tell a caller than "this controller has no cursor".
313 return Err(match error.kind() {
314 std::io::ErrorKind::PermissionDenied => {
315 CursorError::Io("not DRM master".to_owned())
316 }
317 _ => CursorError::Unsupported,
318 });
319 }
320
321 // One buffer: the legacy ioctls take one handle and have no flip to
322 // alternate on.
323 let size = preferred_size(config.preferred_size, PlanePath::Legacy, None);
324 let buffers = allocate_pair(device, size, plane.fourcc)?;
325
326 Ok(Self {
327 device,
328 crtc_id: config.crtc_id,
329 plane,
330 rotation_mask: 0,
331 props: PropertyStore::new(),
332 buffers,
333 which: PingPong::single(),
334 cursor: None,
335 placement: None,
336 shift: (0, 0),
337 drawn_frame: None,
338 position: (0, 0),
339 visible: true,
340 rotation: Rotation::None,
341 zpos: None,
342 installed: false,
343 })
344 }
345
346 /// The plane this is drawing on.
347 #[must_use]
348 pub const fn plane(&self) -> SelectedPlane {
349 self.plane
350 }
351
352 /// The plane the cursor occupies, for a caller keeping its own commits
353 /// off it: the plane it draws on on the atomic paths; on the legacy path,
354 /// the CRTC's cursor plane the kernel drives when
355 /// [`RendererConfig::prefer_legacy`] chose the legacy ioctls over it, else
356 /// `0`.
357 #[must_use]
358 pub const fn reserved_plane_id(&self) -> u32 {
359 match self.plane.path {
360 PlanePath::Legacy => self.plane.legacy_plane_id,
361 PlanePath::AtomicCursor | PlanePath::AtomicOverlay => self.plane.plane_id,
362 }
363 }
364
365 /// The size of the cursor buffer, in pixels.
366 #[must_use]
367 pub const fn size(&self) -> (u32, u32) {
368 (self.buffers[0].width(), self.buffers[0].height())
369 }
370
371 /// Whether the plane can turn the sprite itself.
372 ///
373 /// When it can, the pixels are drawn upright and the hardware does the
374 /// turn. When it cannot -- i915's cursor planes expose the property and
375 /// list only 0 and 180 -- the pixels are turned as they are drawn, and
376 /// the hardware is asked for none. Doing both turns it twice.
377 const fn rotates_in_hardware(&self) -> bool {
378 self.rotation_mask & self.rotation.drm_mask() != 0
379 }
380
381 /// How the sprite is turned as it is drawn.
382 const fn draw_rotation(&self) -> Rotation {
383 if self.rotates_in_hardware() {
384 Rotation::None
385 } else {
386 self.rotation
387 }
388 }
389
390 /// Whether the hardware turns the buffer as it scans it out.
391 ///
392 /// The edge shift is not drawn then: the buffer's top-left is not the
393 /// screen's, so moving the pixels up and left would move them the wrong
394 /// way. Such a cursor keeps its negative position at the edge.
395 const fn hardware_turns_the_sprite(&self) -> bool {
396 self.rotates_in_hardware() && !matches!(self.rotation, Rotation::None)
397 }
398
399 /// The hotspot in the buffer as drawn, after any edge shift.
400 fn drawn_hotspot(&self) -> Option<(u32, u32)> {
401 self.placement.map(|placed| {
402 (
403 placed.hotspot_x.saturating_sub(self.shift.0),
404 placed.hotspot_y.saturating_sub(self.shift.1),
405 )
406 })
407 }
408
409 /// Change which way up the cursor is, redrawing it.
410 ///
411 /// # Errors
412 ///
413 /// - [`CursorError::Unsupported`] for any rotation but
414 /// [`Rotation::None`] on [`PlanePath::Legacy`], leaving the cursor as it
415 /// was. `drmModeSetCursor` has nowhere to put one, as at
416 /// [`Renderer::create`], and the reference refuses here too.
417 /// - [`CursorError::Io`] if the buffer cannot be mapped.
418 pub fn set_rotation(&mut self, rotation: Rotation) -> Result<(), CursorError> {
419 if rotation != Rotation::None && self.plane.path == PlanePath::Legacy {
420 return Err(CursorError::Unsupported);
421 }
422 self.rotation = rotation;
423 match self.drawn_frame {
424 Some(index) => self.draw(index),
425 None => Ok(()),
426 }
427 }
428
429 /// Load a cursor and draw its first frame.
430 ///
431 /// # Errors
432 ///
433 /// [`CursorError::Io`] if the buffer cannot be mapped.
434 pub fn set_cursor(&mut self, cursor: Cursor) -> Result<(), CursorError> {
435 self.cursor = Some(cursor);
436 self.drawn_frame = None;
437 // The next commit installs again, as the reference's does: on the
438 // legacy path a driver may upload the sprite only when handed a
439 // buffer, and a move alone would leave the old shape showing.
440 self.installed = false;
441 self.draw(0)
442 }
443
444 /// Move the pointer.
445 pub const fn move_to(&mut self, x: i32, y: i32) {
446 self.position = (x, y);
447 }
448
449 /// Show or hide the cursor without unloading it.
450 pub const fn set_visible(&mut self, visible: bool) {
451 self.visible = visible;
452 }
453
454 /// Bring the buffer up to date with the pointer's position.
455 ///
456 /// Near the top or left edge of the CRTC the sprite is drawn moved up and
457 /// left inside its buffer, so the plane starts at the edge rather than
458 /// off it: some drivers refuse a negative position (drm-cxx#384). A move
459 /// into or out of that band, or within it, redraws the current frame.
460 /// [`Renderer::commit`] calls this; a caller coalescing through
461 /// [`Renderer::stage_into`] calls it first. Skipped, the commit carries
462 /// the negative position, as before.
463 ///
464 /// Returns whether the buffer changed.
465 ///
466 /// # Errors
467 ///
468 /// [`CursorError::Io`] if the buffer cannot be mapped.
469 pub fn prepare(&mut self) -> Result<bool, CursorError> {
470 let (Some(placement), Some(index)) = (self.placement, self.drawn_frame) else {
471 return Ok(false);
472 };
473 let wanted = if self.hardware_turns_the_sprite() {
474 (0, 0)
475 } else {
476 edge_shift(self.position, (placement.hotspot_x, placement.hotspot_y))
477 };
478 if wanted == self.shift {
479 return Ok(false);
480 }
481 self.draw(index)?;
482 // The legacy ioctls may upload the sprite only when handed a buffer,
483 // as for a new shape.
484 if self.plane.path == PlanePath::Legacy {
485 self.installed = false;
486 }
487 Ok(true)
488 }
489
490 /// Advance an animation, drawing a new frame if one is due.
491 ///
492 /// Returns whether the buffer changed, so a caller can skip a commit that
493 /// would publish the same pixels. A static cursor never changes.
494 ///
495 /// # Errors
496 ///
497 /// [`CursorError::Io`] if the buffer cannot be mapped.
498 pub fn tick(&mut self, elapsed: Duration) -> Result<bool, CursorError> {
499 let Some(cursor) = self.cursor.as_ref() else {
500 return Ok(false);
501 };
502 if !cursor.is_animated() {
503 return Ok(false);
504 }
505 let wanted = cursor.frame_index_at(elapsed);
506 if self.drawn_frame == Some(wanted) {
507 return Ok(false);
508 }
509 self.draw(wanted)?;
510 Ok(true)
511 }
512
513 /// Write the cursor's current state into `request`.
514 ///
515 /// For coalescing with the rest of a frame. The caller calls
516 /// [`Renderer::prepare`] first, commits, and calls
517 /// [`Renderer::published`] afterwards.
518 ///
519 /// # Errors
520 ///
521 /// [`CursorError::MissingProperty`] if the plane lacks a required
522 /// property -- always, on [`PlanePath::Legacy`],
523 /// which has no plane to stage: use [`Renderer::commit`] there.
524 pub fn stage_into(&self, request: &mut AtomicRequest) -> Result<usize, CursorError> {
525 let buffer = &self.buffers[self.which.commit_target()];
526 let fb_id = if self.visible {
527 buffer.fb_id().unwrap_or(0)
528 } else {
529 // Disarming the plane is how a cursor hides: there is no property
530 // for "invisible", and a zero-alpha buffer still costs bandwidth.
531 0
532 };
533 let state = PlaneState {
534 plane_id: self.plane.plane_id,
535 crtc_id: if self.visible { self.crtc_id } else { 0 },
536 fb_id,
537 width: buffer.width(),
538 height: buffer.height(),
539 pointer_x: self.position.0,
540 pointer_y: self.position.1,
541 hotspot: self.drawn_hotspot(),
542 // What the hardware is asked for. When it cannot do the turn,
543 // `draw` already did it and this must ask for none.
544 rotation: if self.rotates_in_hardware() {
545 self.rotation
546 } else {
547 Rotation::None
548 },
549 rotation_mask: self.rotation_mask,
550 };
551 let mut written = stage(request, &self.props, &state)?;
552 written += stage_zpos(request, &self.props, self.plane.plane_id, self.zpos)?;
553 Ok(written)
554 }
555
556 /// Note that a commit carrying [`Renderer::stage_into`]'s writes landed.
557 ///
558 /// See [`PingPong::published`] for what happens when it did not.
559 pub const fn published(&mut self) {
560 self.which.published();
561 }
562
563 /// Stage and commit on their own.
564 ///
565 /// For callers with nothing to coalesce with. A compositor should prefer
566 /// [`Renderer::stage_into`].
567 ///
568 /// # Errors
569 ///
570 /// [`CursorError::MissingProperty`] from staging, or [`CursorError::Io`]
571 /// if the kernel refuses the commit.
572 pub fn commit(&mut self) -> Result<(), CursorError> {
573 self.prepare()?;
574 if self.plane.path == PlanePath::Legacy {
575 return self.commit_legacy();
576 }
577 let mut request = AtomicRequest::new();
578 self.stage_into(&mut request)?;
579 // Enabling a plane is a modeset; moving one already enabled is not.
580 // Blocking either way: a non-blocking cursor commit contends with
581 // vblank pacing and returns EBUSY as soon as the mouse outpaces the
582 // refresh rate.
583 let flags = if self.installed {
584 AtomicCommitFlags::empty()
585 } else {
586 AtomicCommitFlags::ALLOW_MODESET
587 };
588 request
589 .commit(self.device, flags)
590 .map_err(|error| CursorError::Io(error.to_string()))?;
591 self.installed = true;
592 self.published();
593 Ok(())
594 }
595
596 /// Install or move the cursor through `drmModeSetCursor`.
597 #[allow(
598 deprecated,
599 reason = "see create_legacy: this path exists for hardware with no \
600 cursor plane to use instead"
601 )]
602 fn commit_legacy(&mut self) -> Result<(), CursorError> {
603 use drm::control::Device as _;
604
605 let crtc = crtc_handle(self.crtc_id).ok_or(CursorError::Unsupported)?;
606 let hotspot = self.drawn_hotspot().unwrap_or((0, 0));
607
608 if !self.visible {
609 self.device
610 .set_cursor::<LegacyCursor>(crtc, None)
611 .map_err(|error| CursorError::Io(error.to_string()))?;
612 self.installed = false;
613 return Ok(());
614 }
615
616 if !self.installed {
617 let buffer = LegacyCursor {
618 handle: self.buffers[0]
619 .gem_handle()
620 .ok_or_else(|| CursorError::Io("cursor buffer has no handle".to_owned()))?,
621 width: self.buffers[0].width(),
622 height: self.buffers[0].height(),
623 pitch: self.buffers[0].stride(),
624 };
625 self.device
626 .set_cursor(crtc, Some(&buffer))
627 .map_err(|error| CursorError::Io(error.to_string()))?;
628 self.installed = true;
629 }
630
631 let (x, y) = crate::stage::plane_origin(self.position, hotspot);
632 self.device
633 .move_cursor(crtc, (x, y))
634 .map_err(|error| CursorError::Io(error.to_string()))?;
635 Ok(())
636 }
637
638 /// Give up the device for a session pause.
639 ///
640 /// See [`Paused`] for what is released and what cannot be.
641 #[must_use]
642 pub fn pause(mut self) -> Paused {
643 let size = self.buffers[0].width();
644 for buffer in &mut self.buffers {
645 buffer.forget();
646 }
647 // The descriptor is going, so dropping must not try to hide the
648 // cursor through it, as the reference skips that while paused.
649 self.installed = false;
650 Paused {
651 crtc_id: self.crtc_id,
652 plane: self.plane,
653 rotation_mask: self.rotation_mask,
654 rotation: self.rotation,
655 zpos: self.zpos,
656 cursor: self.cursor.take(),
657 drawn_frame: self.drawn_frame,
658 position: self.position,
659 visible: self.visible,
660 size,
661 }
662 }
663
664 /// Draw frame `index` into the buffer that is not on screen.
665 fn draw(&mut self, index: usize) -> Result<(), CursorError> {
666 let Some(cursor) = self.cursor.as_ref() else {
667 return Ok(());
668 };
669 let frame = cursor
670 .frames()
671 .get(index)
672 .unwrap_or_else(|| cursor.first())
673 .clone();
674
675 // Read what depends on `self` before the buffer is borrowed mutably.
676 let rotation = self.draw_rotation();
677 let hardware_turned = self.hardware_turns_the_sprite();
678 let position = self.position;
679 let fourcc = self.plane.fourcc;
680 let target = self.which.draw_target();
681 let buffer = &mut self.buffers[target];
682 let (width, height) = (buffer.width(), buffer.height());
683 let stride_px = (buffer.stride() / 4) as usize;
684
685 let mut scratch = vec![0u32; height as usize * stride_px];
686 let placement = sprite::compose(&frame, rotation, &mut scratch, width, height, stride_px);
687 let shift = if hardware_turned {
688 (0, 0)
689 } else {
690 edge_shift(position, (placement.hotspot_x, placement.hotspot_y))
691 };
692 sprite::shift_up_left(&mut scratch, width, height, stride_px, shift);
693
694 {
695 let mut mapping = buffer.map(MapAccess::Write);
696 for y in 0..height {
697 let row = mapping
698 .row_mut(y)
699 .ok_or_else(|| CursorError::Io("cursor buffer row out of range".to_owned()))?;
700 let from = y as usize * stride_px;
701 for (x, pixel) in scratch[from..from + width as usize].iter().enumerate() {
702 let at = x * 4;
703 row[at..at + 4].copy_from_slice(&pack(*pixel, fourcc).to_ne_bytes());
704 }
705 }
706 }
707
708 self.placement = Some(placement);
709 self.shift = shift;
710 self.drawn_frame = Some(index);
711 self.which.mark_drawn();
712 Ok(())
713 }
714}
715
716/// The largest cursor the driver admits to, or `None` if it will not say.
717///
718/// Both dimensions, because a driver free to differ on them would otherwise
719/// have the width silently applied to the height. The smaller wins: the buffer
720/// is square, and asking for a rectangle the card cannot take is how a probe
721/// that should have succeeded ends up walking all the way down the ladder.
722fn cursor_cap(device: &Device) -> Option<u32> {
723 let cap = |which| {
724 drm::Device::get_driver_capability(device, which)
725 .ok()
726 .and_then(|value| u32::try_from(value).ok())
727 .filter(|value| *value != 0)
728 };
729 let width = cap(drm::DriverCapability::CursorWidth)?;
730 let height = cap(drm::DriverCapability::CursorHeight)?;
731 Some(width.min(height))
732}
733
734/// Permute a shadow pixel into the plane's channel order.
735///
736/// The shadow is `0xAARRGGBB` in a host word, which is `ARGB8888` in memory on
737/// a little-endian host. The other three cursor formats are the same eight-bit
738/// channels in a different order, so each is a permutation of that word rather
739/// than a conversion: no precision is lost and no channel is invented.
740///
741/// A format not in [`CURSOR_FORMATS`](crate::CURSOR_FORMATS) cannot be selected, so the fallthrough
742/// is unreachable by construction; it returns the pixel unchanged rather than
743/// panicking, because a wrongly ordered cursor is a visible bug and a dead
744/// process is a black screen.
745pub(crate) const fn pack(pixel: u32, fourcc: u32) -> u32 {
746 let (a, r, g, b) = (
747 pixel >> 24,
748 (pixel >> 16) & 0xff,
749 (pixel >> 8) & 0xff,
750 pixel & 0xff,
751 );
752 match fourcc {
753 drmkit_fmt::fourcc::RGBA8888 => (r << 24) | (g << 16) | (b << 8) | a,
754 drmkit_fmt::fourcc::ABGR8888 => (a << 24) | (b << 16) | (g << 8) | r,
755 drmkit_fmt::fourcc::BGRA8888 => (b << 24) | (g << 16) | (r << 8) | a,
756 // `ARGB8888`, and anything that never reaches selection.
757 _ => pixel,
758 }
759}
760
761/// A dumb buffer, described the way `drmModeSetCursor` wants it.
762struct LegacyCursor {
763 handle: u32,
764 width: u32,
765 height: u32,
766 pitch: u32,
767}
768
769impl drm::buffer::Buffer for LegacyCursor {
770 fn size(&self) -> (u32, u32) {
771 (self.width, self.height)
772 }
773 fn format(&self) -> drm::buffer::DrmFourcc {
774 drm::buffer::DrmFourcc::Argb8888
775 }
776 fn pitch(&self) -> u32 {
777 self.pitch
778 }
779 fn handle(&self) -> drm::buffer::Handle {
780 // The kernel is about to be handed this, so a zero would be a bug
781 // upstream of here rather than something to paper over.
782 drm::buffer::Handle::from(
783 core::num::NonZeroU32::new(self.handle)
784 .unwrap_or(core::num::NonZeroU32::new(1).expect("one is not zero")),
785 )
786 }
787}
788
789/// A CRTC id as a handle.
790fn crtc_handle(crtc_id: u32) -> Option<drm::control::crtc::Handle> {
791 core::num::NonZeroU32::new(crtc_id).map(drm::control::crtc::Handle::from)
792}
793
794/// Allocate a pair of cursor buffers with framebuffers registered.
795fn allocate_pair(device: &Device, size: u32, fourcc: u32) -> Result<[Buffer; 2], CursorError> {
796 let config = Config {
797 width: size,
798 height: size,
799 fourcc,
800 bpp: 32,
801 add_fb: true,
802 };
803 let first = Buffer::create(device, &config).map_err(|e| CursorError::Io(e.to_string()))?;
804 let second = Buffer::create(device, &config).map_err(|e| CursorError::Io(e.to_string()))?;
805 Ok([first, second])
806}
807
808/// Ask the kernel whether this plane will take a buffer of this size.
809fn test_plane(
810 device: &Device,
811 props: &PropertyStore,
812 plane: &SelectedPlane,
813 crtc_id: u32,
814 fb_id: u32,
815 size: u32,
816 zpos: Option<u64>,
817) -> bool {
818 let mut request = AtomicRequest::new();
819 let state = PlaneState {
820 plane_id: plane.plane_id,
821 crtc_id,
822 fb_id,
823 width: size,
824 height: size,
825 pointer_x: 0,
826 pointer_y: 0,
827 // Nothing has been drawn, so there is no hotspot to state yet.
828 hotspot: None,
829 rotation: Rotation::None,
830 rotation_mask: 0,
831 };
832 if stage(&mut request, props, &state).is_err()
833 || stage_zpos(&mut request, props, plane.plane_id, zpos).is_err()
834 {
835 return false;
836 }
837 request.test(device, AtomicCommitFlags::empty()).is_ok()
838}
839
840/// Where the cursor's plane stacks: the top of its `zpos` range, as the
841/// reference puts it, and no higher than the CRTC can blend.
842///
843/// Without it the plane keeps whatever `zpos` it had, which on a driver whose
844/// overlays all start at 0 is the primary's own stage: the SA8155P refuses
845/// two planes on one stage that overlap ("invalid coordinates, stage:0"), so
846/// the cursor never committed there. Its CRTC blends nine planes, zpos 0 to
847/// 8, while every plane advertises up to 10, hence the cap.
848///
849/// `None` where the plane has no `zpos` or a fixed one: there is nothing to
850/// write.
851pub(crate) fn cursor_zpos(
852 plane: &drmkit_planes::PlaneCapabilities,
853 blend_limit: Option<usize>,
854) -> Option<u64> {
855 let (Some(min), Some(max)) = (plane.zpos_min, plane.zpos_max) else {
856 return None;
857 };
858 if drmkit_planes::zpos_fixed(plane) {
859 return None;
860 }
861 let top = blend_limit
862 .and_then(|limit| u64::try_from(limit).ok())
863 .map_or(max, |limit| max.min(limit.saturating_sub(1)));
864 Some(top.max(min))
865}
866
867/// Write `zpos` to the plane, when there is one to write.
868fn stage_zpos(
869 request: &mut AtomicRequest,
870 props: &PropertyStore,
871 plane_id: u32,
872 zpos: Option<u64>,
873) -> Result<usize, CursorError> {
874 let Some(zpos) = zpos else {
875 return Ok(0);
876 };
877 let id = props
878 .property_id(plane_id, "zpos")
879 .map_err(|_| CursorError::MissingProperty)?;
880 request
881 .add_property(plane_id, id, zpos)
882 .map_err(|_| CursorError::MissingProperty)?;
883 Ok(1)
884}
885
886/// Whether a plane is driving a CRTC at all -- this one included.
887///
888/// As the reference decides it: an overlay this CRTC is already showing is
889/// the compositor's layer, and taking it for the cursor costs that layer just
890/// as surely as taking one from another output would. Its comment says
891/// "another CRTC"; its code tests for any, which is the better reading.
892fn plane_in_use(device: &Device, plane_id: u32) -> bool {
893 use drm::control::Device as _;
894
895 let Some(handle) = core::num::NonZeroU32::new(plane_id) else {
896 return false;
897 };
898 device
899 .get_plane(drm::control::plane::Handle::from(handle))
900 .ok()
901 .and_then(|info| info.crtc())
902 .is_some()
903}
904
905/// A cursor renderer whose device has gone away.
906///
907/// A session pause revokes the descriptor: the kernel state behind it is gone,
908/// so the buffers cannot be destroyed and the framebuffers cannot be removed
909/// -- those ioctls would go to a dead descriptor, or worse to whatever the
910/// number has since been reused for.
911///
912/// The mappings are a different matter and are released here. Closing a
913/// descriptor does not unmap anything -- a VMA holds its own reference to the
914/// file -- so a compositor that skipped this would leak one mapping per VT
915/// switch, quietly, for as long as it ran.
916///
917/// This exists as a type rather than a flag so that a paused renderer cannot
918/// be committed. There is nothing to commit to.
919#[derive(Debug)]
920pub struct Paused {
921 crtc_id: u32,
922 plane: SelectedPlane,
923 rotation_mask: u64,
924 rotation: Rotation,
925 zpos: Option<u64>,
926 cursor: Option<Cursor>,
927 drawn_frame: Option<usize>,
928 position: (i32, i32),
929 visible: bool,
930 size: u32,
931}
932
933impl Paused {
934 /// Rebuild on a new descriptor.
935 ///
936 /// The same plane, the same size, and the cursor redrawn at whatever frame
937 /// it had reached -- so the first commit after a resume carries the right
938 /// pixels rather than an empty buffer or a stale one.
939 ///
940 /// The size is not re-probed. The hardware has not changed, and a resume
941 /// is when a compositor is trying to get something on screen quickly
942 /// rather than spend test commits rediscovering what it already knew.
943 ///
944 /// # Errors
945 ///
946 /// [`CursorError::Io`] if the buffers cannot be reallocated or the plane's
947 /// properties cannot be read on the new descriptor.
948 pub fn resume(self, device: &Device) -> Result<Renderer<'_>, CursorError> {
949 let mut props = PropertyStore::new();
950 if self.plane.path != PlanePath::Legacy {
951 props
952 .cache_properties(device, self.plane.plane_id, ObjectType::Plane)
953 .map_err(|error| CursorError::Io(error.to_string()))?;
954 }
955
956 let mut renderer = Renderer {
957 device,
958 crtc_id: self.crtc_id,
959 plane: self.plane,
960 rotation_mask: self.rotation_mask,
961 props,
962 buffers: allocate_pair(device, self.size, self.plane.fourcc)?,
963 which: if self.plane.path == PlanePath::Legacy {
964 PingPong::single()
965 } else {
966 PingPong::new()
967 },
968 cursor: self.cursor,
969 placement: None,
970 shift: (0, 0),
971 drawn_frame: None,
972 position: self.position,
973 visible: self.visible,
974 rotation: self.rotation,
975 zpos: self.zpos,
976 // The new descriptor knows nothing of the old plane state, so the
977 // next commit re-arms everything.
978 installed: false,
979 };
980
981 if renderer.cursor.is_some() {
982 renderer.draw(self.drawn_frame.unwrap_or(0))?;
983 }
984 Ok(renderer)
985 }
986
987 /// Whether a cursor is still loaded, and will be redrawn on resume.
988 #[must_use]
989 pub const fn has_cursor(&self) -> bool {
990 self.cursor.is_some()
991 }
992}