Add the drm_sink module: register a real struct drm_device with a
hand-rolled atomic mode-setting pipeline so the dock appears to userspace
as a mode-settable card/renderD node -- one CRTC driven by a primary
plane and 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.

probe() now allocates and registers the DRM device on the control
interface; the bring-up work item caches the dock EDID on it and, once
(if) the CP engages, publishes the live session so the KMS callbacks can
emit runtime CP (mode-set on a modeset, cursor on motion). On every
page-flip the primary plane's update vmaps the framebuffer, encodes it
with the Vino codec and pushes it to the EP08 video endpoint, gated on
CP_ENGAGED so frames are only sent once the dock's cipher is live.

Signed-off-by: Mike Lothian <[email protected]>
Assisted-by: Claude:claude-opus-4-8 [Claude-Code]
---
 drivers/gpu/drm/vino/drm_sink.rs | 1333 ++++++++++++++++++++++++++++++
 drivers/gpu/drm/vino/vino.rs     |  145 +++-
 2 files changed, 1458 insertions(+), 20 deletions(-)
 create mode 100644 drivers/gpu/drm/vino/drm_sink.rs

diff --git a/drivers/gpu/drm/vino/drm_sink.rs b/drivers/gpu/drm/vino/drm_sink.rs
new file mode 100644
index 000000000000..afbf883fba36
--- /dev/null
+++ b/drivers/gpu/drm/vino/drm_sink.rs
@@ -0,0 +1,1333 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Phase 3 (DRM/KMS sink): register a real `struct drm_device` with a full 
atomic
+//! mode-setting pipeline so the dock appears to userspace as a 
`card`/`renderD` node
+//! that can be `drmModeSetCrtc`'d. A hand-rolled atomic pipeline: one CRTC 
driven by a
+//! primary plane (`primary_atomic_update` -> EP08 scanout) and a cursor plane
+//! (`cursor_atomic_update` -> cursor CP), 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. (Earlier this was 
`drm_simple_display_pipe`,
+//! swapped out because that helper is primary-plane-only and can't carry a 
cursor plane.)
+//! The scanout/cursor writes are gated on the CP-arming blocker (see 
`docs/BLOCKER.md`). The
+//! KMS C bindings are pulled in by
+//! `patches/drm/0001` (bindings_helper.h headers + the `Driver::FEAT_MODESET` 
/
+//! `Driver::FEAT_ATOMIC` flags + a public `Device::as_raw`); see 
`patches/README.md`.
+
+use core::ptr;
+use kernel::{
+    bindings, drm,
+    error::{
+        code::{EINVAL, ENOMEM},
+        to_result,
+    },
+    prelude::*,
+    sync::{aref::ARef, new_mutex, Mutex},
+    types::Opaque,
+};
+
+/// Fallback connector mode advertised by `get_modes` when the dock has not 
delivered a real
+/// downstream EDID yet. The live scanout geometry follows the actual 
framebuffer/negotiated
+/// mode (see `scanout_one`), so this is only the no-EDID default, not a hard 
scanout limit.
+const FALLBACK_W: i32 = 1920;
+const FALLBACK_H: i32 = 1080;
+
+/// The DRM driver marker type.
+pub(super) struct VinoDrmDriver;
+
+/// Convenience alias for our concrete `drm::Device`.
+pub(super) type VinoDrmDevice = drm::Device<VinoDrmDriver>;
+
+/// `DRM_FORMAT_XRGB8888` (`fourcc_code('X','R','2','4')`); the dock scans out 
32bpp.
+const DRM_FORMAT_XRGB8888: u32 = 0x3432_5258;
+/// `DRM_FORMAT_ARGB8888` (`fourcc_code('A','R','2','4')`); the cursor sprite 
carries alpha.
+const DRM_FORMAT_ARGB8888: u32 = 0x3432_5241;
+/// Primary-plane format list (opaque 32bpp scanout).
+static PRIMARY_FORMATS: [u32; 1] = [DRM_FORMAT_XRGB8888];
+/// Cursor-plane format list (alpha sprite). ARGB8888 little-endian memory 
order is
+/// `B,G,R,A` per pixel -- already the BGRA byte layout `cp::cursor_image` 
wants.
+static CURSOR_FORMATS: [u32; 1] = [DRM_FORMAT_ARGB8888];
+/// Hardware cursor sprite size (the dock cursor is 64x64; 
`DRM_CAP_CURSOR_WIDTH/HEIGHT`).
+const CURSOR_SIZE: u32 = 64;
+/// `GAMMA_LUT` size advertised on the CRTC. 256 entries matches the 8-bit 
scanout channels;
+/// the LUT is applied host-side in the scanout conversion (see 
`read_gamma_lut`).
+const GAMMA_SIZE: u32 = 256;
+
+/// Per-mode pixel-clock ceiling (kHz) for a single head -- about 4K@60 (CEA 
594 MHz).
+/// `mode_valid` prunes any single mode above this from a connector's 
advertised list.
+const MAX_HEAD_CLOCK_KHZ: i32 = 600_000;
+/// Combined pixel-clock budget (kHz) summed over all *active* heads -- the 
dock's DL3 link
+/// ceiling. This is a deliberately conservative *raw* pixel-rate proxy 
(DisplayLink compresses
+/// the stream, so the true USB budget is higher and content-dependent; the 
WHT codec exists for
+/// the tight cases). At 1 GHz it admits one 4K@60, 4K@60 + QHD@60, or 
dual-QHD@60, and rejects
+/// dual-4K -- matching the D6000's real multi-monitor envelope. Tune to taste 
/ per dock
+/// model.
+const MAX_TOTAL_CLOCK_KHZ: i64 = 1_000_000;
+
+/// Mutable scanout state, guarded because the atomic `update` callback may run
+/// concurrently with itself across heads. Holds the stateful Vino encoder 
(created
+/// lazily on the first flip, once the buffer geometry is known) and the EP08 
frame
+/// sequence counter.
+pub(super) struct ScanoutState {
+    enc: Option<super::video::Encoder>,
+    /// Reusable `width*height` RGB565 conversion buffer, allocated once 
alongside `enc`.
+    /// Previously `encode_and_send` did a fresh `KVec::with_capacity(w*h)` on 
every pageflip;
+    /// at 1080p that is a ~4 MiB *contiguous* kmalloc (order 11), above the 
allocator's limit,
+    /// so the page allocator WARNed and returned `ENOMEM` every frame. 
vmalloc-backed +
+    /// persistent: virtually-contiguous (no high-order page need) and 
allocated once.
+    cur: VVec<u16>,
+    seq: u32,
+    /// Geometry (`width`, `height`) the encoder/`cur` were allocated for. The 
scanout follows
+    /// the live framebuffer size, so a mode switch re-allocates them when 
this no longer
+    /// matches.
+    dims: (usize, usize),
+    /// The [`super::cp::Timing`] of the mode the compositor last enabled on 
the CRTC, captured
+    /// in [`crtc_atomic_enable`] via [`super::cp::timing_from_drm_mode`]. 
This is the
+    /// multi-mode hook:
+    /// userspace can pick *any* mode from the EDID-derived list and the chosen
+    /// `drm_display_mode`
+    /// is recorded here so the live mode-set CP message reflects it (rather 
than always the
+    /// EDID-preferred timing). The CP send itself is gated on the engagement 
wall + session
+    /// plumbing (see the doc note in `crtc_atomic_enable`).
+    active_timing: Option<super::cp::Timing>,
+    /// Size of the last EP08 frame produced, used to pre-reserve the next 
frame's
+    /// buffer. The encoded stream size is stable frame-to-frame (it tracks 
the damage
+    /// area), so seeding `KVec::with_capacity` from it makes the encode grow 
the buffer
+    /// at most once instead of reallocating repeatedly as runs are appended.
+    hint: usize,
+}
+
+/// The live CP session the bring-up work item publishes once the dock engages 
the cipher
+/// (`acks > 0`), so the KMS callbacks can seal+send runtime CP messages -- a 
mode-set when the
+/// compositor switches mode, a cursor message on pointer motion -- that 
continue the SAME
+/// keystream the bring-up setup left off at. `wire_seq` is the AES-CTR block 
counter (advanced
+/// by the content blocks of each send; the appended Dl3Cmac tag is not part 
of the keystream)
+/// and `counter` the dock-echoed inner CP counter. Both advance per send 
under the mutex.
+pub(super) struct CpLink {
+    ks: [u8; 16],
+    riv: [u8; 8],
+    wire_seq: u32,
+    counter: u16,
+}
+
+/// Number of display heads the D6000 dock drives. The protocol routes video 
by endpoint
+/// (head 0 -> EP 0x08, head 1 -> EP 0x0a; heads 2/3 -> 0x0b/0x0c are 
documented but their CP
+/// riv / EDID-request encoding is unconfirmed, so only 2 heads are wired). 
The DL3 CP stream
+/// selects the head via the riv `byte0 ^ 0x80` (head 0 base / head 1 flipped).
+pub(super) const NHEADS: usize = 2;
+/// Per-head video bulk-OUT endpoint (`PROTOCOL.md`). Index by [`Head::index`].
+const HEAD_EP: [u8; NHEADS] = [0x08, 0x0a];
+
+/// One display head: its own CRTC + primary plane (scanout) + cursor plane + 
encoder +
+/// connector, plus per-head scanout state and cached monitor EDID. The 
vtables are shared
+/// across heads (in [`VinoDrmData`]); the callbacks recover the head from the 
C object
+/// pointer. All C objects are zeroed at init and filled by [`kms_init`].
+#[pin_data]
+pub(super) struct Head {
+    /// 0-based head index. Selects the video EP ([`HEAD_EP`]) and the CP riv 
(head 1 flips
+    /// the riv `byte0`); see the scanout EP and [`VinoDrmData::send_cp`].
+    index: u8,
+    /// One-shot: this head's cursor `create` (sprite dimensions) was sent 
before its first
+    /// image upload (per head -- the global one would skip head 1's create).
+    cursor_primed: core::sync::atomic::AtomicBool,
+    #[pin]
+    scanout: Mutex<ScanoutState>,
+    /// This head's downstream-monitor EDID (`None` until the CP channel 
delivers it). Only
+    /// head 0's EDID is read during bring-up; per-head EDID requests are 
unconfirmed, so
+    /// head 1 falls back to a fixed CVT mode in `get_modes`.
+    #[pin]
+    cached_edid: Mutex<Option<KVec<u8>>>,
+    #[pin]
+    crtc: Opaque<bindings::drm_crtc>,
+    #[pin]
+    primary: Opaque<bindings::drm_plane>,
+    #[pin]
+    cursor: Opaque<bindings::drm_plane>,
+    #[pin]
+    encoder: Opaque<bindings::drm_encoder>,
+    #[pin]
+    connector: Opaque<bindings::drm_connector>,
+}
+
+// SAFETY: as for `VinoDrmData` below -- the embedded C KMS objects are 
written only during
+// single-threaded probe and thereafter serialised by the DRM core's own locks.
+unsafe impl Send for Head {}
+// SAFETY: see the `Send` impl above.
+unsafe impl Sync for Head {}
+
+impl Head {
+    fn new(index: u8) -> impl PinInit<Self, Error> {
+        fn z<T>() -> impl PinInit<Opaque<T>, Error> {
+            // SAFETY: an all-zero C KMS object is a valid starting point (all 
callback
+            // pointers NULL); `kms_init` populates the rest via raw pointers 
under `unsafe`.
+            Opaque::try_ffi_init(|p: *mut T| {
+                unsafe { ptr::write_bytes(p, 0, 1) };
+                Ok(())
+            })
+        }
+        try_pin_init!(Self {
+            index,
+            cursor_primed: core::sync::atomic::AtomicBool::new(false),
+            scanout <- new_mutex!(ScanoutState {
+                enc: None,
+                cur: VVec::new(),
+                seq: 0,
+                dims: (0, 0),
+                active_timing: None,
+                hint: 0,
+            }),
+            cached_edid <- new_mutex!(Option::<KVec<u8>>::None),
+            crtc <- z(),
+            primary <- z(),
+            cursor <- z(),
+            encoder <- z(),
+            connector <- z(),
+        })
+    }
+
+    /// This head's video bulk-OUT endpoint.
+    fn video_ep(&self) -> u8 {
+        HEAD_EP[self.index as usize]
+    }
+
+    /// Fire a hotplug on this head's connector so the compositor re-probes 
[`detect`].
+    fn fire_hotplug(&self) {
+        // SAFETY: called after `drm_dev_register`; the embedded connector is 
initialised
+        // and its `dev` is our live drm_device. Safe from process context.
+        unsafe {
+            let dev = (*self.connector.get()).dev;
+            if !dev.is_null() {
+                bindings::drm_kms_helper_hotplug_event(dev);
+            }
+        }
+    }
+}
+
+/// DRM device-private data. Holds [`NHEADS`] display [`Head`]s (each a CRTC + 
primary +
+/// cursor plane + encoder + connector) and the shared KMS vtables inline, so 
they keep
+/// stable addresses for the device's lifetime. All C objects zeroed at init; 
filled by
+/// [`kms_init`]. Also keeps the bound USB interface (to reach the video EPs) 
and the engaged
+/// CP session.
+#[pin_data]
+pub(super) struct VinoDrmData {
+    intf: ARef<super::usb::Interface>,
+    /// The engaged CP session for runtime KMS-driven sends (`None` until
+    /// [`VinoDrmData::publish_session`]). See [`CpLink`] and 
[`VinoDrmData::send_cp`].
+    #[pin]
+    cp_link: Mutex<Option<CpLink>>,
+    #[pin]
+    head0: Head,
+    #[pin]
+    head1: Head,
+    // Shared vtables (one set for all heads; the callbacks recover the head 
from the C
+    // object pointer). One `drm_plane_funcs` for both planes; per-plane 
helper funcs because
+    // the primary's `atomic_update` scans out while the cursor's sends cursor 
CP.
+    #[pin]
+    conn_funcs: Opaque<bindings::drm_connector_funcs>,
+    #[pin]
+    conn_helper: Opaque<bindings::drm_connector_helper_funcs>,
+    #[pin]
+    crtc_funcs: Opaque<bindings::drm_crtc_funcs>,
+    #[pin]
+    crtc_helper: Opaque<bindings::drm_crtc_helper_funcs>,
+    #[pin]
+    plane_funcs: Opaque<bindings::drm_plane_funcs>,
+    #[pin]
+    primary_helper: Opaque<bindings::drm_plane_helper_funcs>,
+    #[pin]
+    cursor_helper: Opaque<bindings::drm_plane_helper_funcs>,
+    #[pin]
+    encoder_funcs: Opaque<bindings::drm_encoder_funcs>,
+    #[pin]
+    mode_cfg_funcs: Opaque<bindings::drm_mode_config_funcs>,
+}
+
+// SAFETY: the embedded C KMS objects are written only during single-threaded
+// `probe()` (before `drm_dev_register`), and thereafter are owned and 
serialised by
+// the DRM core under its own modeset/atomic locks -- Rust never aliases them 
again.
+// This is the conventional assertion for drivers embedding C KMS state until 
the
+// kernel grows safe Rust KMS abstractions.
+unsafe impl Send for VinoDrmData {}
+// SAFETY: see the `Send` impl above.
+unsafe impl Sync for VinoDrmData {}
+
+impl VinoDrmData {
+    /// Zero-initialise all embedded C objects (so each `Option<fn>` vtable 
slot is
+    /// `None`); [`kms_init`] then fills in only the callbacks we implement. 
`intf`
+    /// is the bound USB interface, kept so the scanout path can reach EP08.
+    pub(super) fn new(intf: ARef<super::usb::Interface>) -> impl PinInit<Self, 
Error> {
+        fn z<T>() -> impl PinInit<Opaque<T>, Error> {
+            // SAFETY: an all-zero C KMS object / funcs table is a valid 
starting
+            // point (all callback pointers NULL); the `_init` helpers 
populate the
+            // rest, and we only read it back through raw pointers under 
`unsafe`.
+            Opaque::try_ffi_init(|p: *mut T| {
+                unsafe { ptr::write_bytes(p, 0, 1) };
+                Ok(())
+            })
+        }
+        try_pin_init!(Self {
+            intf,
+            cp_link <- new_mutex!(Option::<CpLink>::None),
+            head0 <- Head::new(0),
+            head1 <- Head::new(1),
+            conn_funcs <- z(),
+            conn_helper <- z(),
+            crtc_funcs <- z(),
+            crtc_helper <- z(),
+            plane_funcs <- z(),
+            primary_helper <- z(),
+            cursor_helper <- z(),
+            encoder_funcs <- z(),
+            mode_cfg_funcs <- z(),
+        })
+    }
+
+    /// The display heads, in index order.
+    fn heads(&self) -> [&Head; NHEADS] {
+        [&self.head0, &self.head1]
+    }
+
+    /// Recover the [`Head`] that owns a given C KMS object, by pointer 
identity. Used by the
+    /// connector/CRTC/plane callbacks (which receive a raw C pointer) to find 
their head.
+    fn head_by_connector(&self, c: *mut bindings::drm_connector) -> 
Option<&Head> {
+        self.heads().into_iter().find(|h| h.connector.get() == c)
+    }
+    fn head_by_crtc(&self, c: *mut bindings::drm_crtc) -> Option<&Head> {
+        self.heads().into_iter().find(|h| h.crtc.get() == c)
+    }
+    fn head_by_primary(&self, p: *mut bindings::drm_plane) -> Option<&Head> {
+        self.heads().into_iter().find(|h| h.primary.get() == p)
+    }
+    fn head_by_cursor(&self, p: *mut bindings::drm_plane) -> Option<&Head> {
+        self.heads().into_iter().find(|h| h.cursor.get() == p)
+    }
+
+    /// Cache the dock's EDID (read during probe) on head 0 for [`get_modes`] 
to install, then
+    /// fire a hotplug so the compositor re-probes the connector -- which now 
reports connected
+    /// (see [`detect`]) and exposes the monitor's real mode list. Only head 
0's downstream EDID
+    /// is read during bring-up (per-head EDID requests are unconfirmed).
+    pub(super) fn set_edid(&self, blob: KVec<u8>) {
+        *self.head0.cached_edid.lock() = Some(blob);
+        self.head0.fire_hotplug();
+    }
+
+    /// Fire a hotplug on every head's connector so the compositor re-probes 
[`detect`] -- used
+    /// after the bring-up work item completes to expose the live-scanout 
outputs.
+    pub(super) fn fire_hotplug(&self) {
+        for h in self.heads() {
+            h.fire_hotplug();
+        }
+    }
+
+    /// Publish the engaged CP session so the KMS callbacks can send runtime 
CP messages.
+    /// Called once by the bring-up work item after the dock acks (`acks > 0`).
+    /// `wire_seq`/`counter` are the next free values past the bring-up CP 
setup.
+    pub(super) fn publish_session(&self, ks: &[u8; 16], riv: &[u8; 8], 
wire_seq: u32, counter: u16) {
+        *self.cp_link.lock() = Some(CpLink { ks: *ks, riv: *riv, wire_seq, 
counter });
+    }
+
+    /// Seal and send one interactive CP message on EP02 for head 
`head_index`, advancing the
+    /// session keystream. The DL3 CP stream selects the head via the riv 
`byte0 ^ 0x80` (head 0
+    /// base, head 1 flipped). `build(counter)` produces the inner CP message 
for the
+    /// dock-echoed `counter` it is handed (e.g. [`super::cp::set_mode`]); 
`tag_reserved`
+    /// trailing bytes are dropped before the live Dl3Cmac is appended 
(set-mode reserves a
+    /// 16-byte placeholder; messages with no placeholder pass 0). Returns 
`Ok(())` as a
+    /// **no-op when CP is not engaged**. The `cp_link` mutex serialises 
concurrent KMS
+    /// callbacks. Runs from the atomic-commit context (same as the scanout), 
so the blocking
+    /// `bulk_send` is fine. NOTE: head 1's `wire_seq`/counter sharing with 
head 0 is an
+    /// assumption (both share `cp_link`); the riv differs so the keystreams 
differ. Unconfirmed
+    /// (CP-wall-gated) -- revisit when the dock engages.
+    pub(super) fn send_cp(
+        &self,
+        head_index: u8,
+        id: u16,
+        tag_reserved: usize,
+        build: impl FnOnce(u16) -> Result<KVec<u8>>,
+    ) -> Result {
+        let mut guard = self.cp_link.lock();
+        // `&mut *guard` forces the guard's `DerefMut` to `&mut 
Option<CpLink>` so `as_mut`
+        // resolves to `Option::as_mut` (the guard has its own inherent 
`as_mut`).
+        let Some(link) = (&mut *guard).as_mut() else {
+            return Ok(()); // CP not engaged -- nothing to send
+        };
+        // Head select: head 1's CP stream flips the riv byte0 (see 
`decode_any`/CP-HANDSHAKE).
+        let mut riv = link.riv;
+        if head_index == 1 {
+            riv[0] ^= 0x80;
+        }
+        let msg = build(link.counter)?;
+        let content = &msg[..msg.len().saturating_sub(tag_reserved)];
+        let frame = super::cp::seal_interactive(&link.ks, &riv, id, 
link.wire_seq, content)?;
+        let dev: &super::usb::Device = self.intf.as_ref();
+        dev.bulk_send(super::EP_CTRL_OUT, &frame, super::timeout())?;
+        // Advance the AES-CTR block counter by the content blocks only (the 
appended Dl3Cmac
+        // tag is sent in clear, not keystreamed) and bump the dock-echoed 
inner counter.
+        link.wire_seq = link.wire_seq.wrapping_add(((content.len() + 15) / 16) 
as u32);
+        link.counter = link.counter.wrapping_add(1);
+        Ok(())
+    }
+}
+
+/// GEM object inner data. Empty: the shmem-backed `drm::gem::shmem::Object` 
(which
+/// wires `drm_gem_shmem_dumb_create`, so userspace 
`DRM_IOCTL_MODE_CREATE_DUMB`
+/// works) is enough until the EP08 scanout path consumes the framebuffers.
+#[pin_data]
+pub(super) struct VinoObject {}
+
+impl drm::gem::DriverObject for VinoObject {
+    type Driver = VinoDrmDriver;
+    type Args = ();
+
+    fn new<Ctx: drm::DeviceContext>(
+        _dev: &drm::Device<VinoDrmDriver, Ctx>,
+        _size: usize,
+        _args: (),
+    ) -> impl PinInit<Self, Error> {
+        try_pin_init!(VinoObject {})
+    }
+}
+
+/// Per-open DRM client state. Empty of driver data, but its lifetime is used 
to
+/// pin the module for the duration of an open DRM file (see 
[`VinoDrmFile::open`]).
+#[pin_data(PinnedDrop)]
+pub(super) struct VinoDrmFile {}
+
+impl drm::file::DriverFile for VinoDrmFile {
+    type Driver = VinoDrmDriver;
+
+    fn open(_dev: &drm::Device<Self::Driver>) -> Result<Pin<KBox<Self>>> {
+        let file = KBox::try_pin_init(try_pin_init!(Self {}), GFP_KERNEL)?;
+        // Pin this module while a DRM file is open. The Rust DRM 
`file_operations`
+        // are built with `owner = NULL` (drm/gem/mod.rs `create_fops`), so 
the DRM
+        // core's `try_module_get(fops->owner)` on open is a no-op: an open 
card fd
+        // does NOT keep the driver loaded. Unloading vino (rmmod, or USB 
teardown at
+        // shutdown) while a compositor still holds `/dev/dri/cardN` then 
frees the
+        // module's `.rodata` -- where the fops live -- under that open fd, so 
the next
+        // ioctl/close dereferences freed memory and oopses the kernel 
(observed: KWin
+        // UAF in `__x64_sys_ioctl` / `put_files_struct`, "recursive fault, 
reboot
+        // needed"). Take an explicit module reference here, released 1:1 in
+        // `PinnedDrop` (run by `postclose_callback` on file close), to 
restore the
+        // pin the NULL `fops.owner` drops. Remove once the binding sets 
`fops.owner`.
+        // SAFETY: we are executing inside this module's own DRM `open` 
callback, so
+        // the module is live; taking an extra reference via `__module_get` is 
sound.
+        unsafe { bindings::__module_get(crate::THIS_MODULE.as_ptr()) };
+        Ok(file)
+    }
+}
+
+#[pinned_drop]
+impl PinnedDrop for VinoDrmFile {
+    fn drop(self: Pin<&mut Self>) {
+        // Release the module reference taken in `open` (balanced 
one-per-open-file).
+        // SAFETY: balances the `__module_get` in `open`; `THIS_MODULE` is 
valid for
+        // the lifetime of the module.
+        unsafe { bindings::module_put(crate::THIS_MODULE.as_ptr()) };
+    }
+}
+
+const INFO: drm::DriverInfo = drm::DriverInfo {
+    major: 0,
+    minor: 1,
+    patchlevel: 0,
+    name: c"vino",
+    desc: c"DisplayLink DL3 (Dell D6000) DRM driver",
+};
+
+#[vtable]
+impl drm::Driver for VinoDrmDriver {
+    type Data = VinoDrmData;
+    type File = VinoDrmFile;
+    type Object<Ctx: drm::DeviceContext> = drm::gem::shmem::Object<VinoObject, 
Ctx>;
+
+    const INFO: drm::DriverInfo = INFO;
+    // Atomic KMS driver (CRTC/plane/connector via the simple display pipe).
+    // Mirrors the FEAT_RENDER idiom added by patches/drm/0001.
+    const FEAT_MODESET: bool = true;
+    const FEAT_ATOMIC: bool = true;
+
+    // No driver-private ioctls (GEM/dumb + KMS handled by the DRM core).
+    kernel::declare_drm_ioctls! {}
+}
+
+// ---- KMS C callbacks ------------------------------------------------------
+
+/// Install a real EDID blob on the connector via the standard DRM EDID
+/// infrastructure and return the number of modes added (0 on failure). This
+/// reuses the kernel helpers -- no synthetic EDID. See CONTROL-PLANE.md.
+fn install_edid(connector: *mut bindings::drm_connector, blob: &[u8]) -> i32 {
+    // SAFETY: `blob` is a valid byte buffer; `drm_edid_alloc` copies it.
+    let edid = unsafe { bindings::drm_edid_alloc(blob.as_ptr().cast(), 
blob.len()) };
+    if edid.is_null() {
+        return 0;
+    }
+    // SAFETY: `connector` is valid during probe; `edid` is freshly allocated 
above.
+    unsafe { bindings::drm_edid_connector_update(connector, edid) };
+    // SAFETY: connector valid; adds the EDID-derived modes, returns the count.
+    let n = unsafe { bindings::drm_edid_connector_add_modes(connector) };
+    // SAFETY: `edid` was allocated by `drm_edid_alloc` and is no longer 
needed.
+    unsafe { bindings::drm_edid_free(edid) };
+    n
+}
+
+/// Connector `.mode_valid`: reject any single mode whose pixel clock exceeds 
the per-head
+/// ceiling ([`MAX_HEAD_CLOCK_KHZ`], ~4K@60), so the compositor never offers 
an over-spec mode
+/// on
+/// one head. The *combined* across-heads budget is enforced separately at 
commit by
+/// [`vino_atomic_check`].
+unsafe extern "C" fn mode_valid(
+    _connector: *mut bindings::drm_connector,
+    mode: *const bindings::drm_display_mode,
+) -> bindings::drm_mode_status {
+    // SAFETY: `mode` is a valid drm_display_mode for the duration of the call.
+    let clock = unsafe { (*mode).clock };
+    if clock > MAX_HEAD_CLOCK_KHZ {
+        bindings::drm_mode_status_MODE_CLOCK_HIGH
+    } else {
+        bindings::drm_mode_status_MODE_OK
+    }
+}
+
+/// `mode_config.funcs.atomic_check`: run the standard atomic checks, then 
reject the commit if
+/// the **combined** pixel clock of all active heads would exceed the dock's 
USB/DL3 budget
+/// ([`MAX_TOTAL_CLOCK_KHZ`]) -- e.g. two simultaneous 4K modes. For each 
head, the proposed
+/// (new) CRTC state is used when the head is part of this commit, else its 
current committed
+/// state; only `enable && active` heads count.
+unsafe extern "C" fn vino_atomic_check(
+    dev: *mut bindings::drm_device,
+    state: *mut bindings::drm_atomic_commit,
+) -> i32 {
+    // SAFETY: `dev`/`state` are valid for the duration of the atomic check.
+    let rc = unsafe { bindings::drm_atomic_helper_check(dev, state) };
+    if rc != 0 {
+        return rc;
+    }
+    // SAFETY: `dev` is our live, registered drm_device.
+    let data: &VinoDrmData = unsafe { VinoDrmDevice::from_raw(dev) };
+    let mut total_khz: i64 = 0;
+    for head in data.heads() {
+        let crtc = head.crtc.get();
+        // SAFETY: read-only new-state accessor (a `rust_helper`, exposed 
without the prefix);
+        // NULL when this head is not in the commit -- then fall back to its 
current state.
+        let mut cs = unsafe { bindings::drm_atomic_get_new_crtc_state(state, 
crtc) };
+        if cs.is_null() {
+            // SAFETY: `crtc` is initialised; `.state` is its current 
committed state (or NULL).
+            cs = unsafe { (*crtc).state };
+        }
+        if cs.is_null() {
+            continue;
+        }
+        // SAFETY: `cs` is a live drm_crtc_state.
+        let (enable, active, clock) = unsafe { ((*cs).enable, (*cs).active, 
(*cs).mode.clock) };
+        if enable && active {
+            total_khz += clock as i64;
+        }
+    }
+    if total_khz > MAX_TOTAL_CLOCK_KHZ {
+        pr_warn!(
+            "vino: modeset rejected -- combined {total_khz} kHz pixel clock 
over the {} kHz dock budget\n",
+            MAX_TOTAL_CLOCK_KHZ
+        );
+        return EINVAL.to_errno();
+    }
+    0
+}
+
+/// Connector `.get_modes`: install the dock's real EDID (read during probe) 
when
+/// available; otherwise fall back to a single 1920x1080@60 CVT mode. Reading 
the
+/// real EDID gives the true monitor name/size and its native mode list (see 
the
+/// EDID Read path); the fallback keeps the connector usable when nothing is
+/// plugged into the dock or the CP channel has not yet delivered the EDID.
+unsafe extern "C" fn get_modes(connector: *mut bindings::drm_connector) -> i32 
{
+    // SAFETY: `connector` is a valid, initialised connector during probe.
+    let dev = unsafe { (*connector).dev };
+    // SAFETY: `dev` is our live, registered drm_device.
+    let ddev = unsafe { VinoDrmDevice::from_raw(dev) };
+    let data: &VinoDrmData = ddev;
+    if let Some(head) = data.head_by_connector(connector) {
+        let guard = head.cached_edid.lock();
+        if let Some(blob) = guard.as_ref() {
+            let n = install_edid(connector, blob);
+            if n > 0 {
+                return n;
+            }
+        }
+    }
+    // Fallback: single FALLBACK_W x FALLBACK_H @60 CVT mode, marked preferred.
+    // SAFETY: `dev` is a valid drm_device; drm_cvt_mode allocates a mode.
+    let mode = unsafe {
+        bindings::drm_cvt_mode(dev, FALLBACK_W, FALLBACK_H, 60, false, false, 
false)
+    };
+    if mode.is_null() {
+        return 0;
+    }
+    // SAFETY: `mode` is freshly allocated and owned by the connector after 
add.
+    unsafe { bindings::drm_mode_probed_add(connector, mode) };
+    // SAFETY: connector is valid; set the fallback mode as preferred.
+    unsafe { bindings::drm_set_preferred_mode(connector, FALLBACK_W, 
FALLBACK_H) };
+    1
+}
+
+/// Connector `.detect`: report **disconnected** until the dock's downstream 
EDID has
+/// actually been read over the CP channel, then **connected**. A virtual 
connector
+/// that always reports connected makes the compositor light up a phantom 
output it
+/// cannot drive -- no pixels reach the dock until the CP/EP08 path is up -- 
which froze
+/// KWin on plug (SSH stayed alive; unplug recovered it). Gating on a real 
EDID mirrors
+/// how `gud`/`udl` report monitor presence; [`VinoDrmData::set_edid`] fires a 
hotplug
+/// so the compositor re-probes and enables the output once the EDID arrives.
+unsafe extern "C" fn detect(
+    connector: *mut bindings::drm_connector,
+    _force: bool,
+) -> bindings::drm_connector_status {
+    // SAFETY: `connector` is a valid connector embedded in our DRM 
device-private.
+    let dev = unsafe { (*connector).dev };
+    // SAFETY: `dev` is our live, registered drm_device.
+    let ddev = unsafe { VinoDrmDevice::from_raw(dev) };
+    let data: &VinoDrmData = ddev;
+    let live_ready = 
super::CP_ENGAGED.load(core::sync::atomic::Ordering::SeqCst);
+    let has_edid = data
+        .head_by_connector(connector)
+        .is_some_and(|h| h.cached_edid.lock().is_some());
+    if has_edid || live_ready {
+        // Force-connect (with the get_modes 1080p fallback) so a compositor 
drives the CRTC and
+        // `primary_atomic_update` fires live frames -- but only once CP is 
engaged (not merely
+        // after
+        // bring-up): connecting the output makes the compositor push EP08 
video, and doing that
+        // before the dock has engaged CP makes it fault and USB-reset in a 
loop (the "EP08
+        // write
+        // wedges the hub" mode). So stay disconnected until CP is up -- or a 
real EDID arrived.
+        bindings::drm_connector_status_connector_status_connected
+    } else {
+        bindings::drm_connector_status_connector_status_disconnected
+    }
+}
+
+/// CRTC `.atomic_enable`: the display is turning on (scanout begins). 
Captures the mode the
+/// compositor selected -- any entry from the connector's full EDID-derived 
list -- as a
+/// set-mode [`super::cp::Timing`] in [`ScanoutState::active_timing`] and 
pushes a live
+/// mode-set CP message for it (no-op until CP engages). The geometry change 
is also honoured
+/// by the scanout path (`encode_and_send` re-inits on `dims` change).
+unsafe extern "C" fn crtc_atomic_enable(
+    crtc: *mut bindings::drm_crtc,
+    _state: *mut bindings::drm_atomic_commit,
+) {
+    // SAFETY: in `.atomic_enable` the crtc and its committed `state` are 
valid; `state->mode`
+    // is a live drm_display_mode and `timing_from_drm_mode` only reads it.
+    let cs = unsafe { (*crtc).state };
+    if cs.is_null() {
+        return;
+    }
+    let timing = unsafe { super::cp::timing_from_drm_mode(&(*cs).mode) };
+    pr_info!(
+        "vino: KMS CRTC enable -- display ON, mode {}x{}@{} (scanout 
begins)\n",
+        timing.hactive, timing.vactive, timing.refresh_hz
+    );
+    // SAFETY: `crtc` is valid; its `dev` is our live drm_device.
+    let dev = unsafe { (*crtc).dev };
+    if dev.is_null() {
+        return;
+    }
+    // SAFETY: `dev` is our registered drm_device.
+    let data: &VinoDrmData = unsafe { VinoDrmDevice::from_raw(dev) };
+    let Some(head) = data.head_by_crtc(crtc) else {
+        return;
+    };
+    head.scanout.lock().active_timing = Some(timing);
+    // Push a live mode-set for the chosen mode (on this head's CP stream) so 
the dock switches
+    // to it at runtime, not just the EDID-preferred mode the bring-up setup 
sent. `set_mode`
+    // reserves a 16-byte tag placeholder. A no-op until the cipher is engaged.
+    if let Err(e) = data.send_cp(head.index, 0x48, 16, |ctr| 
super::cp::set_mode(ctr, &timing)) {
+        pr_warn!("vino: head{} runtime mode-set send failed ({e:?})\n", 
head.index);
+    }
+}
+
+/// CRTC `.atomic_disable`: the display is turning off.
+/// CRTC `.atomic_disable`: the display is turning off -- DPMS-off / blank / 
suspend all land
+/// here in atomic KMS (the compositor clears the CRTC `active` state). The 
compositor stops
+/// page-flipping, so no new frames are pushed; this resets the head's scanout 
state so a later
+/// re-enable (DPMS-on) re-inits the encoder and sends a **full keyframe** 
rather than diffing
+/// against a shadow the dock may have dropped while blanked, and re-uploads 
the cursor sprite.
+///
+/// The dock holds the last frame when video stops (it has its own scanout 
buffer), so the
+/// monitor freezes the last image rather than going black; a true 
backlight-standby would need
+/// a dock power command that is not decoded (DLM's 
`Standby`/`Suspend`/`TempPowerOff` are
+/// internal, vtable-dispatched events with no wire frame -- the same dead-end 
as gamma).
+unsafe extern "C" fn crtc_atomic_disable(
+    crtc: *mut bindings::drm_crtc,
+    _state: *mut bindings::drm_atomic_commit,
+) {
+    // SAFETY: in `.atomic_disable` the crtc and its `dev` are valid.
+    let dev = unsafe { (*crtc).dev };
+    if dev.is_null() {
+        return;
+    }
+    // SAFETY: `dev` is our registered drm_device.
+    let data: &VinoDrmData = unsafe { VinoDrmDevice::from_raw(dev) };
+    let Some(head) = data.head_by_crtc(crtc) else {
+        return;
+    };
+    {
+        let mut st = head.scanout.lock();
+        st.enc = None; // force a full re-init + keyframe on the next enable
+        st.dims = (0, 0);
+    }
+    head.cursor_primed
+        .store(false, core::sync::atomic::Ordering::SeqCst);
+    pr_info!("vino: KMS CRTC disable -- head{} display OFF (scanout 
stopped)\n", head.index);
+}
+
+/// Cursor plane `.atomic_update`: the cursor sprite and/or position changed. 
Sends the cursor
+/// CP messages (create once + image when a sprite framebuffer is present, 
then a move). Gated
+/// on CP engagement; a no-op on current hardware (the CP wall). See 
[`cursor_send`].
+unsafe extern "C" fn cursor_atomic_update(
+    plane: *mut bindings::drm_plane,
+    _state: *mut bindings::drm_atomic_commit,
+) {
+    if !super::CP_ENGAGED.load(core::sync::atomic::Ordering::SeqCst) {
+        return;
+    }
+    // SAFETY: in `.atomic_update` the plane and its committed state are valid 
for the commit.
+    let (dev_raw, fb, w, h, cx, cy) = unsafe {
+        let st = (*plane).state;
+        if st.is_null() {
+            return;
+        }
+        (
+            (*plane).dev,
+            (*st).fb,
+            (*st).crtc_w as usize,
+            (*st).crtc_h as usize,
+            (*st).crtc_x,
+            (*st).crtc_y,
+        )
+    };
+    // SAFETY: `dev_raw` is our live, registered drm_device.
+    let data: &VinoDrmData = unsafe { VinoDrmDevice::from_raw(dev_raw) };
+    let Some(head) = data.head_by_cursor(plane) else {
+        return;
+    };
+    if let Err(e) = cursor_send(data, head, fb, w, h, cx, cy) {
+        pr_warn!("vino: head{} cursor update failed ({e:?})\n", head.index);
+    }
+}
+
+/// Primary plane `.atomic_update`: a new framebuffer was flipped in -- the 
scanout hook.
+/// Maps the framebuffer, converts XRGB8888 -> RGB565, Vino-encodes the changed
+/// region against the previous frame, and bulk-writes the EP08 video frame.
+///
+/// The EP08 write only happens once the dock has engaged CP (see 
`docs/BLOCKER.md`):
+/// until then the dock NAKs/stalls EP08, so a normal module load must not 
push frames on
+/// every flip and thrash the dock. With the CP-engagement wall unsolved this 
never fires
+/// on real hardware.
+unsafe extern "C" fn primary_atomic_update(
+    plane: *mut bindings::drm_plane,
+    _state: *mut bindings::drm_atomic_commit,
+) {
+    // Don't touch EP08 until the dock has engaged CP. Pushing video (and the 
one-shot
+    // clear_halt of EPs 8/10/11/12) at a dock with a dead CP channel makes it 
fault and
+    // USB-reset, which re-probes the driver in a ~2.7 s loop.
+    if !super::CP_ENGAGED.load(core::sync::atomic::Ordering::SeqCst) {
+        return;
+    }
+    // SAFETY: in `.atomic_update` the plane and its committed state are 
valid; the plane
+    // state and its framebuffer are valid for the duration of the commit.
+    let (dev_raw, fb, w, h, damage, rotation) = unsafe {
+        let st = (*plane).state;
+        if st.is_null() {
+            return;
+        }
+        // Plane destination geometry == the negotiated mode (the compositor 
sizes the primary
+        // plane 1:1 with a virtual output), so this drives the dynamic 
scanout resolution.
+        let (w, h) = ((*st).crtc_w as usize, (*st).crtc_h as usize);
+        ((*plane).dev, (*st).fb, w, h, damage_bbox(st), (*st).rotation)
+    };
+    if fb.is_null() {
+        return;
+    }
+    // Recover our device-private data + this plane's head from the raw 
drm_device.
+    // SAFETY: `dev_raw` is our live, registered drm_device.
+    let ddev = unsafe { VinoDrmDevice::from_raw(dev_raw) };
+    let data: &VinoDrmData = ddev;
+    let Some(head) = data.head_by_primary(plane) else {
+        return;
+    };
+
+    use core::sync::atomic::Ordering::Relaxed;
+    // Throttle: while scanout is failing (dock NAKing because CP isn't 
engaged), skip the
+    // upcoming pageflips set by the backoff below instead of 
converting+encoding+sending a
+    // frame the dock will just drop. The backoff is shared across heads (a 
coarse global rate
+    // limit) -- fine while it never fires on real hardware (CP wall).
+    let skip = super::SCANOUT_SKIP.load(Relaxed);
+    if skip > 0 {
+        super::SCANOUT_SKIP.store(skip - 1, Relaxed);
+        return;
+    }
+    // Read this head's CRTC GAMMA_LUT (if a compositor set one) and apply it 
host-side in the
+    // conversion below -- there is no dock-side gamma message (see 
`read_gamma_lut`).
+    let gamma = read_gamma_lut(head);
+    match scanout_one(data, head, fb, w, h, damage, rotation, gamma.as_ref()) {
+        Ok(()) => {
+            let n = super::SCANOUT_FAILS.swap(0, Relaxed);
+            super::SCANOUT_SKIP.store(0, Relaxed);
+            if n > 0 {
+                pr_info!("vino: scanout recovered after {n} failed 
frame(s)\n");
+            }
+        }
+        Err(e) => {
+            // The dock NAKs every EP08 write (EPROTO) until CP engages -- 
expected and not
+            // actionable. Log the first failure and then at exponentially 
sparser points so
+            // dmesg isn't flooded, and back off the scanout rate.
+            let n = super::SCANOUT_FAILS.fetch_add(1, Relaxed) + 1;
+            if n == 1 || n.is_power_of_two() {
+                pr_err!("vino: scanout frame failed ({e:?}) [x{n}] -- 
throttling\n");
+            }
+            // Linear backoff capped at 120 frames (~2 s @ 60 Hz) between 
probe attempts, so
+            // recovery (CP engaging) is still detected within ~2 s while idle 
CPU stays low.
+            super::SCANOUT_SKIP.store(core::cmp::min(n, 120), Relaxed);
+        }
+    }
+}
+
+/// Map an output pixel `(dx, dy)` back to its source-framebuffer pixel `(sx, 
sy)` under a DRM
+/// plane `rotation` bitmask (`DRM_MODE_ROTATE_*` | `DRM_MODE_REFLECT_*`, the 
values the
+/// standard `drm_plane_create_rotation_property` exposes). `sw`/`sh` are the 
SOURCE
+/// (framebuffer) dimensions; the output dimensions are `(sw, sh)` for 0 
deg/180 deg and `(sh, sw)`
+/// for 90 deg/270 deg (the caller swaps source vs output accordingly). 
Rotation is clockwise;
+/// reflection is applied in source space after rotation. Pure + total 
(saturating), so it is
+/// unit-tested directly. Used by [`encode_and_send`] to honour the 
connector's rotation
+/// property -- DLM rotates host-side, vino rotates in the scanout encode.
+pub(super) fn rot_src(
+    rotation: u32,
+    dx: usize,
+    dy: usize,
+    sw: usize,
+    sh: usize,
+) -> (usize, usize) {
+    let xmax = sw.saturating_sub(1);
+    let ymax = sh.saturating_sub(1);
+    let rot = rotation & bindings::DRM_MODE_ROTATE_MASK;
+    let (mut sx, mut sy) = if rot == bindings::DRM_MODE_ROTATE_90 {
+        (dy, ymax.saturating_sub(dx))
+    } else if rot == bindings::DRM_MODE_ROTATE_180 {
+        (xmax.saturating_sub(dx), ymax.saturating_sub(dy))
+    } else if rot == bindings::DRM_MODE_ROTATE_270 {
+        (xmax.saturating_sub(dy), dx)
+    } else {
+        (dx, dy) // ROTATE_0 / unset
+    };
+    if rotation & bindings::DRM_MODE_REFLECT_X != 0 {
+        sx = xmax.saturating_sub(sx);
+    }
+    if rotation & bindings::DRM_MODE_REFLECT_Y != 0 {
+        sy = ymax.saturating_sub(sy);
+    }
+    (sx, sy)
+}
+
+/// True if `rotation` swaps width/height (90 deg or 270 deg), so the source 
framebuffer is
+/// portrait while the scanned-out display is landscape (or vice versa).
+fn rotation_swaps_dims(rotation: u32) -> bool {
+    let r = rotation & bindings::DRM_MODE_ROTATE_MASK;
+    r == bindings::DRM_MODE_ROTATE_90 || r == bindings::DRM_MODE_ROTATE_270
+}
+
+/// True if `rotation` is anything other than the identity (plain 0 deg), i.e. 
the scanout must
+/// remap every pixel and cannot take the damage-clip fast path.
+fn rotation_active(rotation: u32) -> bool {
+    rotation != 0 && rotation != bindings::DRM_MODE_ROTATE_0
+}
+
+/// Damage bounding box (pixels, clamped to the scanout) for this atomic 
update, or
+/// `None` meaning "convert the whole frame". Reads the standard 
`FB_DAMAGE_CLIPS` blob the
+/// compositor attaches to the plane state and unions its rects. The Vino 
encoder already
+/// shadow-diffs against the previous frame, so unchanged regions emit nothing 
regardless;
+/// the win here is skipping the XRGB8888->RGB565 conversion of those regions. 
Returns `None`
+/// when no damage is advertised, `ignore_damage_clips` is set, or the union 
is degenerate
+/// (all treated as a full-frame update).
+///
+/// SAFETY: `st` must be a valid `drm_plane_state` for the duration of the 
call.
+unsafe fn damage_bbox(
+    st: *const bindings::drm_plane_state,
+) -> Option<(usize, usize, usize, usize)> {
+    // SAFETY: caller guarantees `st` is a live plane state.
+    let (blob, ignore) = unsafe { ((*st).fb_damage_clips, 
(*st).ignore_damage_clips) };
+    if ignore || blob.is_null() {
+        return None;
+    }
+    // SAFETY: `blob` is non-null and lives as long as the plane state.
+    let (data, len) = unsafe { ((*blob).data as *const 
bindings::drm_mode_rect, (*blob).length) };
+    let n = len / core::mem::size_of::<bindings::drm_mode_rect>();
+    if data.is_null() || n == 0 {
+        return None;
+    }
+    let (mut x0, mut y0, mut x1, mut y1) = (i32::MAX, i32::MAX, i32::MIN, 
i32::MIN);
+    for i in 0..n {
+        // SAFETY: `i < n`, the rect array length implied by `blob.length`.
+        let r = unsafe { &*data.add(i) };
+        x0 = x0.min(r.x1);
+        y0 = y0.min(r.y1);
+        x1 = x1.max(r.x2);
+        y1 = y1.max(r.y2);
+    }
+    // Clamp to the plane's destination geometry and reject empty/degenerate 
boxes (fall back to
+    // a full frame). Read crtc_w/crtc_h off the plane state so the clamp 
tracks the live mode,
+    // not a fixed 1080p.
+    // SAFETY: caller guarantees `st` is a live plane state.
+    let (pw, ph) = unsafe { ((*st).crtc_w as i32, (*st).crtc_h as i32) };
+    let cx0 = x0.clamp(0, pw) as usize;
+    let cy0 = y0.clamp(0, ph) as usize;
+    let cx1 = x1.clamp(0, pw) as usize;
+    let cy1 = y1.clamp(0, ph) as usize;
+    if cx1 <= cx0 || cy1 <= cy0 {
+        return None;
+    }
+    Some((cx0, cy0, cx1, cy1))
+}
+
+/// Read the CRTC's `GAMMA_LUT` and flatten it into three 256-entry 8-bit 
lookup tables
+/// (R, G, B), or `None` when no gamma is set (the common case -- the 
conversion then runs at
+/// full speed). DLM gamma-corrects pixels **host-side** before encoding; the 
DL3 dock has no
+/// gamma CP message (the `NotifyGammaCurve`/`SetGammaMode` handlers are 
DLM-internal,
+/// vtable-dispatched, and emit no wire frame -- confirmed against the 
decompile and every
+/// capture), so vino applies the LUT in the scanout exactly like it applies 
`rotation`. The
+/// blob holds `n` `drm_color_lut` entries (u16 per channel); 8-bit input `i` 
maps to entry
+/// `i*(n-1)/255` and takes that entry's high 8 bits.
+fn read_gamma_lut(head: &Head) -> Option<[[u8; 256]; 3]> {
+    // SAFETY: `head.crtc` was initialised in `kms_init`; its committed 
`state` and the
+    // gamma_lut blob it references are valid for the duration of the atomic 
commit.
+    let blob = unsafe {
+        let cs = (*head.crtc.get()).state;
+        if cs.is_null() {
+            return None;
+        }
+        (*cs).gamma_lut
+    };
+    if blob.is_null() {
+        return None;
+    }
+    // SAFETY: `blob` is a live drm_property_blob for the commit; 
`data`/`length` are valid.
+    let (ptr, len) =
+        unsafe { ((*blob).data as *const bindings::drm_color_lut, 
(*blob).length) };
+    let n = len / core::mem::size_of::<bindings::drm_color_lut>();
+    if ptr.is_null() || n == 0 {
+        return None;
+    }
+    let mut t = [[0u8; 256]; 3];
+    for i in 0..256usize {
+        let idx = if n == 1 { 0 } else { i * (n - 1) / 255 };
+        // SAFETY: `idx < n`, within the blob's `n` `drm_color_lut` entries.
+        let e = unsafe { &*ptr.add(idx) };
+        t[0][i] = (e.red >> 8) as u8;
+        t[1][i] = (e.green >> 8) as u8;
+        t[2][i] = (e.blue >> 8) as u8;
+    }
+    Some(t)
+}
+
+/// vmap `fb`, encode it, and push one EP08 frame. Split out so `?` can be 
used. `damage`
+/// bounds the XRGB8888->RGB565 conversion to the changed region (see 
[`damage_bbox`]).
+fn scanout_one(
+    data: &VinoDrmData,
+    head: &Head,
+    fb: *mut bindings::drm_framebuffer,
+    w: usize,
+    h: usize,
+    damage: Option<(usize, usize, usize, usize)>,
+    rotation: u32,
+    gamma: Option<&[[u8; 256]; 3]>,
+) -> Result {
+    // `w`/`h` are the plane's destination (displayed) geometry (== the 
negotiated mode),
+    // threaded in from `primary_atomic_update`, so the scanout follows the 
live mode (e.g. the
+    // dock's
+    // native 4K) instead of a hardcoded 1080p. `drm_framebuffer` is opaque in 
the bindings, so
+    // the geometry comes from the plane state; our XRGB8888 buffers are 
packed. Under a
+    // 90 deg/270 deg
+    // `rotation` the source framebuffer is portrait relative to the display, 
so its row pitch
+    // tracks the *source* width -- `encode_and_send` derives that from 
`rotation`.
+    if w == 0 || h == 0 {
+        return Err(EINVAL);
+    }
+
+    // Map the framebuffer's backing pages into the kernel address space.
+    // SAFETY: `iosys_map` is POD (a pointer union + bool); all-zero is a 
valid,
+    // "not mapped" value that `drm_gem_fb_vmap` overwrites for present planes.
+    let mut map: [bindings::iosys_map; 4] = unsafe { core::mem::zeroed() };
+    let mut dmap: [bindings::iosys_map; 4] = unsafe { core::mem::zeroed() };
+    // SAFETY: `fb` is a valid framebuffer with GEM-backed storage.
+    to_result(unsafe { bindings::drm_gem_fb_vmap(fb, map.as_mut_ptr(), 
dmap.as_mut_ptr()) })?;
+
+    // SAFETY: plane 0's CPU virtual address, valid until `drm_gem_fb_vunmap`.
+    let vaddr = unsafe { map[0].__bindgen_anon_1.vaddr } as *const u8;
+    let result = if vaddr.is_null() {
+        Err(EINVAL)
+    } else {
+        encode_and_send(data, head, vaddr, w, h, damage, rotation, gamma)
+    };
+
+    // SAFETY: balances the vmap above with the same `map`.
+    unsafe { bindings::drm_gem_fb_vunmap(fb, map.as_mut_ptr()) };
+    result
+}
+
+/// Convert the mapped XRGB8888 frame to RGB565, Vino-encode it against the 
previous
+/// frame, and bulk-write the resulting EP08 frame to the dock.
+fn encode_and_send(
+    data: &VinoDrmData,
+    head: &Head,
+    vaddr: *const u8,
+    w: usize,
+    h: usize,
+    damage: Option<(usize, usize, usize, usize)>,
+    rotation: u32,
+    gamma: Option<&[[u8; 256]; 3]>,
+) -> Result {
+    // Convert XRGB8888 (LE bytes B,G,R,X) -> RGB565 and encode, all under the 
scanout lock.
+    // The conversion fills a PERSISTENT `cur` buffer (allocated once with the 
encoder) in
+    // place -- no per-frame ~4 MiB kmalloc, which is what was failing with 
ENOMEM and flooding
+    // the log. The encoder's shadow buffer is mutable state, so the lock is 
needed regardless.
+    let frame = {
+        let mut st = head.scanout.lock();
+        // On the first frame `cur` is freshly zeroed, so the whole buffer 
must be filled
+        // regardless of the advertised damage (a partial fill would scan out 
black around
+        // the damage box). Afterwards, unchanged regions of `cur` already 
hold the previous
+        // frame (== the shadow the encoder diffs against), so converting only 
the damage box
+        // is correct and skips the rest of the XRGB8888->RGB565 work.
+        // Re-initialise the encoder/shadow/conversion buffers on the first 
frame AND whenever
+        // the framebuffer geometry changes (a mode switch), so they always 
match `cur`'s size.
+        let first = st.enc.is_none() || st.dims != (w, h);
+        if first {
+            st.enc = Some(super::video::Encoder::new(w, h, 
super::video::Mode::Rle)?);
+            st.cur = VVec::from_elem(0u16, w * h, GFP_KERNEL)?;
+            st.dims = (w, h);
+            st.hint = 0; // previous frame's size no longer applies at the new 
geometry
+        }
+        // Source framebuffer geometry: a 90 deg/270 deg rotation makes the 
source portrait relative
+        // to the displayed `w`x`h`, so its packed row pitch tracks the 
*source* width.
+        let (sw, sh) = if rotation_swaps_dims(rotation) { (h, w) } else { (w, 
h) };
+        let pitch = sw * 4;
+        // Damage clips are in source coordinates and don't map cleanly 
through a rotation, so
+        // convert the whole frame on a (re)allocation OR whenever a 
rotation/reflection is in
+        // effect; the encoder still shadow-diffs, so unchanged pixels emit 
nothing regardless.
+        // A gamma LUT recolours every pixel, so it also forces a full convert 
(still no extra
+        // wire traffic -- the recoloured output is identical frame-to-frame 
for static
+        // content,
+        // so the shadow-diff emits nothing for unchanged regions).
+        let full = first || rotation_active(rotation) || gamma.is_some();
+        let (x0, y0, x1, y1) = if full { (0, 0, w, h) } else { 
damage.unwrap_or((0, 0, w, h)) };
+        // Split-borrow the fields so the in-place fill and the &mut encode 
can coexist.
+        let ScanoutState { enc, cur, seq, hint, dims: _, active_timing: _ } = 
&mut *st;
+        for dy in y0..y1 {
+            for dx in x0..x1 {
+                // Output pixel (dx,dy) -> source pixel under the plane 
rotation/reflection.
+                let (sx, sy) = rot_src(rotation, dx, dy, sw, sh);
+                // SAFETY: `sy*pitch + sx*4 + 3` is within the mapped source 
framebuffer
+                // (`sw*sh*4` bytes); `rot_src` guarantees `sx < sw`, `sy < 
sh`.
+                let px =
+                    unsafe { (vaddr.add(sy * pitch + sx * 4) as *const 
u32).read_unaligned() };
+                let (mut r, mut g, mut b) =
+                    (((px >> 16) & 0xff) as usize, ((px >> 8) & 0xff) as 
usize, (px & 0xff) as usize);
+                // Apply the CRTC gamma LUT host-side (DLM gamma-corrects 
pixels before
+                // encoding -- there is no dock-side gamma CP message; see 
`read_gamma_lut`).
+                if let Some(t) = gamma {
+                    r = t[0][r] as usize;
+                    g = t[1][g] as usize;
+                    b = t[2][b] as usize;
+                }
+                cur[dy * w + dx] =
+                    (((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)) as u16;
+            }
+        }
+        let s = *seq;
+        *seq = seq.wrapping_add(1);
+        let enc = enc.as_mut().ok_or(ENOMEM)?;
+        // Encode straight into the outgoing frame buffer: reserve the EP08 
header up
+        // front, append the codec stream in place, then back-patch the header 
now that
+        // the payload length is known. This replaces a 
two-allocation/extra-copy path
+        // (encode -> KVec, then frame_to_ep08 -> second KVec) with a single 
buffer,
+        // and `hint` pre-sizes it from the last frame so the encode rarely 
reallocates.
+        const HDR: usize = super::video::EP08_HDR_LEN;
+        let mut frame = KVec::with_capacity((*hint).max(HDR + 64), 
GFP_KERNEL)?;
+        frame.extend_from_slice(&[0u8; HDR], GFP_KERNEL)?; // header 
placeholder
+        enc.encode_into(&*cur, &mut frame)?;
+        let payload_len = frame.len() - HDR;
+        super::video::write_ep08_header(&mut frame[..HDR], payload_len, s);
+        *hint = frame.len();
+        frame
+    };
+
+    // Push the frame to this head's video endpoint (lock released).
+    let dev: &super::usb::Device = data.intf.as_ref();
+    // First live-scanout frame: clear-halt the four iface-0 bulk-OUT video 
endpoints
+    // (0x08 main + 0x0a/0x0b/0x0c aux, covering every head) so the first 
write doesn't
+    // ETIMEDOUT on a stale endpoint toggle. DLM clear-halts these at 
engagement (the
+    // "startRender" step). Once, globally.
+    if !super::EP08_SCANOUT_PRIMED.swap(true, 
core::sync::atomic::Ordering::SeqCst) {
+        for ep in [0x08u8, 0x0a, 0x0b, 0x0c] {
+            let _ = dev.clear_halt(ep);
+        }
+        pr_info!("vino: video endpoints primed (clear-halt 8/10/11/12)\n");
+    }
+    // Head 0 -> EP 0x08, head 1 -> EP 0x0a (see `HEAD_EP`).
+    dev.bulk_send(head.video_ep(), &frame, super::timeout())?;
+    Ok(())
+}
+
+/// Send the cursor CP messages for the current sprite + position (called from
+/// [`cursor_atomic_update`]). `fb` is the cursor sprite framebuffer 
(`None`/null = hidden),
+/// `w`x`h` its size, `(cx, cy)` the on-CRTC position. Sends `create` (once, 
the constant
+/// sprite size), `image` (the ARGB8888 sprite -- its little-endian memory 
bytes are already
+/// the
+/// `B,G,R,A` order the dock wants, copied row-by-row to honour the 
framebuffer pitch), then a
+/// `move`. Every send routes through [`VinoDrmData::send_cp`], so all of this 
is a no-op until
+/// the CP cipher engages (the wall). A hidden cursor currently just re-issues 
a move (a
+/// dedicated hide message is a future refinement).
+fn cursor_send(
+    data: &VinoDrmData,
+    head: &Head,
+    fb: *mut bindings::drm_framebuffer,
+    w: usize,
+    h: usize,
+    cx: i32,
+    cy: i32,
+) -> Result {
+    let hid = head.index; // cursor messages carry the head id at off22; CP 
routes by head too
+    let (mx, my) = (
+        cx.clamp(0, u16::MAX as i32) as u16,
+        cy.clamp(0, u16::MAX as i32) as u16,
+    );
+    if fb.is_null() || w == 0 || h == 0 {
+        // Hidden cursor: no sprite to upload, just track the position.
+        return data.send_cp(hid, 0x1a, 0, |ctr| super::cp::cursor_move(ctr, 
hid, mx, my));
+    }
+    // Declare the sprite dimensions once per head, then upload the bitmap. We 
don't diff sprite
+    // content yet, so the image is re-sent on every sprite-present update.
+    if !head.cursor_primed.swap(true, core::sync::atomic::Ordering::SeqCst) {
+        data.send_cp(hid, 0x1b, 0, |ctr| super::cp::cursor_create(ctr, w as 
u16, h as u16))?;
+    }
+    // vmap the sprite; copy `w*h*4` BGRA bytes row-by-row (the source pitch 
is `w*4` for our
+    // packed cursor buffers, but copying per-row keeps it correct if that 
ever changes).
+    let pitch = w * 4;
+    // SAFETY: `iosys_map` is POD; all-zero is a valid "not mapped" value 
`drm_gem_fb_vmap`
+    // fills.
+    let mut map: [bindings::iosys_map; 4] = unsafe { core::mem::zeroed() };
+    let mut dmap: [bindings::iosys_map; 4] = unsafe { core::mem::zeroed() };
+    // SAFETY: `fb` is a valid cursor framebuffer with GEM-backed storage.
+    to_result(unsafe { bindings::drm_gem_fb_vmap(fb, map.as_mut_ptr(), 
dmap.as_mut_ptr()) })?;
+    // SAFETY: plane 0's CPU virtual address, valid until `drm_gem_fb_vunmap`.
+    let vaddr = unsafe { map[0].__bindgen_anon_1.vaddr } as *const u8;
+    let res = if vaddr.is_null() {
+        Err(EINVAL)
+    } else {
+        (|| -> Result {
+            let mut bgra = KVec::with_capacity(w * h * 4, GFP_KERNEL)?;
+            for y in 0..h {
+                // SAFETY: `[y*pitch, y*pitch + w*4)` is within the mapped 
`h*pitch` sprite.
+                let row = unsafe { core::slice::from_raw_parts(vaddr.add(y * 
pitch), w * 4) };
+                bgra.extend_from_slice(row, GFP_KERNEL)?;
+            }
+            data.send_cp(hid, 0x1c, 0, |ctr| {
+                super::cp::cursor_image(ctr, w as u16, h as u16, &bgra)
+            })
+        })()
+    };
+    // SAFETY: balances the vmap above with the same `map`.
+    unsafe { bindings::drm_gem_fb_vunmap(fb, map.as_mut_ptr()) };
+    res?;
+    data.send_cp(hid, 0x1a, 0, |ctr| super::cp::cursor_move(ctr, hid, mx, my))
+}
+
+/// Wire up the atomic KMS pipeline on `ddev` (called after `drm::Device::new` 
and
+/// before `drm_dev_register`). Sets `mode_config`, builds the virtual 
connector,
+/// and initialises the atomic CRTC + primary/cursor planes + virtual encoder.
+pub(super) fn kms_init<C: drm::DeviceContext>(
+    ddev: &drm::Device<VinoDrmDriver, C>,
+) -> Result {
+    let raw = ddev.as_raw();
+    // Deref `drm::Device<T>` -> `T::Data` to reach the embedded C objects.
+    let data: &VinoDrmData = ddev;
+
+    // SAFETY: `raw` is a valid, not-yet-registered drm_device; the 
funcs/objects
+    // referenced below live in device-private memory (`data`) for its 
lifetime.
+    unsafe {
+        to_result(bindings::drmm_mode_config_init(raw))?;
+
+        let mc = &mut (*raw).mode_config;
+        mc.min_width = 0;
+        mc.min_height = 0;
+        mc.max_width = 4096;
+        mc.max_height = 4096;
+        // Advertise a 64x64 hardware cursor (the dock's cursor sprite size) 
so userspace
+        // drives the cursor plane instead of compositing the pointer into the 
framebuffer.
+        mc.cursor_width = CURSOR_SIZE;
+        mc.cursor_height = CURSOR_SIZE;
+        let mcf = data.mode_cfg_funcs.get();
+        (*mcf).fb_create = Some(bindings::drm_gem_fb_create);
+        // `vino_atomic_check` = the standard atomic check + the combined 
cross-head USB
+        // bandwidth budget (rejects e.g. two simultaneous 4K modes).
+        (*mcf).atomic_check = Some(vino_atomic_check);
+        (*mcf).atomic_commit = Some(bindings::drm_atomic_helper_commit);
+        mc.funcs = mcf;
+
+        // ---- Shared vtables (one set for every head; the callbacks recover 
the head from
+        // the C object pointer). Plane/CRTC `atomic_check` are left NULL: a 
virtual sink
+        // accepts any configuration, and the helpers still invoke 
`atomic_update`/
+        // `atomic_enable` because the objects are assigned to the CRTC.
+
+        // Connector funcs + helper. We report presence from the cached EDID 
(see `detect`)
+        // and deliver HPD ourselves (`set_edid`/`fire_hotplug`).
+        let cf = data.conn_funcs.get();
+        (*cf).fill_modes = 
Some(bindings::drm_helper_probe_single_connector_modes);
+        (*cf).detect = Some(detect);
+        (*cf).destroy = Some(bindings::drm_connector_cleanup);
+        (*cf).reset = Some(bindings::drm_atomic_helper_connector_reset);
+        (*cf).atomic_duplicate_state =
+            Some(bindings::drm_atomic_helper_connector_duplicate_state);
+        (*cf).atomic_destroy_state =
+            Some(bindings::drm_atomic_helper_connector_destroy_state);
+        (*data.conn_helper.get()).get_modes = Some(get_modes);
+        // Prune any single mode above the per-head pixel-clock ceiling 
(~4K@60).
+        (*data.conn_helper.get()).mode_valid = Some(mode_valid);
+
+        // One `drm_plane_funcs` shared by both planes; per-plane helper funcs 
(the primary's
+        // `atomic_update` scans out, the cursor's sends cursor CP).
+        let plf = data.plane_funcs.get();
+        (*plf).update_plane = Some(bindings::drm_atomic_helper_update_plane);
+        (*plf).disable_plane = Some(bindings::drm_atomic_helper_disable_plane);
+        (*plf).destroy = Some(bindings::drm_plane_cleanup);
+        (*plf).reset = Some(bindings::drm_atomic_helper_plane_reset);
+        (*plf).atomic_duplicate_state =
+            Some(bindings::drm_atomic_helper_plane_duplicate_state);
+        (*plf).atomic_destroy_state = 
Some(bindings::drm_atomic_helper_plane_destroy_state);
+        (*data.primary_helper.get()).atomic_update = 
Some(primary_atomic_update);
+        (*data.cursor_helper.get()).atomic_update = Some(cursor_atomic_update);
+
+        // CRTC funcs + helper.
+        let crf = data.crtc_funcs.get();
+        (*crf).set_config = Some(bindings::drm_atomic_helper_set_config);
+        (*crf).page_flip = Some(bindings::drm_atomic_helper_page_flip);
+        (*crf).destroy = Some(bindings::drm_crtc_cleanup);
+        (*crf).reset = Some(bindings::drm_atomic_helper_crtc_reset);
+        (*crf).atomic_duplicate_state =
+            Some(bindings::drm_atomic_helper_crtc_duplicate_state);
+        (*crf).atomic_destroy_state = 
Some(bindings::drm_atomic_helper_crtc_destroy_state);
+        let crh = data.crtc_helper.get();
+        (*crh).atomic_enable = Some(crtc_atomic_enable);
+        (*crh).atomic_disable = Some(crtc_atomic_disable);
+
+        // Encoder funcs.
+        (*data.encoder_funcs.get()).destroy = 
Some(bindings::drm_encoder_cleanup);
+
+        // Build each head's objects (connector + primary/cursor planes + CRTC 
+ encoder).
+        for head in data.heads() {
+            build_head(raw, data, head)?;
+        }
+
+        drm_mode_config_reset(raw);
+    }
+    Ok(())
+}
+
+/// Build one head's KMS objects -- connector + primary plane (scanout) + 
cursor plane + CRTC +
+/// virtual encoder -- using the shared vtables already filled in `data`. Each 
is a complete
+/// independent output (its own CRTC), so the compositor sees [`NHEADS`] 
monitors and routes
+/// each to its own video EP / CP stream (see [`Head`]).
+///
+/// SAFETY: `raw` is a valid, not-yet-registered drm_device; the `data`/`head` 
C objects live
+/// in device-private memory for its lifetime.
+unsafe fn build_head(raw: *mut bindings::drm_device, data: &VinoDrmData, head: 
&Head) -> Result {
+    // SAFETY: see the function contract; every object/vtable below is 
device-private memory.
+    unsafe {
+        // Connector.
+        let conn = head.connector.get();
+        to_result(bindings::drm_connector_init(
+            raw,
+            conn,
+            data.conn_funcs.get(),
+            bindings::DRM_MODE_CONNECTOR_VIRTUAL as i32,
+        ))?;
+        (*conn).helper_private = data.conn_helper.get();
+        (*conn).polled = bindings::DRM_CONNECTOR_POLL_HPD as u8;
+
+        // Primary plane (XRGB8888 scanout). `possible_crtcs` is fixed up once 
the CRTC exists.
+        let primary = head.primary.get();
+        to_result(bindings::drm_universal_plane_init(
+            raw,
+            primary,
+            0,
+            data.plane_funcs.get(),
+            PRIMARY_FORMATS.as_ptr(),
+            PRIMARY_FORMATS.len() as u32,
+            ptr::null(),
+            bindings::drm_plane_type_DRM_PLANE_TYPE_PRIMARY,
+            ptr::null(),
+        ))?;
+        (*primary).helper_private = data.primary_helper.get();
+
+        // Cursor plane (ARGB8888 sprite).
+        let cursor = head.cursor.get();
+        to_result(bindings::drm_universal_plane_init(
+            raw,
+            cursor,
+            0,
+            data.plane_funcs.get(),
+            CURSOR_FORMATS.as_ptr(),
+            CURSOR_FORMATS.len() as u32,
+            ptr::null(),
+            bindings::drm_plane_type_DRM_PLANE_TYPE_CURSOR,
+            ptr::null(),
+        ))?;
+        (*cursor).helper_private = data.cursor_helper.get();
+
+        // CRTC with both planes, plus a GAMMA_LUT (applied host-side in 
`read_gamma_lut`).
+        let crtc = head.crtc.get();
+        to_result(bindings::drm_crtc_init_with_planes(
+            raw,
+            crtc,
+            primary,
+            cursor,
+            data.crtc_funcs.get(),
+            ptr::null(),
+        ))?;
+        (*crtc).helper_private = data.crtc_helper.get();
+        bindings::drm_crtc_enable_color_mgmt(crtc, 0, false, GAMMA_SIZE);
+
+        // The CRTC now has an index: bind both planes and the encoder to it.
+        let crtc_mask = 1u32 << (*crtc).index;
+        (*primary).possible_crtcs = crtc_mask;
+        (*cursor).possible_crtcs = crtc_mask;
+
+        // Virtual encoder bound to this head's connector.
+        let encoder = head.encoder.get();
+        to_result(bindings::drm_encoder_init(
+            raw,
+            encoder,
+            data.encoder_funcs.get(),
+            bindings::DRM_MODE_ENCODER_VIRTUAL as i32,
+            ptr::null(),
+        ))?;
+        (*encoder).possible_crtcs = crtc_mask;
+        to_result(bindings::drm_connector_attach_encoder(conn, encoder))?;
+
+        // Rotation property on the primary plane (DLM rotates host-side; vino 
remaps in the
+        // scanout encode -- see `rot_src`). Canonical helper; non-fatal on 
failure.
+        let supported = 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 rc = bindings::drm_plane_create_rotation_property(
+            primary,
+            bindings::DRM_MODE_ROTATE_0,
+            supported,
+        );
+        if rc != 0 {
+            pr_warn!("vino: head{} rotation property unavailable ({rc})\n", 
head.index);
+        }
+    }
+    Ok(())
+}
+
+/// Thin wrapper so the `unsafe` block above reads cleanly.
+unsafe fn drm_mode_config_reset(raw: *mut bindings::drm_device) {
+    // SAFETY: `raw` is a valid drm_device with mode_config initialised.
+    unsafe { bindings::drm_mode_config_reset(raw) };
+}
diff --git a/drivers/gpu/drm/vino/vino.rs b/drivers/gpu/drm/vino/vino.rs
index e9e6324b717b..1091dcc992c7 100644
--- a/drivers/gpu/drm/vino/vino.rs
+++ b/drivers/gpu/drm/vino/vino.rs
@@ -44,6 +44,7 @@
 use kernel::{
     alloc::flags::GFP_KERNEL,
     bindings,
+    drm,
     device::{self, Core},
     error::code::{ENODEV, EINVAL},
     prelude::*,
@@ -79,6 +80,24 @@ fn timeout() -> Delta {
 /// hardware -- the dock runs the whole plaintext handshake but never engages 
the encrypted CP.
 static CP_ENGAGED: core::sync::atomic::AtomicBool = 
core::sync::atomic::AtomicBool::new(false);
 
+/// One-shot: clear-halt + prime the video endpoints before the first 
live-scanout EP08 write.
+static EP08_SCANOUT_PRIMED: core::sync::atomic::AtomicBool =
+    core::sync::atomic::AtomicBool::new(false);
+
+/// Consecutive failed live-scanout frames, for log rate-limiting. Until CP 
engages, the dock
+/// NAKs every EP08 write (EPROTO), so without this every compositor pageflip 
would spam dmesg.
+static SCANOUT_FAILS: core::sync::atomic::AtomicU64 = 
core::sync::atomic::AtomicU64::new(0);
+
+/// Pageflip throttle: number of upcoming pageflips to skip before the next 
scanout attempt
+/// (a backoff while the dock NAKs). A single successful frame clears it.
+static SCANOUT_SKIP: core::sync::atomic::AtomicU64 = 
core::sync::atomic::AtomicU64::new(0);
+
+/// Set once the bring-up work item finishes (AKE/CP attempt done). `detect` 
only connects the
+/// live-scanout connector AFTER this, so a compositor enabling the output 
cannot start EP08
+/// scanout on top of the still-running AKE on the same USB device.
+static BRINGUP_COMPLETE: core::sync::atomic::AtomicBool =
+    core::sync::atomic::AtomicBool::new(false);
+
 mod proto;
 mod crypto;
 mod rng;
@@ -103,9 +122,13 @@ struct Session {
     cap_announce: KVec<u8>,
 }
 
+mod drm_sink;
+
 /// Per-bound-interface driver state.
 struct VinoDriver {
     _intf: ARef<usb::Interface>,
+    /// The registered `drm::Device` (only on the control interface, iface 0).
+    _ddev: Option<ARef<drm_sink::VinoDrmDevice>>,
 }
 
 /// Deferred bring-up work item: the bring-up sequence run on the system 
workqueue instead
@@ -115,6 +138,7 @@ struct VinoDriver {
 #[pin_data]
 struct BringUp {
     intf: ARef<usb::Interface>,
+    ddev: Option<ARef<drm_sink::VinoDrmDevice>>,
     #[pin]
     work: Work<BringUp>,
 }
@@ -124,10 +148,14 @@ impl HasWork<Self> for BringUp { self.work }
 }
 
 impl BringUp {
-    fn new(intf: ARef<usb::Interface>) -> Result<Arc<Self>> {
+    fn new(
+        intf: ARef<usb::Interface>,
+        ddev: Option<ARef<drm_sink::VinoDrmDevice>>,
+    ) -> Result<Arc<Self>> {
         Arc::pin_init(
             pin_init!(BringUp {
                 intf,
+                ddev,
                 work <- new_work!("vino::bring_up"),
             }),
             GFP_KERNEL,
@@ -141,39 +169,73 @@ impl WorkItem for BringUp {
     fn run(this: Arc<BringUp>) {
         let cdev: &device::Device = this.intf.as_ref();
         let dev: &usb::Device = this.intf.as_ref();
-        // WIP scaffold: plaintext bring-up, the clean-room HDCP 2.2 
AKE/LC/SKE, then the
-        // post-SKE CP setup. Bind regardless of the outcome -- there is no 
display path until
-        // the dock engages the encrypted control plane, which it currently 
never does (see the
-        // "help wanted" note at the top of the file). The DRM sink lands in a 
later patch.
+        let ddev = &this.ddev;
+        // WIP scaffold: attempt the plaintext bring-up, then the clean-room 
HDCP 2.2
+        // AKE/LC/SKE, then the post-SKE CP setup. Bind regardless of the 
outcome -- there
+        // is no display path until the dock engages the encrypted control 
plane, which it
+        // currently never does (see the "help wanted" note at the top of the 
file).
         match VinoDriver::bring_up(dev) {
             Ok(()) => {
                 dev_info!(cdev, "vino: plaintext session init OK\n");
                 match VinoDriver::run_ake(dev) {
                     Ok(session) => {
                         dev_info!(cdev, "vino: HDCP AKE + LC + SKE complete 
(session keyed)\n");
+                        // Dev diagnostic: the live session key/riv, so the 
dock's encrypted
+                        // EP84 replies can be decoded offline from a usbmon 
capture. Behind
+                        // pr_debug, so compiled out unless dynamic debug is 
enabled.
                         pr_debug!("vino: SESSION ks={:02x?} riv={:02x?}\n", 
&session.ks, &session.riv);
-                        // Phase 2c: drive the post-SKE CP setup. 
send_cp_setup re-seals DLM's
-                        // captured setup template under THIS session's live 
ks/riv and sends it;
-                        // `acks` counts the dock's encrypted wsub=0x45 
replies. THIS IS THE WALL:
-                        // on a cold dock `acks` stays 0 -- the dock runs the 
entire plaintext
-                        // handshake but never engages the encrypted CP.
+
+                        // Phase 2c: drive the post-SKE CP setup. 
send_cp_setup re-seals
+                        // DLM's captured setup template under THIS session's 
live ks/riv and
+                        // sends it; `acks` counts the dock's encrypted 
wsub=0x45 replies.
+                        // THIS IS THE WALL: on a cold dock `acks` stays 0 -- 
the dock runs the
+                        // entire plaintext handshake but never engages the 
encrypted CP.
                         let mut edid_out: Option<KVec<u8>> = None;
                         match VinoDriver::send_cp_setup(dev, &session, &mut 
edid_out) {
-                            Ok((n, acks, _wseq_end, _ctr_end)) => {
+                            Ok((n, acks, wseq_end, ctr_end)) => {
                                 dev_info!(cdev,
                                     "vino: CP setup sent -- {n} messages, 
{acks} dock CP acks (wsub=0x45)\n");
-                                // CP engagement gates EP08 video (added in a 
later patch): until
-                                // the dock acks, pushing pixels at it wedges 
the hub.
+                                // CP engagement gates EP08 video: until the 
dock acks, pushing
+                                // pixels at it wedges the hub.
                                 CP_ENGAGED.store(acks > 0, 
core::sync::atomic::Ordering::SeqCst);
+                                // Publish the engaged session to the DRM 
device so the KMS
+                                // callbacks
+                                // can send runtime CP (mode-set on a modeset, 
cursor on motion),
+                                // continuing this keystream. Only when the 
dock actually engaged.
+                                if acks > 0 {
+                                    if let Some(d) = ddev.as_ref() {
+                                        let data: &drm_sink::VinoDrmData = d;
+                                        data.publish_session(
+                                            &session.ks, &session.riv, 
wseq_end, ctr_end,
+                                        );
+                                    }
+                                }
                             }
                             Err(e) => dev_info!(cdev, "vino: CP setup 
incomplete ({e:?}) -- WIP\n"),
                         }
+                        // Cache the dock's EDID on the DRM device (when the 
CP channel
+                        // delivered it) so the connector's get_modes installs 
the real
+                        // monitor descriptor via the standard DRM EDID 
helpers.
+                        if let (Some(blob), Some(d)) = (edid_out, 
ddev.as_ref()) {
+                            let n = blob.len();
+                            let data: &drm_sink::VinoDrmData = d;
+                            data.set_edid(blob);
+                            dev_info!(cdev, "vino: cached dock EDID for 
connector ({n} bytes)\n");
+                        }
                     }
                     Err(e) => dev_info!(cdev, "vino: HDCP AKE incomplete 
({e:?}) -- WIP\n"),
                 }
             }
             Err(e) => dev_info!(cdev, "vino: session init incomplete ({e:?}) 
-- WIP\n"),
         }
+        // Bring-up attempt finished: allow the live-scanout connector to 
report connected
+        // and let a compositor drive EP08 frames, without racing the 
handshake.
+        BRINGUP_COMPLETE.store(true, core::sync::atomic::Ordering::SeqCst);
+        if let Some(d) = ddev.as_ref() {
+            let data: &drm_sink::VinoDrmData = d;
+            data.fire_hotplug();
+            dev_info!(cdev, "vino: bring-up complete -- live-scanout connector 
now connected\n");
+        }
     }
 }
 
@@ -1596,16 +1658,59 @@ fn probe<'bound>(
                 return Err(ENODEV);
             }
             dev_info!(cdev, "vino: bound D6000 interface {ifnum} (idle -- 
control is iface 0)\n");
-            return Ok(Self { _intf: intf.into() });
+            return Ok(Self { _intf: intf.into(), _ddev: None });
         }
         dev_info!(cdev, "vino: bound DisplayLink D6000 -- plaintext session 
bring-up\n");
 
-        // Bring-up is blocking synchronous USB I/O; hand it to the system 
workqueue so
-        // probe() returns immediately and userspace stays responsive. The 
work item holds
-        // a refcounted handle to the interface, so the bulk endpoints outlive 
probe(); USB
-        // I/O after an intervening disconnect simply errors and is logged.
+        // Phase 3: register a real DRM/KMS device on the control interface so 
the dock
+        // shows up as a mode-settable `card`/`renderD` node (atomic KMS via 
the simple
+        // display pipe, one 1080p virtual connector, GEM-shmem dumb buffers). 
Non-fatal:
+        // bring-up still proceeds (and the interface still binds) if any step 
fails, so
+        // a DRM-core hiccup can't regress the USB session work.
+        // Hold a refcounted handle to the bound interface; one copy goes into 
the DRM
+        // device-private (for the EP08 scanout path), one stays in 
`VinoDriver`.
         let intf_ref: ARef<usb::Interface> = intf.into();
-        match BringUp::new(intf_ref.clone()) {
+        // DRM device lifecycle (drm-rust API): allocate an 
`UnregisteredDevice`, wire up
+        // the KMS pipeline on it while still unregistered, then hand it to
+        // `Registration::new_foreign_owned` (which registers it and ties its 
lifetime to
+        // the bound USB device via devres, returning a borrowed `&Device`).
+        let ddev: Option<ARef<drm_sink::VinoDrmDevice>> =
+            match drm::UnregisteredDevice::<drm_sink::VinoDrmDriver>::new(
+                cdev,
+                drm_sink::VinoDrmData::new(intf_ref.clone()),
+            ) {
+                Ok(unreg) => match drm_sink::kms_init(&unreg) {
+                    Ok(()) => match 
drm::driver::Registration::new_foreign_owned(unreg, cdev, 0) {
+                        Ok(reg_dev) => {
+                            dev_info!(cdev, "vino: DRM+KMS device registered 
(card node live, 1080p)\n");
+                            Some(reg_dev.into())
+                        }
+                        Err(e) => {
+                            dev_info!(cdev, "vino: DRM registration failed 
({e:?}) -- continuing without card node\n");
+                            None
+                        }
+                    },
+                    Err(e) => {
+                        dev_info!(cdev, "vino: KMS init failed ({e:?}) -- 
continuing without card node\n");
+                        None
+                    }
+                },
+                Err(e) => {
+                    dev_info!(cdev, "vino: drm::UnregisteredDevice::new failed 
({e:?}) -- continuing\n");
+                    None
+                }
+            };
+
+        // Bring-up (preamble + HDCP AKE + ~6 s of lockstep CP replay) is all 
blocking
+        // synchronous USB I/O. Running it inline here pins the USB 
driver-model probe
+        // thread while the DRM card node is already registered and live, 
which stalled
+        // the compositor (KWin) on first plug until the dock was physically 
yanked. Hand
+        // it to the system workqueue so `probe()` returns immediately and 
userspace KMS
+        // stays responsive. The work item holds refcounted handles to the 
interface (for
+        // the bulk endpoints) and the DRM device (for EDID caching), so they 
outlive
+        // `probe()`; USB I/O after an intervening disconnect simply errors 
and is logged,
+        // exactly like any other failed bring-up step.
+        match BringUp::new(intf_ref.clone(), ddev.clone()) {
             Ok(work) => {
                 let _ = workqueue::system().enqueue(work);
                 dev_info!(cdev, "vino: bring-up queued on system workqueue\n");
@@ -1613,7 +1718,7 @@ fn probe<'bound>(
             Err(e) => dev_info!(cdev, "vino: failed to queue bring-up ({e:?}) 
-- WIP\n"),
         }
 
-        Ok(Self { _intf: intf_ref })
+        Ok(Self { _intf: intf_ref, _ddev: ddev })
     }
 
     fn disconnect<'bound>(intf: &'bound usb::Interface<Core<'_>>, _data: 
Pin<&Self>) {
-- 
2.54.0


Reply via email to