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}