[PATCH v3 11/13] drm/vino: add the USB driver frontend

Mike Lothian <[email protected]>
Newsgroups org.kernel.vger.rust-for-linux,org.freedesktop.lists.dri-devel,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
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));
+    }
+}
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.