drmkit-cursor 0.1.3

Cursor themes and rasterized cursors
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
// SPDX-License-Identifier: MIT

//! Putting a cursor on a plane and keeping it there.
//!
//! This is where the pieces meet: [`select_plane`] picks the plane,
//! [`probe_size`] settles the buffer size, [`compose`](crate::compose) draws a frame into the
//! buffer that is not on screen, [`PingPong`] says which that is, and [`stage`]
//! writes the result onto the plane.
//!
//! The staging is deliberately separate from committing. A compositor wants
//! the cursor in the same atomic request as the rest of its frame -- a cursor
//! that lands a commit later than the window it belongs to is visibly wrong
//! when either is moving. [`Renderer::commit`] exists for callers with nothing
//! to coalesce with.

use std::time::Duration;

use drmkit_core::{AtomicCommitFlags, AtomicRequest, Device, ObjectType, PropertyStore};
use drmkit_dumb::{Buffer, Config, MapAccess};
use drmkit_planes::PlaneRegistry;

use crate::CursorError;
use crate::cursor::Cursor;
use crate::pingpong::PingPong;
use crate::plane::{PlanePath, SelectedPlane, select_plane};
use crate::size::{preferred_size, probe_size};
use crate::sprite::{self, Placement, Rotation};
use crate::stage::{PlaneState, edge_shift, stage};

/// How to set a renderer up.
#[derive(Debug, Clone, Copy)]
#[allow(
    clippy::exhaustive_structs,
    reason = "built by callers with `..Default::default()`, which a new field does not break"
)]
pub struct RendererConfig {
    /// The CRTC the cursor belongs to, and its index in the resource list.
    ///
    /// Both, because the id is what gets written to `CRTC_ID` and the index is
    /// what `possible_crtcs` is a bitmask over. Deriving one from the other
    /// needs the resource list, which the caller already has.
    pub crtc_id: u32,
    /// As [`RendererConfig::crtc_id`].
    pub crtc_index: u32,
    /// Cursor size in pixels, or zero to take the driver's preference.
    pub preferred_size: u32,
    /// Use this plane rather than choosing one. Zero to choose.
    pub forced_plane_id: u32,
    /// Whether falling back to `drmModeSetCursor` is acceptable.
    pub allow_legacy: bool,
    /// Drive the CRTC's cursor plane through `drmModeSetCursor` and
    /// `drmModeMoveCursor` rather than atomic commits.
    ///
    /// On an atomic driver the legacy cursor ioctls run as an async plane
    /// update that does not wait for vblank, so a moving pointer does not
    /// contend with the caller's scanout commits. The atomic path's blocking
    /// one-plane commit per move does: on vc4 it cost the scanout two vblanks
    /// in three while the pointer moved (drm-cxx `0925491`). Only when the
    /// CRTC has a cursor plane that takes `ARGB8888`; needs `allow_legacy`;
    /// ignored with `forced_plane_id`. See
    /// [`Renderer::reserved_plane_id`].
    pub prefer_legacy: bool,
    /// Which way up the cursor is drawn.
    pub rotation: Rotation,
}

impl Default for RendererConfig {
    fn default() -> Self {
        Self {
            crtc_id: 0,
            crtc_index: 0,
            preferred_size: 0,
            forced_plane_id: 0,
            // A cursor that cannot be committed with the frame is still better
            // than none, so the permissive default matches the reference.
            allow_legacy: true,
            prefer_legacy: false,
            rotation: Rotation::None,
        }
    }
}

/// A cursor on a plane.
///
/// Borrows the device: the buffers and the framebuffers registered for them
/// belong to it, and outliving it would leave this holding ids the kernel has
/// recycled.
#[derive(Debug)]
pub struct Renderer<'a> {
    device: &'a Device,
    crtc_id: u32,
    plane: SelectedPlane,
    rotation_mask: u64,
    props: PropertyStore,
    buffers: [Buffer; 2],
    which: PingPong,
    /// The loaded cursor, and where its current frame landed in the buffer.
    cursor: Option<Cursor>,
    placement: Option<Placement>,
    /// How far the drawn sprite was moved up and left, from
    /// [`edge_shift`]. Subtracted from
    /// `placement`'s hotspot wherever the hotspot is used.
    shift: (u32, u32),
    /// Which animation frame is in the buffer, so a tick that does not change
    /// it does no work.
    drawn_frame: Option<usize>,
    /// Where the pointer is.
    position: (i32, i32),
    visible: bool,
    /// Which way up the cursor is drawn.
    rotation: Rotation,
    /// The `zpos` the plane is written with, from [`cursor_zpos`]; `None`
    /// where it has no settable one.
    zpos: Option<u64>,
    /// Legacy only: whether the cursor has been handed to the CRTC yet.
    ///
    /// The first commit installs it; later ones are cheap moves.
    installed: bool,
}

impl Drop for Renderer<'_> {
    /// Take the cursor off the CRTC, as the reference's destructor does, so
    /// the next client does not inherit the sprite.
    ///
    /// Only once something was installed: a plane the kernel never saw this
    /// use can refuse being cleared. On the legacy path, freeing the buffers
    /// does not do it -- the kernel holds its own reference to the cursor's
    /// framebuffer -- so without this the sprite stays on screen.
    #[allow(
        deprecated,
        reason = "the legacy path installed the cursor with drmModeSetCursor, \
                  so that is what takes it off"
    )]
    fn drop(&mut self) {
        use drm::control::Device as _;

        if !self.installed {
            return;
        }
        if self.plane.path == PlanePath::Legacy {
            if let Some(crtc) = crtc_handle(self.crtc_id) {
                let _ = self.device.set_cursor::<LegacyCursor>(crtc, None);
            }
            return;
        }
        let mut request = AtomicRequest::new();
        let plane_id = self.plane.plane_id;
        for name in ["FB_ID", "CRTC_ID"] {
            if let Ok(property) = self.props.property_id(plane_id, name) {
                let _ = request.add_property(plane_id, property, 0);
            }
        }
        let _ = request.commit(self.device, AtomicCommitFlags::empty());
    }
}

impl<'a> Renderer<'a> {
    /// Set up a cursor on `config.crtc_id`.
    ///
    /// Chooses a plane, settles a buffer size by asking the kernel what it
    /// will take, and allocates. No cursor is loaded yet.
    ///
    /// # Errors
    ///
    /// - [`CursorError::NoPlane`] or [`CursorError::PlaneUnusable`] from
    ///   [`select_plane`].
    /// - [`CursorError::Unsupported`] on the legacy path, if `config.rotation`
    ///   is not [`Rotation::None`] or the CRTC refuses a cursor -- no cursor
    ///   hardware, so composite one instead.
    /// - [`CursorError::Io`] if a buffer cannot be allocated or the plane's
    ///   properties cannot be read, or, on the legacy path, if the caller is
    ///   not DRM master.
    pub fn create(
        device: &'a Device,
        registry: &PlaneRegistry,
        config: &RendererConfig,
    ) -> Result<Self, CursorError> {
        let forced = (config.forced_plane_id != 0).then_some(config.forced_plane_id);
        let bound_elsewhere = |plane_id: u32| plane_in_use(device, plane_id);
        let plane = select_plane(
            registry,
            config.crtc_index,
            forced,
            config.allow_legacy,
            config.prefer_legacy,
            &bound_elsewhere,
        )?;

        if plane.path == PlanePath::Legacy {
            return Self::create_legacy(device, config, plane);
        }

        let mut props = PropertyStore::new();
        props
            .cache_properties(device, plane.plane_id, ObjectType::Plane)
            .map_err(|error| CursorError::Io(error.to_string()))?;

        let rotation_mask = registry
            .by_id(plane.plane_id)
            .map_or(0, |caps| caps.rotation_bits);
        let zpos = registry
            .by_id(plane.plane_id)
            .and_then(|caps| cursor_zpos(caps, registry.blend_stage_limit(config.crtc_index)));

        // The driver's own preference, asked of the device.
        //
        // This used to read `plane.cursor_max_w`, which nothing ever fills:
        // `PlaneRegistry::probe` sets it to zero on every plane, so the cap was
        // always absent and every atomic path silently fell back to 64 -- a
        // 64x64 cursor on a card advertising 512x512, four times coarser than
        // the hardware offers, with a doc comment promising the opposite. The
        // reference asks the device directly at this point, and so does this.
        //
        // `DRM_CAP_CURSOR_WIDTH` is a device capability rather than a plane
        // one, which is why it cannot come from the registry. It is also an
        // upper bound rather than a list of accepted sizes, so `probe_size`
        // still walks the ladder down from it and asks the kernel.
        let cap = cursor_cap(device);
        let preferred = preferred_size(config.preferred_size, plane.path, cap);

        let mut buffers: Option<[Buffer; 2]> = None;
        let size = probe_size(preferred, |candidate| {
            match allocate_pair(device, candidate, plane.fourcc) {
                Ok(pair) => {
                    // The kernel is asked about the real thing: a request that
                    // arms this plane with this framebuffer at this size.
                    let accepted = pair[0].fb_id().is_some_and(|fb_id| {
                        test_plane(
                            device,
                            &props,
                            &plane,
                            config.crtc_id,
                            fb_id,
                            candidate,
                            zpos,
                        )
                    });
                    if accepted {
                        buffers = Some(pair);
                    }
                    accepted
                }
                Err(_) => false,
            }
        });

        let buffers = match buffers {
            Some(pair) => pair,
            // Nothing was accepted, which is usually not about size at all --
            // with no mode set every test fails for unrelated reasons. Take
            // the fallback size and let the first real commit say what is
            // wrong.
            None => allocate_pair(device, size, plane.fourcc)?,
        };

        Ok(Self {
            device,
            crtc_id: config.crtc_id,
            plane,
            rotation_mask,
            props,
            buffers,
            which: PingPong::new(),
            cursor: None,
            placement: None,
            shift: (0, 0),
            drawn_frame: None,
            position: (0, 0),
            visible: true,
            rotation: config.rotation,
            zpos,
            installed: false,
        })
    }

    /// Set up on the legacy `drmModeSetCursor` path.
    ///
    /// The call has no `TEST_ONLY`, so the probe is a cursor *disable*: a
    /// no-op on any controller that has cursor hardware, and a refusal on one
    /// that does not. i.MX's LCDIF and tilcdc have no cursor at all, and are
    /// exactly the parts where plane selection fell through to here -- finding
    /// out now lets the caller composite a cursor instead of getting a
    /// renderer that dies on its first show.
    ///
    /// A permission failure is reported separately, because "not DRM master"
    /// and "no cursor hardware" want different responses and look identical
    /// from the errno alone.
    #[allow(
        deprecated,
        reason = "drmModeSetCursor is deprecated in favor of a cursor plane, \
                  which is what every other path here uses. This is the \
                  fallback for controllers that have no such plane."
    )]
    fn create_legacy(
        device: &'a Device,
        config: &RendererConfig,
        plane: SelectedPlane,
    ) -> Result<Self, CursorError> {
        use drm::control::Device as _;

        if config.rotation != Rotation::None {
            // drmModeSetCursor has nowhere to put a rotation, and pre-rotating
            // in software would still leave the hotspot wrong for a caller who
            // thinks the hardware did it.
            return Err(CursorError::Unsupported);
        }

        let crtc = crtc_handle(config.crtc_id).ok_or(CursorError::Unsupported)?;
        if let Err(error) = device.set_cursor::<LegacyCursor>(crtc, None) {
            // EACCES and EPERM mean "not DRM master", which is a different
            // thing to tell a caller than "this controller has no cursor".
            return Err(match error.kind() {
                std::io::ErrorKind::PermissionDenied => {
                    CursorError::Io("not DRM master".to_owned())
                }
                _ => CursorError::Unsupported,
            });
        }

        // One buffer: the legacy ioctls take one handle and have no flip to
        // alternate on.
        let size = preferred_size(config.preferred_size, PlanePath::Legacy, None);
        let buffers = allocate_pair(device, size, plane.fourcc)?;

        Ok(Self {
            device,
            crtc_id: config.crtc_id,
            plane,
            rotation_mask: 0,
            props: PropertyStore::new(),
            buffers,
            which: PingPong::single(),
            cursor: None,
            placement: None,
            shift: (0, 0),
            drawn_frame: None,
            position: (0, 0),
            visible: true,
            rotation: Rotation::None,
            zpos: None,
            installed: false,
        })
    }

    /// The plane this is drawing on.
    #[must_use]
    pub const fn plane(&self) -> SelectedPlane {
        self.plane
    }

    /// The plane the cursor occupies, for a caller keeping its own commits
    /// off it: the plane it draws on on the atomic paths; on the legacy path,
    /// the CRTC's cursor plane the kernel drives when
    /// [`RendererConfig::prefer_legacy`] chose the legacy ioctls over it, else
    /// `0`.
    #[must_use]
    pub const fn reserved_plane_id(&self) -> u32 {
        match self.plane.path {
            PlanePath::Legacy => self.plane.legacy_plane_id,
            PlanePath::AtomicCursor | PlanePath::AtomicOverlay => self.plane.plane_id,
        }
    }

    /// The size of the cursor buffer, in pixels.
    #[must_use]
    pub const fn size(&self) -> (u32, u32) {
        (self.buffers[0].width(), self.buffers[0].height())
    }

    /// Whether the plane can turn the sprite itself.
    ///
    /// When it can, the pixels are drawn upright and the hardware does the
    /// turn. When it cannot -- i915's cursor planes expose the property and
    /// list only 0 and 180 -- the pixels are turned as they are drawn, and
    /// the hardware is asked for none. Doing both turns it twice.
    const fn rotates_in_hardware(&self) -> bool {
        self.rotation_mask & self.rotation.drm_mask() != 0
    }

    /// How the sprite is turned as it is drawn.
    const fn draw_rotation(&self) -> Rotation {
        if self.rotates_in_hardware() {
            Rotation::None
        } else {
            self.rotation
        }
    }

    /// Whether the hardware turns the buffer as it scans it out.
    ///
    /// The edge shift is not drawn then: the buffer's top-left is not the
    /// screen's, so moving the pixels up and left would move them the wrong
    /// way. Such a cursor keeps its negative position at the edge.
    const fn hardware_turns_the_sprite(&self) -> bool {
        self.rotates_in_hardware() && !matches!(self.rotation, Rotation::None)
    }

    /// The hotspot in the buffer as drawn, after any edge shift.
    fn drawn_hotspot(&self) -> Option<(u32, u32)> {
        self.placement.map(|placed| {
            (
                placed.hotspot_x.saturating_sub(self.shift.0),
                placed.hotspot_y.saturating_sub(self.shift.1),
            )
        })
    }

    /// Change which way up the cursor is, redrawing it.
    ///
    /// # Errors
    ///
    /// - [`CursorError::Unsupported`] for any rotation but
    ///   [`Rotation::None`] on [`PlanePath::Legacy`], leaving the cursor as it
    ///   was. `drmModeSetCursor` has nowhere to put one, as at
    ///   [`Renderer::create`], and the reference refuses here too.
    /// - [`CursorError::Io`] if the buffer cannot be mapped.
    pub fn set_rotation(&mut self, rotation: Rotation) -> Result<(), CursorError> {
        if rotation != Rotation::None && self.plane.path == PlanePath::Legacy {
            return Err(CursorError::Unsupported);
        }
        self.rotation = rotation;
        match self.drawn_frame {
            Some(index) => self.draw(index),
            None => Ok(()),
        }
    }

    /// Load a cursor and draw its first frame.
    ///
    /// # Errors
    ///
    /// [`CursorError::Io`] if the buffer cannot be mapped.
    pub fn set_cursor(&mut self, cursor: Cursor) -> Result<(), CursorError> {
        self.cursor = Some(cursor);
        self.drawn_frame = None;
        // The next commit installs again, as the reference's does: on the
        // legacy path a driver may upload the sprite only when handed a
        // buffer, and a move alone would leave the old shape showing.
        self.installed = false;
        self.draw(0)
    }

    /// Move the pointer.
    pub const fn move_to(&mut self, x: i32, y: i32) {
        self.position = (x, y);
    }

    /// Show or hide the cursor without unloading it.
    pub const fn set_visible(&mut self, visible: bool) {
        self.visible = visible;
    }

    /// Bring the buffer up to date with the pointer's position.
    ///
    /// Near the top or left edge of the CRTC the sprite is drawn moved up and
    /// left inside its buffer, so the plane starts at the edge rather than
    /// off it: some drivers refuse a negative position (drm-cxx#384). A move
    /// into or out of that band, or within it, redraws the current frame.
    /// [`Renderer::commit`] calls this; a caller coalescing through
    /// [`Renderer::stage_into`] calls it first. Skipped, the commit carries
    /// the negative position, as before.
    ///
    /// Returns whether the buffer changed.
    ///
    /// # Errors
    ///
    /// [`CursorError::Io`] if the buffer cannot be mapped.
    pub fn prepare(&mut self) -> Result<bool, CursorError> {
        let (Some(placement), Some(index)) = (self.placement, self.drawn_frame) else {
            return Ok(false);
        };
        let wanted = if self.hardware_turns_the_sprite() {
            (0, 0)
        } else {
            edge_shift(self.position, (placement.hotspot_x, placement.hotspot_y))
        };
        if wanted == self.shift {
            return Ok(false);
        }
        self.draw(index)?;
        // The legacy ioctls may upload the sprite only when handed a buffer,
        // as for a new shape.
        if self.plane.path == PlanePath::Legacy {
            self.installed = false;
        }
        Ok(true)
    }

    /// Advance an animation, drawing a new frame if one is due.
    ///
    /// Returns whether the buffer changed, so a caller can skip a commit that
    /// would publish the same pixels. A static cursor never changes.
    ///
    /// # Errors
    ///
    /// [`CursorError::Io`] if the buffer cannot be mapped.
    pub fn tick(&mut self, elapsed: Duration) -> Result<bool, CursorError> {
        let Some(cursor) = self.cursor.as_ref() else {
            return Ok(false);
        };
        if !cursor.is_animated() {
            return Ok(false);
        }
        let wanted = cursor.frame_index_at(elapsed);
        if self.drawn_frame == Some(wanted) {
            return Ok(false);
        }
        self.draw(wanted)?;
        Ok(true)
    }

    /// Write the cursor's current state into `request`.
    ///
    /// For coalescing with the rest of a frame. The caller calls
    /// [`Renderer::prepare`] first, commits, and calls
    /// [`Renderer::published`] afterwards.
    ///
    /// # Errors
    ///
    /// [`CursorError::MissingProperty`] if the plane lacks a required
    /// property -- always, on [`PlanePath::Legacy`],
    /// which has no plane to stage: use [`Renderer::commit`] there.
    pub fn stage_into(&self, request: &mut AtomicRequest) -> Result<usize, CursorError> {
        let buffer = &self.buffers[self.which.commit_target()];
        let fb_id = if self.visible {
            buffer.fb_id().unwrap_or(0)
        } else {
            // Disarming the plane is how a cursor hides: there is no property
            // for "invisible", and a zero-alpha buffer still costs bandwidth.
            0
        };
        let state = PlaneState {
            plane_id: self.plane.plane_id,
            crtc_id: if self.visible { self.crtc_id } else { 0 },
            fb_id,
            width: buffer.width(),
            height: buffer.height(),
            pointer_x: self.position.0,
            pointer_y: self.position.1,
            hotspot: self.drawn_hotspot(),
            // What the hardware is asked for. When it cannot do the turn,
            // `draw` already did it and this must ask for none.
            rotation: if self.rotates_in_hardware() {
                self.rotation
            } else {
                Rotation::None
            },
            rotation_mask: self.rotation_mask,
        };
        let mut written = stage(request, &self.props, &state)?;
        written += stage_zpos(request, &self.props, self.plane.plane_id, self.zpos)?;
        Ok(written)
    }

    /// Note that a commit carrying [`Renderer::stage_into`]'s writes landed.
    ///
    /// See [`PingPong::published`] for what happens when it did not.
    pub const fn published(&mut self) {
        self.which.published();
    }

    /// Stage and commit on their own.
    ///
    /// For callers with nothing to coalesce with. A compositor should prefer
    /// [`Renderer::stage_into`].
    ///
    /// # Errors
    ///
    /// [`CursorError::MissingProperty`] from staging, or [`CursorError::Io`]
    /// if the kernel refuses the commit.
    pub fn commit(&mut self) -> Result<(), CursorError> {
        self.prepare()?;
        if self.plane.path == PlanePath::Legacy {
            return self.commit_legacy();
        }
        let mut request = AtomicRequest::new();
        self.stage_into(&mut request)?;
        // Enabling a plane is a modeset; moving one already enabled is not.
        // Blocking either way: a non-blocking cursor commit contends with
        // vblank pacing and returns EBUSY as soon as the mouse outpaces the
        // refresh rate.
        let flags = if self.installed {
            AtomicCommitFlags::empty()
        } else {
            AtomicCommitFlags::ALLOW_MODESET
        };
        request
            .commit(self.device, flags)
            .map_err(|error| CursorError::Io(error.to_string()))?;
        self.installed = true;
        self.published();
        Ok(())
    }

    /// Install or move the cursor through `drmModeSetCursor`.
    #[allow(
        deprecated,
        reason = "see create_legacy: this path exists for hardware with no \
                  cursor plane to use instead"
    )]
    fn commit_legacy(&mut self) -> Result<(), CursorError> {
        use drm::control::Device as _;

        let crtc = crtc_handle(self.crtc_id).ok_or(CursorError::Unsupported)?;
        let hotspot = self.drawn_hotspot().unwrap_or((0, 0));

        if !self.visible {
            self.device
                .set_cursor::<LegacyCursor>(crtc, None)
                .map_err(|error| CursorError::Io(error.to_string()))?;
            self.installed = false;
            return Ok(());
        }

        if !self.installed {
            let buffer = LegacyCursor {
                handle: self.buffers[0]
                    .gem_handle()
                    .ok_or_else(|| CursorError::Io("cursor buffer has no handle".to_owned()))?,
                width: self.buffers[0].width(),
                height: self.buffers[0].height(),
                pitch: self.buffers[0].stride(),
            };
            self.device
                .set_cursor(crtc, Some(&buffer))
                .map_err(|error| CursorError::Io(error.to_string()))?;
            self.installed = true;
        }

        let (x, y) = crate::stage::plane_origin(self.position, hotspot);
        self.device
            .move_cursor(crtc, (x, y))
            .map_err(|error| CursorError::Io(error.to_string()))?;
        Ok(())
    }

    /// Give up the device for a session pause.
    ///
    /// See [`Paused`] for what is released and what cannot be.
    #[must_use]
    pub fn pause(mut self) -> Paused {
        let size = self.buffers[0].width();
        for buffer in &mut self.buffers {
            buffer.forget();
        }
        // The descriptor is going, so dropping must not try to hide the
        // cursor through it, as the reference skips that while paused.
        self.installed = false;
        Paused {
            crtc_id: self.crtc_id,
            plane: self.plane,
            rotation_mask: self.rotation_mask,
            rotation: self.rotation,
            zpos: self.zpos,
            cursor: self.cursor.take(),
            drawn_frame: self.drawn_frame,
            position: self.position,
            visible: self.visible,
            size,
        }
    }

    /// Draw frame `index` into the buffer that is not on screen.
    fn draw(&mut self, index: usize) -> Result<(), CursorError> {
        let Some(cursor) = self.cursor.as_ref() else {
            return Ok(());
        };
        let frame = cursor
            .frames()
            .get(index)
            .unwrap_or_else(|| cursor.first())
            .clone();

        // Read what depends on `self` before the buffer is borrowed mutably.
        let rotation = self.draw_rotation();
        let hardware_turned = self.hardware_turns_the_sprite();
        let position = self.position;
        let fourcc = self.plane.fourcc;
        let target = self.which.draw_target();
        let buffer = &mut self.buffers[target];
        let (width, height) = (buffer.width(), buffer.height());
        let stride_px = (buffer.stride() / 4) as usize;

        let mut scratch = vec![0u32; height as usize * stride_px];
        let placement = sprite::compose(&frame, rotation, &mut scratch, width, height, stride_px);
        let shift = if hardware_turned {
            (0, 0)
        } else {
            edge_shift(position, (placement.hotspot_x, placement.hotspot_y))
        };
        sprite::shift_up_left(&mut scratch, width, height, stride_px, shift);

        {
            let mut mapping = buffer.map(MapAccess::Write);
            for y in 0..height {
                let row = mapping
                    .row_mut(y)
                    .ok_or_else(|| CursorError::Io("cursor buffer row out of range".to_owned()))?;
                let from = y as usize * stride_px;
                for (x, pixel) in scratch[from..from + width as usize].iter().enumerate() {
                    let at = x * 4;
                    row[at..at + 4].copy_from_slice(&pack(*pixel, fourcc).to_ne_bytes());
                }
            }
        }

        self.placement = Some(placement);
        self.shift = shift;
        self.drawn_frame = Some(index);
        self.which.mark_drawn();
        Ok(())
    }
}

/// The largest cursor the driver admits to, or `None` if it will not say.
///
/// Both dimensions, because a driver free to differ on them would otherwise
/// have the width silently applied to the height. The smaller wins: the buffer
/// is square, and asking for a rectangle the card cannot take is how a probe
/// that should have succeeded ends up walking all the way down the ladder.
fn cursor_cap(device: &Device) -> Option<u32> {
    let cap = |which| {
        drm::Device::get_driver_capability(device, which)
            .ok()
            .and_then(|value| u32::try_from(value).ok())
            .filter(|value| *value != 0)
    };
    let width = cap(drm::DriverCapability::CursorWidth)?;
    let height = cap(drm::DriverCapability::CursorHeight)?;
    Some(width.min(height))
}

/// Permute a shadow pixel into the plane's channel order.
///
/// The shadow is `0xAARRGGBB` in a host word, which is `ARGB8888` in memory on
/// a little-endian host. The other three cursor formats are the same eight-bit
/// channels in a different order, so each is a permutation of that word rather
/// than a conversion: no precision is lost and no channel is invented.
///
/// A format not in [`CURSOR_FORMATS`](crate::CURSOR_FORMATS) cannot be selected, so the fallthrough
/// is unreachable by construction; it returns the pixel unchanged rather than
/// panicking, because a wrongly ordered cursor is a visible bug and a dead
/// process is a black screen.
pub(crate) const fn pack(pixel: u32, fourcc: u32) -> u32 {
    let (a, r, g, b) = (
        pixel >> 24,
        (pixel >> 16) & 0xff,
        (pixel >> 8) & 0xff,
        pixel & 0xff,
    );
    match fourcc {
        drmkit_fmt::fourcc::RGBA8888 => (r << 24) | (g << 16) | (b << 8) | a,
        drmkit_fmt::fourcc::ABGR8888 => (a << 24) | (b << 16) | (g << 8) | r,
        drmkit_fmt::fourcc::BGRA8888 => (b << 24) | (g << 16) | (r << 8) | a,
        // `ARGB8888`, and anything that never reaches selection.
        _ => pixel,
    }
}

/// A dumb buffer, described the way `drmModeSetCursor` wants it.
struct LegacyCursor {
    handle: u32,
    width: u32,
    height: u32,
    pitch: u32,
}

impl drm::buffer::Buffer for LegacyCursor {
    fn size(&self) -> (u32, u32) {
        (self.width, self.height)
    }
    fn format(&self) -> drm::buffer::DrmFourcc {
        drm::buffer::DrmFourcc::Argb8888
    }
    fn pitch(&self) -> u32 {
        self.pitch
    }
    fn handle(&self) -> drm::buffer::Handle {
        // The kernel is about to be handed this, so a zero would be a bug
        // upstream of here rather than something to paper over.
        drm::buffer::Handle::from(
            core::num::NonZeroU32::new(self.handle)
                .unwrap_or(core::num::NonZeroU32::new(1).expect("one is not zero")),
        )
    }
}

/// A CRTC id as a handle.
fn crtc_handle(crtc_id: u32) -> Option<drm::control::crtc::Handle> {
    core::num::NonZeroU32::new(crtc_id).map(drm::control::crtc::Handle::from)
}

/// Allocate a pair of cursor buffers with framebuffers registered.
fn allocate_pair(device: &Device, size: u32, fourcc: u32) -> Result<[Buffer; 2], CursorError> {
    let config = Config {
        width: size,
        height: size,
        fourcc,
        bpp: 32,
        add_fb: true,
    };
    let first = Buffer::create(device, &config).map_err(|e| CursorError::Io(e.to_string()))?;
    let second = Buffer::create(device, &config).map_err(|e| CursorError::Io(e.to_string()))?;
    Ok([first, second])
}

/// Ask the kernel whether this plane will take a buffer of this size.
fn test_plane(
    device: &Device,
    props: &PropertyStore,
    plane: &SelectedPlane,
    crtc_id: u32,
    fb_id: u32,
    size: u32,
    zpos: Option<u64>,
) -> bool {
    let mut request = AtomicRequest::new();
    let state = PlaneState {
        plane_id: plane.plane_id,
        crtc_id,
        fb_id,
        width: size,
        height: size,
        pointer_x: 0,
        pointer_y: 0,
        // Nothing has been drawn, so there is no hotspot to state yet.
        hotspot: None,
        rotation: Rotation::None,
        rotation_mask: 0,
    };
    if stage(&mut request, props, &state).is_err()
        || stage_zpos(&mut request, props, plane.plane_id, zpos).is_err()
    {
        return false;
    }
    request.test(device, AtomicCommitFlags::empty()).is_ok()
}

/// Where the cursor's plane stacks: the top of its `zpos` range, as the
/// reference puts it, and no higher than the CRTC can blend.
///
/// Without it the plane keeps whatever `zpos` it had, which on a driver whose
/// overlays all start at 0 is the primary's own stage: the SA8155P refuses
/// two planes on one stage that overlap ("invalid coordinates, stage:0"), so
/// the cursor never committed there. Its CRTC blends nine planes, zpos 0 to
/// 8, while every plane advertises up to 10, hence the cap.
///
/// `None` where the plane has no `zpos` or a fixed one: there is nothing to
/// write.
pub(crate) fn cursor_zpos(
    plane: &drmkit_planes::PlaneCapabilities,
    blend_limit: Option<usize>,
) -> Option<u64> {
    let (Some(min), Some(max)) = (plane.zpos_min, plane.zpos_max) else {
        return None;
    };
    if drmkit_planes::zpos_fixed(plane) {
        return None;
    }
    let top = blend_limit
        .and_then(|limit| u64::try_from(limit).ok())
        .map_or(max, |limit| max.min(limit.saturating_sub(1)));
    Some(top.max(min))
}

/// Write `zpos` to the plane, when there is one to write.
fn stage_zpos(
    request: &mut AtomicRequest,
    props: &PropertyStore,
    plane_id: u32,
    zpos: Option<u64>,
) -> Result<usize, CursorError> {
    let Some(zpos) = zpos else {
        return Ok(0);
    };
    let id = props
        .property_id(plane_id, "zpos")
        .map_err(|_| CursorError::MissingProperty)?;
    request
        .add_property(plane_id, id, zpos)
        .map_err(|_| CursorError::MissingProperty)?;
    Ok(1)
}

/// Whether a plane is driving a CRTC at all -- this one included.
///
/// As the reference decides it: an overlay this CRTC is already showing is
/// the compositor's layer, and taking it for the cursor costs that layer just
/// as surely as taking one from another output would. Its comment says
/// "another CRTC"; its code tests for any, which is the better reading.
fn plane_in_use(device: &Device, plane_id: u32) -> bool {
    use drm::control::Device as _;

    let Some(handle) = core::num::NonZeroU32::new(plane_id) else {
        return false;
    };
    device
        .get_plane(drm::control::plane::Handle::from(handle))
        .ok()
        .and_then(|info| info.crtc())
        .is_some()
}

/// A cursor renderer whose device has gone away.
///
/// A session pause revokes the descriptor: the kernel state behind it is gone,
/// so the buffers cannot be destroyed and the framebuffers cannot be removed
/// -- those ioctls would go to a dead descriptor, or worse to whatever the
/// number has since been reused for.
///
/// The mappings are a different matter and are released here. Closing a
/// descriptor does not unmap anything -- a VMA holds its own reference to the
/// file -- so a compositor that skipped this would leak one mapping per VT
/// switch, quietly, for as long as it ran.
///
/// This exists as a type rather than a flag so that a paused renderer cannot
/// be committed. There is nothing to commit to.
#[derive(Debug)]
pub struct Paused {
    crtc_id: u32,
    plane: SelectedPlane,
    rotation_mask: u64,
    rotation: Rotation,
    zpos: Option<u64>,
    cursor: Option<Cursor>,
    drawn_frame: Option<usize>,
    position: (i32, i32),
    visible: bool,
    size: u32,
}

impl Paused {
    /// Rebuild on a new descriptor.
    ///
    /// The same plane, the same size, and the cursor redrawn at whatever frame
    /// it had reached -- so the first commit after a resume carries the right
    /// pixels rather than an empty buffer or a stale one.
    ///
    /// The size is not re-probed. The hardware has not changed, and a resume
    /// is when a compositor is trying to get something on screen quickly
    /// rather than spend test commits rediscovering what it already knew.
    ///
    /// # Errors
    ///
    /// [`CursorError::Io`] if the buffers cannot be reallocated or the plane's
    /// properties cannot be read on the new descriptor.
    pub fn resume(self, device: &Device) -> Result<Renderer<'_>, CursorError> {
        let mut props = PropertyStore::new();
        if self.plane.path != PlanePath::Legacy {
            props
                .cache_properties(device, self.plane.plane_id, ObjectType::Plane)
                .map_err(|error| CursorError::Io(error.to_string()))?;
        }

        let mut renderer = Renderer {
            device,
            crtc_id: self.crtc_id,
            plane: self.plane,
            rotation_mask: self.rotation_mask,
            props,
            buffers: allocate_pair(device, self.size, self.plane.fourcc)?,
            which: if self.plane.path == PlanePath::Legacy {
                PingPong::single()
            } else {
                PingPong::new()
            },
            cursor: self.cursor,
            placement: None,
            shift: (0, 0),
            drawn_frame: None,
            position: self.position,
            visible: self.visible,
            rotation: self.rotation,
            zpos: self.zpos,
            // The new descriptor knows nothing of the old plane state, so the
            // next commit re-arms everything.
            installed: false,
        };

        if renderer.cursor.is_some() {
            renderer.draw(self.drawn_frame.unwrap_or(0))?;
        }
        Ok(renderer)
    }

    /// Whether a cursor is still loaded, and will be redrawn on resume.
    #[must_use]
    pub const fn has_cursor(&self) -> bool {
        self.cursor.is_some()
    }
}