Skip to main content

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}