[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)); + } +}