Rust for Linux List
 help / color / mirror / Atom feed
From: Mike Lothian <mike@fireburn.co.uk>
To: rust-for-linux@vger.kernel.org
Cc: dri-devel@lists.freedesktop.org,
	"David Airlie" <airlied@gmail.com>,
	"Simona Vetter" <simona@ffwll.ch>,
	"Thomas Zimmermann" <tzimmermann@suse.de>,
	"Maarten Lankhorst" <maarten.lankhorst@linux.intel.com>,
	"Maxime Ripard" <mripard@kernel.org>,
	"Danilo Krummrich" <dakr@kernel.org>,
	"Lyude Paul" <lyude@redhat.com>,
	"Miguel Ojeda" <ojeda@kernel.org>,
	"Boqun Feng" <boqun@kernel.org>, "Gary Guo" <gary@garyguo.net>,
	"Björn Roy Baron" <bjorn3_gh@protonmail.com>,
	"Benno Lossin" <lossin@kernel.org>,
	"Andreas Hindborg" <a.hindborg@kernel.org>,
	"Alice Ryhl" <aliceryhl@google.com>,
	"Trevor Gross" <tmgross@umich.edu>,
	linux-kernel@vger.kernel.org,
	"Mike Lothian" <mike@fireburn.co.uk>
Subject: [RFC PATCH v2 09/10] drm/vino: two heads, 90/270 rotation, damage clips and connector probe
Date: Fri,  3 Jul 2026 04:02:14 +0100	[thread overview]
Message-ID: <20260703030217.2886-10-mike@fireburn.co.uk> (raw)
In-Reply-To: <20260703030217.2886-1-mike@fireburn.co.uk>

Consume the new connector detect()/mode_valid() and plane damage-clip
bindings, and finish the KMS feature set the driver previously stubbed:

  - Two display heads. probe() builds an independent CRTC + primary/cursor
    plane + encoder + connector per head (see HEADS), each pinned to its
    own video endpoint (VIDEO_EPS: head 0 -> EP08, head 1 -> EP0b) via a
    head index carried in VinoPlane. The scanout and cursor CP messages
    route by that head; per-head EDID uses a per-head connector pointer
    array. The CP mode-set has no decoded head field, so a head is
    conveyed on the wire only by which endpoint its frames go to.

  - 90/270 plane rotation. The rotation property now advertises all four
    90-degree rotations plus reflection; the scanout maps each output
    pixel back to its source pixel with rot_src(), and src_dims() swaps
    the source/output dimensions for 90/270 (rot_src itself was already
    correct given source dimensions -- only its callers were feeding it
    output dims).

  - Damage clips. The RLE scanout takes the old+new plane state, merges
    the client's damage clips (RawPlaneState::damage_merged) into one
    rectangle, and re-converts only that region into the shadow the
    encoder diffs against (identity rotation only; the WHT keyframe path
    is unaffected).

  - Connector detect()/mode_valid(). A head reports connected once its
    downstream EDID arrives (no phantom output for an unpopulated head),
    and modes above MAX_HEAD_CLOCK_KHZ (~4K@60) are pruned.

Still inert until the dock engages CP (see docs/BLOCKER.md).

Signed-off-by: Mike Lothian <mike@fireburn.co.uk>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
 drivers/gpu/drm/vino/drm_sink.rs | 426 +++++++++++++++++++++----------
 drivers/gpu/drm/vino/vino.rs     |   8 +-
 2 files changed, 292 insertions(+), 142 deletions(-)

diff --git a/drivers/gpu/drm/vino/drm_sink.rs b/drivers/gpu/drm/vino/drm_sink.rs
index 898cfae8fd4f..ee04a5af9f7d 100644
--- a/drivers/gpu/drm/vino/drm_sink.rs
+++ b/drivers/gpu/drm/vino/drm_sink.rs
@@ -2,31 +2,38 @@
 
 //! DRM/KMS sink: register a real `struct drm_device` with an atomic mode-setting
 //! pipeline so the dock appears to userspace as a `card`/`renderD` node that can be
-//! `drmModeSetCrtc`'d. One CRTC driven by a single primary plane
-//! ([`VinoPlane::atomic_update`] -> EP08 scanout), a virtual encoder, and a virtual
-//! connector whose mode list comes from the dock's real EDID (falling back to 1080p),
-//! with GEM-shmem dumb buffers and `drm_gem_fb_create` framebuffers.
+//! `drmModeSetCrtc`'d. Two independent display heads (see [`HEADS`]), each a CRTC driven by a
+//! primary plane ([`VinoPlane::atomic_update`] -> per-head video endpoint scanout), a cursor plane,
+//! a virtual encoder, and a virtual connector whose mode list comes from the dock's real EDID
+//! (falling back to 1080p), with GEM-shmem dumb buffers and `drm_gem_fb_create` framebuffers.
 //!
 //! Built on the safe KMS mode-object layer (`kernel::drm::kms`), not the raw
 //! `bindings::drm_*` C API: `VinoDrmDriver` implements `drm::kms::KmsDriver`, and each
 //! mode object (`VinoCrtc`/`VinoPlane`/`VinoConnector`/`VinoEncoder`) implements the
 //! matching `Driver*` trait rather than hand-assembling a C vtable.
 //!
-//! Wired onto the safe KMS layer: primary plane (EP08 scanout), a `Type::Cursor` plane (bitmap +
-//! position forwarded via `cp::cursor_{create,image,move}`), a 256-entry CRTC `GAMMA_LUT` (applied
-//! in the scanout), a primary-plane rotation property (0/180 + reflect, applied per source pixel
-//! via `rot_src`), and a DDC/CI virtual I2C adapter ([`VinoI2c`], tunnelling monitor-control writes
-//! to the dock over CP -- brightness/contrast/etc. via `ddcutil`), alongside the DPMS-power VCP the
-//! CRTC hooks already send.
+//! Wired onto the safe KMS layer:
+//! - Per-head primary plane scanout to that head's video endpoint ([`VIDEO_EPS`]) and a
+//!   `Type::Cursor` plane (bitmap + position forwarded via `cp::cursor_{create,image,move}` with
+//!   the head as the CP `head` field).
+//! - A 256-entry CRTC `GAMMA_LUT` (applied in the scanout) and a full plane rotation property
+//!   (all four 90-degree rotations plus X/Y reflection), applied per source pixel via `rot_src`
+//!   (90/270 swap the source/output dimensions -- see [`src_dims`]).
+//! - Frame-damage clips: the RLE scanout re-converts only the client's changed rectangles
+//!   (`RawPlaneState::for_each_damage_clip`) for identity rotation.
+//! - Connector `detect()` (connected once the head's EDID arrives) and `mode_valid()` (prune modes
+//!   above [`MAX_HEAD_CLOCK_KHZ`]).
+//! - A DDC/CI virtual I2C adapter ([`VinoI2c`], tunnelling monitor-control writes to the dock over
+//!   CP -- brightness/contrast/etc. via `ddcutil`), alongside the DPMS-power VCP the CRTC hooks
+//!   send.
 //!
-//! Not yet ported (needs `kernel::drm::kms` extension points that don't exist yet; not fabricated):
-//! - A second display head (the DL3 protocol supports up to 4; one is wired here).
-//!   `VinoPlane`/`VinoCrtc` hold their state inline, so a head is a second `probe()` call away.
-//! - 90/270 rotation (swaps width/height, unlike the dimension-preserving rotations above).
+//! Not yet done (needs hardware capture past the CP wall; not fabricated):
+//! - Per-head EDID reads (the bring-up reads head 0's EDID only) and per-head mode-set / DDC
+//!   differentiation on the wire -- the CP mode-set (`id=0x48`) has no decoded head/stream field,
+//!   so the head is conveyed only by which video endpoint its frames go to.
+//! - The WHT keyframe codec ignores damage clips (strip-based); damage applies to the RLE path.
 //! - DDC/CI *reads* (Get-VCP) -- need the dock's CP reply path; and brightness/contrast as
-//!   *connector properties* (the I2C adapter above is the interface for now).
-//! - The `.detect`/`mode_valid` connector hooks -- `DriverConnector` only exposes `get_modes`.
-//! - Damage-clip bounded conversion (always converts the full frame).
+//!   *connector properties* (the I2C adapter is the interface for now).
 //!
 //! None of this is reachable on real hardware yet regardless: the dock never engages
 //! its content-protection channel for vino (see `docs/BLOCKER.md`), so `atomic_update`
@@ -36,9 +43,10 @@
     bindings, drm,
     drm::kms::{
         self,
-        connector::{self, ConnectorGuard},
+        connector::{self, Connector, ConnectorGuard, ModeStatus, Status},
         crtc::{self, CrtcAtomicCommit, RawCrtc as _, RawCrtcState as _},
         encoder,
+        modes::DisplayMode,
         plane::{self, PlaneAtomicCommit, RawPlaneState as _},
         KmsDriver, ModeConfigGuard, ModeConfigInfo, ModeObject as _, NewKmsDevice, Probing,
     },
@@ -65,16 +73,24 @@
 /// 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
-/// currently unused wiring for when it does.
-#[allow(dead_code)]
+/// Per-mode pixel-clock ceiling (kHz) -- about 4K@60 (CEA 594 MHz). Modes above this are pruned
+/// by the connector `mode_valid` hook ([`VinoConnector::mode_valid`]).
 const MAX_HEAD_CLOCK_KHZ: i32 = 600_000;
 
-/// The one wired display head's video bulk-OUT endpoint (see the module doc -- only
-/// head 0 is wired for now).
-const VIDEO_EP: u8 = 0x08;
+/// Number of display heads wired. The DL3 protocol supports up to 4; two are wired here (the
+/// common dual-monitor dock), each scanned out to its own video endpoint ([`VIDEO_EPS`]).
+const HEADS: usize = 2;
+
+/// Per-head video bulk-OUT endpoint: head 0 -> EP08, head 1 -> EP0b (captured from DLM driving a
+/// two-monitor dock). The endpoint is the head selector for scanout; the cursor uses the CP `head`
+/// field (see [`VinoPlane::atomic_update`]).
+const VIDEO_EPS: [u8; HEADS] = [0x08, 0x0b];
+
+/// Maximum number of individual frame-damage rectangles re-converted per flip before they are
+/// collapsed into a single bounding box. Bounds the stack array used on the atomic-commit path
+/// (no per-flip allocation); a compositor that reports more clips than this just gets a coarser
+/// (still correct) repaint.
+const MAX_DAMAGE_CLIPS: usize = 16;
 
 /// The DRM driver marker type.
 pub(super) struct VinoDrmDriver;
@@ -123,10 +139,11 @@ pub(super) struct VinoDrmData {
     intf: ARef<super::usb::Interface>,
     #[pin]
     cp_link: Mutex<Option<CpLink>>,
-    /// The device's one connector, stashed by [`KmsDriver::probe`] so [`VinoDrmData::set_edid`]
-    /// can reach its cached-EDID slot without needing DRM's mode-object list. Written once,
+    /// The device's per-head connectors, stashed by [`KmsDriver::probe`] so
+    /// [`VinoDrmData::set_edid`] can reach a head's cached-EDID slot without needing DRM's
+    /// mode-object list. Written once,
     /// during single-threaded probe, before the device is registered; read-only thereafter.
-    connector: core::sync::atomic::AtomicPtr<VinoConnector>,
+    connectors: [core::sync::atomic::AtomicPtr<VinoConnector>; HEADS],
     /// The CRTC's gamma ramp cached from the atomic hook as three 256-entry 8-bit LUTs
     /// (`[r; 256] ++ [g; 256] ++ [b; 256]`), or `None` for identity. Cached here (not read from
     /// the CRTC state) because scanout runs in the plane path; it is `Copy`, so the scanout
@@ -140,7 +157,9 @@ pub(super) fn new(intf: ARef<super::usb::Interface>) -> impl PinInit<Self, Error
         try_pin_init!(Self {
             intf,
             cp_link <- new_mutex!(Option::<CpLink>::None),
-            connector: core::sync::atomic::AtomicPtr::new(core::ptr::null_mut()),
+            connectors: core::array::from_fn(|_| {
+                core::sync::atomic::AtomicPtr::new(core::ptr::null_mut())
+            }),
             gamma <- new_mutex!(None),
         })
     }
@@ -210,12 +229,14 @@ pub(super) fn set_vcp(&self, vcp: u8, value: u16) -> Result {
         self.send_cp(0x15, 0, |ctr| super::cp::ddc_set_vcp(ctr, vcp, value))
     }
 
-    /// Cache the dock's EDID (read during probe) for the connector's `get_modes` to install,
-    /// then fire a hotplug so the compositor re-probes the connector. Only the connector
-    /// itself holds the cached blob (see [`VinoConnector::cached_edid`]); this just forwards
-    /// it there via the pointer [`KmsDriver::probe`] stashed in `self.connector`.
-    pub(super) fn set_edid(&self, dev: &VinoDrmDevice, blob: KVec<u8>) {
-        let ptr = self.connector.load(core::sync::atomic::Ordering::Acquire);
+    /// Cache a head's downstream EDID (read during probe) for that connector's `get_modes` to
+    /// install, then fire a hotplug so the compositor re-probes it. Only the connector itself
+    /// holds the cached blob (see [`VinoConnector::cached_edid`]); this forwards it there via the
+    /// pointer [`KmsDriver::probe`] stashed in `self.connectors[head]`. Out-of-range `head` is a
+    /// no-op.
+    pub(super) fn set_edid(&self, dev: &VinoDrmDevice, head: usize, blob: KVec<u8>) {
+        let Some(slot) = self.connectors.get(head) else { return };
+        let ptr = slot.load(core::sync::atomic::Ordering::Acquire);
         let Some(connector) = (unsafe { ptr.as_ref() }) else { return };
         *connector.cached_edid.lock() = Some(blob);
         dev.hotplug_event();
@@ -319,69 +340,77 @@ fn mode_config_info(_dev: &drm::Device<Self, drm::Uninit>) -> Result<ModeConfigI
     }
 
     fn probe(dev: &NewKmsDevice<'_, Self, Probing>) -> Result {
-        // Order matters: `possible_crtcs` for the plane/encoder is a bitmask of CRTC
-        // *indices*, which only exist once `UnregisteredCrtc::new` runs -- but planes
-        // must exist before the CRTC that references them. With exactly one CRTC ever
-        // created here, its index is always 0, so `possible_crtcs = 1` is correct by
-        // construction rather than needing the CRTC up front.
-        let primary = plane::UnregisteredPlane::<VinoPlane>::new(
-            dev,
-            1,
-            &PRIMARY_FORMATS,
-            None,
-            plane::Type::Primary,
-            None,
-            false,
-        )?;
-        // Advertise the rotations vino's re-encode handles 1:1 (dimension-preserving): the
-        // scanout applies them per source pixel via `rot_src`. 90/270 (which swap width/height)
-        // are intentionally omitted.
-        primary.create_rotation_property(
-            bindings::DRM_MODE_ROTATE_0,
-            bindings::DRM_MODE_ROTATE_0
-                | bindings::DRM_MODE_ROTATE_180
-                | bindings::DRM_MODE_REFLECT_X
-                | bindings::DRM_MODE_REFLECT_Y,
-        )?;
-        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,
-            Some(&cursor),
-            None,
-            (),
-        )?;
-        // Advertise a 256-entry GAMMA_LUT; the scanout applies it (cached via the CRTC hooks).
-        crtc_obj.enable_gamma(256);
-        let enc = encoder::UnregisteredEncoder::<VinoEncoder>::new(
-            dev,
-            encoder::Type::Virtual,
-            crtc_obj.mask(),
-            0,
-            None,
-            (),
-        )?;
-        let conn =
-            connector::UnregisteredConnector::<VinoConnector>::new(dev, connector::Type::Virtual, ())?;
-        conn.attach_encoder(&*enc)?;
-        // Stash a pointer to our own connector data (not the wrapping `Connector<T>`, which
-        // this crate has no public way to reconstruct from a reference) so
-        // `VinoDrmData::set_edid` can reach it later without walking DRM's mode-object list.
-        // `conn` outlives the device (destroyed only alongside it), so this is valid for as
-        // long as `dev.connector` is read.
         let data: &VinoDrmData = dev;
-        data.connector.store(
-            &**conn as *const VinoConnector as *mut VinoConnector,
-            core::sync::atomic::Ordering::Release,
-        );
+        // Build one independent head (CRTC + primary/cursor plane + encoder + connector) per
+        // wired display, each pinned to its own video endpoint via its head index.
+        for head in 0..HEADS {
+            // `possible_crtcs` for the plane/encoder is a bitmask of CRTC *indices*, which only
+            // exist once `UnregisteredCrtc::new` runs -- but planes must exist before the CRTC
+            // that references them. CRTCs are created here one per head in order, so this head's
+            // CRTC index is `head` and its mask is `1 << head`.
+            let crtc_mask = 1u32 << head;
+            let primary = plane::UnregisteredPlane::<VinoPlane>::new(
+                dev,
+                crtc_mask,
+                &PRIMARY_FORMATS,
+                None,
+                plane::Type::Primary,
+                None,
+                PlaneArgs { head: head as u8, is_cursor: false },
+            )?;
+            // Advertise every rotation vino's re-encode can produce by remapping source pixels
+            // (`rot_src`): the four 90-degree rotations plus the two reflections.
+            primary.create_rotation_property(
+                bindings::DRM_MODE_ROTATE_0,
+                bindings::DRM_MODE_ROTATE_0
+                    | bindings::DRM_MODE_ROTATE_90
+                    | bindings::DRM_MODE_ROTATE_180
+                    | bindings::DRM_MODE_ROTATE_270
+                    | bindings::DRM_MODE_REFLECT_X
+                    | bindings::DRM_MODE_REFLECT_Y,
+            )?;
+            let cursor = plane::UnregisteredPlane::<VinoPlane>::new(
+                dev,
+                crtc_mask,
+                &CURSOR_FORMATS,
+                None,
+                plane::Type::Cursor,
+                None,
+                PlaneArgs { head: head as u8, is_cursor: true },
+            )?;
+            let crtc_obj = crtc::UnregisteredCrtc::<VinoCrtc>::new(
+                dev,
+                primary,
+                Some(&cursor),
+                None,
+                head as u8,
+            )?;
+            // Advertise a 256-entry GAMMA_LUT; the scanout applies it (cached via the CRTC hooks).
+            crtc_obj.enable_gamma(256);
+            let enc = encoder::UnregisteredEncoder::<VinoEncoder>::new(
+                dev,
+                encoder::Type::Virtual,
+                crtc_obj.mask(),
+                0,
+                None,
+                (),
+            )?;
+            let conn = connector::UnregisteredConnector::<VinoConnector>::new(
+                dev,
+                connector::Type::Virtual,
+                (),
+            )?;
+            conn.attach_encoder(&*enc)?;
+            // Stash a pointer to our own connector data (not the wrapping `Connector<T>`, which
+            // this crate has no public way to reconstruct from a reference) so
+            // `VinoDrmData::set_edid` can reach it later without walking DRM's mode-object list.
+            // `conn` outlives the device (destroyed only alongside it), so this is valid for as
+            // long as `dev.connectors[head]` is read.
+            data.connectors[head].store(
+                &**conn as *const VinoConnector as *mut VinoConnector,
+                core::sync::atomic::Ordering::Release,
+            );
+        }
         Ok(())
     }
 }
@@ -389,7 +418,11 @@ fn probe(dev: &NewKmsDevice<'_, Self, Probing>) -> Result {
 // ---- CRTC -----------------------------------------------------------------
 
 #[pin_data]
-pub(super) struct VinoCrtc;
+pub(super) struct VinoCrtc {
+    /// Which display head (0-based) this CRTC drives. Used for diagnostics; the mode-set/DDC CP
+    /// messages this CRTC sends are not yet head-differentiated on the wire (see the module doc).
+    head: u8,
+}
 
 #[derive(Clone, Default)]
 pub(super) struct VinoCrtcState;
@@ -400,26 +433,32 @@ impl crtc::DriverCrtcState for VinoCrtcState {
 
 #[vtable]
 impl crtc::DriverCrtc for VinoCrtc {
-    type Args = ();
+    type Args = u8;
     type Driver = VinoDrmDriver;
     type State = VinoCrtcState;
     type VblankImpl = core::marker::PhantomData<Self>;
 
-    fn new(_device: &drm::Device<Self::Driver, drm::Uninit>, _args: &()) -> impl PinInit<Self, Error> {
-        try_pin_init!(VinoCrtc {})
+    fn new(
+        _device: &drm::Device<Self::Driver, drm::Uninit>,
+        head: &u8,
+    ) -> impl PinInit<Self, Error> {
+        try_pin_init!(VinoCrtc { head: *head })
     }
 
     /// The display is turning on (scanout begins). Pushes a live mode-set CP message for the
     /// negotiated mode and brings the monitor out of DPMS standby -- both no-ops until CP
     /// engages (the wall).
     fn atomic_enable(commit: CrtcAtomicCommit<'_, Self>) {
-        let data: &VinoDrmData = commit.crtc().drm_dev();
+        let crtc = commit.crtc();
+        let head = crtc.head;
+        let data: &VinoDrmData = crtc.drm_dev();
         let new = commit.take_new_state();
         // Cache the gamma ramp for the scanout to apply.
         data.update_gamma(new.gamma_lut());
         let timing = super::cp::timing_from_drm_mode(new.mode());
         pr_info!(
-            "vino: KMS CRTC enable -- display ON, mode {}x{}@{} (scanout begins)\n",
+            "vino: KMS CRTC enable -- head {} display ON, mode {}x{}@{} (scanout begins)\n",
+            head,
             timing.hactive,
             timing.vactive,
             timing.refresh_hz
@@ -435,10 +474,12 @@ fn atomic_enable(commit: CrtcAtomicCommit<'_, Self>) {
     /// against a shadow the dock may have dropped, and blanks the monitor via DDC/CI -- a
     /// no-op until CP engages.
     fn atomic_disable(commit: CrtcAtomicCommit<'_, Self>) {
-        let data: &VinoDrmData = commit.crtc().drm_dev();
+        let crtc = commit.crtc();
+        let head = crtc.head;
+        let data: &VinoDrmData = crtc.drm_dev();
         data.update_gamma(None);
         let _ = data.set_vcp(super::cp::VCP_POWER_MODE, super::cp::POWER_OFF);
-        pr_info!("vino: KMS CRTC disable -- display OFF (scanout stopped)\n");
+        pr_info!("vino: KMS CRTC disable -- head {head} display OFF (scanout stopped)\n");
     }
 }
 
@@ -447,10 +488,21 @@ fn atomic_disable(commit: CrtcAtomicCommit<'_, Self>) {
 // 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`).
 
+/// Constructor arguments for a [`VinoPlane`]: which head it belongs to and whether it is that
+/// head's cursor plane (vs. its primary scanout plane).
+#[derive(Clone, Copy)]
+pub(super) struct PlaneArgs {
+    head: u8,
+    is_cursor: bool,
+}
+
 #[pin_data]
 pub(super) struct VinoPlane {
     #[pin]
     scanout: Mutex<ScanoutState>,
+    /// Which display head (0-based) this plane belongs to. Selects the scanout video endpoint
+    /// ([`VIDEO_EPS`]) and the cursor CP `head` field.
+    head: u8,
     /// 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),
@@ -467,11 +519,14 @@ impl plane::DriverPlaneState for VinoPlaneState {
 
 #[vtable]
 impl plane::DriverPlane for VinoPlane {
-    type Args = bool;
+    type Args = PlaneArgs;
     type Driver = VinoDrmDriver;
     type State = VinoPlaneState;
 
-    fn new(_device: &drm::Device<Self::Driver, drm::Uninit>, is_cursor: bool) -> impl PinInit<Self, Error> {
+    fn new(
+        _device: &drm::Device<Self::Driver, drm::Uninit>,
+        args: PlaneArgs,
+    ) -> impl PinInit<Self, Error> {
         try_pin_init!(VinoPlane {
             scanout <- new_mutex!(ScanoutState {
                 enc: None,
@@ -480,7 +535,8 @@ fn new(_device: &drm::Device<Self::Driver, drm::Uninit>, is_cursor: bool) -> imp
                 dims: (0, 0),
                 hint: 0,
             }),
-            is_cursor,
+            head: args.head,
+            is_cursor: args.is_cursor,
             cursor_last: core::sync::atomic::AtomicUsize::new(0),
         })
     }
@@ -498,14 +554,14 @@ fn atomic_update(commit: PlaneAtomicCommit<'_, Self>) {
             return;
         }
         let plane = commit.plane();
+        let head = plane.head;
         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;
+            let new = commit.take_new_state();
             match new.framebuffer::<VinoDrmDriver>() {
                 Some(fb) => {
                     let w = fb.width() as u16;
@@ -514,15 +570,17 @@ fn atomic_update(commit: PlaneAtomicCommit<'_, Self>) {
                     // 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));
+                        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 _ = 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));
+                    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),
@@ -530,6 +588,8 @@ fn atomic_update(commit: PlaneAtomicCommit<'_, Self>) {
             return;
         }
 
+        // Primary plane: take both old and new state so the frame-damage clips can be merged.
+        let (old, new) = commit.take_old_new_state();
         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
@@ -537,6 +597,37 @@ fn atomic_update(commit: PlaneAtomicCommit<'_, Self>) {
         let (w, h) = (new.crtc_w() as usize, new.crtc_h() as usize);
         // Plane rotation/reflection (identity unless the compositor set the rotation property).
         let rotation = new.rotation();
+        // Collect the client's individual frame-damage clips (the rectangles that
+        // `damage_merged()` would collapse into one bounding box), each clamped to the output, so
+        // only the genuinely changed rectangles are re-converted from the source rather than their
+        // whole enclosing box. Only for identity rotation (the clips are in un-rotated source
+        // space; mapping them through 90/270 is not worth it for the throttled fallback path), and
+        // never on the WHT keyframe path -- see `encode_and_send`. A fixed stack array keeps the
+        // atomic-commit path allocation-free; on overflow the clips collapse into one bounding box.
+        // An empty list means "convert the whole output" (used for the rotated/reflected case).
+        let mut clips = [(0usize, 0usize, 0usize, 0usize); MAX_DAMAGE_CLIPS];
+        let mut nclips = 0usize;
+        if rotation & bindings::DRM_MODE_ROTATE_MASK == bindings::DRM_MODE_ROTATE_0
+            && rotation & (bindings::DRM_MODE_REFLECT_X | bindings::DRM_MODE_REFLECT_Y) == 0
+        {
+            new.for_each_damage_clip(old, |r| {
+                let c = (
+                    (r.x1.max(0) as usize).min(w),
+                    (r.y1.max(0) as usize).min(h),
+                    (r.x2.max(0) as usize).min(w),
+                    (r.y2.max(0) as usize).min(h),
+                );
+                if nclips < MAX_DAMAGE_CLIPS {
+                    clips[nclips] = c;
+                    nclips += 1;
+                } else {
+                    // Overflow: collapse everything so far (and `c`) into clips[0]'s bounding box.
+                    let b = clips[0];
+                    clips[0] = (b.0.min(c.0), b.1.min(c.1), b.2.max(c.2), b.3.max(c.3));
+                    nclips = 1;
+                }
+            });
+        }
 
         use core::sync::atomic::Ordering::Relaxed;
         // Throttle: while scanout is failing (dock NAKing because CP isn't engaged), skip the
@@ -547,7 +638,7 @@ fn atomic_update(commit: PlaneAtomicCommit<'_, Self>) {
             super::SCANOUT_SKIP.store(skip - 1, Relaxed);
             return;
         }
-        match scanout_one(data, plane, fb, rotation, w, h) {
+        match scanout_one(data, plane, fb, rotation, &clips[..nclips], w, h) {
             Ok(()) => {
                 let n = super::SCANOUT_FAILS.swap(0, Relaxed);
                 super::SCANOUT_SKIP.store(0, Relaxed);
@@ -608,12 +699,27 @@ fn read_cursor_bgra(
     Ok(out)
 }
 
-/// vmap `fb`, encode it, and push one EP08 frame. Split out so `?` can be used.
+/// Source (framebuffer) dimensions for an output of `ow`x`oh` pixels under plane `rotation`.
+/// The 90/270 rotations swap width and height between the framebuffer and the displayed output;
+/// the others preserve them.
+fn src_dims(rotation: u32, ow: usize, oh: usize) -> (usize, usize) {
+    let rot = rotation & bindings::DRM_MODE_ROTATE_MASK;
+    if rot == bindings::DRM_MODE_ROTATE_90 || rot == bindings::DRM_MODE_ROTATE_270 {
+        (oh, ow)
+    } else {
+        (ow, oh)
+    }
+}
+
+/// vmap `fb`, encode it, and push one video frame to the head's endpoint. Split out so `?` can be
+/// used. `w`/`h` are the OUTPUT (displayed) dimensions; `clips` are the client's changed
+/// rectangles in output space (empty = repaint the whole output).
 fn scanout_one(
     data: &VinoDrmData,
     plane: &plane::Plane<VinoPlane>,
     fb: &kms::framebuffer::Framebuffer<VinoDrmDriver>,
     rotation: u32,
+    clips: &[(usize, usize, usize, usize)],
     w: usize,
     h: usize,
 ) -> Result {
@@ -626,7 +732,7 @@ fn scanout_one(
     // The real source stride: GEM dumb buffers pad the pitch (alignment), so it is not necessarily
     // `w * 4` -- read it from the framebuffer rather than assuming.
     let pitch = fb.pitch(0) as usize;
-    encode_and_send(data, plane, vmap.as_ptr(), pitch, rotation, w, h)
+    encode_and_send(data, plane, vmap.as_ptr(), pitch, rotation, clips, w, h)
 }
 
 /// Encode the mapped frame with the byte-exact Vino WHT **colour** codec and bulk-write the
@@ -646,11 +752,13 @@ fn encode_and_send_wht(
 ) -> Result {
     let seq0 = plane.scanout.lock().seq;
     let gamma = data.gamma_snapshot();
+    // Source dimensions (swapped from the output for 90/270 rotation).
+    let (sw, sh) = src_dims(rotation, w, h);
     let (frames, next_seq) = super::video::wht::colour_frame_ep08(w, h, seq0, |dx, dy| {
         // Map the output pixel back to its source pixel under the plane rotation/reflection.
-        let (sx, sy) = rot_src(rotation, dx, dy, w, h);
-        // SAFETY: `sy*pitch + sx*4 + 3` is within the mapped source framebuffer (`pitch*h` bytes);
-        // `rot_src` returns `sx < w <= pitch/4`, `sy < h`.
+        let (sx, sy) = rot_src(rotation, dx, dy, sw, sh);
+        // SAFETY: `sy*pitch + sx*4 + 3` is within the mapped source framebuffer (`pitch*sh`
+        // bytes); `rot_src` returns `sx < sw <= pitch/4`, `sy < sh`.
         let px = unsafe { (vaddr.add(sy * pitch + sx * 4) as *const u32).read_unaligned() };
         apply_gamma(
             &gamma,
@@ -665,8 +773,9 @@ fn encode_and_send_wht(
     // in `disconnect()`, which first unplugs the DRM device, so it is bound for this push.
     let dev = unsafe { data.intf.as_bound() };
     prime_video_eps(dev);
+    let ep = VIDEO_EPS[plane.head as usize];
     for frame in frames.iter() {
-        dev.bulk_send(VIDEO_EP, frame, super::timeout(), GFP_KERNEL)?;
+        dev.bulk_send(ep, frame, super::timeout(), GFP_KERNEL)?;
     }
     Ok(())
 }
@@ -679,13 +788,14 @@ fn encode_and_send(
     vaddr: *const u8,
     pitch: usize,
     rotation: u32,
+    clips: &[(usize, usize, usize, usize)],
     w: usize,
     h: usize,
 ) -> Result {
     // WHT colour codec path (default off): the byte-exact, bandwidth-efficient DLM-quality
     // codec. Requires a 64x16-aligned mode; for non-aligned geometry (e.g. 1080p, height
     // 1080 % 16 = 8) fall through to the RLE path until partial-strip edge handling is
-    // captured.
+    // captured. The keyframe codec ignores damage clips.
     if super::EP08_WHT_CODEC
         && w % super::video::wht::STRIP_W == 0
         && h % super::video::wht::STRIP_H == 0
@@ -693,6 +803,8 @@ fn encode_and_send(
         return encode_and_send_wht(data, plane, vaddr, pitch, rotation, w, h);
     }
     let gamma = data.gamma_snapshot();
+    // Source dimensions (swapped from the output for 90/270 rotation).
+    let (sw, sh) = src_dims(rotation, w, h);
     let frame = {
         let mut st = plane.scanout.lock();
         // On the first frame `cur` is freshly zeroed, so the whole buffer must be filled.
@@ -708,21 +820,31 @@ fn encode_and_send(
             st.hint = 0;
         }
         let ScanoutState { enc, cur, seq, hint, dims: _ } = &mut *st;
-        for dy in 0..h {
-            for dx in 0..w {
-                // Map the output pixel back to its source pixel under the plane rotation.
-                let (sx, sy) = rot_src(rotation, dx, dy, w, h);
-                // SAFETY: `sy*pitch + sx*4 + 3` is within the mapped source framebuffer
-                // (`pitch*h` bytes); `rot_src` returns `sx < w <= pitch/4`, `sy < h`.
-                let px = unsafe { (vaddr.add(sy * pitch + sx * 4) as *const u32).read_unaligned() };
-                let (r, g, b) = apply_gamma(
-                    &gamma,
-                    ((px >> 16) & 0xff) as u8,
-                    ((px >> 8) & 0xff) as u8,
-                    (px & 0xff) as u8,
-                );
-                let (r, g, b) = (r as u32, g as u32, b as u32);
-                cur[dy * w + dx] = (((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)) as u16;
+        // Re-convert only the client's changed rectangles into `cur`; the rest already holds the
+        // previous frame, which the encoder diffs against. On the first frame there is no valid
+        // previous frame (and on a rotated output there are no usable clips), so convert the whole
+        // output. `clips` are already clamped to the output in `atomic_update`.
+        let full = [(0usize, 0usize, w, h)];
+        let regions: &[(usize, usize, usize, usize)] =
+            if first || clips.is_empty() { &full } else { clips };
+        for &(x0, y0, x1, y1) in regions {
+            for dy in y0..y1 {
+                for dx in x0..x1 {
+                    // Map the output pixel back to its source pixel under the plane rotation.
+                    let (sx, sy) = rot_src(rotation, dx, dy, sw, sh);
+                    // SAFETY: `sy*pitch + sx*4 + 3` is within the mapped source framebuffer
+                    // (`pitch*sh` bytes); `rot_src` returns `sx < sw <= pitch/4`, `sy < sh`.
+                    let px =
+                        unsafe { (vaddr.add(sy * pitch + sx * 4) as *const u32).read_unaligned() };
+                    let (r, g, b) = apply_gamma(
+                        &gamma,
+                        ((px >> 16) & 0xff) as u8,
+                        ((px >> 8) & 0xff) as u8,
+                        (px & 0xff) as u8,
+                    );
+                    let (r, g, b) = (r as u32, g as u32, b as u32);
+                    cur[dy * w + dx] = (((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)) as u16;
+                }
             }
         }
         let s = *seq;
@@ -746,7 +868,7 @@ fn encode_and_send(
     // DRM device, so it is bound for the duration of this push.
     let dev = unsafe { data.intf.as_bound() };
     prime_video_eps(dev);
-    dev.bulk_send(VIDEO_EP, &frame, super::timeout(), GFP_KERNEL)?;
+    dev.bulk_send(VIDEO_EPS[plane.head as usize], &frame, super::timeout(), GFP_KERNEL)?;
     Ok(())
 }
 
@@ -769,7 +891,9 @@ fn new(_device: &drm::Device<Self::Driver, drm::Uninit>, _args: ()) -> impl PinI
 
 #[pin_data]
 pub(super) struct VinoConnector {
-    /// This connector's downstream-monitor EDID (`None` until the CP channel delivers it).
+    /// This connector's downstream-monitor EDID (`None` until the CP channel delivers it). Which
+    /// head a connector belongs to is tracked by its slot in [`VinoDrmData::connectors`], so the
+    /// connector itself needs no head field.
     #[pin]
     cached_edid: Mutex<Option<KVec<u8>>>,
 }
@@ -814,6 +938,28 @@ fn get_modes<'a>(
         connector.set_preferred_mode((FALLBACK_W as u32, FALLBACK_H as u32));
         n
     }
+
+    /// Report the head connected once the dock has delivered this head's downstream EDID (a real
+    /// monitor is attached and described), disconnected until then. A head with no monitor (e.g.
+    /// the second head of a single-monitor dock) stays disconnected rather than advertising a
+    /// phantom output.
+    fn detect(connector: &Connector<Self>, _force: bool) -> Status {
+        if connector.cached_edid.lock().is_some() {
+            Status::Connected
+        } else {
+            Status::Disconnected
+        }
+    }
+
+    /// Prune modes whose pixel clock exceeds a single head's bandwidth ceiling
+    /// ([`MAX_HEAD_CLOCK_KHZ`], ~4K@60).
+    fn mode_valid(_connector: &Connector<Self>, mode: &DisplayMode) -> ModeStatus {
+        if mode.clock() > MAX_HEAD_CLOCK_KHZ {
+            ModeStatus::ClockHigh
+        } else {
+            ModeStatus::Ok
+        }
+    }
 }
 
 // ---- DDC/CI I2C adapter -----------------------------------------------------
diff --git a/drivers/gpu/drm/vino/vino.rs b/drivers/gpu/drm/vino/vino.rs
index fd8d351dd1d9..049e8c4cb0f7 100644
--- a/drivers/gpu/drm/vino/vino.rs
+++ b/drivers/gpu/drm/vino/vino.rs
@@ -372,8 +372,12 @@ fn run(this: Arc<BringUp>) {
                             let n = blob.len();
                             let dev: &drm_sink::VinoDrmDevice = d;
                             let data: &drm_sink::VinoDrmData = dev;
-                            data.set_edid(dev, blob);
-                            dev_info!(cdev, "vino: cached dock EDID for connector ({n} bytes)\n");
+                            // The bring-up reads the dock's primary downstream EDID; head 0.
+                            data.set_edid(dev, 0, blob);
+                            dev_info!(
+                                cdev,
+                                "vino: cached dock EDID for head 0 connector ({n} bytes)\n"
+                            );
                         }
                     }
                     Err(e) => dev_info!(cdev, "vino: HDCP AKE incomplete ({e:?}) -- WIP\n"),
-- 
2.55.0


  parent reply	other threads:[~2026-07-03  3:02 UTC|newest]

Thread overview: 23+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-17 15:12 [RFC PATCH 0/7] drm/vino: DisplayLink DL3 dock driver (RFC, help wanted) Mike Lothian
2026-06-17 15:12 ` [RFC PATCH 1/7] drm/vino: add DisplayLink DL3 dock skeleton and plaintext bring-up Mike Lothian
2026-06-17 15:17   ` Miguel Ojeda
2026-06-18 10:39   ` Julian Braha
2026-06-17 15:12 ` [RFC PATCH 2/7] drm/vino: add the clean-room HDCP 2.2 AKE/LC/SKE Mike Lothian
2026-06-17 16:18   ` Eric Biggers
2026-06-17 15:12 ` [RFC PATCH 3/7] drm/vino: add the AES-CTR/AES-CMAC control-plane seal and arm Mike Lothian
2026-06-17 15:12 ` [RFC PATCH 4/7] drm/vino: add the Vino (RawRl mode-2) framebuffer codec Mike Lothian
2026-06-17 15:12 ` [RFC PATCH 5/7] drm/vino: register a DRM/KMS device and scan out to EP08 Mike Lothian
2026-06-17 15:12 ` [RFC PATCH 6/7] drm/vino: add DDC/CI brightness/contrast, DPMS power and DFU info Mike Lothian
2026-06-17 15:12 ` [RFC PATCH 7/7] drm/vino: add KUnit self-tests for the protocol and crypto paths Mike Lothian
2026-06-17 15:55 ` [RFC PATCH 0/7] drm/vino: DisplayLink DL3 dock driver (RFC, help wanted) Danilo Krummrich
2026-07-03  3:02 ` [RFC PATCH v2 00/10] drm/vino: DisplayLink DL3 dock driver Mike Lothian
2026-07-03  3:02   ` [RFC PATCH v2 01/10] drm/vino: add DisplayLink DL3 dock skeleton and protocol framing Mike Lothian
2026-07-03  3:02   ` [RFC PATCH v2 02/10] drm/vino: add the clean-room HDCP 2.2 AKE/LC/SKE handshake Mike Lothian
2026-07-03  3:02   ` [RFC PATCH v2 03/10] drm/vino: add the AES-CTR/AES-CMAC control-plane seal and arm sequence Mike Lothian
2026-07-03  3:02   ` [RFC PATCH v2 04/10] drm/vino: add the Vino framebuffer codec Mike Lothian
2026-07-03  3:02   ` [RFC PATCH v2 05/10] drm/vino: add the DRM/KMS sink, built on the safe KMS mode-object layer Mike Lothian
2026-07-03  3:02   ` [RFC PATCH v2 06/10] drm/vino: add the DisplayLink DL3 dock driver Mike Lothian
2026-07-03  3:02   ` [RFC PATCH v2 07/10] drm/vino: wire the hardware cursor plane Mike Lothian
2026-07-03  3:02   ` [RFC PATCH v2 08/10] drm/vino: wire CRTC gamma, plane rotation and DDC/CI monitor controls Mike Lothian
2026-07-03  3:02   ` Mike Lothian [this message]
2026-07-03  3:02   ` [RFC PATCH v2 10/10] drm/vino: hrtimer-driven software vblank Mike Lothian

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260703030217.2886-10-mike@fireburn.co.uk \
    --to=mike@fireburn.co.uk \
    --cc=a.hindborg@kernel.org \
    --cc=airlied@gmail.com \
    --cc=aliceryhl@google.com \
    --cc=bjorn3_gh@protonmail.com \
    --cc=boqun@kernel.org \
    --cc=dakr@kernel.org \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=gary@garyguo.net \
    --cc=linux-kernel@vger.kernel.org \
    --cc=lossin@kernel.org \
    --cc=lyude@redhat.com \
    --cc=maarten.lankhorst@linux.intel.com \
    --cc=mripard@kernel.org \
    --cc=ojeda@kernel.org \
    --cc=rust-for-linux@vger.kernel.org \
    --cc=simona@ffwll.ch \
    --cc=tmgross@umich.edu \
    --cc=tzimmermann@suse.de \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox