[PATCH v3 11/13] drm/vino: add the USB driver frontend
From: Mike Lothian
Date: Wed Aug 26 2026 - 12:47:46 EST
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 <mike@xxxxxxxxxxxxxx>
---
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));
+ }
+}