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 <[email protected]>
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

Reply via email to