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