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

Reply via email to