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