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`] 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, 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    /// Which animation frame is in the buffer, so a tick that does not change
102    /// it does no work.
103    drawn_frame: Option<usize>,
104    /// Where the pointer is.
105    position: (i32, i32),
106    visible: bool,
107    /// Which way up the cursor is drawn.
108    rotation: Rotation,
109    /// Legacy only: whether the cursor has been handed to the CRTC yet.
110    ///
111    /// The first commit installs it; later ones are cheap moves.
112    installed: bool,
113}
114
115impl<'a> Renderer<'a> {
116    /// Set up a cursor on `config.crtc_id`.
117    ///
118    /// Chooses a plane, settles a buffer size by asking the kernel what it
119    /// will take, and allocates. No cursor is loaded yet.
120    ///
121    /// # Errors
122    ///
123    /// - [`CursorError::NoPlane`] or [`CursorError::PlaneUnusable`] from
124    ///   [`select_plane`].
125    /// - [`CursorError::Unsupported`] if the only option is the legacy path,
126    ///   which is not implemented here yet.
127    /// - [`CursorError::Io`] if a buffer cannot be allocated or the plane's
128    ///   properties cannot be read.
129    pub fn create(
130        device: &'a Device,
131        registry: &PlaneRegistry,
132        config: &RendererConfig,
133    ) -> Result<Self, CursorError> {
134        let forced = (config.forced_plane_id != 0).then_some(config.forced_plane_id);
135        let bound_elsewhere =
136            |plane_id: u32| plane_bound_elsewhere(device, plane_id, config.crtc_id);
137        let plane = select_plane(
138            registry,
139            config.crtc_index,
140            forced,
141            config.allow_legacy,
142            config.prefer_legacy,
143            &bound_elsewhere,
144        )?;
145
146        if plane.path == PlanePath::Legacy {
147            return Self::create_legacy(device, config, plane);
148        }
149
150        let mut props = PropertyStore::new();
151        props
152            .cache_properties(device, plane.plane_id, ObjectType::Plane)
153            .map_err(|error| CursorError::Io(error.to_string()))?;
154
155        let rotation_mask = registry
156            .by_id(plane.plane_id)
157            .map_or(0, |caps| caps.rotation_bits);
158
159        // The driver's own preference, asked of the device.
160        //
161        // This used to read `plane.cursor_max_w`, which nothing ever fills:
162        // `PlaneRegistry::probe` sets it to zero on every plane, so the cap was
163        // always absent and every atomic path silently fell back to 64 -- a
164        // 64x64 cursor on a card advertising 512x512, four times coarser than
165        // the hardware offers, with a doc comment promising the opposite. The
166        // reference asks the device directly at this point, and so does this.
167        //
168        // `DRM_CAP_CURSOR_WIDTH` is a device capability rather than a plane
169        // one, which is why it cannot come from the registry. It is also an
170        // upper bound rather than a list of accepted sizes, so `probe_size`
171        // still walks the ladder down from it and asks the kernel.
172        let cap = cursor_cap(device);
173        let preferred = preferred_size(config.preferred_size, plane.path, cap);
174
175        let mut buffers: Option<[Buffer; 2]> = None;
176        let size = probe_size(preferred, |candidate| {
177            match allocate_pair(device, candidate, plane.fourcc) {
178                Ok(pair) => {
179                    // The kernel is asked about the real thing: a request that
180                    // arms this plane with this framebuffer at this size.
181                    let accepted = pair[0].fb_id().is_some_and(|fb_id| {
182                        test_plane(device, &props, &plane, config.crtc_id, fb_id, candidate)
183                    });
184                    if accepted {
185                        buffers = Some(pair);
186                    }
187                    accepted
188                }
189                Err(_) => false,
190            }
191        });
192
193        let buffers = match buffers {
194            Some(pair) => pair,
195            // Nothing was accepted, which is usually not about size at all --
196            // with no mode set every test fails for unrelated reasons. Take
197            // the fallback size and let the first real commit say what is
198            // wrong.
199            None => allocate_pair(device, size, plane.fourcc)?,
200        };
201
202        Ok(Self {
203            device,
204            crtc_id: config.crtc_id,
205            plane,
206            rotation_mask,
207            props,
208            buffers,
209            which: PingPong::new(),
210            cursor: None,
211            placement: None,
212            drawn_frame: None,
213            position: (0, 0),
214            visible: true,
215            rotation: config.rotation,
216            installed: false,
217        })
218    }
219
220    /// Set up on the legacy `drmModeSetCursor` path.
221    ///
222    /// The call has no `TEST_ONLY`, so the probe is a cursor *disable*: a
223    /// no-op on any controller that has cursor hardware, and a refusal on one
224    /// that does not. i.MX's LCDIF and tilcdc have no cursor at all, and are
225    /// exactly the parts where plane selection fell through to here -- finding
226    /// out now lets the caller composite a cursor instead of getting a
227    /// renderer that dies on its first show.
228    ///
229    /// A permission failure is reported separately, because "not DRM master"
230    /// and "no cursor hardware" want different responses and look identical
231    /// from the errno alone.
232    #[allow(
233        deprecated,
234        reason = "drmModeSetCursor is deprecated in favour of a cursor plane, \
235                  which is what every other path here uses. This is the \
236                  fallback for controllers that have no such plane."
237    )]
238    fn create_legacy(
239        device: &'a Device,
240        config: &RendererConfig,
241        plane: SelectedPlane,
242    ) -> Result<Self, CursorError> {
243        use drm::control::Device as _;
244
245        if config.rotation != Rotation::None {
246            // drmModeSetCursor has nowhere to put a rotation, and pre-rotating
247            // in software would still leave the hotspot wrong for a caller who
248            // thinks the hardware did it.
249            return Err(CursorError::Unsupported);
250        }
251
252        let crtc = crtc_handle(config.crtc_id).ok_or(CursorError::Unsupported)?;
253        if let Err(error) = device.set_cursor::<LegacyCursor>(crtc, None) {
254            // EACCES and EPERM mean "not DRM master", which is a different
255            // thing to tell a caller than "this controller has no cursor".
256            return Err(match error.kind() {
257                std::io::ErrorKind::PermissionDenied => {
258                    CursorError::Io("not DRM master".to_owned())
259                }
260                _ => CursorError::Unsupported,
261            });
262        }
263
264        // One buffer: the call uploads on install and would need reinstalling
265        // for a content change anyway.
266        let size = preferred_size(config.preferred_size, PlanePath::Legacy, None);
267        let buffers = allocate_pair(device, size, plane.fourcc)?;
268
269        Ok(Self {
270            device,
271            crtc_id: config.crtc_id,
272            plane,
273            rotation_mask: 0,
274            props: PropertyStore::new(),
275            buffers,
276            which: PingPong::single(),
277            cursor: None,
278            placement: None,
279            drawn_frame: None,
280            position: (0, 0),
281            visible: true,
282            rotation: Rotation::None,
283            installed: false,
284        })
285    }
286
287    /// The plane this is drawing on.
288    #[must_use]
289    pub const fn plane(&self) -> SelectedPlane {
290        self.plane
291    }
292
293    /// The plane the cursor occupies, for a caller keeping its own commits
294    /// off it: the plane it draws on on the atomic paths; on the legacy path,
295    /// the CRTC's cursor plane the kernel drives when
296    /// [`RendererConfig::prefer_legacy`] chose the legacy ioctls over it, else
297    /// `0`.
298    #[must_use]
299    pub const fn reserved_plane_id(&self) -> u32 {
300        match self.plane.path {
301            PlanePath::Legacy => self.plane.legacy_plane_id,
302            PlanePath::AtomicCursor | PlanePath::AtomicOverlay => self.plane.plane_id,
303        }
304    }
305
306    /// The size of the cursor buffer, in pixels.
307    #[must_use]
308    pub const fn size(&self) -> (u32, u32) {
309        (self.buffers[0].width(), self.buffers[0].height())
310    }
311
312    /// Whether the plane can turn the sprite itself.
313    ///
314    /// When it can, the pixels are drawn upright and the hardware does the
315    /// turn. When it cannot -- i915's cursor planes expose the property and
316    /// list only 0 and 180 -- the pixels are turned as they are drawn, and
317    /// the hardware is asked for none. Doing both turns it twice.
318    const fn rotates_in_hardware(&self) -> bool {
319        self.rotation_mask & self.rotation.drm_mask() != 0
320    }
321
322    /// How the sprite is turned as it is drawn.
323    const fn draw_rotation(&self) -> Rotation {
324        if self.rotates_in_hardware() {
325            Rotation::None
326        } else {
327            self.rotation
328        }
329    }
330
331    /// Change which way up the cursor is, redrawing it.
332    ///
333    /// # Errors
334    ///
335    /// [`CursorError::Io`] if the buffer cannot be mapped.
336    pub fn set_rotation(&mut self, rotation: Rotation) -> Result<(), CursorError> {
337        self.rotation = rotation;
338        match self.drawn_frame {
339            Some(index) => self.draw(index),
340            None => Ok(()),
341        }
342    }
343
344    /// Load a cursor and draw its first frame.
345    ///
346    /// # Errors
347    ///
348    /// [`CursorError::Io`] if the buffer cannot be mapped.
349    pub fn set_cursor(&mut self, cursor: Cursor) -> Result<(), CursorError> {
350        self.cursor = Some(cursor);
351        self.drawn_frame = None;
352        self.draw(0)
353    }
354
355    /// Move the pointer.
356    pub const fn move_to(&mut self, x: i32, y: i32) {
357        self.position = (x, y);
358    }
359
360    /// Show or hide the cursor without unloading it.
361    pub const fn set_visible(&mut self, visible: bool) {
362        self.visible = visible;
363    }
364
365    /// Advance an animation, drawing a new frame if one is due.
366    ///
367    /// Returns whether the buffer changed, so a caller can skip a commit that
368    /// would publish the same pixels. A static cursor never changes.
369    ///
370    /// # Errors
371    ///
372    /// [`CursorError::Io`] if the buffer cannot be mapped.
373    pub fn tick(&mut self, elapsed: Duration) -> Result<bool, CursorError> {
374        let Some(cursor) = self.cursor.as_ref() else {
375            return Ok(false);
376        };
377        if !cursor.is_animated() {
378            return Ok(false);
379        }
380        let wanted = cursor.frame_index_at(elapsed);
381        if self.drawn_frame == Some(wanted) {
382            return Ok(false);
383        }
384        self.draw(wanted)?;
385        Ok(true)
386    }
387
388    /// Write the cursor's current state into `request`.
389    ///
390    /// For coalescing with the rest of a frame. The caller commits, and calls
391    /// [`Renderer::published`] afterwards.
392    ///
393    /// # Errors
394    ///
395    /// [`CursorError::MissingProperty`] if the plane lacks a required
396    /// property.
397    pub fn stage_into(&self, request: &mut AtomicRequest) -> Result<usize, CursorError> {
398        let buffer = &self.buffers[self.which.commit_target()];
399        let fb_id = if self.visible {
400            buffer.fb_id().unwrap_or(0)
401        } else {
402            // Disarming the plane is how a cursor hides: there is no property
403            // for "invisible", and a zero-alpha buffer still costs bandwidth.
404            0
405        };
406        let state = PlaneState {
407            plane_id: self.plane.plane_id,
408            crtc_id: if self.visible { self.crtc_id } else { 0 },
409            fb_id,
410            width: buffer.width(),
411            height: buffer.height(),
412            pointer_x: self.position.0,
413            pointer_y: self.position.1,
414            hotspot: self
415                .placement
416                .map(|placed| (placed.hotspot_x, placed.hotspot_y)),
417            // What the hardware is asked for. When it cannot do the turn,
418            // `draw` already did it and this must ask for none.
419            rotation: if self.rotates_in_hardware() {
420                self.rotation
421            } else {
422                Rotation::None
423            },
424            rotation_mask: self.rotation_mask,
425        };
426        stage(request, &self.props, &state)
427    }
428
429    /// Note that a commit carrying [`Renderer::stage_into`]'s writes landed.
430    ///
431    /// See [`PingPong::published`] for what happens when it did not.
432    pub const fn published(&mut self) {
433        self.which.published();
434    }
435
436    /// Stage and commit on their own.
437    ///
438    /// For callers with nothing to coalesce with. A compositor should prefer
439    /// [`Renderer::stage_into`].
440    ///
441    /// # Errors
442    ///
443    /// [`CursorError::MissingProperty`] from staging, or [`CursorError::Io`]
444    /// if the kernel refuses the commit.
445    pub fn commit(&mut self) -> Result<(), CursorError> {
446        if self.plane.path == PlanePath::Legacy {
447            return self.commit_legacy();
448        }
449        let mut request = AtomicRequest::new();
450        self.stage_into(&mut request)?;
451        // Enabling a plane is a modeset; moving one already enabled is not.
452        // Blocking either way: a non-blocking cursor commit contends with
453        // vblank pacing and returns EBUSY as soon as the mouse outpaces the
454        // refresh rate.
455        let flags = if self.installed {
456            AtomicCommitFlags::empty()
457        } else {
458            AtomicCommitFlags::ALLOW_MODESET
459        };
460        request
461            .commit(self.device, flags)
462            .map_err(|error| CursorError::Io(error.to_string()))?;
463        self.installed = true;
464        self.published();
465        Ok(())
466    }
467
468    /// Install or move the cursor through `drmModeSetCursor`.
469    #[allow(
470        deprecated,
471        reason = "see create_legacy: this path exists for hardware with no \
472                  cursor plane to use instead"
473    )]
474    fn commit_legacy(&mut self) -> Result<(), CursorError> {
475        use drm::control::Device as _;
476
477        let crtc = crtc_handle(self.crtc_id).ok_or(CursorError::Unsupported)?;
478        let hotspot = self
479            .placement
480            .map_or((0, 0), |placed| (placed.hotspot_x, placed.hotspot_y));
481
482        if !self.visible {
483            self.device
484                .set_cursor::<LegacyCursor>(crtc, None)
485                .map_err(|error| CursorError::Io(error.to_string()))?;
486            self.installed = false;
487            return Ok(());
488        }
489
490        if !self.installed {
491            let buffer = LegacyCursor {
492                handle: self.buffers[0]
493                    .gem_handle()
494                    .ok_or_else(|| CursorError::Io("cursor buffer has no handle".to_owned()))?,
495                width: self.buffers[0].width(),
496                height: self.buffers[0].height(),
497                pitch: self.buffers[0].stride(),
498            };
499            self.device
500                .set_cursor(crtc, Some(&buffer))
501                .map_err(|error| CursorError::Io(error.to_string()))?;
502            self.installed = true;
503        }
504
505        let (x, y) = crate::stage::plane_origin(self.position, hotspot);
506        self.device
507            .move_cursor(crtc, (x, y))
508            .map_err(|error| CursorError::Io(error.to_string()))?;
509        Ok(())
510    }
511
512    /// Give up the device for a session pause.
513    ///
514    /// See [`Paused`] for what is released and what cannot be.
515    #[must_use]
516    pub fn pause(mut self) -> Paused {
517        let size = self.buffers[0].width();
518        for buffer in &mut self.buffers {
519            buffer.forget();
520        }
521        Paused {
522            crtc_id: self.crtc_id,
523            plane: self.plane,
524            rotation_mask: self.rotation_mask,
525            rotation: self.rotation,
526            cursor: self.cursor,
527            drawn_frame: self.drawn_frame,
528            position: self.position,
529            visible: self.visible,
530            size,
531        }
532    }
533
534    /// Draw frame `index` into the buffer that is not on screen.
535    fn draw(&mut self, index: usize) -> Result<(), CursorError> {
536        let Some(cursor) = self.cursor.as_ref() else {
537            return Ok(());
538        };
539        let frame = cursor
540            .frames()
541            .get(index)
542            .unwrap_or_else(|| cursor.first())
543            .clone();
544
545        // Read what depends on `self` before the buffer is borrowed mutably.
546        let rotation = self.draw_rotation();
547        let fourcc = self.plane.fourcc;
548        let target = self.which.draw_target();
549        let buffer = &mut self.buffers[target];
550        let (width, height) = (buffer.width(), buffer.height());
551        let stride_px = (buffer.stride() / 4) as usize;
552
553        let mut scratch = vec![0u32; height as usize * stride_px];
554        let placement = sprite::compose(&frame, rotation, &mut scratch, width, height, stride_px);
555
556        {
557            let mut mapping = buffer.map(MapAccess::Write);
558            for y in 0..height {
559                let row = mapping
560                    .row_mut(y)
561                    .ok_or_else(|| CursorError::Io("cursor buffer row out of range".to_owned()))?;
562                let from = y as usize * stride_px;
563                for (x, pixel) in scratch[from..from + width as usize].iter().enumerate() {
564                    let at = x * 4;
565                    row[at..at + 4].copy_from_slice(&pack(*pixel, fourcc).to_ne_bytes());
566                }
567            }
568        }
569
570        self.placement = Some(placement);
571        self.drawn_frame = Some(index);
572        self.which.mark_drawn();
573        Ok(())
574    }
575}
576
577/// The largest cursor the driver admits to, or `None` if it will not say.
578///
579/// Both dimensions, because a driver free to differ on them would otherwise
580/// have the width silently applied to the height. The smaller wins: the buffer
581/// is square, and asking for a rectangle the card cannot take is how a probe
582/// that should have succeeded ends up walking all the way down the ladder.
583fn cursor_cap(device: &Device) -> Option<u32> {
584    let cap = |which| {
585        drm::Device::get_driver_capability(device, which)
586            .ok()
587            .and_then(|value| u32::try_from(value).ok())
588            .filter(|value| *value != 0)
589    };
590    let width = cap(drm::DriverCapability::CursorWidth)?;
591    let height = cap(drm::DriverCapability::CursorHeight)?;
592    Some(width.min(height))
593}
594
595/// Permute a shadow pixel into the plane's channel order.
596///
597/// The shadow is `0xAARRGGBB` in a host word, which is `ARGB8888` in memory on
598/// a little-endian host. The other three cursor formats are the same eight-bit
599/// channels in a different order, so each is a permutation of that word rather
600/// than a conversion: no precision is lost and no channel is invented.
601///
602/// A format not in [`CURSOR_FORMATS`] cannot be selected, so the fallthrough
603/// is unreachable by construction; it returns the pixel unchanged rather than
604/// panicking, because a wrongly ordered cursor is a visible bug and a dead
605/// process is a black screen.
606pub(crate) const fn pack(pixel: u32, fourcc: u32) -> u32 {
607    let (a, r, g, b) = (
608        pixel >> 24,
609        (pixel >> 16) & 0xff,
610        (pixel >> 8) & 0xff,
611        pixel & 0xff,
612    );
613    match fourcc {
614        drmkit_fmt::fourcc::RGBA8888 => (r << 24) | (g << 16) | (b << 8) | a,
615        drmkit_fmt::fourcc::ABGR8888 => (a << 24) | (b << 16) | (g << 8) | r,
616        drmkit_fmt::fourcc::BGRA8888 => (b << 24) | (g << 16) | (r << 8) | a,
617        // `ARGB8888`, and anything that never reaches selection.
618        _ => pixel,
619    }
620}
621
622/// A dumb buffer, described the way `drmModeSetCursor` wants it.
623struct LegacyCursor {
624    handle: u32,
625    width: u32,
626    height: u32,
627    pitch: u32,
628}
629
630impl drm::buffer::Buffer for LegacyCursor {
631    fn size(&self) -> (u32, u32) {
632        (self.width, self.height)
633    }
634    fn format(&self) -> drm::buffer::DrmFourcc {
635        drm::buffer::DrmFourcc::Argb8888
636    }
637    fn pitch(&self) -> u32 {
638        self.pitch
639    }
640    fn handle(&self) -> drm::buffer::Handle {
641        // The kernel is about to be handed this, so a zero would be a bug
642        // upstream of here rather than something to paper over.
643        drm::buffer::Handle::from(
644            core::num::NonZeroU32::new(self.handle)
645                .unwrap_or(core::num::NonZeroU32::new(1).expect("one is not zero")),
646        )
647    }
648}
649
650/// A CRTC id as a handle.
651fn crtc_handle(crtc_id: u32) -> Option<drm::control::crtc::Handle> {
652    core::num::NonZeroU32::new(crtc_id).map(drm::control::crtc::Handle::from)
653}
654
655/// Allocate a pair of cursor buffers with framebuffers registered.
656fn allocate_pair(device: &Device, size: u32, fourcc: u32) -> Result<[Buffer; 2], CursorError> {
657    let config = Config {
658        width: size,
659        height: size,
660        fourcc,
661        bpp: 32,
662        add_fb: true,
663    };
664    let first = Buffer::create(device, &config).map_err(|e| CursorError::Io(e.to_string()))?;
665    let second = Buffer::create(device, &config).map_err(|e| CursorError::Io(e.to_string()))?;
666    Ok([first, second])
667}
668
669/// Ask the kernel whether this plane will take a buffer of this size.
670fn test_plane(
671    device: &Device,
672    props: &PropertyStore,
673    plane: &SelectedPlane,
674    crtc_id: u32,
675    fb_id: u32,
676    size: u32,
677) -> bool {
678    let mut request = AtomicRequest::new();
679    let state = PlaneState {
680        plane_id: plane.plane_id,
681        crtc_id,
682        fb_id,
683        width: size,
684        height: size,
685        pointer_x: 0,
686        pointer_y: 0,
687        // Nothing has been drawn, so there is no hotspot to state yet.
688        hotspot: None,
689        rotation: Rotation::None,
690        rotation_mask: 0,
691    };
692    if stage(&mut request, props, &state).is_err() {
693        return false;
694    }
695    request.test(device, AtomicCommitFlags::empty()).is_ok()
696}
697
698/// Whether a plane is currently feeding some CRTC other than `crtc_id`.
699fn plane_bound_elsewhere(device: &Device, plane_id: u32, crtc_id: u32) -> bool {
700    use drm::control::Device as _;
701
702    let Some(handle) = core::num::NonZeroU32::new(plane_id) else {
703        return false;
704    };
705    device
706        .get_plane(drm::control::plane::Handle::from(handle))
707        .ok()
708        .and_then(|info| info.crtc())
709        .is_some_and(|bound| u32::from(bound) != crtc_id)
710}
711
712/// A cursor renderer whose device has gone away.
713///
714/// A session pause revokes the descriptor: the kernel state behind it is gone,
715/// so the buffers cannot be destroyed and the framebuffers cannot be removed
716/// -- those ioctls would go to a dead descriptor, or worse to whatever the
717/// number has since been reused for.
718///
719/// The mappings are a different matter and are released here. Closing a
720/// descriptor does not unmap anything -- a VMA holds its own reference to the
721/// file -- so a compositor that skipped this would leak one mapping per VT
722/// switch, quietly, for as long as it ran.
723///
724/// This exists as a type rather than a flag so that a paused renderer cannot
725/// be committed. There is nothing to commit to.
726#[derive(Debug)]
727pub struct Paused {
728    crtc_id: u32,
729    plane: SelectedPlane,
730    rotation_mask: u64,
731    rotation: Rotation,
732    cursor: Option<Cursor>,
733    drawn_frame: Option<usize>,
734    position: (i32, i32),
735    visible: bool,
736    size: u32,
737}
738
739impl Paused {
740    /// Rebuild on a new descriptor.
741    ///
742    /// The same plane, the same size, and the cursor redrawn at whatever frame
743    /// it had reached -- so the first commit after a resume carries the right
744    /// pixels rather than an empty buffer or a stale one.
745    ///
746    /// The size is not re-probed. The hardware has not changed, and a resume
747    /// is when a compositor is trying to get something on screen quickly
748    /// rather than spend test commits rediscovering what it already knew.
749    ///
750    /// # Errors
751    ///
752    /// [`CursorError::Io`] if the buffers cannot be reallocated or the plane's
753    /// properties cannot be read on the new descriptor.
754    pub fn resume(self, device: &Device) -> Result<Renderer<'_>, CursorError> {
755        let mut props = PropertyStore::new();
756        if self.plane.path != PlanePath::Legacy {
757            props
758                .cache_properties(device, self.plane.plane_id, ObjectType::Plane)
759                .map_err(|error| CursorError::Io(error.to_string()))?;
760        }
761
762        let mut renderer = Renderer {
763            device,
764            crtc_id: self.crtc_id,
765            plane: self.plane,
766            rotation_mask: self.rotation_mask,
767            props,
768            buffers: allocate_pair(device, self.size, self.plane.fourcc)?,
769            which: if self.plane.path == PlanePath::Legacy {
770                PingPong::single()
771            } else {
772                PingPong::new()
773            },
774            cursor: self.cursor,
775            placement: None,
776            drawn_frame: None,
777            position: self.position,
778            visible: self.visible,
779            rotation: self.rotation,
780            // The new descriptor knows nothing of the old plane state, so the
781            // next commit re-arms everything.
782            installed: false,
783        };
784
785        if renderer.cursor.is_some() {
786            renderer.draw(self.drawn_frame.unwrap_or(0))?;
787        }
788        Ok(renderer)
789    }
790
791    /// Whether a cursor is still loaded, and will be redrawn on resume.
792    #[must_use]
793    pub const fn has_cursor(&self) -> bool {
794        self.cursor.is_some()
795    }
796}