[RFC PATCH v2 07/10] drm/vino: wire the hardware cursor plane

From: Mike Lothian

Date: Thu Jul 02 2026 - 23:21:58 EST


The pre-safe-KMS vino had a hardware cursor (a DRM_PLANE_TYPE_CURSOR plane whose
updates were tunnelled to the dock as the cp::cursor_{create,image,move} CP
messages); the v2 safe-KMS refactor dropped it because the safe KMS layer didn't
expose plane position/hotspot accessors. Those exist now, so re-wire it.

VinoPlane gains an `is_cursor` role (the safe KMS layer allows one plane type per
driver, so one type serves both the primary and cursor planes). probe() creates a
Type::Cursor plane (ARGB8888, max_cursor 64x64) alongside the primary and attaches
it to the CRTC. The cursor plane's atomic_update forwards to the dock over CP:
cursor_move (position, from the new plane crtc_x()/crtc_y()) on every update, and
cursor_create + cursor_image (the w*h*4 BGRA bitmap, read from the framebuffer via
the new Framebuffer::width()/pitch()/vmap()) only when the bitmap changes. Like
scanout it is gated behind CP engagement, so it is inert until the dock's
content-protection channel comes up.

The cp::cursor_* builders and their KUnit tests are unchanged.

Signed-off-by: Mike Lothian <mike@xxxxxxxxxxxxxx>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
drivers/gpu/drm/vino/drm_sink.rs | 98 +++++++++++++++++++++++++++++---
1 file changed, 89 insertions(+), 9 deletions(-)

diff --git a/drivers/gpu/drm/vino/drm_sink.rs b/drivers/gpu/drm/vino/drm_sink.rs
index 450c7c84a067..fde52336fcd1 100644
--- a/drivers/gpu/drm/vino/drm_sink.rs
+++ b/drivers/gpu/drm/vino/drm_sink.rs
@@ -18,9 +18,10 @@
//! wired here). `VinoPlane`/`VinoCrtc` hold their state inline rather than behind a
//! pointer-identity lookup table, so adding a head is a second `probe()` call away,
//! not a redesign.
-//! - A cursor plane, CRTC gamma LUT, and plane rotation property -- `kernel::drm::kms`
-//! doesn't yet expose `drm_plane_create_rotation_property` or a second (cursor)
-//! plane argument shape beyond what `UnregisteredCrtc::new` already threads through.
+//! - CRTC gamma LUT and plane rotation property -- `kernel::drm::kms` doesn't yet expose
+//! `drm_gamma_lut`/`drm_plane_create_rotation_property`. (The cursor plane *is* wired now:
+//! `VinoPlane` serves both the primary and a `Type::Cursor` plane, and `atomic_update` forwards
+//! the cursor bitmap/position to the dock via `cp::cursor_{create,image,move}`.)
//! - DDC/CI brightness/contrast as connector properties, and the `.detect`/`mode_valid`
//! connector hooks (report disconnected until a real EDID arrives; reject
//! over-budget modes) -- `DriverConnector` only exposes `get_modes` right now.
@@ -57,6 +58,11 @@
/// Primary-plane format list (opaque 32bpp scanout).
static PRIMARY_FORMATS: [u32; 1] = [DRM_FORMAT_XRGB8888];

+/// `DRM_FORMAT_ARGB8888` (`fourcc_code('A','R','2','4')`); the dock's cursor bitmap carries alpha.
+const DRM_FORMAT_ARGB8888: u32 = 0x3432_5241;
+/// Cursor-plane format list.
+static CURSOR_FORMATS: [u32; 1] = [DRM_FORMAT_ARGB8888];
+
/// Per-mode pixel-clock ceiling (kHz) -- about 4K@60 (CEA 594 MHz). With only one head wired
/// there is no combined-heads budget to enforce (see the module doc); a real ceiling still
/// needs the connector `mode_valid` hook this port doesn't expose yet, so this constant is
@@ -275,7 +281,7 @@ fn mode_config_info(_dev: &drm::Device<Self, drm::Uninit>) -> Result<ModeConfigI
Ok(ModeConfigInfo {
min_resolution: (0, 0),
max_resolution: (4096, 4096),
- max_cursor: (0, 0),
+ max_cursor: (64, 64),
preferred_depth: 32,
preferred_fourcc: Some(DRM_FORMAT_XRGB8888),
})
@@ -294,12 +300,21 @@ fn probe(dev: &NewKmsDevice<'_, Self, Probing>) -> Result {
None,
plane::Type::Primary,
None,
- (),
+ false,
+ )?;
+ let cursor = plane::UnregisteredPlane::<VinoPlane>::new(
+ dev,
+ 1,
+ &CURSOR_FORMATS,
+ None,
+ plane::Type::Cursor,
+ None,
+ true,
)?;
let crtc_obj = crtc::UnregisteredCrtc::<VinoCrtc>::new(
dev,
primary,
- None::<&plane::UnregisteredPlane<VinoPlane>>,
+ Some(&cursor),
None,
(),
)?;
@@ -381,12 +396,20 @@ fn atomic_disable(commit: CrtcAtomicCommit<'_, Self>) {
}
}

-// ---- Primary plane / scanout -----------------------------------------------
+// ---- Planes: primary (scanout) + cursor -------------------------------------
+//
+// The safe KMS layer allows one `DriverPlane` type per driver, so `VinoPlane` serves both the
+// primary and cursor planes, told apart by `is_cursor` (from the plane's `Args`).

#[pin_data]
pub(super) struct VinoPlane {
#[pin]
scanout: Mutex<ScanoutState>,
+ /// Whether this is the cursor plane (vs. the primary scanout plane).
+ is_cursor: bool,
+ /// The framebuffer last uploaded to the dock as the cursor bitmap (raw address, `0` = none),
+ /// so a bare cursor move only re-sends the position, not the whole image. Cursor plane only.
+ cursor_last: core::sync::atomic::AtomicUsize,
}

#[derive(Clone, Default)]
@@ -398,11 +421,11 @@ impl plane::DriverPlaneState for VinoPlaneState {

#[vtable]
impl plane::DriverPlane for VinoPlane {
- type Args = ();
+ type Args = bool;
type Driver = VinoDrmDriver;
type State = VinoPlaneState;

- fn new(_device: &drm::Device<Self::Driver, drm::Uninit>, _args: ()) -> impl PinInit<Self, Error> {
+ fn new(_device: &drm::Device<Self::Driver, drm::Uninit>, is_cursor: bool) -> impl PinInit<Self, Error> {
try_pin_init!(VinoPlane {
scanout <- new_mutex!(ScanoutState {
enc: None,
@@ -411,6 +434,8 @@ fn new(_device: &drm::Device<Self::Driver, drm::Uninit>, _args: ()) -> impl PinI
dims: (0, 0),
hint: 0,
}),
+ is_cursor,
+ cursor_last: core::sync::atomic::AtomicUsize::new(0),
})
}

@@ -429,6 +454,36 @@ fn atomic_update(commit: PlaneAtomicCommit<'_, Self>) {
let plane = commit.plane();
let data: &VinoDrmData = plane.drm_dev();
let new = commit.take_new_state();
+
+ // Cursor plane: forward the cursor bitmap/position to the dock over CP (id=0x1b create,
+ // 0x401c image, 0x1a move -- see `cp::cursor_*`). A no-op until CP engages, like scanout.
+ if plane.is_cursor {
+ use core::sync::atomic::Ordering::Relaxed;
+ const HEAD: u8 = 0;
+ match new.framebuffer::<VinoDrmDriver>() {
+ Some(fb) => {
+ let w = fb.width() as u16;
+ let h = fb.height() as u16;
+ // Re-upload the bitmap only when the framebuffer changes; always update the
+ // position (a bare cursor move must not re-send the whole image).
+ let key = fb as *const _ as usize;
+ if plane.cursor_last.swap(key, Relaxed) != key {
+ let _ = data.send_cp(0x1b, 0, |ctr| super::cp::cursor_create(ctr, HEAD, w, h));
+ if let Ok(bgra) = read_cursor_bgra(fb, w as usize, h as usize) {
+ let _ = data
+ .send_cp(0x401c, 0, |ctr| super::cp::cursor_image(ctr, HEAD, w, h, &bgra));
+ }
+ }
+ let x = new.crtc_x().max(0) as u16;
+ let y = new.crtc_y().max(0) as u16;
+ let _ = data.send_cp(0x1a, 0, |ctr| super::cp::cursor_move(ctr, HEAD, x, y));
+ }
+ // Cursor disabled: forget the last bitmap so it re-uploads if it comes back.
+ None => plane.cursor_last.store(0, Relaxed),
+ }
+ return;
+ }
+
let Some(fb) = new.framebuffer::<VinoDrmDriver>() else { return };
// The plane's destination geometry mirrors the negotiated mode (the compositor sizes the
// primary plane 1:1 with the virtual output), so this drives the dynamic scanout
@@ -480,6 +535,31 @@ fn prime_video_eps(dev: &super::usb::Interface<kernel::device::Bound>) {
}
}

+/// Map the cursor framebuffer and convert it to the `w*h*4` BGRA bitmap the dock expects
+/// (`cp::cursor_image`). The source is XRGB/ARGB8888; each pixel is written out as B, G, R, A.
+fn read_cursor_bgra(
+ fb: &kms::framebuffer::Framebuffer<VinoDrmDriver>,
+ w: usize,
+ h: usize,
+) -> Result<KVec<u8>> {
+ let vmap = fb.vmap()?;
+ let src = vmap.as_ptr();
+ let pitch = fb.pitch(0) as usize;
+ let mut out = KVec::with_capacity(w * h * 4, GFP_KERNEL)?;
+ for dy in 0..h {
+ for dx in 0..w {
+ // SAFETY: `dy*pitch + dx*4 + 3` is within the mapped cursor framebuffer (`pitch*h`
+ // bytes); `dx < w <= pitch/4`, `dy < h`.
+ let px = unsafe { (src.add(dy * pitch + dx * 4) as *const u32).read_unaligned() };
+ out.push((px & 0xff) as u8, GFP_KERNEL)?; // B
+ out.push(((px >> 8) & 0xff) as u8, GFP_KERNEL)?; // G
+ out.push(((px >> 16) & 0xff) as u8, GFP_KERNEL)?; // R
+ out.push(((px >> 24) & 0xff) as u8, GFP_KERNEL)?; // A
+ }
+ }
+ Ok(out)
+}
+
/// vmap `fb`, encode it, and push one EP08 frame. Split out so `?` can be used.
fn scanout_one(
data: &VinoDrmData,
--
2.55.0