Vino drives DisplayLink DL3 docks as ordinary DRM devices, replacing the
out-of-tree EVDI module and the closed-source DisplayLinkManager daemon.

Add the probe and its lifecycle: bind the display *function* rather than a
product ID, so a dock that postdates this driver is still offered to it;
place the device by family; run bring-up and the presence and EDID worker
off the probe path; and tear down without leaving a DRM minor or a URB
behind.

DisplayLink's own udev rules match vendor 17e9 and then trigger on the
interface, with no product test anywhere. Reverse engineering the wire
protocol found the same split independently: interface protocol 0x03 is a
DL3 display function, and 0x00 is the older udl hardware, which is a
different driver's problem.

The presence worker absorbs a sink that drops and returns within a second
or two, because the dock does that on its own and the repair costs a
dock-wide re-activation. A sink that keeps flapping is not settling: the
dock reports the connector present while nothing drives it, and the panel
stays dark through a bring-up that reports success. Repair that one by
taking the connector away, so the compositor puts it back and the mode set
that answers re-drives the sink. The number of repairs is bounded, so a
dock that flaps as a matter of course cannot hold the driver in a loop of
re-activations, which would leave neither panel lit.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <[email protected]>
---
 drivers/gpu/drm/vino/vino.rs | 1747 ++++++++++++++++++++++++++++++++++
 1 file changed, 1747 insertions(+)
 create mode 100644 drivers/gpu/drm/vino/vino.rs

diff --git a/drivers/gpu/drm/vino/vino.rs b/drivers/gpu/drm/vino/vino.rs
new file mode 100644
index 000000000000..0813faa43c03
--- /dev/null
+++ b/drivers/gpu/drm/vino/vino.rs
@@ -0,0 +1,1747 @@
+// SPDX-License-Identifier: GPL-2.0
+// SPDX-FileCopyrightText: Copyright (C) 2026 Mike Lothian
+
+//! DRM/KMS driver for DisplayLink DL3 docks.
+//!
+//! Vino drives the Dell Universal Dock D6000 using a clean-room 
implementation of its USB control,
+//! HDCP authentication and compressed video protocols. Each device owns its 
control session and
+//! exposes two atomic KMS pipelines backed by shmem GEM objects.
+
+use kernel::{
+    alloc::flags::GFP_KERNEL,
+    alloc::Flags,
+    device::{self, Core},
+    drm,
+    drm::display::hdcp as drm_hdcp,
+    error::code::{EBUSY, EINVAL, ENODEV, EPROTO, ETIMEDOUT},
+    prelude::*,
+    sync::{aref::ARef, new_mutex, Arc, Mutex},
+    time::{
+        delay::{fsleep, udelay},
+        Delta, Instant, Monotonic,
+    },
+    usb,
+    workqueue::{impl_has_work, new_work, Work, WorkItem},
+};
+
+/// Whether the load-time `debug` parameter requested verbose protocol and 
scanout diagnostics.
+pub(crate) fn debug_enabled() -> bool {
+    *crate::module_parameters::debug.value() != 0
+}
+
+/// Whether this one module load may disclose its ephemeral session material 
for a wire capture.
+///
+/// This is deliberately separate from ordinary debug logging: the values make 
a usbmon trace
+/// decryptable and must never appear during a normal load.
+fn trace_crypto_enabled() -> bool {
+    *crate::module_parameters::trace_crypto.value() != 0
+}
+
+/// Emit a driver diagnostic only when the load-time `debug` parameter is 
nonzero.
+macro_rules! vino_debug {
+    ($($arg:tt)*) => {
+        if crate::debug_enabled() {
+            kernel::pr_info!($($arg)*);
+        }
+    };
+}
+
+/// Device-prefixed counterpart to [`vino_debug`].
+macro_rules! vino_dev_debug {
+    ($dev:expr, $($arg:tt)*) => {
+        if crate::debug_enabled() {
+            kernel::dev_info!($dev, $($arg)*);
+        }
+    };
+}
+
+/// A byte slice on one line, as `08 0a 08 0a`.
+///
+/// `{:#04x?}` renders an array one element per line: the alternate flag asks 
the derived `Debug`
+/// for pretty output, which in a log line is unreadable.
+pub(crate) struct HexList<'a>(pub(crate) &'a [u8]);
+
+impl kernel::fmt::Display for HexList<'_> {
+    fn fmt(&self, f: &mut kernel::fmt::Formatter<'_>) -> kernel::fmt::Result {
+        for (i, byte) in self.0.iter().enumerate() {
+            write!(f, "{}{byte:02x}", if i == 0 { "" } else { " " })?;
+        }
+        Ok(())
+    }
+}
+
+/// DisplayLink vendor id.
+const VID_DISPLAYLINK: u16 = 0x17e9;
+/// Dell Universal Dock D6000 (DL3 family) product id.
+const PID_D6000: u16 = 0x6006;
+/// WAVLINK DL7400 and relatives: "Universal DP Quad Display Docking 16G", 
identity tail
+/// `NavaDock`, i.e. the Navarro platform on DL-7000 silicon.
+const PID_DL7400: u16 = 0x7000;
+
+/// Dock identification and the per-dock parameters the rest of the driver 
reads.
+mod profile;
+/// USB endpoint resolution and the I/O handle transfers go through.
+mod usb_link;
+
+pub(crate) use profile::{DockProfile, EP_CTRL_IN, EP_CTRL_OUT};
+pub(crate) use usb_link::{Endpoints, UsbLink, EP84_BUF};
+
+/// USB transfer timeout used during session setup.
+fn timeout() -> Delta {
+    Delta::from_millis(1000)
+}
+
+/// Short timeout for draining a per-message control reply after a runtime 
`send_cp`.
+///
+/// EP84 remains in lockstep with EP02, but not every message elicits a reply. 
A NAK or timeout
+/// therefore means that there is nothing to drain and must not stall scanout 
or keepalive work.
+pub(crate) fn cp_reply_timeout() -> Delta {
+    Delta::from_millis(8)
+}
+
+/// Time allowed for the downstream receiver to calculate H' during repeater 
authentication.
+///
+/// The dock acknowledges `AKE_No_Stored_km` before that calculation is 
complete, so an
+/// acknowledgment cannot be used as the readiness signal.
+// The DL7400's downstream receiver produces H' about 235--240 ms after 
AKE_No_Stored_km in the
+// working DLM transaction.  Wake just before that result instead of advancing 
after an arbitrary
+// shorter quiet window; `wait_per_connector_push(0x07)` below remains the 
actual completion gate.
+const HDCP_HPRIME_WAIT_US: i64 = 220_000;
+
+/// How long a connector's EDID fetch waits for the dock's asynchronous reply.
+///
+/// The `id=0x194` push follows the fetch acknowledgment by several messages, 
so the reply to the
+/// fetch itself proves nothing. Two seconds is what a cold downstream DDC 
read has been seen to
+/// take; a connector with nothing plugged into it spends the whole window and 
then reports no EDID,
+/// which is the correct answer for it.
+const EDID_REPLY_WAIT: Delta = Delta::from_secs(2);
+
+/// Wait until `anchor` is at least `target_us` old.
+fn hold_until(anchor: Instant<Monotonic>, target_us: i64) {
+    const SPIN_MARGIN_US: i64 = 400;
+    let now = anchor.elapsed().as_micros_ceil();
+    if now >= target_us {
+        return;
+    }
+    if target_us - now > SPIN_MARGIN_US {
+        fsleep(Delta::from_micros(target_us - now - SPIN_MARGIN_US));
+    }
+    let now = anchor.elapsed().as_micros_ceil();
+    if now < target_us {
+        udelay(Delta::from_micros(target_us - now));
+    }
+}
+
+mod ake;
+mod color;
+mod cp;
+mod crypto;
+mod firmware;
+mod hdcp;
+mod proto;
+mod rng;
+mod video;
+mod video_arm;
+
+/// The state a completed HDCP 2.2 AKE leaves for control-plane setup.
+struct Session {
+    ks: kernel::crypto::Secret<{ drm_hdcp::ENCRYPTED_SESSION_KEY_LEN }>,
+    riv: [u8; drm_hdcp::RIV_LEN],
+    /// Next inner sequence counter after the AKE messages sent by [`run_ake`].
+    next_ctr: u16,
+    /// Receiver key retained for each downstream repeater authentication.
+    rsa: kernel::crypto::akcipher::RsaPublicKey,
+    rxid_list: KVec<u8>,
+}
+
+/// Tally of one [`drain_ep84`](VinoDriver::drain_ep84) sweep.
+///
+/// An acknowledgment is counted only after its inner header decrypts 
successfully. A tagged
+/// frame which does not decrypt is counted separately as a rejection.
+#[derive(Default, Clone, Copy)]
+struct Ep84Drain {
+    reads: usize,
+    acks: usize,
+    rejects: usize,
+    /// Sticky EDID-readiness result across combined sweeps.
+    edid_ready: bool,
+    /// Inner counter echoed by a per-connector display-capability reply.
+    display_cap_ctr: Option<u16>,
+    /// Fresh per-connector `rrx` used by downstream repeater authentication.
+    per_connector_rrx: Option<[u8; drm_hdcp::RRX_LEN]>,
+    /// Bit `msg_id` is set for every downstream-HDCP push observed in this 
sweep.
+    per_connector_seen: u32,
+    per_connector_repeater: Option<bool>,
+    per_connector_hprime: Option<[u8; drm_hdcp::H_PRIME_LEN]>,
+    per_connector_lprime: Option<[u8; drm_hdcp::L_PRIME_LEN]>,
+    /// Navarro's receiver-list payload is nine authenticated list-header 
bytes followed by V'.
+    per_connector_v: Option<([u8; 9], [u8; drm_hdcp::V_PRIME_HALF_LEN])>,
+    per_connector_auth_status: Option<u8>,
+    per_connector_mprime: Option<[u8; drm_hdcp::H_PRIME_LEN]>,
+}
+
+impl Ep84Drain {
+    /// Fold another sweep's counts into this running total.
+    fn add(&mut self, o: Ep84Drain) {
+        self.reads += o.reads;
+        self.acks += o.acks;
+        self.rejects += o.rejects;
+        self.edid_ready |= o.edid_ready;
+        self.display_cap_ctr = self.display_cap_ctr.or(o.display_cap_ctr);
+        self.per_connector_rrx = 
self.per_connector_rrx.or(o.per_connector_rrx);
+        self.per_connector_seen |= o.per_connector_seen;
+        self.per_connector_repeater = 
self.per_connector_repeater.or(o.per_connector_repeater);
+        self.per_connector_hprime = 
self.per_connector_hprime.or(o.per_connector_hprime);
+        self.per_connector_lprime = 
self.per_connector_lprime.or(o.per_connector_lprime);
+        self.per_connector_v = self.per_connector_v.or(o.per_connector_v);
+        self.per_connector_auth_status = self
+            .per_connector_auth_status
+            .or(o.per_connector_auth_status);
+        self.per_connector_mprime = 
self.per_connector_mprime.or(o.per_connector_mprime);
+    }
+
+    fn observe_perhead(&mut self, push: cp::PerheadHdcpPush) {
+        if push.msg_id < 32 {
+            self.per_connector_seen |= 1u32 << push.msg_id;
+        }
+        match push.msg_id {
+            // AKE_Send_Cert: the first vendor payload byte is the repeater 
flag.
+            0x03 if push.payload_len >= 1 => {
+                self.per_connector_repeater = Some(push.payload[0] != 0);
+            }
+            0x06 if push.payload_len >= drm_hdcp::RRX_LEN => {
+                let mut v = [0u8; drm_hdcp::RRX_LEN];
+                v.copy_from_slice(&push.payload[..drm_hdcp::RRX_LEN]);
+                self.per_connector_rrx = Some(v);
+            }
+            0x07 if push.payload_len >= drm_hdcp::H_PRIME_LEN => {
+                let mut v = [0u8; drm_hdcp::H_PRIME_LEN];
+                v.copy_from_slice(&push.payload[..drm_hdcp::H_PRIME_LEN]);
+                self.per_connector_hprime = Some(v);
+            }
+            0x0a if push.payload_len >= drm_hdcp::L_PRIME_LEN => {
+                let mut v = [0u8; drm_hdcp::L_PRIME_LEN];
+                v.copy_from_slice(&push.payload[..drm_hdcp::L_PRIME_LEN]);
+                self.per_connector_lprime = Some(v);
+            }
+            // ReceiverID_List: RxInfo/seq/list header (9 bytes), V' (16 
bytes), padding.
+            0x0c if push.payload_len >= 9 + drm_hdcp::V_PRIME_HALF_LEN => {
+                let mut list = [0u8; 9];
+                let mut vprime = [0u8; drm_hdcp::V_PRIME_HALF_LEN];
+                list.copy_from_slice(&push.payload[..9]);
+                vprime.copy_from_slice(&push.payload[9..9 + 
drm_hdcp::V_PRIME_HALF_LEN]);
+                self.per_connector_v = Some((list, vprime));
+            }
+            // DisplayLink prefixes ReceiverAuthStatus with one vendor status 
byte. The HDCP
+            // value is payload[1] (`00 04` in all four working DLM 
per-connector exchanges).
+            0x12 if push.payload_len >= 2 => {
+                self.per_connector_auth_status = Some(push.payload[1]);
+            }
+            0x11 if push.payload_len >= drm_hdcp::H_PRIME_LEN => {
+                let mut v = [0u8; drm_hdcp::H_PRIME_LEN];
+                v.copy_from_slice(&push.payload[..drm_hdcp::H_PRIME_LEN]);
+                self.per_connector_mprime = Some(v);
+            }
+            _ => {}
+        }
+    }
+
+    fn saw_perhead(&self, msg_id: u8) -> bool {
+        msg_id < 32 && self.per_connector_seen & (1u32 << msg_id) != 0
+    }
+}
+
+mod drm_sink;
+
+/// The USB driver itself. Stateless: everything per-binding lives in 
[`VinoBoundData`].
+/// Log what this device is, and what it exposes, before any protocol runs.
+///
+/// A DisplayLink generation is not identifiable from the USB IDs alone -- the 
DL3 protocol vino
+/// speaks does not apply to a DL-1x5 part, and the first sign of that is a 
control session timing
+/// out long after bind succeeded. Printing the descriptor and the endpoint 
inventory up front means
+/// a report from unfamiliar hardware carries what is needed to place it, 
without a debug build:
+/// `bcdDevice` is the vendor's revision, and the endpoint list distinguishes 
a full DL3 control
+/// device (bulk OUT 0x02 + bulk IN 0x84 + video 0x08) from a part that only 
has one bulk pipe.
+fn log_device_identity(
+    cdev: &device::Device<Core<'_>>,
+    intf: &usb::Interface<Core<'_>>,
+    ifnum: u8,
+) {
+    // The descriptor describes the whole device, so print it once rather than 
per interface.
+    if ifnum == 0 {
+        let dev: &usb::Device<Core<'_>> = intf.as_ref();
+        let vid = dev.vendor_id();
+        let pid = dev.product_id();
+        let bcd = dev.bcd_device();
+        let usb_bcd = dev.bcd_usb();
+        vino_dev_debug!(
+            cdev,
+            "USB {vid:04x}:{pid:04x} bcdDevice {:x}.{:02x} bcdUSB {:x}.{:02x} 
speed {}\n",
+            bcd >> 8,
+            bcd & 0xff,
+            usb_bcd >> 8,
+            usb_bcd & 0xff,
+            dev.speed_str()
+        );
+        // The USB core only caches these when the device answered the string 
requests.
+        match (dev.manufacturer(), dev.product()) {
+            (Some(m), Some(p)) => vino_dev_debug!(cdev, "{m} {p}\n"),
+            (None, Some(p)) => vino_dev_debug!(cdev, "{p}\n"),
+            (Some(m), None) => vino_dev_debug!(cdev, "{m} (no product 
string)\n"),
+            (None, None) => vino_dev_debug!(cdev, "no manufacturer/product 
strings\n"),
+        }
+    }
+    for ep in intf.cur_altsetting().endpoints() {
+        let dir = match ep.endpoint_dir() {
+            kernel::usb::ch9::Direction::In => "in",
+            kernel::usb::ch9::Direction::Out => "out",
+        };
+        let kind = match ep.endpoint_type() {
+            usb::EndpointType::Control => "control",
+            usb::EndpointType::Isoc => "isoc",
+            usb::EndpointType::Bulk => "bulk",
+            usb::EndpointType::Int => "int",
+        };
+        // bEndpointAddress as the descriptor carries it: number plus the 
direction bit.
+        let addr = ep.endpoint_number()
+            | match ep.endpoint_dir() {
+                kernel::usb::ch9::Direction::In => 0x80,
+                kernel::usb::ch9::Direction::Out => 0,
+            };
+        vino_dev_debug!(cdev, "  ep {addr:#04x} {kind}-{dir} maxp {}\n", 
ep.maxp());
+    }
+}
+
+struct VinoDriver;
+
+/// Per-bound-interface driver state.
+///
+/// Carries the DRM [`Registration`](drm::Registration), whose lifetime is 
tied to this bound
+/// device, so unbinding unregisters the card through the accepted 
registration teardown rather
+/// than a driver-local force-unplug.
+struct VinoBoundData {
+    _intf: ARef<usb::Interface>,
+    /// The registered DRM card, dropped on unbind.
+    ///
+    /// `None` only on idle non-control interfaces. On the control interface 
it owns the DRM
+    /// registration and provides `disconnect()` access to the device state.
+    registration: Option<drm::Registration<'static, drm_sink::VinoDrmDriver>>,
+    /// Owned handle to the deferred bring-up work (control interface only). 
`disconnect()` takes
+    /// the option under the mutex before synchronously cancelling the work 
and unplugging DRM.
+    /// The mutex itself is heap-pinned because kernel locks must not move 
after initialization.
+    bringup: Pin<KBox<Mutex<Option<Arc<BringUp>>>>>,
+    /// The `/sys/class/firmware/` upload interface, on the DFU interface only.
+    ///
+    /// Held here so it is unregistered when the interface unbinds: the upload 
callbacks reach the
+    /// dock through the I/O window, which closes at the same time.
+    _fw_upload: 
Option<kernel::firmware::upload::Registration<firmware::Upload>>,
+    /// Backing store for the name `_fw_upload` was registered under.
+    ///
+    /// `firmware_upload_register` keeps the pointer it is handed rather than 
copying the string,
+    /// so the name has to outlive the registration. Declared after it so it 
is dropped second.
+    _fw_upload_name: Option<KBox<kernel::str::CString>>,
+}
+
+/// Deferred bring-up work item.
+///
+/// The device's dedicated session queue keeps blocking authentication and 
steady-state control I/O
+/// out of the USB probe path and the shared system workqueues.
+#[pin_data]
+struct BringUp {
+    ddev: ARef<drm_sink::VinoDrmDevice>,
+    /// Which dock this is. The bring-up sequence differs by platform (see 
[`DockProfile`]), and
+    /// the work item runs long after `probe` has returned, so it carries the 
profile itself.
+    profile: &'static DockProfile,
+    #[pin]
+    work: Work<BringUp>,
+}
+
+impl_has_work! {
+    impl HasWork<Self> for BringUp { self.work }
+}
+
+impl BringUp {
+    fn new(
+        ddev: ARef<drm_sink::VinoDrmDevice>,
+        profile: &'static DockProfile,
+    ) -> Result<Arc<Self>> {
+        Arc::pin_init(
+            pin_init!(BringUp {
+                ddev,
+                profile,
+                work <- new_work!("vino::bring_up"),
+            }),
+            GFP_KERNEL,
+        )
+    }
+}
+
+/// How often one connector's sink has flapped, and how often vino has 
repaired it.
+///
+/// A sink that drops and returns within a second or two heals on its own, and 
a repair costs a
+/// dock-wide re-activation, so a single flap is absorbed. One that keeps 
flapping is not settling:
+/// after the dock is handed to another host and back it reports a connector 
present while nothing
+/// drives its sink, and the panel stays dark through a bring-up that reports 
success. Measured on a
+/// lit dock, no flap at all over seventy seconds; on one left dark that way, 
nine a minute on both
+/// connectors.
+#[derive(Copy, Clone)]
+struct FlapTracker {
+    seen: u32,
+    since: Option<Instant<Monotonic>>,
+    repairs: u32,
+}
+
+impl FlapTracker {
+    /// Flaps inside [`Self::WINDOW`] after which the sink is repaired rather 
than absorbed.
+    const REPAIR_COUNT: u32 = 3;
+    const WINDOW_MS: i64 = 60_000;
+    /// Repairs one connector may take before vino leaves it alone.
+    ///
+    /// A dock that flaps as a matter of course must not be able to hold vino 
in a loop of
+    /// re-activations: a bounded few and then silence is recoverable, an 
unbounded stream is worse
+    /// than the fault it is answering.
+    const REPAIR_LIMIT: u32 = 3;
+
+    const fn new() -> Self {
+        Self {
+            seen: 0,
+            since: None,
+            repairs: 0,
+        }
+    }
+
+    /// Record a flap that healed on its own, and say whether this is the one 
to repair on.
+    fn healed(&mut self, now: Instant<Monotonic>) -> bool {
+        if self
+            .since
+            .is_none_or(|t| (now - t).as_millis() >= Self::WINDOW_MS)
+        {
+            self.since = Some(now);
+            self.seen = 0;
+        }
+        self.seen += 1;
+        if self.seen < Self::REPAIR_COUNT || self.repairs >= 
Self::REPAIR_LIMIT {
+            return false;
+        }
+        self.seen = 0;
+        self.since = None;
+        self.repairs += 1;
+        true
+    }
+}
+
+impl WorkItem for BringUp {
+    type Pointer = Arc<BringUp>;
+
+    fn run(this: Arc<BringUp>) {
+        let profile = this.profile;
+        let data: &drm_sink::VinoDrmData = &this.ddev;
+        // Naming the interface needs no I/O token, so the retry loop below 
can log without
+        // holding one.
+        let cdev: &device::Device = data.io.interface().as_ref();
+        let ddev = &this.ddev;
+        // Establish the transport, authenticate the link and configure the 
encrypted control
+        // session before publishing the connectors. A transient failure must 
not leave an
+        // otherwise bound device inert until it is physically replugged.
+        // A dock can refuse a session outright: it answers every control 
request while NAKing the
+        // first EP02 bulk write until it times out. Back off to about half a 
minute before giving
+        // the device up.
+        const SESSION_ATTEMPTS: usize = 8;
+        let mut established = false;
+        for attempt in 1..=SESSION_ATTEMPTS {
+            if data.is_shutting_down() {
+                return;
+            }
+            // The token is taken per attempt and dropped before the backoff. 
Holding one across a
+            // sleep that reaches seconds means a device reset cannot quiesce 
the driver: the USB
+            // core's pre-reset waits for the last token, the reset that would 
recover a dock which
+            // has stopped answering waits behind this loop, and an unbind 
waits behind the reset.
+            let Ok(link) = UsbLink::open(&data.io, data.endpoints) else {
+                return;
+            };
+            let dev = &link;
+            let result = (|| -> Result {
+                VinoDriver::bring_up(dev, profile)?;
+                vino_dev_debug!(cdev, "plaintext session initialized\n");
+                let mut session = VinoDriver::run_ake(dev)?;
+                vino_dev_debug!(cdev, "HDCP AKE + LC + SKE complete\n");
+
+                let mut edid_out: Option<KVec<u8>> = None;
+                let mut edid_connectors: [Option<KVec<u8>>; 
VinoDriver::CP_SETUP_CONNECTORS] =
+                    core::array::from_fn(|_| None);
+                let mut video_keys = core::array::from_fn(|_| 
kernel::crypto::Secret::zeroed());
+                let mut connectors_present = [false; 
VinoDriver::CP_SETUP_CONNECTORS];
+                let mut discovery_deferred = [false; 
VinoDriver::CP_SETUP_CONNECTORS];
+                let mut stream_opened = 0u32;
+                let (n, wseq_end, ctr_end) = VinoDriver::send_cp_setup(
+                    dev,
+                    profile,
+                    &mut session,
+                    &mut edid_out,
+                    &mut edid_connectors,
+                    &mut video_keys,
+                    &mut connectors_present,
+                    &mut discovery_deferred,
+                    &mut stream_opened,
+                )?;
+                vino_dev_debug!(cdev, "encrypted control setup complete ({n} 
messages)\n");
+
+                // `send_cp_setup` only returns after an authenticated reply 
proves that the dock
+                // engaged the session. Publish it before connector state so 
runtime recovery can
+                // immediately finish any per-connector discovery transaction 
that was deferred.
+                let drm_dev: &drm_sink::VinoDrmDevice = ddev;
+                let data: &drm_sink::VinoDrmData = drm_dev;
+                data.set_cp_engaged(true);
+                data.publish_session(
+                    dev,
+                    &session.ks,
+                    &session.riv,
+                    wseq_end,
+                    ctr_end,
+                    profile.protocol.ep84_queue_depth,
+                );
+                // Only the connectors whose stream this burst actually opened 
have consumed their
+                // first sealed block. A connector with no sink yet is opened 
by whatever drives it
+                // later, and must still start its chain at block zero.
+                data.set_video_keys(video_keys, stream_opened);
+                // The silence watchdog cannot live on this thread: this is 
the thread it exists
+                // to notice has stopped running.
+                data.start_cp_watchdog(drm_dev);
+
+                // One line naming what setup found on every physical socket, 
including the ones
+                // this dock does not drive as distinct streams. Which socket 
a monitor is in is
+                // otherwise invisible from dmesg, and it decides whether a 
dark output is a sink
+                // problem at all: a connector vino never drives cannot light 
whatever is plugged
+                // into it. `cap` is the socket's DISPLAY-CAP push, `edid` its 
raw EDID; the pair
+                // distinguishes an empty socket from one whose sink cannot be 
read.
+                for connector in 0..usize::from(profile.topology.connectors) {
+                    vino_dev_debug!(
+                        cdev,
+                        "socket {} -- cap:{} edid:{} deferred:{} driven:{}\n",
+                        connector + 1,
+                        if connectors_present[connector] {
+                            "yes"
+                        } else {
+                            "no "
+                        },
+                        if edid_connectors[connector].is_some() {
+                            "yes"
+                        } else {
+                            "no "
+                        },
+                        if discovery_deferred[connector] {
+                            "yes"
+                        } else {
+                            "no "
+                        },
+                        if data.runtime_connector(connector) {
+                            "yes"
+                        } else {
+                            "no "
+                        },
+                    );
+                }
+
+                // Cache complete per-connector discovery results before 
emitting the single initial
+                // hotplug event. A timed-out connector remains absent and the 
keepalive's existing
+                // bounded re-engagement path retries it without discarding 
the live session.
+                for (connector, slot) in edid_connectors
+                    .into_iter()
+                    .enumerate()
+                    .take(usize::from(profile.topology.connectors))
+                {
+                    if discovery_deferred[connector] {
+                        continue;
+                    }
+                    let have_edid = slot.is_some();
+                    if let Some(blob) = slot {
+                        let n = blob.len();
+                        data.set_edid(connector, blob);
+                        vino_dev_debug!(
+                            cdev,
+                            "cached socket {socket} EDID ({n} bytes)\n",
+                            socket = connector + 1
+                        );
+                    }
+                    // A recovered EDID is the presence signal on both 
platforms. Publishing a
+                    // connector without one puts a fallback mode into an 
empty socket and makes
+                    // the dock lay out buffers for an output that does not 
exist.
+                    if have_edid {
+                        data.set_connected(connector);
+                        dev_info!(
+                            cdev,
+                            "socket {socket} monitor connected\n",
+                            socket = connector + 1
+                        );
+                    }
+                }
+
+                // Navarro normally receives each EDID on the fetch drain, 
exactly as DLM does, but
+                // after a dock re-enumeration a response can arrive seconds 
late. Publishing that
+                // partial topology lets userspace mode-set one connector 
while its sibling is still
+                // arriving, which resets this dock, so retry the deferred 
connectors before the
+                // single initial hotplug. They are interleaved: a connector 
that never answers must
+                // not hold up one that would. A normal setup sends no 
additional control messages.
+                if profile.protocol.per_connector_onehot {
+                    /// How long the deferred connectors are retried before 
the topology is
+                    /// published.
+                    const INITIAL_RECOVERY_MS: i64 = 6000;
+                    /// Extra time granted after a connector answers, for a 
sibling close behind it.
+                    const SIBLING_GRACE_MS: i64 = 1500;
+
+                    let mut pending: [bool; VinoDriver::CP_SETUP_CONNECTORS] =
+                        core::array::from_fn(|connector| {
+                            connector < 
usize::from(profile.topology.connectors)
+                                && discovery_deferred[connector]
+                                && data.runtime_connector(connector)
+                                && !data.connector_present(connector)
+                        });
+                    // One probe answers "is this socket empty?"; a re-engage 
is seven messages
+                    // carrying ~575 ms of mandated delay. A probe that cannot 
answer is not
+                    // evidence of absence, so only a definite negative stands 
a connector down.
+                    for connector in 0..VinoDriver::CP_SETUP_CONNECTORS {
+                        if pending[connector]
+                            && data.probe_connector_present(dev, connector as 
u8) == Some(false)
+                        {
+                            pending[connector] = false;
+                        }
+                    }
+
+                    let started = Instant::<Monotonic>::now();
+                    let mut give_up = started + 
Delta::from_millis(INITIAL_RECOVERY_MS);
+                    let expired = |give_up: Instant<Monotonic>| {
+                        (Instant::<Monotonic>::now() - give_up).as_millis() >= 0
+                    };
+                    let mut pass = 0u32;
+                    while pending.iter().any(|p| *p)
+                        && !data.is_shutting_down()
+                        && !expired(give_up)
+                    {
+                        pass += 1;
+                        for connector in 0..VinoDriver::CP_SETUP_CONNECTORS {
+                            // Tested per connector, not per pass: a re-engage 
the dock ignores
+                            // costs seconds, so a pass across four of them 
would run well past the
+                            // window before anything looked at it.
+                            if !pending[connector] || expired(give_up) {
+                                continue;
+                            }
+                            // Nothing to recover where the dock reports no 
presence: the connector
+                            // is offered and driven without an EDID, and 
re-engaging asserts the
+                            // closed bracket, which resets a sink that is 
already lit.
+                            if !data.reports_presence() {
+                                pending[connector] = false;
+                                continue;
+                            }
+                            if let Ok(true) = data.reengage_connector(dev, 
connector as u8) {
+                                data.set_connected(connector);
+                                pending[connector] = false;
+                                vino_dev_debug!(
+                                    cdev,
+                                    "socket {socket} monitor connected during 
initial \
+                                     recovery (pass {pass})\n",
+                                    socket = connector + 1
+                                );
+                                let grace = Instant::<Monotonic>::now()
+                                    + Delta::from_millis(SIBLING_GRACE_MS);
+                                if (grace - give_up).as_millis() > 0 {
+                                    give_up = grace;
+                                }
+                            }
+                        }
+                        fsleep(Delta::from_millis(250));
+                    }
+                    let waited = (Instant::<Monotonic>::now() - 
started).as_millis();
+                    for connector in 0..VinoDriver::CP_SETUP_CONNECTORS {
+                        if pending[connector] {
+                            dev_warn!(
+                                cdev,
+                                "socket {socket} never answered its EDID fetch 
\
+                                 ({pass} passes over {waited} ms); publishing 
without it\n",
+                                socket = connector + 1
+                            );
+                        }
+                    }
+                }
+                Ok(())
+            })();
+
+            drop(link);
+            match result {
+                Ok(()) => {
+                    established = true;
+                    break;
+                }
+                Err(e) if attempt < SESSION_ATTEMPTS => {
+                    let backoff = 250i64 << (attempt - 1).min(5);
+                    dev_warn!(
+                        cdev,
+                        "control-session attempt {attempt}/{SESSION_ATTEMPTS} 
failed \
+                         ({e:?}); retrying in {backoff} ms\n"
+                    );
+                    fsleep(Delta::from_millis(backoff));
+                }
+                Err(e) => dev_err!(
+                    cdev,
+                    "control session failed after {SESSION_ATTEMPTS} attempts 
({e:?})\n"
+                ),
+            }
+        }
+        if !established {
+            return;
+        }
+        let Ok(link) = UsbLink::open(&data.io, data.endpoints) else {
+            return;
+        };
+        let dev = &link;
+        {
+            let drm_dev: &drm_sink::VinoDrmDevice = ddev;
+            // Ridge needs a bounded training interval before userspace can 
submit a mode set.
+            // Navarro's working transcript has already performed its fixed 
status sequence in
+            // `send_cp_setup`; another 1.3 seconds inserted ~84 messages 
before its first clear.
+            if data.cp_engaged() && !profile.protocol.per_connector_onehot {
+                let data: &drm_sink::VinoDrmData = drm_dev;
+                let start = Instant::<Monotonic>::now();
+                let window = Delta::from_millis(1300);
+                let mut polls = 0u32;
+                while Instant::<Monotonic>::now() - start < window && 
!data.is_shutting_down() {
+                    let _ = data.send_cp(dev, 0x14, 0, |ctr| 
cp::device_query_req(ctr, 0x000c));
+                    polls += 1;
+                    fsleep(Delta::from_millis(15));
+                }
+                vino_dev_debug!(cdev, "link ready after {polls} status 
polls\n");
+            }
+            // Whether userspace has been given a connector to drive. The hold 
below keeps the
+            // control link to itself until the mode set that answers this 
topology arrives, so it
+            // is only meaningful once there is one to answer. Arming it over 
an empty topology
+            // silences the link, and the downstream recovery below with it, 
for as long as the
+            // escape allows -- which is exactly the period a monitor still 
waking up needs to be
+            // asked for its EDID again.
+            let mut topology_published =
+                (0..data.connector_count()).any(|connector| 
data.connector_present(connector));
+            if data.dock_wide_modeset() && topology_published {
+                data.hold_cp_for_initial_modeset();
+            }
+            // This is the first point common to every generation at which KMS 
may touch the dock:
+            // encrypted setup and initial discovery are complete, Ella/Ridge 
have finished their
+            // pre-mode-set readiness interval, and Navarro's 
setup-to-first-mode-set hold is armed.
+            // Publish before hotplug so any atomic state userspace produces 
from that event sees
+            // it.
+            data.publish_kms_activation_ready(drm_dev);
+            drm_dev.hotplug_event();
+            vino_dev_debug!(cdev, "encrypted control session ready\n");
+
+            // The dock requires a continuous control dialogue for the 
lifetime of the session.
+            let data: &drm_sink::VinoDrmData = drm_dev;
+            vino_dev_debug!(cdev, "starting control keepalive\n");
+            let mut sent = 0u32;
+            // Heartbeats have an independent fixed cadence alongside the 
status queries.
+            const HEARTBEAT_PERIOD: Delta = Delta::from_secs(3);
+            let mut next_heartbeat = Instant::<Monotonic>::now() + 
HEARTBEAT_PERIOD;
+            // Probe downstream presence slowly and debounce transitions.
+            // How often status is queried is the dock's business, not this 
loop's: where video
+            // shares this endpoint each query is bytes queued against a frame 
and a reply the dock
+            // has to produce mid-scanout. See `DockProfile::status_period_ms`.
+            let status_period = Delta::from_millis(data.status_period_ms());
+            let mut next_status = Instant::<Monotonic>::now();
+            const PRESENCE_PERIOD: Delta = Delta::from_millis(1000);
+            let mut next_presence = Instant::<Monotonic>::now() + 
PRESENCE_PERIOD;
+            let mut connector_known = [false; VinoDriver::CP_SETUP_CONNECTORS];
+            // The presence probe's last verdict per connector, or `None` 
until it has answered
+            // once. Distinguishes "no monitor here" from "not asked yet", 
which the blind
+            // re-engage retry below needs and `connector_known` cannot 
express.
+            let mut connector_probed: [Option<bool>; 
VinoDriver::CP_SETUP_CONNECTORS] =
+                [None; VinoDriver::CP_SETUP_CONNECTORS];
+            // Whether this connector has ever had a monitor in this session. 
Standing the EDID
+            // recovery down means never asking that socket again, so it is 
only ever right for a
+            // sink that was there and went away. Applied to a socket that has 
not answered yet it
+            // is a guess about hardware the dock has not finished looking at, 
and a monitor still
+            // waking up loses its whole session to it.
+            let mut connector_ever_known = [false; 
VinoDriver::CP_SETUP_CONNECTORS];
+            // Blind sink re-engagements left for a socket that has never had 
a monitor; see the
+            // negative-probe branch below. Bounded because an engage is seven 
paced messages and
+            // an empty socket must not pay for them for the life of the 
session. Ten attempts
+            // at the four-second retry cadence is forty seconds of trying, 
which is what a D6000
+            // needs: fewer recovers its sink only sometimes.
+            const BLIND_ENGAGE_ATTEMPTS: u8 = 10;
+            let mut blind_engage_left = [BLIND_ENGAGE_ATTEMPTS; 
VinoDriver::CP_SETUP_CONNECTORS];
+            let mut connector_debounce = [0u8; 
VinoDriver::CP_SETUP_CONNECTORS];
+            // Floor on the gap between presence probes. A downstream event 
brings the probe
+            // forward; without a floor it would run once per loop iteration 
for as long as the
+            // dock keeps talking.
+            const PRESENCE_MIN_GAP: Delta = Delta::from_millis(50);
+            /// How long a connector must read absent before its monitor is 
called removed.
+            ///
+            /// A removal has to be debounced in TIME, not in probes: every 
`id=0x44` reply sets the
+            /// downstream-event flag, and a presence probe's own reply *is* 
an `id=0x44`, so the
+            /// watcher kept pulling itself forward to `PRESENCE_MIN_GAP` and 
"two consecutive
+            /// contrary reads" fired 132 ms after the first negative.
+            ///
+            /// Measured on a lit, idle DL-7400: the absent runs are 0.11 s to 
2.29 s, twenty-nine
+            /// of them over three minutes, reaching 2.46 s around a mode 
change.
+            ///
+            /// The debounce is only half of it. Those blips are the dock 
really dropping the sink,
+            /// and letting the connector disappear is what repairs them: the 
compositor re-enables
+            /// the output and the resulting mode set relights the panel. 
Debouncing alone leaves
+            /// the connector dark for good, so this works only together with
+            /// `repair_flapped_connector`.
+            const PRESENCE_REMOVE_MS: i64 = 5000;
+            let mut connector_absent_since: [Option<Instant<Monotonic>>;
+                VinoDriver::CP_SETUP_CONNECTORS] = [None; 
VinoDriver::CP_SETUP_CONNECTORS];
+            // Whether a connector's current run of negative probes has lasted 
long enough to be
+            // acted on, starting the run if this is its first answer.
+            //
+            // Both the removal path and the EDID-recovery stand-down need 
this and for the same
+            // reason, so they share one notion of it.
+            let sustained_absent = |run: &mut Option<Instant<Monotonic>>| -> 
bool {
+                let since = *run.get_or_insert_with(Instant::<Monotonic>::now);
+                (Instant::<Monotonic>::now() - since).as_millis() >= 
PRESENCE_REMOVE_MS
+            };
+            // A recovered sink need not emit a uniquely identifiable event, 
so a connector whose
+            // discovery was deferred is retried at a bounded cadence until 
the probe answers.
+            const REENGAGE_RETRY: Delta = Delta::from_millis(4000);
+            let mut next_reengage = [Instant::<Monotonic>::now(); 
VinoDriver::CP_SETUP_CONNECTORS];
+            let mut flap = [FlapTracker::new(); 
VinoDriver::CP_SETUP_CONNECTORS];
+            /// Settling period after re-engagement during which a negative 
probe is ignored.
+            const PRESENCE_GRACE: Delta = Delta::from_millis(10_000);
+            let mut presence_grace = [Instant::<Monotonic>::now(); 
VinoDriver::CP_SETUP_CONNECTORS];
+            /// Quiet window a runtime arrival waits out before userspace is 
told.
+            ///
+            /// A mode set is dock-wide, so two connectors announced 
separately make the compositor
+            /// reconfigure the dock twice and it re-enumerates. Each arrival 
restarts the window
+            /// and one event covers the burst. A removal is announced 
immediately.
+            const HOTPLUG_COALESCE: Delta = Delta::from_millis(1500);
+            let mut hotplug_due: Option<Instant<Monotonic>> = None;
+            // When the current run of silent probes started; only read while 
`connector_silent >
+            // 0`.
+            for h in 0..data.connector_count() {
+                if !data.runtime_connector(h) {
+                    continue;
+                }
+                connector_known[h] = data.connector_present(h);
+                connector_ever_known[h] = connector_known[h];
+            }
+            // A normal hotplug commit claims this hold almost immediately. 
Keep a bounded escape
+            // for a userspace session which elects not to light either 
connector at all.
+            //
+            // Timed, not counted: an iteration of the hold is a push drain of 
up to eight
+            // one-millisecond reads, so counting iterations as milliseconds 
overstates the escape
+            // by three to nine times and leaves the link silent for a good 
fraction of a minute.
+            const INITIAL_MODESET_QUIET: Delta = Delta::from_millis(5000);
+            let mut initial_quiet_until: Option<Instant<Monotonic>> = None;
+            // The cold activation owns EP02 for several seconds. Deadlines 
which expire while it
+            // owns the link must be re-based when it releases it; otherwise 
the first post-close
+            // loop sends an overdue heartbeat and presence probes ahead of 
the status dialogue.
+            // DLM instead continues with status counters 184, 185, ... 
immediately after its
+            // closing markers.
+            let mut timeline_was_exclusive = false;
+            while !data.is_shutting_down() {
+                // The dock stopped answering and the session was abandoned. 
Take the outputs down
+                // rather than poll a link that cannot carry anything: 
userspace can move its
+                // windows off a connector that has disappeared, but not off 
one that is merely
+                // frozen. Recovery is a replug, which rebinds and starts a 
fresh session.
+                if !data.cp_link_alive() {
+                    data.drop_connectors_with_session(drm_dev);
+                    break;
+                }
+                if data.initial_modeset_quiet() {
+                    // Quiet means no unsolicited EP02 writes; DLM still has 
its one EP84 reader
+                    // continuously posted and reaped. Keep draining pushes 
while userspace is
+                    // preparing the first mode set so that transaction does 
not begin behind a
+                    // multi-second status backlog.
+                    data.drain_cp_pushes(dev, 8);
+                    let deadline = *initial_quiet_until
+                        .get_or_insert_with(|| Instant::<Monotonic>::now() + 
INITIAL_MODESET_QUIET);
+                    if (Instant::<Monotonic>::now() - deadline).as_millis() >= 
0 {
+                        initial_quiet_until = None;
+                        data.release_initial_modeset_quiet();
+                        vino_dev_debug!(
+                            cdev,
+                            "no initial mode set after {} ms; releasing 
control keepalive\n",
+                            INITIAL_MODESET_QUIET.as_millis()
+                        );
+                    } else {
+                        fsleep(Delta::from_millis(1));
+                        continue;
+                    }
+                }
+                // Mode-set markers and video activation form one exclusive 
transaction.
+                if data.cp_timeline_exclusive() {
+                    timeline_was_exclusive = true;
+                    // The KMS worker owns EP02, but it releases `cp_link` 
between scheduled
+                    // writes. Reap asynchronous EP84 traffic in those gaps 
just as DLM's reader
+                    // thread does; request replies remain protected because 
`send_cp_reply`
+                    // holds the mutex until it sees the matching counter.
+                    data.drain_cp_pushes(dev, 8);
+                    fsleep(Delta::from_millis(1));
+                    continue;
+                }
+                if timeline_was_exclusive {
+                    let resumed = Instant::<Monotonic>::now();
+                    next_heartbeat = resumed + HEARTBEAT_PERIOD;
+                    next_presence = resumed + PRESENCE_PERIOD;
+                    timeline_was_exclusive = false;
+                }
+                if (Instant::<Monotonic>::now() - next_status).as_millis() >= 
0 {
+                    if data
+                        .send_cp(dev, 0x14, 0, |ctr| cp::device_query_req(ctr, 
0x000c))
+                        .is_ok()
+                    {
+                        sent += 1;
+                    }
+                    next_status = Instant::<Monotonic>::now() + status_period;
+                }
+                // Compare through the signed `Delta` returned by subtracting
+                // two instants.
+                let now = Instant::<Monotonic>::now();
+                if (now - next_heartbeat).as_millis() >= 0 {
+                    let _ = data.send_cp(dev, 0x16, 0, cp::heartbeat);
+                    // Advance from the previous deadline so a slow send does 
not cause drift.
+                    next_heartbeat = next_heartbeat + HEARTBEAT_PERIOD;
+                    if (now - next_heartbeat).as_millis() > 0 {
+                        next_heartbeat = now + HEARTBEAT_PERIOD; // fell far 
behind; resynchronise
+                    }
+                }
+                // Consume asynchronous pushes instead of leaving them for the 
next paired read.
+                const MAX_UNPAIRED_DRAIN: usize = 4;
+                data.drain_cp_pushes(dev, MAX_UNPAIRED_DRAIN);
+                // Recover a connector whose setup-time discovery was deferred 
or timed out. This is
+                // a recovery, not a poll: once the presence probe has 
answered for a connector that
+                // answer is authoritative and this stands down, or an empty 
socket costs seven
+                // unanswered CP messages every `REENGAGE_RETRY` for the life 
of the session.
+                {
+                    let now_r = Instant::<Monotonic>::now();
+                    for h in 0..data.connector_count() {
+                        let socket = h + 1;
+                        if !data.runtime_connector(h) {
+                            continue;
+                        }
+                        if connector_probed[h] == Some(false) {
+                            continue;
+                        }
+                        if connector_known[h] || (now_r - 
next_reengage[h]).as_millis() < 0 {
+                            continue;
+                        }
+                        // Where the dock says nothing about what is plugged 
in, this recovery has
+                        // no signal to act on and its cost is visible: 
`reengage_connector` asserts
+                        // the closed bracket first, so re-running it every 
`REENGAGE_RETRY` resets
+                        // a sink that is already lit and driven, and the 
panel flashes.
+                        if !data.reports_presence() {
+                            continue;
+                        }
+                        // A blanked connector's sink is idle because vino 
asked for it. Re-engaging
+                        // it here would also clear `self_blanked`, since 
`reengage_connector` does
+                        // so on entry, and the connector would then be torn 
down mid-blank.
+                        if data.is_self_blanked(h) {
+                            continue;
+                        }
+                        next_reengage[h] = Instant::<Monotonic>::now() + 
REENGAGE_RETRY;
+                        // Same trade as the initial recovery: one cheap probe 
instead of seven
+                        // paced messages. It also keeps an empty socket's 
retry from interleaving
+                        // ~575 ms of engage traffic into a mode-set 
transaction on another
+                        // connector, which is measurable as delayed 
activation, not merely as
+                        // noise.
+                        if data.probe_connector_present(dev, h as u8) == 
Some(false) {
+                            // Do not stand down on the first negative. A 
recovered EDID is this
+                            // dock's presence signal; the probe is a weaker 
one that reports a lit
+                            // sink absent for up to 2.5 s at a time. Latching 
here costs a monitor
+                            // slow to answer at bring-up its whole session: 
it is never asked for
+                            // an EDID again, and only re-enumerating the dock 
brings it back.
+                            //
+                            // So hold a negative to the same evidence a 
removal needs, and only
+                            // for a socket that has had a monitor in it. A 
socket that has never
+                            // answered goes on being probed: the re-engage is 
skipped either way
+                            // while the answer is negative, so that costs one 
probe message per
+                            // `REENGAGE_RETRY` and buys the case this whole 
path exists for -- a
+                            // panel that is still coming out of standby when 
the dock is first
+                            // asked about it.
+                            if connector_ever_known[h]
+                                && sustained_absent(&mut 
connector_absent_since[h])
+                            {
+                                connector_probed[h] = Some(false);
+                            }
+                            // A socket that has never had a monitor is where 
a negative answer is
+                            // worth least. This dock reports a connector 
absent precisely while its
+                            // EDID handler is not engaged for that connector, 
and engaging it is
+                            // what the call below does -- so waiting for a 
positive first is
+                            // waiting for the thing the re-engage produces. 
Spend a bounded number
+                            // of blind attempts there, and only while nothing 
on this dock is lit,
+                            // so a dock that is already driving a panel never 
has engage traffic
+                            // interleaved into its mode sets.
+                            let nothing_lit = !connector_known.iter().any(|&k| 
k);
+                            // Where one EDID handler serves every connector, 
engaging it for this
+                            // one takes it away from the connector that has 
it, and the fetch that
+                            // follows returns that connector's monitor -- 
which is then published
+                            // here as this socket's, so a single monitor 
appears to move between
+                            // sockets and each move tears its connector down. 
A negative answer is
+                            // the whole answer on such a dock: the engage the 
discovery path
+                            // already ran is what makes it truthful.
+                            if data.shared_edid_handler() {
+                                continue;
+                            }
+                            if connector_ever_known[h] || !nothing_lit || 
blind_engage_left[h] == 0
+                            {
+                                continue;
+                            }
+                            blind_engage_left[h] -= 1;
+                        }
+                        connector_absent_since[h] = None;
+                        vino_dev_debug!(
+                            cdev,
+                            "socket {socket} absent -- retrying the sink 
re-engage\n"
+                        );
+                        // A valid EDID proves presence even while the generic 
status reply still
+                        // reflects an unengaged EDID handler.
+                        match data.reengage_connector(dev, h as u8) {
+                            Ok(true) => {
+                                data.set_connected(h);
+                                connector_known[h] = true;
+                                connector_ever_known[h] = true;
+                                connector_debounce[h] = 0;
+                                presence_grace[h] = 
Instant::<Monotonic>::now() + PRESENCE_GRACE;
+                                vino_dev_debug!(
+                                    cdev,
+                                    "socket {socket} monitor connected after 
sink re-engagement\n"
+                                );
+                                hotplug_due = Some(Instant::<Monotonic>::now() 
+ HOTPLUG_COALESCE);
+                            }
+                            Ok(false) => {}
+                            Err(e) => {
+                                vino_dev_debug!(
+                                    cdev,
+                                    "socket {socket} sink re-engagement failed 
({e:?})\n"
+                                )
+                            }
+                        }
+                        next_presence = Instant::<Monotonic>::now();
+                    }
+                }
+                // A topology push brings presence probing forward.  Do not 
also cancel the
+                // absent-connector re-engage backoff here: Navarro emits an 
`id=0x44` reply for
+                // every ordinary presence probe, and `drain_cp_pushes` 
deliberately reports that as
+                // a downstream event.  Resetting `next_reengage` on each such 
reply turned two
+                // empty sockets into a continuous engage/EDID loop instead of 
the documented
+                // four-second retry cadence.  The probe below observes an 
actual arrival and then
+                // re-engages that specific connector immediately.
+                if data.take_downstream_event() {
+                    // Bring the probe forward, but never below 
`PRESENCE_MIN_GAP`.
+                    let soonest = Instant::<Monotonic>::now() + 
PRESENCE_MIN_GAP;
+                    if (next_presence - soonest).as_millis() > 0 {
+                        next_presence = soonest;
+                    }
+                }
+                let now_p = Instant::<Monotonic>::now();
+                if (now_p - next_presence).as_millis() >= 0 {
+                    next_presence = now_p + PRESENCE_PERIOD;
+                    for h in 0..data.connector_count() {
+                        let socket = h + 1;
+                        if !data.runtime_connector(h) {
+                            continue;
+                        }
+                        // A missing reply carries no status bit, so it is not 
evidence that this
+                        // monitor disappeared; wait for a decodable negative 
instead of tearing
+                        // down a live connector.
+                        let Some(present) = data.probe_connector_present(dev, 
h as u8) else {
+                            continue;
+                        };
+                        // Nothing this probe says about a connector vino 
blanked is news, in either
+                        // direction: the absence is vino's own doing, and 
this dock also flaps a
+                        // blanked sink back to *present*, which would 
re-engage it and clear
+                        // `self_blanked` -- leaving the next sustained 
negative free to tear the
+                        // connector down mid-blank. The flag is cleared by 
the wake, in
+                        // `atomic_enable`.
+                        if data.is_self_blanked(h) {
+                            connector_debounce[h] = 0;
+                            connector_absent_since[h] = None;
+                            continue;
+                        }
+                        // The probe has spoken for this connector, so the 
blind re-engage retry
+                        // above stands down for it -- but only a *positive* 
answer is authoritative
+                        // straight away. A negative one has to outlast 
`PRESENCE_REMOVE_MS`, and
+                        // has to be about a socket that has had a monitor in 
it, for the same
+                        // reasons it does above.
+                        if present {
+                            connector_probed[h] = Some(true);
+                            // The absent run is cleared below, not here: the 
"flap healed on its
+                            // own" line reads it with `take()` and would 
never fire again.
+                        } else if connector_ever_known[h]
+                            && sustained_absent(&mut connector_absent_since[h])
+                        {
+                            connector_probed[h] = Some(false);
+                        }
+                        if present == connector_known[h] {
+                            connector_debounce[h] = 0;
+                            // The sink came back before the removal deadline, 
so the connector was
+                            // never dropped and nothing downstream will 
re-drive this connector.
+                            // The dock has forgotten it, so vino has to put 
it back itself. Do not
+                            // re-drive the connector here. Most of these 
blips heal on their own --
+                            // the dock brings the sink back within a second 
or two -- and a repair
+                            // costs a full dock-wide re-activation, four 
seconds of cold
+                            // choreography for both panels. Firing one per 
flap puts the dock into
+                            // a permanent re-activation loop, one every five 
to fifteen seconds,
+                            // and neither panel stays lit. Absorbing the blip 
is the whole point; a
+                            // drop that does *not* heal still falls through 
to the timed removal
+                            // below.
+                            if present && 
connector_absent_since[h].take().is_some() {
+                                let now = Instant::<Monotonic>::now();
+                                if flap[h].healed(now) {
+                                    // Take the connector away so the 
compositor puts it back: the
+                                    // mode set that answers is what re-drives 
the sink, and it is
+                                    // the same repair a sustained absence 
gets below.
+                                    connector_known[h] = false;
+                                    connector_debounce[h] = 0;
+                                    next_reengage[h] = now + REENGAGE_RETRY;
+                                    data.set_disconnected(h);
+                                    dev_info!(
+                                        cdev,
+                                        "socket {socket} sink will not settle; 
dropping the connector so it is re-driven\n"
+                                    );
+                                    hotplug_due = None;
+                                    drm_dev.hotplug_event();
+                                    next_presence = 
Instant::<Monotonic>::now() + PRESENCE_PERIOD;
+                                    continue;
+                                }
+                                vino_dev_debug!(
+                                    cdev,
+                                    "socket {socket} sink flap healed on its 
own\n"
+                                );
+                            }
+                            continue;
+                        }
+                        // Inside the settling window after a recovery, a 
negative answer is not
+                        // evidence -- see `PRESENCE_GRACE`.
+                        if !present
+                            && (Instant::<Monotonic>::now() - 
presence_grace[h]).as_millis() < 0
+                        {
+                            connector_debounce[h] = 0;
+                            connector_absent_since[h] = None;
+                            continue;
+                        }
+                        if present {
+                            // Two consecutive contrary reads before 
announcing an arrival.
+                            connector_absent_since[h] = None;
+                            connector_debounce[h] = 
connector_debounce[h].saturating_add(1);
+                            if connector_debounce[h] < 2 {
+                                continue;
+                            }
+                        } else {
+                            // A removal must be sustained: the dock reports a 
lit sink absent for
+                            // seconds at a time. Counting probes instead of 
time does not work --
+                            // every `id=0x44` reply sets the downstream-event 
flag, and a probe's
+                            // own reply is one, so the watcher pulls itself 
forward and "two
+                            // contrary reads" fires 132 ms after the first 
negative.
+                            if !sustained_absent(&mut 
connector_absent_since[h]) {
+                                continue;
+                            }
+                            connector_absent_since[h] = None;
+                        }
+                        connector_debounce[h] = 0;
+                        if present {
+                            // An attempt that came back without an EDID has 
already answered, and
+                            // this path never read the deadline it set: the 
attempt clears the
+                            // debounce, two more probes rebuild it, and a 
socket the dock calls
+                            // present with nothing plugged into it re-engages 
every two seconds
+                            // for the life of the session. Seven paced 
control messages, on a dock
+                            // whose vendor sends one status query in the same 
interval and shares
+                            // the endpoint with its pixels.
+                            if (Instant::<Monotonic>::now() - 
next_reengage[h]).as_millis() < 0 {
+                                continue;
+                            }
+                            // Re-engage the downstream sink before accepting 
another mode set.
+                            match data.reengage_connector(dev, h as u8) {
+                                Ok(true) => {}
+                                Ok(false) => {
+                                    next_reengage[h] = 
Instant::<Monotonic>::now() + REENGAGE_RETRY;
+                                    continue;
+                                }
+                                Err(e) => {
+                                    vino_dev_debug!(
+                                        cdev,
+                                        "socket {socket} sink re-engagement 
failed ({e:?})\n"
+                                    );
+                                    next_reengage[h] = 
Instant::<Monotonic>::now() + REENGAGE_RETRY;
+                                    continue;
+                                }
+                            }
+                            data.set_connected(h);
+                            connector_known[h] = true;
+                            connector_ever_known[h] = true;
+                            flap[h] = FlapTracker::new();
+                            next_reengage[h] = Instant::<Monotonic>::now() + 
REENGAGE_RETRY;
+                            presence_grace[h] = Instant::<Monotonic>::now() + 
PRESENCE_GRACE;
+                            dev_info!(cdev, "socket {socket} monitor 
connected\n");
+                            // Same downstream readiness wait as a fresh 
bring-up before notifying
+                            // userspace, so KWin's mode-set lands on a 
settled downstream link.
+                            let rs = Instant::<Monotonic>::now();
+                            while (Instant::<Monotonic>::now() - 
rs).as_millis() < 1300
+                                && !data.is_shutting_down()
+                            {
+                                let _ = data
+                                    .send_cp(dev, 0x14, 0, |ctr| 
cp::device_query_req(ctr, 0x000c));
+                                fsleep(Delta::from_millis(15));
+                            }
+                            hotplug_due = Some(Instant::<Monotonic>::now() + 
HOTPLUG_COALESCE);
+                        } else {
+                            connector_known[h] = false;
+                            next_reengage[h] = Instant::<Monotonic>::now() + 
REENGAGE_RETRY;
+                            data.set_disconnected(h);
+                            dev_info!(cdev, "socket {socket} monitor 
disconnected\n");
+                            // This event also covers any arrival still 
waiting out its window.
+                            hotplug_due = None;
+                            drm_dev.hotplug_event();
+                        }
+                        // Re-baseline the heartbeat/presence deadlines 
skipped during the wait.
+                        next_presence = Instant::<Monotonic>::now() + 
PRESENCE_PERIOD;
+                    }
+                }
+                // Announce a settled burst of arrivals as one topology change.
+                if let Some(due) = hotplug_due {
+                    if (Instant::<Monotonic>::now() - due).as_millis() >= 0 {
+                        hotplug_due = None;
+                        // A monitor whose sink was not ready at bring-up is 
published here
+                        // instead, and the mode set answering it is still 
this session's first.
+                        // It needs the same quiet link a bring-up gives its 
own: the activation
+                        // is dock-wide, and the re-engage retries aimed at 
the sockets that are
+                        // genuinely empty are ~575 ms of paced traffic each, 
landing in the
+                        // middle of it otherwise.
+                        if data.dock_wide_modeset() && !topology_published {
+                            initial_quiet_until = None;
+                            data.hold_cp_for_initial_modeset();
+                        }
+                        topology_published = true;
+                        drm_dev.hotplug_event();
+                    }
+                }
+                // A dock that tears the link down over a silent video 
endpoint needs feeding even
+                // when the compositor has nothing to redraw.
+                data.send_video_keepalive(dev);
+                // A dock whose video shares this endpoint has its scanout 
workers stood down for
+                // the duration of every control message, and a worker that 
bails does not re-arm
+                // itself. Wake them here, where a device handle is in hand, 
so a connector with
+                // nothing else to trigger it still resumes.
+                if data.video_on_ctrl_pipe() {
+                    data.enqueue_scanout_all(drm_dev);
+                }
+                fsleep(Delta::from_millis(13));
+            }
+            vino_dev_debug!(cdev, "CP keepalive finished ({sent} polls)\n");
+        }
+    }
+}
+
+/// Control-session bring-up: plaintext init, link AKE, and the sealed 
per-connector setup.
+mod session;
+
+/// Which DisplayLink function an interface exposes, i.e. why this driver was 
offered it.
+///
+/// This is what the ID table carries. A table of product IDs cannot say 
anything useful about
+/// hardware nobody has tested, but the interface descriptor says what a 
function *is*, and that
+/// is stable across every dock in the family.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) enum Function {
+    /// The DL3 display function: the control endpoints and every video 
endpoint.
+    Display,
+    /// The DFU interface, which carries the identity descriptor and firmware 
updates.
+    Dfu,
+}
+
+/// Vendor-specific class, which every DisplayLink display function uses.
+const CLASS_VENDOR: u8 = 0xff;
+/// Interface protocol of a DL3 display function. `0x00` is the old `udl` 
hardware, which is a
+/// different driver's problem, so keying on this excludes it for free.
+const PROTOCOL_DL3: u8 = 0x03;
+/// Application-specific class, subclass and protocol of a USB DFU runtime 
interface.
+const CLASS_DFU: (u8, u8, u8) = (0xfe, 0x01, 0x01);
+
+// DisplayLink's own udev rules match `17e9/*` and then trigger on the 
interface, with no product
+// test anywhere. Reverse engineering found the same split independently. 
Binding the *function*
+// rather than a list of tested products is what lets a dock nobody here owns 
come up; the
+// identity descriptor read in `probe` is the safety valve that keeps that 
honest.
+kernel::usb_device_table!(
+    USB_TABLE,
+    MODULE_USB_TABLE,
+    <VinoDriver as usb::Driver>::IdInfo,
+    [
+        (
+            usb::DeviceId::from_vendor_and_interface_info(
+                VID_DISPLAYLINK,
+                CLASS_VENDOR,
+                0x00,
+                PROTOCOL_DL3
+            ),
+            Function::Display
+        ),
+        (
+            usb::DeviceId::from_vendor_and_interface_info(
+                VID_DISPLAYLINK,
+                CLASS_DFU.0,
+                CLASS_DFU.1,
+                CLASS_DFU.2
+            ),
+            Function::Dfu
+        ),
+    ]
+);
+
+impl usb::Driver for VinoDriver {
+    type IdInfo = Function;
+    type Data<'bound> = VinoBoundData;
+    const ID_TABLE: usb::IdTable<Self::IdInfo> = &USB_TABLE;
+    // The dock goes on scanning out its last decoded frame for as long as it 
is powered, so a
+    // driver that simply stops talking leaves both monitors lit on a frozen 
desktop. Telling them
+    // to power down is the last thing this driver does, and it can only be 
done while the
+    // interface's endpoints still exist -- which by default they do not by 
the time any callback
+    // runs. `quiesce` cancels every outstanding transfer itself.
+    const SOFT_UNBIND: bool = true;
+
+    fn probe<'bound>(
+        intf: &'bound usb::Interface<Core<'_>>,
+        _id: &usb::DeviceId,
+        info: &'bound Self::IdInfo,
+        io: Arc<usb::IoWindow>,
+    ) -> impl PinInit<Self::Data<'bound>, Error> + 'bound {
+        let cdev: &device::Device<Core<'_>> = intf.as_ref();
+        // The control endpoints (0x02/0x84) and the whole HDCP session live 
on the display
+        // function -- drive bring-up only there so the preamble and AKE do 
not run once per
+        // interface and pollute the dock's state machine. An interface with 
no active alternate
+        // setting has no endpoints to drive.
+        let function = *info;
+        let ifnum = intf.number().ok_or(ENODEV)?;
+        log_device_identity(cdev, intf, ifnum);
+
+        // What this hardware *is*, asked of the hardware. `read_identity` 
walks the ordinary
+        // configuration descriptor: one standard control transfer, no session 
and no crypto, so
+        // it works at probe on either interface and long before the dock will 
talk to anyone.
+        let identity = io
+            .enter()
+            .and_then(|link| firmware::read_identity(&link))
+            .ok();
+        let identity_family = 
identity.as_ref().and_then(firmware::Identity::family);
+
+        // Writing firmware the dock does not need is a deliberate act: its 
DFU interface does
+        // not support upload, so there is no way to read the running image 
back and nothing to
+        // restore from if the write goes wrong.
+        let force = *crate::module_parameters::force_flash.value() != 0;
+        if function == Function::Dfu {
+            // Every DFU request is addressed to this interface. A failed 
check is not fatal: a
+            // dock runs perfectly well on the firmware it shipped with.
+            match (identity.as_ref().ok_or(ENODEV)).and_then(|id| {
+                let link = io.enter()?;
+                dev_info!(cdev, "{id} running firmware {}\n", id.version);
+                firmware::update_if_newer(&link, cdev, id, u16::from(ifnum), 
force)
+            }) {
+                Ok(()) => {}
+                Err(e) => dev_info!(cdev, "dock firmware check skipped 
({e:?})\n"),
+            }
+        }
+        // The manual path: userspace writes an image and vino flashes it, 
whatever version it is.
+        // This is how a re-flash of the running version or a downgrade is 
done at all, since the
+        // automatic check refuses both. Published only on the DFU interface, 
and only for a dock
+        // whose family is recognised -- an image for another family is 
refused in `prepare`.
+        let mut fw_upload_name: Option<KBox<kernel::str::CString>> = None;
+        let fw_upload = if function == Function::Dfu {
+            match identity_family {
+                Some(family) => {
+                    let ctx = Arc::new(
+                        firmware::UploadCtx {
+                            window: io.clone(),
+                            cancelled: 
core::sync::atomic::AtomicBool::new(false),
+                            family,
+                        },
+                        GFP_KERNEL,
+                    )?;
+                    // Named per device, not `vino-dock`. The name becomes a 
device name inside
+                    // the shared `firmware` class, so a fixed one lets only 
the first dock
+                    // register and leaves the node saying nothing about which 
dock it flashes --
+                    // with two docks attached that is a route to flashing the 
wrong one.
+                    let name = 
kernel::str::CString::try_from_fmt(kernel::prelude::fmt!(
+                        "vino-dock-{}",
+                        cdev.name()
+                    ))
+                    .and_then(|n| KBox::new(n, 
GFP_KERNEL).map_err(Into::into));
+                    match name {
+                        Ok(name) => match 
kernel::firmware::upload::Registration::new(
+                            &THIS_MODULE,
+                            cdev,
+                            &name,
+                            ctx,
+                        ) {
+                            Ok(reg) => {
+                                dev_info!(
+                                    cdev,
+                                    "firmware upload available at 
/sys/class/firmware/{}\n",
+                                    &**name
+                                );
+                                fw_upload_name = Some(name);
+                                Some(reg)
+                            }
+                            Err(e) => {
+                                dev_warn!(cdev, "no firmware upload interface 
({e:?})\n");
+                                None
+                            }
+                        },
+                        Err(e) => {
+                            dev_warn!(cdev, "no firmware upload interface 
({e:?})\n");
+                            None
+                        }
+                    }
+                }
+                None => None,
+            }
+        } else {
+            None
+        };
+        if function == Function::Dfu {
+            vino_dev_debug!(
+                cdev,
+                "bound interface {ifnum} (idle -- control is the display 
function)\n"
+            );
+            return Ok(VinoBoundData {
+                _intf: intf.into(),
+                registration: None,
+                bringup: KBox::pin_init(new_mutex!(None), GFP_KERNEL)?,
+                _fw_upload: fw_upload,
+                _fw_upload_name: fw_upload_name,
+            });
+        }
+
+        // The DFU interface is probed independently of this one and writes 
firmware in its own
+        // probe, which reboots the dock. Establishing a control session 
against a dock that is
+        // about to drop off the bus only produces timeouts and a device that 
is torn down and
+        // rebuilt, so leave it alone: the dock re-enumerates on the new 
firmware and this probe
+        // runs again with nothing pending. The attempt limit is what stops 
that repeating.
+        if let Some(id) = identity.as_ref() {
+            if firmware::update_pending(cdev, id, force) {
+                dev_info!(
+                    cdev,
+                    "dock firmware update pending; the display function binds 
once it has run\n"
+                );
+                return Err(ENODEV);
+            }
+        }
+
+        // The safety valve for matching on the interface rather than on a 
product ID. A dock that
+        // answers with a family nobody here has driven is declined by name, 
so its owner gets a
+        // log line and a report to send instead of a driver guessing at its 
wire format -- and
+        // the way a dock rejects a guess is to reset itself. A dock that 
could not be *asked*
+        // falls back to the product-ID quirk table, because a transient 
descriptor read must not
+        // cost a working device its display.
+        let profile = match identity_family {
+            Some(family) => match profile::for_family(family) {
+                Some(profile) => profile,
+                None => {
+                    let id = identity.as_ref().ok_or(ENODEV)?;
+                    dev_info!(
+                        cdev,
+                        "{id} is not a family this driver drives yet; 
declining. \
+                         A report makes it supportable: 
Documentation/gpu/vino.rst\n"
+                    );
+                    return Err(ENODEV);
+                }
+            },
+            None => {
+                let usbdev: &usb::Device<Core<'_>> = intf.as_ref();
+                match profile::for_product(usbdev.product_id()) {
+                    Some(profile) => {
+                        dev_warn!(
+                            cdev,
+                            "identity descriptor unreadable; using the quirk 
entry for \
+                             {:04x}\n",
+                            usbdev.product_id()
+                        );
+                        profile
+                    }
+                    None => {
+                        dev_info!(
+                            cdev,
+                            "no identity descriptor and no quirk entry; 
declining. \
+                             A report makes it supportable: 
Documentation/gpu/vino.rst\n"
+                        );
+                        return Err(ENODEV);
+                    }
+                }
+            }
+        };
+        // One line per bind, naming the hardware the driver decided it is 
holding. On unfamiliar
+        // hardware this is what says whether the dock was recognised or fell 
back to a stranger's
+        // profile, so it stays out of the debug gate. The endpoint map that 
follows from it is a
+        // debug detail.
+        dev_info!(cdev, "{}\n", profile.name);
+        vino_dev_debug!(
+            cdev,
+            "video endpoints {}, 10-bit capable {}\n",
+            HexList(&profile.topology.video_endpoints),
+            profile.capabilities.hdr_capable
+        );
+        // Register the DRM/KMS device on the control interface. Keep a 
refcounted interface handle
+        // in the bound data while the DRM device retains the I/O window used 
by its workers.
+        let intf_ref: ARef<usb::Interface> = intf.into();
+
+        // Resolve the dock's endpoints against the display function's 
descriptor once, so every
+        // later transfer names a direction/type-checked endpoint instead of a 
bare address.
+        let (endpoints, connectors) = Endpoints::resolve(intf, profile)?;
+        if connectors != profile.topology.connectors {
+            dev_warn!(
+                cdev,
+                "{connectors} connector(s) backed by video endpoints, not the 
{} this \
+                 profile describes; driving what the device exposes\n",
+                profile.topology.connectors
+            );
+        }
+
+        // DRM device lifecycle: allocate an `UnregisteredDevice`, wire up the 
KMS pipeline on it
+        // while still unregistered, then register it. The `Registration` is 
stored in the bound
+        // data below, so the card is unregistered by the ordered unbind 
rather than by a
+        // driver-local force-unplug.
+        let unreg = drm::UnregisteredDevice::<drm_sink::VinoDrmDriver>::new(
+            intf,
+            // The ten-bit and cursor flags and the connector count have to 
arrive here, not in the
+            // profile block below: the KMS objects are built during this 
call, and they decide
+            // then whether to offer a 10-bit format, the HDR connector 
properties and a cursor
+            // plane, and how many connectors to build at all.
+            drm_sink::VinoDrmData::new(
+                io.clone(),
+                endpoints,
+                profile.capabilities.hdr_capable,
+                profile.capabilities.hw_cursor,
+                connectors,
+            ),
+            &THIS_MODULE,
+        )?;
+        // `Core` derefs to `Bound`; name the context explicitly so `as_ref()`
+        // resolves to the bound parent required by DRM registration.
+        let bound_intf: &usb::Interface<device::Bound> = intf;
+        let parent: &device::Device<device::Bound> = bound_intf.as_ref();
+        let registration = drm::Registration::new_static(parent, unreg, (), 
0)?;
+        let ddev: ARef<drm_sink::VinoDrmDevice> = registration.device().into();
+        vino_dev_debug!(cdev, "DRM/KMS device registered\n");
+
+        // The session preamble, HDCP authentication and control setup use 
blocking USB transfers.
+        // Run them on the device's ordered session queue so probe can return 
immediately. The work
+        // item owns the DRM device, and the bound data retains a handle so 
quiesce can cancel or
+        // flush it before the I/O window closes.
+        // Gate video on what this platform's video path is known to accept.
+        {
+            let d: &drm_sink::VinoDrmData = &ddev;
+            // `force_video` exists to answer one question on a dock whose 
profile disables video:
+            // whether the platform actually requires its sealed stream-open, 
or whether correct
+            // record framing alone is enough. It is off by default because 
the way a dock rejects
+            // a malformed video write is to reset itself, taking the control 
session with it.
+            // This device's codec geometry, passed into every codec call made 
on its behalf.
+            // It is per device because two docks of different generations lay 
a strip's sixteen
+            // blocks over different pixels; see `video::haar::Geometry`.
+            d.set_codec_geometry(
+                profile.protocol.strip_blocks_x,
+                profile.protocol.interlaced_bands,
+                profile.protocol.band_parity_bit,
+                profile.protocol.connector_selector_shift,
+                profile.protocol.stream_id_mask,
+                profile.protocol.dock_buffers,
+                profile.protocol.code_tables,
+                profile.protocol.steady_record_sub_bit,
+            );
+            d.set_frame_delivery(profile.protocol.frame_delivery);
+            d.set_probe_bracket(profile.protocol.probe_bracket);
+            d.set_stream_pacing(profile.protocol.stream_pacing);
+            d.set_mode_limits(
+                profile.capabilities.pixel_budget,
+                profile.capabilities.max_refresh_hz,
+                profile.capabilities.max_connector_clock_khz,
+            );
+            d.set_mode_behaviour(profile);
+            d.set_video_on_ctrl_pipe(profile.topology.video_on_ctrl_pipe);
+            d.set_frame_period_ms(profile.protocol.frame_period_ms);
+            d.set_carrier_frames(profile.protocol.carrier_frames);
+            d.set_status_period_ms(profile.protocol.status_period_ms);
+            d.set_arm_burst(profile.protocol.arm_burst);
+            d.set_allocation(&profile.protocol.allocation);
+            d.set_reports_presence(profile.protocol.reports_presence);
+            d.set_shared_edid_handler(profile.quirks.shared_edid_handler);
+            
d.set_split_full_packet_frame(profile.quirks.split_full_packet_frame);
+            d.set_video_stream_desc(
+                profile.protocol.layout_word,
+                profile.protocol.stream_marker_kind,
+                profile.protocol.code_tables,
+            );
+            d.set_sink_down_state(profile.protocol.sink_down_state);
+            
d.set_post_mode_sink_states(profile.protocol.post_mode_sink_states);
+            d.set_pre_mode_sink_state(profile.protocol.pre_mode_sink_state);
+        }
+        let bringup = BringUp::new(ddev.clone(), profile)?;
+        let bringup_slot = KBox::pin_init(new_mutex!(Some(bringup.clone())), 
GFP_KERNEL)?;
+
+        let data: &drm_sink::VinoDrmData = &ddev;
+        data.session_queue().enqueue(bringup).map_err(|_| EBUSY)?;
+
+        Ok(VinoBoundData {
+            _intf: intf_ref,
+            registration: Some(registration),
+            bringup: bringup_slot,
+            // The upload interface lives on the DFU interface, not the 
control one.
+            _fw_upload: None,
+            _fw_upload_name: None,
+        })
+    }
+
+    fn pre_reset<'bound>(
+        _intf: &'bound usb::Interface<Core<'_>>,
+        data: Pin<&VinoBoundData>,
+    ) -> Result {
+        if let Some(reg) = data.registration.as_ref() {
+            let drm_data: &drm_sink::VinoDrmData = reg.device();
+            drm_data.stop_for_reset();
+        }
+        Ok(())
+    }
+
+    /// Ask the USB core to rebind this interface once the reset has completed.
+    ///
+    /// The only state that makes this dock usable is the content-protection 
session, and the reset
+    /// is what destroyed it. There is nothing to restore and no way to 
establish a new session
+    /// except through probe, so a driver that returns success here stays 
bound to a dock that will
+    /// never answer again. A non-zero return marks the interface for 
rebinding, which unbinds and
+    /// probes it afresh.
+    fn post_reset<'bound>(
+        intf: &'bound usb::Interface<Core<'_>>,
+        _data: Pin<&VinoBoundData>,
+    ) -> Result {
+        let dev: &device::Device<Core<'_>> = intf.as_ref();
+        dev_info!(dev, "reset complete; rebinding for a fresh session\n");
+        Err(ENODEV)
+    }
+
+    fn quiesce<'bound>(_intf: &'bound usb::Interface<Core<'_>>, data: 
Pin<&VinoBoundData>) {
+        if let Some(reg) = data.registration.as_ref() {
+            let drm_data: &drm_sink::VinoDrmData = reg.device();
+            // The last chance to tell the dock anything. This hook runs while 
the interface is
+            // still bound, whereas `disconnect()` runs after I/O has been 
revoked -- and the stop
+            // flag published below makes every control transfer refuse by 
design, because a
+            // transfer issued into a disconnect deadlocks `usb_hub_wq`. So 
the sinks are parked
+            // here, first, or not at all.
+            drm_data.park_sinks();
+            // Publish the producers' stop flag before waiting on anything. 
This is only the flag:
+            // the teardown that must not run until USB I/O is quiesced 
(vblank timers, the
+            // device's self-reference cycles) still happens in `shutdown()` 
further down.
+            drm_data.begin_shutdown();
+        }
+
+        // Take the sole driver-owned bring-up handle. The queued work holds 
its own Arc until it
+        // runs or is cancelled, while this local Arc keeps the embedded Work 
pinned and live
+        // throughout the `cancel_sync` below.
+        let bringup = data.bringup.lock().take();
+
+        // Flush the deferred bring-up before the interface is unbound: 
`cancel_sync` dequeues it
+        // if pending and blocks until it returns if already running, so no 
USB I/O races the
+        // unbind. Safe when the work already finished or never ran -- it then 
simply reports that
+        // nothing was pending. The reclaimed `Arc<BringUp>` (returned only if 
the work was still
+        // queued) is dropped here.
+        if let Some(work) = bringup.as_ref() {
+            drop(work.work.cancel_sync());
+        }
+
+        // `bringup` drops here, releasing its DRM reference before I/O is 
revoked.
+    }
+
+    fn disconnect<'bound>(intf: &'bound usb::Interface<Core<'_>>, data: 
Pin<&VinoBoundData>) {
+        let dev: &device::Device<Core<'_>> = intf.as_ref();
+
+        // Stop every producer. The DRM device itself is unregistered by 
`Registration`'s `Drop`
+        // when the bound data is released -- the accepted registration 
teardown already calls
+        // `drm_dev_unplug()`, so there is no driver-local force-unplug here 
any more.
+        //
+        // Take the device through the registration. `shutdown()` also breaks
+        // vblank self-references before registration teardown.
+        if let Some(reg) = data.registration.as_ref() {
+            let drm_data: &drm_sink::VinoDrmData = reg.device();
+            drm_data.shutdown();
+        }
+        dev_info!(dev, "disconnected\n");
+    }
+}
+
+kernel::module_usb_driver! {
+    type: VinoDriver,
+    name: "vino",
+    authors: ["Mike Lothian"],
+    description: "DisplayLink DL3 (Vino) open driver",
+    license: "GPL v2",
+    params: {
+        debug: u8 {
+            default: 0,
+            description: "Enable verbose Vino protocol and scanout 
diagnostics",
+        },
+        trace_crypto: u8 {
+            default: 0,
+            description: "Diagnostic: disclose one session's keys to decrypt a 
USB capture",
+        },
+        rtc_utc_offset_minutes: i32 {
+            default: 0,
+            description: "Minutes east of UTC, for a dock's real-time clock",
+        },
+        force_flash: u8 {
+            default: 0,
+            description: "Write the packaged dock firmware even if the dock is 
not older",
+        },
+        edid_override: u8 {
+            default: 0,
+            description: "Bitmask of connectors whose EDID comes from DRM's 
override",
+        },
+    },
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_presence_flap)]
+mod tests {
+    use super::*;
+
+    /// Flaps `n` times `apart_ms` apart, and reports how many repairs that 
asked for.
+    fn flaps(n: u32, apart_ms: i64) -> u32 {
+        let mut tracker = FlapTracker::new();
+        let start = Instant::<Monotonic>::now();
+        let mut repairs = 0;
+        for i in 0..n {
+            if tracker.healed(start + Delta::from_millis(apart_ms * 
i64::from(i))) {
+                repairs += 1;
+            }
+        }
+        repairs
+    }
+
+    #[test]
+    fn a_blip_is_absorbed_and_sustained_flapping_is_repaired() {
+        // One flap, and a second a long way after it, are blips: the dock 
brings the sink back on
+        // its own and a repair would cost a dock-wide re-activation for 
nothing.
+        assert_eq!(flaps(1, 0), 0);
+        assert_eq!(flaps(2, 1_000), 0);
+        // Flaps spread wider than the window never accumulate, however many 
there are.
+        assert_eq!(flaps(20, FlapTracker::WINDOW_MS), 0);
+
+        // A sink that will not settle is repaired. Nine a minute is what a 
connector left dark by a
+        // warm plug produces, and it asks for a repair rather than being 
absorbed forever.
+        assert!(flaps(FlapTracker::REPAIR_COUNT, 1_000) > 0);
+        assert!(flaps(30, 6_500) > 0);
+    }
+
+    #[test]
+    fn a_flapping_dock_cannot_hold_vino_in_a_repair_loop() {
+        // The repair is a dock-wide re-activation. However long the flapping 
goes on, the number of
+        // them is bounded: a dock that flaps as a matter of course gets a few 
and then silence.
+        assert_eq!(flaps(10_000, 1_000), FlapTracker::REPAIR_LIMIT);
+    }
+
+    #[test]
+    fn a_connector_that_comes_back_starts_again() {
+        // The tracker is reset when a connector is re-established, so a dock 
that misbehaves once
+        // is still repairable the next time rather than having spent its 
budget for the session.
+        let mut tracker = FlapTracker::new();
+        let now = Instant::<Monotonic>::now();
+        for _ in 0..FlapTracker::REPAIR_LIMIT * FlapTracker::REPAIR_COUNT {
+            tracker.healed(now);
+        }
+        assert!(!tracker.healed(now));
+        tracker = FlapTracker::new();
+        for _ in 0..FlapTracker::REPAIR_COUNT - 1 {
+            assert!(!tracker.healed(now));
+        }
+        assert!(tracker.healed(now));
+    }
+}

Reply via email to