Bringing a dock up runs once per bind, in order: a plaintext initialisation
preamble, the link AKE, then the sealed setup burst that authenticates each
downstream connector, transitions the video engine and reads back what the
dock will answer. The burst's ordering and its spacing both come from the
vendor, and a dock driven faster than it stays dark.

Add the three stages as three modules, and the interrupt-endpoint reader
that decodes what the dock pushes rather than counting it and discarding
the bytes.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <[email protected]>
---
 drivers/gpu/drm/vino/session.rs         |  625 ++++++++++
 drivers/gpu/drm/vino/session/replies.rs |  337 ++++++
 drivers/gpu/drm/vino/session/setup.rs   | 1390 +++++++++++++++++++++++
 3 files changed, 2352 insertions(+)
 create mode 100644 drivers/gpu/drm/vino/session.rs
 create mode 100644 drivers/gpu/drm/vino/session/replies.rs
 create mode 100644 drivers/gpu/drm/vino/session/setup.rs

diff --git a/drivers/gpu/drm/vino/session.rs b/drivers/gpu/drm/vino/session.rs
new file mode 100644
index 000000000000..f14a0f47311e
--- /dev/null
+++ b/drivers/gpu/drm/vino/session.rs
@@ -0,0 +1,625 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Bringing a dock's encrypted control session up.
+//!
+//! In order: the plaintext initialisation preamble, the HDCP 2.2 link AKE, 
then the sealed control
+//! setup that authenticates each downstream connector and reads its EDID. 
Everything here runs
+//! once per bind, on the bring-up worker; the steady-state keepalive and the 
KMS transactions live
+//! in [`super::drm_sink`].
+
+use super::*;
+
+mod replies;
+mod setup;
+
+impl VinoDriver {
+    /// Initialize the plaintext control transport.
+    pub(super) fn bring_up(link: &UsbLink<'_>, profile: &DockProfile) -> 
Result {
+        // Control-request preamble: dock identity, interface selection, then 
the
+        // vendor-OUT 0x24 / vendor-IN 0x22 pair that starts the HDCP path.
+        const VENDOR_OUT: u8 = 0x40; // host->link, vendor, device
+        const VENDOR_IN_IFACE: u8 = 0xc1; // device-to-host, vendor, interface 
recipient
+
+        // Individual vendor requests may stall. Only bulk initialization and
+        // its acknowledgment are required.
+        let mut identity_bytes = [0u8; 16];
+        match link.control_recv(
+            0xfe,
+            VENDOR_IN_IFACE,
+            0,
+            1,
+            &mut identity_bytes,
+            timeout(),
+            GFP_KERNEL,
+        ) {
+            // The raw blob is printed only when it cannot be read as an 
identity, which is the
+            // case where someone has to decode it by hand. Info, not debug: 
on unfamiliar
+            // hardware this is what places the device, and needing a debug 
build to see it costs
+            // a whole test round trip.
+            Ok(()) => match firmware::Identity::parse(&identity_bytes) {
+                // The DFU interface names the dock at info level, so one that 
parses is already
+                // reported once.
+                Some(id) => vino_debug!("{id} running firmware {}\n", 
id.version),
+                None => pr_info!("unrecognised device identity = 
{identity_bytes:02x?}\n"),
+            },
+            Err(e) => pr_info!("device identity unavailable ({e:?})\n"),
+        }
+        // A composite driver may only change its own interface.
+        match link.set_alternate_setting(0) {
+            Ok(()) => {}
+            Err(e) => vino_debug!("vino: alternate setting unchanged 
({e:?})\n"),
+        }
+        // The first vendor transition is platform-specific even though both 
platforms use the
+        // same request number. Ridge uses wValue=3; both occurrences in the 
authenticated
+        // Navarro/DLM USB transcript use wValue=0. Sending Ridge's value here 
still permits AKE
+        // and an exact EP02 transcript, but leaves Navarro's video-side state 
machine different
+        // before its later value-0 commit.
+        let vendor_state = profile.protocol.initial_vendor_state;
+        match link.control_send(
+            0x24,
+            VENDOR_OUT,
+            vendor_state,
+            0,
+            &[],
+            timeout(),
+            GFP_KERNEL,
+        ) {
+            Ok(()) => {}
+            Err(e) => vino_debug!("vino: vendor preamble request stalled 
({e:?})\n"),
+        }
+        // Request interface 0 state using the vendor interface recipient.
+        let mut state = [0u8; 28];
+        match link.control_recv(
+            0x22,
+            VENDOR_IN_IFACE,
+            1,
+            0,
+            &mut state,
+            timeout(),
+            GFP_KERNEL,
+        ) {
+            Ok(()) => vino_debug!("vino: interface state = {state:02x?}\n"),
+            Err(e) => vino_debug!("vino: interface state unavailable 
({e:?})\n"),
+        }
+
+        // The dock requires this exact plaintext initialization order. It 
acknowledges the
+        // sequence only after `init_4` and the following probe. The 
interleaved descriptor reads
+        // are best-effort because a short reply still completes the required 
control transfer.
+        const STD_IN: u8 = 0x80; // link->host, standard, device
+        let mut config_descriptor = KVec::from_elem(0u8, 618, GFP_KERNEL)?;
+        let _ = link.control_recv(
+            0x06,
+            STD_IN,
+            0x0200,
+            0,
+            &mut config_descriptor[..40],
+            timeout(),
+            GFP_KERNEL,
+        ); // CONFIG, 40
+        let _ = link.control_recv(
+            0x06,
+            STD_IN,
+            0x0200,
+            0,
+            &mut config_descriptor,
+            timeout(),
+            GFP_KERNEL,
+        );
+
+        // Report EP02's maximum packet size because exact-multiple messages 
require an explicit
+        // terminating short packet.
+        {
+            let total = ((config_descriptor[2] as usize) | 
((config_descriptor[3] as usize) << 8))
+                .min(config_descriptor.len());
+            let mut i = 0usize;
+            while i + 2 <= total {
+                let blen = config_descriptor[i] as usize;
+                if blen == 0 {
+                    break;
+                }
+                if config_descriptor[i + 1] == 0x05
+                    && i + 7 <= total
+                    && config_descriptor[i + 2] == EP_CTRL_OUT
+                {
+                    let wmax = (config_descriptor[i + 4] as u16)
+                        | ((config_descriptor[i + 5] as u16) << 8);
+                    vino_debug!("vino: EP02 max packet size {wmax}\n");
+                }
+                i += blen;
+            }
+        }
+
+        let send_required = |label: &str, msg: &[u8]| -> Result {
+            match link.ctrl_send(msg, timeout(), GFP_KERNEL) {
+                Ok(_) => Ok(vino_debug!("vino: sent {label} ({} bytes)\n", 
msg.len())),
+                Err(e) => {
+                    pr_err!("vino: {label} failed ({e:?})\n");
+                    Err(e)
+                }
+            }
+        };
+        send_required("init_0", &proto::init_0()?)?;
+        send_required("init_25", &proto::init_25()?)?;
+        // Two required string reads between `init_25` and `init_4`.
+        let _ = link.control_recv(
+            0x06,
+            STD_IN,
+            0x0300,
+            0x0000,
+            &mut config_descriptor[..255],
+            timeout(),
+            GFP_KERNEL,
+        ); // STRING #0
+        let _ = link.control_recv(
+            0x06,
+            STD_IN,
+            0x0303,
+            0x0409,
+            &mut config_descriptor[..255],
+            timeout(),
+            GFP_KERNEL,
+        ); // STRING #3 en-US
+        send_required("init_4+probe", &proto::init_4_probe()?)?;
+
+        // Read the single ACK that follows init_4+probe.
+        let mut ack = KVec::from_elem(0u8, 1024, GFP_KERNEL)?;
+        match link.ctrl_recv(&mut ack, timeout(), GFP_KERNEL) {
+            Ok(n) => vino_debug!(
+                "vino: session-init ACK = {n} bytes: {:02x?}\n",
+                &ack[..n.min(40)]
+            ),
+            Err(e) => {
+                pr_err!("vino: session acknowledgment failed ({e:?})\n");
+                return Err(e);
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Number of display connectors the post-msg0 CP setup burst re-states 
the AKE for. Tied to the
+    /// single connector-count knob `drm_sink::MAX_CONNECTORS` so bumping the 
connector count is a
+    /// one-line change (was a duplicated literal `2` that had to be kept in 
sync by hand).
+    pub(super) const CP_SETUP_CONNECTORS: usize = drm_sink::MAX_CONNECTORS;
+
+    /// Reads the next HDCP response (type=4 sub=0x25, sec 5.2) from EP `0x84`,
+    /// skipping any non-HDCP frames (e.g. plain ACKs) in between, and returns 
the
+    /// parsed `(msg_id, payload)`. Bounded retry so a chatty dock can't wedge 
us.
+    fn recv_hdcp(link: &UsbLink<'_>) -> Result<(u8, KVec<u8>)> {
+        const SUB_HDCP_RESP: u16 = 0x25;
+        // The dock interleaves capability blocks up to ~5.8 KiB into the AKE 
reply
+        // stream; size the buffer like the rest of the EP84 reads 
([`EP84_BUF`]) so a
+        // large frame is read whole rather than truncated/`-EOVERFLOW`'d.
+        let mut buf = KVec::from_elem(0u8, EP84_BUF, GFP_KERNEL)?;
+        for _ in 0..24 {
+            // The dock interleaves status and capability pushes with the HDCP 
replies.
+            let n = link.ctrl_recv(&mut buf, timeout(), GFP_KERNEL)?;
+            if n < 16 {
+                continue;
+            }
+            // Include interleaved capability and status frames in 
dynamic-debug output.
+            {
+                let wsub = u16::from_le_bytes([buf[8], buf[9]]);
+                let iid = if n >= 18 {
+                    u16::from_le_bytes([buf[16], buf[17]])
+                } else {
+                    0
+                };
+                let isub = if n >= 20 {
+                    u16::from_le_bytes([buf[18], buf[19]])
+                } else {
+                    0
+                };
+                vino_debug!(
+                    "vino: AKE-EP84 {n}B wsub={wsub:#x} inner_id={iid:#x} 
inner_sub={isub:#x}\n"
+                );
+            }
+            if u16::from_le_bytes([buf[8], buf[9]]) != SUB_HDCP_RESP {
+                continue; // non-HDCP frame -- skip
+            }
+            if let Some((id, payload)) = ake::parse_in(&buf[16..n]) {
+                // Inner msg_id 0 is a status/ACK frame (the dock emits one as 
a
+                // sub=0x25 frame after each OUT message, e.g. the `14 00 76 
00...`
+                // frame after AKE_Init) -- skip it and keep reading for the 
real
+                // HDCP response, mirroring the oracle's recv_hdcp_msg.
+                if id == 0 {
+                    continue;
+                }
+                let mut pl = KVec::with_capacity(payload.len(), GFP_KERNEL)?;
+                pl.extend_from_slice(payload, GFP_KERNEL)?;
+                return Ok((id, pl));
+            }
+        }
+        Err(EINVAL)
+    }
+
+    /// Drain one repeater-authentication acknowledgment before the next 
request.
+    ///
+    /// The dock enforces request/reply lockstep during this phase. The drain 
is bounded and
+    /// best-effort so an idle dock cannot stall teardown.
+    fn pace_cap_ack(link: &UsbLink<'_>, want_ctr: u16, saw_cap_complete: &mut 
bool) {
+        // EP84 frames here can carry an interleaved capability block up to 
~5.8 KiB;
+        // size to [`EP84_BUF`] so a large frame isn't truncated mid-pacing.
+        let Ok(mut buf) = KVec::from_elem(0u8, EP84_BUF, GFP_KERNEL) else {
+            return;
+        };
+        for _ in 0..8 {
+            match link.ctrl_recv(&mut buf, Delta::from_millis(30), GFP_KERNEL) 
{
+                Ok(len) if len >= 22 => {
+                    let wsub = u16::from_le_bytes([buf[8], buf[9]]);
+                    let iid = u16::from_le_bytes([buf[16], buf[17]]);
+                    let isub = u16::from_le_bytes([buf[18], buf[19]]);
+                    let ictr = u16::from_le_bytes([buf[20], buf[21]]);
+                    if iid == 0x0b && isub == 0x84 {
+                        *saw_cap_complete = true;
+                    }
+                    // The per-frame cap-ack: wsub=0x25, inner id=0x14 
sub=0x10 ctr=want.
+                    // An interleaved cap push (sub=0x84) or earlier ack -- 
keep reading.
+                    if wsub == 0x25 && iid == 0x14 && ictr == want_ctr {
+                        return;
+                    }
+                }
+                // A short frame (header-only ack/keepalive): not our cap-ack, 
but the
+                // dock is still talking -- keep pacing rather than bailing 
out.
+                Ok(_) => continue,
+                // Nothing queued within the short window -- the dock is idle, 
don't block.
+                Err(_) => return,
+            }
+        }
+    }
+
+    /// Drain the terminal capability burst before arming the encrypted 
control plane.
+    ///
+    /// The terminal markers are capability-complete (`id=0x0b sub=0x84`) and
+    /// `RepeaterAuth_Stream_Ready`. If the latter is absent, a bounded quiet 
interval after the
+    /// capability marker is accepted for firmware compatibility.
+    fn wait_cap_complete(link: &UsbLink<'_>, kd: &[u8; 32], mut saw_0b: bool) {
+        let Ok(mut buf) = KVec::from_elem(0u8, EP84_BUF, GFP_KERNEL) else {
+            return;
+        };
+        // M' verification is diagnostic until every vendor stream-management 
field is decoded.
+        let sha_kd = crypto::sha256(kd);
+        let mut saw_ready = false;
+        let mut quiet = 0usize;
+        const QUIET_GAP: usize = 3; // ~3 consecutive empty short reads => 
dock done pushing
+        const MAX_ROUNDS: usize = 48;
+        for _ in 0..MAX_ROUNDS {
+            match link.ctrl_recv(&mut buf, Delta::from_millis(5), GFP_KERNEL) {
+                Ok(len) if len >= 20 => {
+                    quiet = 0;
+                    let iid = u16::from_le_bytes([buf[16], buf[17]]);
+                    let isub = u16::from_le_bytes([buf[18], buf[19]]);
+                    let mid = if len >= 26 { buf[25] } else { 0 }; // HDCP 
msg_id (body[9])
+                    if isub == 0x84 && iid == 0x0b {
+                        saw_0b = true;
+                    }
+                    if mid == ake::id::REPEATERAUTH_STREAM_READY && len >= 58 {
+                        saw_ready = true;
+                        let mprime = &buf[26..58];
+                        vino_debug!("vino: AKE: Stream_Ready (0x11) 
M'={mprime:02x?}\n");
+                        // The content-stream-management input contains two
+                        // seven-byte stream entries and a three-byte sequence.
+                        let m_data: [u8; 17] = [
+                            0, 0, 0, 0x04, 0, 0, 0, // stream 0: 
StreamID_Type[0]
+                            0, 0, 0, 0x05, 0, 0, 0, // stream 1: 
StreamID_Type[1]
+                            0, 0, 0, // seq_num_M = 0 (first Stream_Manage, 
big-endian)
+                        ];
+                        let m = crypto::hmac_sha256(&sha_kd, &m_data);
+                        let eq = if &m[..] == mprime { "==" } else { "!=" };
+                        vino_debug!("vino: AKE:   M {} M' (CSM stream-entry 
layout)\n", eq);
+                    } else if mid == ake::id::RECEIVER_AUTH_STATUS && len >= 
27 {
+                        vino_debug!("vino: AKE: 
RECEIVER_AUTH_STATUS=0x{:02x}\n", buf[26]);
+                    }
+                    // Both terminal markers complete the burst; do not add a 
quiet delay here.
+                    if saw_0b && saw_ready {
+                        vino_debug!("vino: repeater authentication 
complete\n");
+                        return;
+                    }
+                }
+                // Empty/short read = a quiet window. Fallback when 
Stream_Ready (0x11) never
+                // arrives:
+                // once id=0x0b has arrived AND the dock has been quiet for 
QUIET_GAP rounds, the
+                // terminal burst is drained -- arm now.
+                _ => {
+                    if saw_0b {
+                        quiet += 1;
+                        if quiet >= QUIET_GAP {
+                            vino_debug!("vino: repeater reply drained 
(ready={saw_ready})\n");
+                            return;
+                        }
+                    }
+                }
+            }
+        }
+        vino_debug!("vino: repeater drain ended (complete={saw_0b}, 
ready={saw_ready})\n");
+    }
+
+    /// Run HDCP 2.2 AKE, locality check, session-key exchange and repeater 
authentication.
+    ///
+    /// `H'`, `L'` and `V'` are verified locally. Outbound messages use 
`type=4 sub=0x04` and the
+    /// inner sequence is:
+    ///
+    /// * ctr=1 session-init ACK (id=0x14/0x76), ctr=2 AKE_Init, ctr=3 
AKE_Transmitter_Info
+    /// * ctr=4 AKE_No_Stored_km, ctr=5 LC_Init, ctr=6 SKE_Send_Eks
+    /// * ctr=7 RepeaterAuth_Send_Ack, ctr=8 RepeaterAuth_Stream_Manage  (then 
msg0 at ctr=9)
+    pub(super) fn run_ake(link: &UsbLink<'_>) -> Result<Session> {
+        use ake::id;
+
+        let mut saw_cap_complete = false;
+
+        // A warm rebind can leave replies from the previous session queued on 
EP84.
+        let flush_probe = Delta::from_millis(3);
+        if let Ok(mut flush) = KVec::from_elem(0u8, EP84_BUF, GFP_KERNEL) {
+            let mut flushed = 0usize;
+            for _ in 0..32 {
+                match link.ctrl_recv(&mut flush, flush_probe, GFP_KERNEL) {
+                    Ok(n) if n > 0 => flushed += 1,
+                    _ => break,
+                }
+            }
+            if flushed > 0 {
+                vino_debug!("vino: flushed {flushed} stale EP84 frame(s) 
before AKE\n");
+            }
+        }
+
+        // The setup phase continues this counter through `Session::next_ctr`.
+        let mut hseq: u32 = 1;
+
+        // (1) session-init ACK (ctr=1, id=0x14/0x76).
+        link.ctrl_send(&ake::session_init_ack(hseq, 0)?, timeout(), 
GFP_KERNEL)?;
+        // The dock requires the counter-1 echo to be drained before AKE_Init.
+        Self::pace_cap_ack(link, hseq as u16, &mut saw_cap_complete);
+        hseq += 1;
+
+        // (2) AKE_Init -- use a fresh rtx and the transmitter capability 
profile.
+        let mut rtx = [0u8; drm_hdcp::RTX_LEN];
+        rng::fill(&mut rtx);
+        link.ctrl_send(
+            &ake::ake_init(hseq, 0, &rtx, &[0; 3])?,
+            timeout(),
+            GFP_KERNEL,
+        )?;
+        hseq += 1;
+
+        // (2) AKE_Send_Cert: payload = REPEATER(1) || cert_rx(522). Extract 
the
+        // RSA-1024 public key (modulus[5..133], exponent[133..136]).
+        let (cid, cert_msg) = Self::recv_hdcp(link)?;
+        const CERT_KEY_END: usize = 5 + drm_hdcp::RSA_MODULUS_LEN + 
drm_hdcp::RSA_EXPONENT_LEN;
+        if cid != id::AKE_SEND_CERT || cert_msg.len() < 1 + CERT_KEY_END {
+            pr_err!(
+                "vino: AKE: bad AKE_Send_Cert (id={cid:#x}, {} B)\n",
+                cert_msg.len()
+            );
+            return Err(EINVAL);
+        }
+        let repeater = cert_msg[0] != 0;
+        let cert = &cert_msg[1..];
+        let mut modulus = [0u8; drm_hdcp::RSA_MODULUS_LEN];
+        modulus.copy_from_slice(&cert[5..5 + drm_hdcp::RSA_MODULUS_LEN]);
+        let mut exponent = [0u8; drm_hdcp::RSA_EXPONENT_LEN];
+        exponent.copy_from_slice(&cert[5 + 
drm_hdcp::RSA_MODULUS_LEN..CERT_KEY_END]);
+
+        // (3) AKE_Transmitter_Info (ctr=3), then read AKE_Receiver_Info 
(RxCaps unused).
+        link.ctrl_send(&ake::ake_transmitter_info(hseq, 0)?, timeout(), 
GFP_KERNEL)?;
+        hseq += 1;
+        let _ = Self::recv_hdcp(link)?;
+
+        // (5) AKE_No_Stored_km -- fresh km, RSA-OAEP-SHA256 to Ekpub(km).
+        let mut km = kernel::crypto::Secret::<{ 
drm_hdcp::ENCRYPTED_SESSION_KEY_LEN }>::zeroed();
+        rng::fill(&mut km[..]);
+        let mut rsa = kernel::crypto::akcipher::RsaPublicKey::new(&modulus, 
&exponent, GFP_KERNEL)?;
+        let ekpub = hdcp::oaep_encrypt_km(&mut rsa, &km)?;
+        // (4) AKE_No_Stored_km (ctr=4). The dock authenticates its downstream 
link before it
+        // answers, so the following receive naturally covers that interval.
+        link.ctrl_send(
+            &ake::ake_no_stored_km(hseq, 0, &ekpub)?,
+            timeout(),
+            GFP_KERNEL,
+        )?;
+        hseq += 1;
+
+        // (6) AKE_Send_Rrx.
+        let (rid, rrx_pl) = Self::recv_hdcp(link)?;
+        if rid != id::AKE_SEND_RRX || rrx_pl.len() < drm_hdcp::RRX_LEN {
+            pr_err!("vino: AKE: bad AKE_Send_Rrx (id={rid:#x})\n");
+            return Err(EINVAL);
+        }
+        let mut rrx = [0u8; drm_hdcp::RRX_LEN];
+        rrx.copy_from_slice(&rrx_pl[..drm_hdcp::RRX_LEN]);
+
+        // (7)/(8) AKE_Send_H_prime -- verify H' = HMAC(kd, rtx^REPEATER).
+        let (hid, hp) = Self::recv_hdcp(link)?;
+        if hid != id::AKE_SEND_H_PRIME || hp.len() < drm_hdcp::H_PRIME_LEN {
+            pr_err!("vino: AKE: bad H' (id={hid:#x})\n");
+            return Err(EINVAL);
+        }
+        let kd = hdcp::derive_kd(&km, &rtx, &rrx)?;
+        if hdcp::compute_h(&kd, &rtx, repeater)[..] != 
hp[..drm_hdcp::H_PRIME_LEN] {
+            pr_err!("vino: AKE: H' mismatch -- authentication failed\n");
+            return Err(EINVAL);
+        }
+        vino_debug!("vino: AKE: H' verified\n");
+
+        // (9) AKE_Send_Pairing_Info (Ekh_km) -- read and discard (no-stored 
path).
+        let _ = Self::recv_hdcp(link)?;
+
+        // (10) Locality Check -- LC_Init(rn) then verify L'.
+        let mut rn = [0u8; drm_hdcp::RN_LEN];
+        rng::fill(&mut rn);
+        // (5) LC_Init (ctr=5).
+        link.ctrl_send(&ake::lc_init(hseq, 0, &rn)?, timeout(), GFP_KERNEL)?;
+        hseq += 1;
+        let (lid, lp) = Self::recv_hdcp(link)?;
+        if lid != id::LC_SEND_L_PRIME || lp.len() < drm_hdcp::L_PRIME_LEN {
+            pr_err!("vino: AKE: bad L' (id={lid:#x})\n");
+            return Err(EINVAL);
+        }
+        if hdcp::compute_l(&kd, &rrx, &rn)[..] != lp[..drm_hdcp::L_PRIME_LEN] {
+            pr_err!("vino: AKE: L' mismatch -- locality check failed\n");
+            return Err(EINVAL);
+        }
+        vino_debug!("vino: AKE: L' verified\n");
+
+        // (11) Session Key Exchange -- send Edkey(ske_ks) and the fresh RIV. 
The wrapped value is
+        // the raw SKE key; both peers apply the control-plane whitening 
constant afterwards.
+        let mut ske_ks =
+            kernel::crypto::Secret::<{ drm_hdcp::ENCRYPTED_SESSION_KEY_LEN 
}>::zeroed();
+        let mut riv = [0u8; drm_hdcp::RIV_LEN];
+        rng::fill(&mut ske_ks[..]);
+        rng::fill(&mut riv);
+        let edkey = hdcp::compute_eks(&km, &rtx, &rrx, &rn, &ske_ks)?;
+        let ks = cp::cp_session_key(&ske_ks);
+        // SKE carries the full RIV. Control AES-CTR toggles byte 7 bit 2;
+        // Dl3Cmac separately transforms byte 0 bit 7.
+        let riv_ske = riv; // deliver the full random RIV before the control 
transform
+        riv[7] ^= 0x04; // OUT CP AES-CTR nonce = delivered ^0x04@byte7 (byte0 
UNCHANGED)
+                        // (6) SKE_Send_Eks (ctr=6).
+        link.ctrl_send(
+            &ake::ske_send_eks(hseq, 0, &edkey, &riv_ske)?,
+            timeout(),
+            GFP_KERNEL,
+        )?;
+        hseq += 1;
+        // (12) RepeaterAuth -- verify V' over the ReceiverID_List, ACK, then 
SM2. Retained (empty
+        // on the non-repeater path) so `send_cp_setup`'s per-connector 
restatement can recompute a
+        // fresh per-connector `V = HMAC(kd_h, rxid_list)` over the same list 
the dock sent.
+        let mut rxid_list: KVec<u8> = KVec::new();
+        if repeater {
+            let (vid, list) = Self::recv_hdcp(link)?;
+            if vid != id::REPEATERAUTH_SEND_RECEIVERID_LIST || list.len() < 16 
{
+                pr_err!("vino: AKE: bad ReceiverID_List (id={vid:#x})\n");
+                return Err(EINVAL);
+            }
+            let split = list.len() - 16;
+            // V' is the first 128 bits; RepeaterAuth_Send_Ack carries the 
second 128 bits.
+            let v_full = hdcp::compute_v_full(&kd, &list[..split]);
+            let mut v_ack = [0u8; 16];
+            v_ack.copy_from_slice(&v_full[16..]);
+            if v_full[..16] != list[split..] {
+                pr_err!("vino: AKE: V' mismatch -- repeater verification 
failed\n");
+                return Err(EINVAL);
+            }
+            vino_debug!("vino: AKE: V' verified\n");
+            rxid_list.extend_from_slice(&list[..split], GFP_KERNEL)?;
+            // (7) RepeaterAuth_Send_Ack (ctr=7).
+            link.ctrl_send(
+                &ake::repeater_auth_send_ack(hseq, 0, &v_ack)?,
+                timeout(),
+                GFP_KERNEL,
+            )?;
+            // Preserve repeater-authentication request/reply lockstep.
+            Self::pace_cap_ack(link, hseq as u16, &mut saw_cap_complete);
+            hseq += 1;
+            // (8) RepeaterAuth_Stream_Manage (ctr=8).
+            link.ctrl_send(
+                &ake::repeater_auth_stream_manage(hseq, 0)?,
+                timeout(),
+                GFP_KERNEL,
+            )?;
+            Self::pace_cap_ack(link, hseq as u16, &mut saw_cap_complete);
+            hseq += 1;
+            // Drain capability-complete and Stream_Ready before arming the 
control plane.
+            Self::wait_cap_complete(link, &kd, saw_cap_complete);
+        }
+
+        // `hseq` points past the last capability/AKE frame; `send_cp_setup` 
continues the inner
+        // counter from here for msg0.
+        if trace_crypto_enabled() {
+            pr_info!(
+                "vino-crypto: control key={:02x?} riv_out={riv:02x?} 
next_ctr={}\n",
+                &ks[..],
+                hseq
+            );
+        }
+        Ok(Session {
+            ks,
+            riv,
+            next_ctr: hseq as u16,
+            rsa,
+            rxid_list,
+        })
+    }
+
+    /// Submit one encrypted control-plane frame without changing protocol 
counters on failure. Name
+    /// a connector's content stream, on a dock where that is a control-plane 
record.
+    ///
+    /// A dock whose video shares the control pipe has no video endpoint to 
open a stream on later,
+    /// so every connector it may ever drive has to be named during setup -- 
including one whose
+    /// downstream authentication did not run, because a monitor plugged in 
afterwards comes up on
+    /// exactly that connector and would then be driven on a stream the dock 
was never told about.
+    /// The vendor names both connectors unconditionally. Docks with a video 
pipe of their own carry
+    /// this ahead of the first frame instead, so they get nothing here.
+    fn announce_stream(
+        link: &UsbLink<'_>,
+        out_q: &mut Option<usb::BulkOutQueue>,
+        profile: &DockProfile,
+        connector: usize,
+    ) -> Result {
+        if !profile.topology.video_on_ctrl_pipe {
+            return Ok(());
+        }
+        let stream_id = profile.geometry().stream_id(connector as u8);
+        Self::submit_cp_frame(
+            link,
+            out_q,
+            &cp::stream_announce(stream_id, cp::STREAM_ANNOUNCE_MARKER),
+        )
+    }
+
+    fn submit_cp_frame(
+        link: &UsbLink<'_>,
+        out_q: &mut Option<usb::BulkOutQueue>,
+        frame: &[u8],
+    ) -> Result {
+        match out_q {
+            // The queued path, which is the one both docks actually use. An 
error here is what
+            // surfaces as `control session failed after N attempts 
(ETIMEDOUT)`; the 40-retry NAK
+            // loop further down is the *unqueued* fallback and does not run, 
which is why
+            // instrumenting it said nothing.
+            Some(queue) => queue.send(link.io(), frame, 
timeout()).inspect_err(|e| {
+                pr_err!(
+                    "vino: EP02 queued submit of {} B failed ({e:?})\n",
+                    frame.len()
+                );
+            }),
+            None => link.ctrl_send(frame, timeout(), GFP_KERNEL).map(|_| ()),
+        }
+    }
+
+    /// Seal and send one live interactive control message.
+    ///
+    /// The OUT is one long-lived URB, then EP84 is drained once after its 
completion. The returned
+    /// tally distinguishes verified acknowledgments from rejected ciphertext.
+    ///
+    /// Do not implement a short-timeout retry here. A USB bulk timeout does 
not prove that no
+    /// bytes reached the device: Navarro accepted several complete 64-byte 
frames just before
+    /// Vino cancelled their 5 ms URBs. Re-submitting the same sealed frame 
consequently put
+    /// duplicate inner counters on the wire (measured at counters 72, 75 and 
95), while DLM sends
+    /// each transaction once. Leaving one URB outstanding lets xHCI retry 
NRDY packets without
+    /// replaying an already accepted application message.
+    fn send_live_cp(
+        link: &UsbLink<'_>,
+        session: &Session,
+        mut q: Option<&mut usb::BulkInQueue>,
+        resp: &mut [u8],
+        edid_out: &mut Option<KVec<u8>>,
+        id: u16,
+        wire_seq: u32,
+        content: &[u8],
+    ) -> Result<Ep84Drain> {
+        let frame = cp::seal_interactive(&session.ks, &session.riv, id, 
wire_seq, content)?;
+
+        let mut tally = Ep84Drain::default();
+        link.ctrl_send(&frame, timeout(), GFP_KERNEL)?;
+        // Collect the dock's reply, including a possible get-EDID id=0x194 
frame.
+        tally.add(Self::drain_ep84(
+            link,
+            q.as_deref_mut(),
+            resp,
+            session,
+            edid_out,
+            Delta::from_millis(10),
+        ));
+        Ok(tally)
+    }
+}
diff --git a/drivers/gpu/drm/vino/session/replies.rs 
b/drivers/gpu/drm/vino/session/replies.rs
new file mode 100644
index 000000000000..b86380d7327c
--- /dev/null
+++ b/drivers/gpu/drm/vino/session/replies.rs
@@ -0,0 +1,337 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Reading the dock's interrupt endpoint.
+//!
+//! The dock answers on EP84 and also pushes unsolicited messages there. A 
reply left undrained
+//! desynchronises the control plane, so the read paths here are what keep the 
session in lockstep
+//! rather than merely convenient.
+
+use super::*;
+
+impl VinoDriver {
+    /// Log one EP84 wire header and its decoded inner header when available.
+    fn log_ep84(session: &Session, frame: &[u8]) {
+        let len = frame.len();
+        let wtype = if len >= 8 {
+            u32::from_le_bytes([frame[4], frame[5], frame[6], frame[7]])
+        } else {
+            0
+        };
+        let wsub = if len >= 10 {
+            u16::from_le_bytes([frame[8], frame[9]])
+        } else {
+            0
+        };
+        let aux = if len >= 12 {
+            u16::from_le_bytes([frame[10], frame[11]])
+        } else {
+            0
+        };
+        let wseq = if len >= 16 {
+            u32::from_le_bytes([frame[12], frame[13], frame[14], frame[15]])
+        } else {
+            0
+        };
+        {
+            // Bound dynamic-debug output and split large frames below 
printk's line limit.
+            let cap = len.min(768);
+            if cap <= 64 {
+                let raw = &frame[..cap];
+                vino_debug!("vino: dock EP84 RAW {len}B {raw:02x?}\n");
+            } else {
+                vino_debug!("vino: dock EP84 RAW {len}B (first {cap} B in 
128-B chunks):\n");
+                let mut o = 0usize;
+                while o < cap {
+                    let e = (o + 128).min(cap);
+                    let chunk = &frame[o..e];
+                    vino_debug!("vino:   ep84[{o:#06x}] {chunk:02x?}\n");
+                    o = e;
+                }
+            }
+        }
+        match cp::decode_any(&session.ks, &session.riv, frame) {
+            Some((rivtag, rid, rsub, rictr, _)) => {
+                vino_debug!("vino: EP84 {rivtag} id={rid:#x} sub={rsub:#x} 
ctr={rictr:#x}\n");
+            }
+            None => vino_debug!(
+                "vino: EP84 type={wtype} sub={wsub:#x} aux={aux:#x} 
seq={wseq:#x} len={len}\n"
+            ),
+        }
+    }
+    /// Read one EP84 frame from the persistent queue or the synchronous 
fallback.
+    pub(super) fn read_ep84(
+        link: &UsbLink<'_>,
+        q: Option<&mut usb::BulkInQueue>,
+        buf: &mut [u8],
+        to: Delta,
+    ) -> Result<usize> {
+        match q {
+            Some(queue) => match queue.recv(link.io(), buf, to) {
+                Ok(Some(n)) => Ok(n),
+                Ok(None) => Err(ETIMEDOUT),
+                Err(e) => Err(e),
+            },
+            None => link.ctrl_recv(buf, to, GFP_KERNEL),
+        }
+    }
+    pub(super) fn drain_ep84(
+        link: &UsbLink<'_>,
+        mut q: Option<&mut usb::BulkInQueue>,
+        buf: &mut [u8],
+        session: &Session,
+        edid_out: &mut Option<KVec<u8>>,
+        // The first read may cover an HDCP computation interval; subsequent 
reads only drain a
+        // contiguous reply burst.
+        first_wait: Delta,
+    ) -> Ep84Drain {
+        const MAX_READS: usize = 16;
+        let mut out = Ep84Drain::default();
+        // Read EP84 before doing any unrelated work: the control plane is 
lockstep.
+        for i in 0..MAX_READS {
+            let wait = if i == 0 {
+                first_wait
+            } else {
+                Delta::from_millis(10)
+            };
+            match Self::read_ep84(link, q.as_deref_mut(), buf, wait) {
+                Ok(len) if len > 0 => {
+                    out.reads += 1;
+                    Self::log_ep84(session, &buf[..len]);
+                    // Decode every downstream-HDCP milestone, not just Rrx.  
A generic encrypted
+                    // envelope acknowledgment does not mean the downstream 
authentication has
+                    // advanced; L', V' and M' are the state-machine gates.
+                    if let Some(push) =
+                        cp::per_connector_hdcp_push(&session.ks, &session.riv, 
&buf[..len])
+                    {
+                        out.observe_perhead(push);
+                    }
+                    if len >= 10 && u16::from_le_bytes([buf[8], buf[9]]) == 
0x45 {
+                        // The 0x45 wire tag is shared by status traffic. Only 
a valid decrypted
+                        // inner header proves that this session's cipher is 
engaged.
+                        match cp::verify_in_ack(&session.ks, &session.riv, 
&buf[..len]) {
+                            Some((id, sub, ctr)) => {
+                                out.acks += 1;
+                                vino_debug!(
+                                    "vino: CP acknowledgment id={id:#x} 
sub={sub:#x} ctr={ctr}\n"
+                                );
+                                // A display-capability reply identifies a
+                                // present monitor and echoes the request 
counter.
+                                if cp::is_display_cap_reply(id, sub) {
+                                    out.display_cap_ctr = Some(ctr);
+                                }
+                                // Capture the first `id=0x194 sub=0x21` EDID
+                                // reply for the standard DRM mode helpers.
+                                if edid_out.is_none() {
+                                    if let Ok(Some(e)) = 
cp::parse_edid_from_reply(
+                                        &session.ks,
+                                        &session.riv,
+                                        &buf[..len],
+                                    ) {
+                                        vino_debug!(
+                                            "vino: EDID read from dock ({} 
bytes)\n",
+                                            e.len()
+                                        );
+                                        *edid_out = Some(e);
+                                    }
+                                }
+                                // Track the downstream-DDC readiness bit so
+                                // the EDID loop can distinguish pending work.
+                                if let Some(true) =
+                                    cp::edid_poll_ready(&session.ks, 
&session.riv, &buf[..len])
+                                {
+                                    out.edid_ready = true;
+                                }
+                            }
+                            None => {
+                                match cp::decode_in_lenient(&session.ks, 
&session.riv, &buf[..len])
+                                {
+                                    // A structurally valid header with an 
uncatalogued sub-id still
+                                    // proves possession of the session key.
+                                    Some((id, sub, ctr)) => {
+                                        out.acks += 1;
+                                        vino_debug!(
+                                            "vino: CP reply id={id:#x} 
sub={sub:#x} ctr={ctr}\n"
+                                        );
+                                        if sub == 0x000c {
+                                            if let Some(inner) = 
cp::inner_plaintext(
+                                                &session.ks,
+                                                &session.riv,
+                                                &buf[..len],
+                                            ) {
+                                                if let Some(line) = 
cp::dock_trace_line(&inner) {
+                                                    vino_debug!(
+                                                        "vino: dock: {}\n",
+                                                        
core::str::from_utf8(&line).unwrap_or("?")
+                                                    );
+                                                }
+                                            }
+                                        }
+                                    }
+                                    // Not a sealed reply. Navarro pushes 
plaintext-framed
+                                    // messages the dock originates rather 
than answers, so a
+                                    // frame vino cannot open is only a 
rejection once that
+                                    // framing has been ruled out too.
+                                    None => match cp::inner_plaintext(
+                                        &session.ks,
+                                        &session.riv,
+                                        &buf[..len],
+                                    ) {
+                                        Some(inner) => {
+                                            out.acks += 1;
+                                            if let Some(line) = 
cp::dock_trace_line(&inner) {
+                                                vino_debug!(
+                                                    "vino: dock: {}\n",
+                                                    
core::str::from_utf8(&line).unwrap_or("?")
+                                                );
+                                            }
+                                        }
+                                        None => {
+                                            out.rejects += 1;
+                                            pr_warn!("vino: invalid encrypted 
CP reply\n");
+                                        }
+                                    },
+                                }
+                            }
+                        }
+                    }
+                }
+                _ => break,
+            }
+        }
+        out
+    }
+    /// Read through the downstream-HDCP reply stream until one specific 
protocol milestone.
+    ///
+    /// Navarro acknowledges every encrypted envelope before it emits the 
corresponding HDCP
+    /// result.  Advancing on the generic ACK alone allowed Vino to send 
SKE/V-ACK/Stream-Manage
+    /// without ever receiving L', V' or M'.  The dock accepted the envelopes 
but never enabled
+    /// the video consumer.  This routine makes the HDCP result, rather than 
the wrapper ACK, the
+    /// sequencing condition.
+    pub(super) fn wait_per_connector_push(
+        link: &UsbLink<'_>,
+        mut q: Option<&mut usb::BulkInQueue>,
+        buf: &mut [u8],
+        session: &Session,
+        want_msg_id: u8,
+        wait: Delta,
+    ) -> Ep84Drain {
+        const MAX_READS: usize = 16;
+        let mut out = Ep84Drain::default();
+        for _ in 0..MAX_READS {
+            match Self::read_ep84(link, q.as_deref_mut(), buf, wait) {
+                Ok(len) if len > 16 => {
+                    out.reads += 1;
+                    Self::log_ep84(session, &buf[..len]);
+                    if let Some(push) =
+                        cp::per_connector_hdcp_push(&session.ks, &session.riv, 
&buf[..len])
+                    {
+                        out.observe_perhead(push);
+                        if out.saw_perhead(want_msg_id) {
+                            return out;
+                        }
+                        continue;
+                    }
+                    if u16::from_le_bytes([buf[8], buf[9]]) != 0x45 {
+                        continue;
+                    }
+                    match cp::verify_in_ack(&session.ks, &session.riv, 
&buf[..len]) {
+                        Some((id, sub, ctr)) => {
+                            out.acks += 1;
+                            vino_debug!(
+                                "vino: CP acknowledgment while waiting for 
HDCP {want_msg_id:#x}: id={id:#x} sub={sub:#x} ctr={ctr}\n"
+                            );
+                        }
+                        None if cp::decode_in_lenient(&session.ks, 
&session.riv, &buf[..len])
+                            .is_some() =>
+                        {
+                            out.acks += 1;
+                        }
+                        None => {
+                            out.rejects += 1;
+                            pr_warn!("vino: invalid encrypted CP reply\n");
+                        }
+                    }
+                }
+                Ok(_) => continue,
+                Err(_) => break,
+            }
+        }
+        out
+    }
+    /// Drain replies until a verified inner counter echoes the submitted 
request.
+    ///
+    /// Asynchronous pushes are processed while waiting, and the operation 
remains bounded.
+    pub(super) fn lockstep_reply(
+        link: &UsbLink<'_>,
+        mut q: Option<&mut usb::BulkInQueue>,
+        buf: &mut [u8],
+        session: &Session,
+        ictr: u16,
+        edid_out: &mut Option<KVec<u8>>,
+    ) -> Ep84Drain {
+        const MAX_READS: usize = 8;
+        let mut out = Ep84Drain::default();
+        for _ in 0..MAX_READS {
+            match Self::read_ep84(link, q.as_deref_mut(), buf, 
Delta::from_millis(30)) {
+                Ok(len) if len > 16 => {
+                    out.reads += 1;
+                    Self::log_ep84(session, &buf[..len]);
+                    if u16::from_le_bytes([buf[8], buf[9]]) != 0x45 {
+                        continue;
+                    }
+                    if let Some(push) =
+                        cp::per_connector_hdcp_push(&session.ks, &session.riv, 
&buf[..len])
+                    {
+                        out.observe_perhead(push);
+                    }
+                    match cp::verify_in_ack(&session.ks, &session.riv, 
&buf[..len]) {
+                        Some((id, sub, ctr)) => {
+                            out.acks += 1;
+                            let echo = if ctr == ictr {
+                                " (echoes our ictr)"
+                            } else {
+                                ""
+                            };
+                            vino_debug!("vino: CP reply id={id:#x} 
sub={sub:#x} ctr={ctr}{echo}\n");
+                            // Opportunistically extract an EDID from an 
id=0x194 reply.
+                            if edid_out.is_none() {
+                                if let Ok(Some(e)) = cp::parse_edid_from_reply(
+                                    &session.ks,
+                                    &session.riv,
+                                    &buf[..len],
+                                ) {
+                                    vino_debug!("vino: EDID read from dock ({} 
bytes)\n", e.len());
+                                    *edid_out = Some(e);
+                                }
+                            }
+                            // Stop early once the dock acknowledges the 
counter we sent.
+                            if ctr == ictr {
+                                break;
+                            }
+                        }
+                        None => match cp::decode_in_lenient(&session.ks, 
&session.riv, &buf[..len])
+                        {
+                            // Decrypts to a plausible CP header, just an 
unlisted `sub` -- a valid
+                            // ack (cipher engaged), not a rejection. See the 
drain_ep84 branch.
+                            Some((id, sub, ctr)) => {
+                                out.acks += 1;
+                                vino_debug!("vino: CP reply id={id:#x} 
sub={sub:#x} ctr={ctr}\n");
+                            }
+                            None => {
+                                out.rejects += 1;
+                                pr_warn!("vino: invalid encrypted CP reply\n");
+                            }
+                        },
+                    }
+                }
+                // A short, header-only frame (bare ack/keepalive, len <= 16): 
not a CP
+                // reply, but the dock is still talking -- keep reading for 
the 0x45 rather
+                // than dropping the rest of the lockstep window.
+                Ok(_) => continue,
+                // Read error / nothing queued within the window: the dock is 
idle, stop.
+                Err(_) => break,
+            }
+        }
+        out
+    }
+}
diff --git a/drivers/gpu/drm/vino/session/setup.rs 
b/drivers/gpu/drm/vino/session/setup.rs
new file mode 100644
index 000000000000..b278ebcf4b4e
--- /dev/null
+++ b/drivers/gpu/drm/vino/session/setup.rs
@@ -0,0 +1,1390 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! The post-authentication setup burst.
+//!
+//! Once the AKE completes the dock still knows nothing about how it is to be 
driven. This is the
+//! sequence that tells it: the dock-wide records, the per-connector HDCP 
restatement, the video
+//! engine transition and the connector-selecting records. Its ordering is 
measured from the
+//! vendor, and a message sent out of turn desynchronises the authenticated 
counter for the rest of
+//! the session.
+
+use super::*;
+
+impl VinoDriver {
+    /// Configure the encrypted control plane after SKE.
+    ///
+    /// The sequence contains the plaintext arm marker, the first encrypted 
message, initialization,
+    /// per-connector authentication and stream finalization. The returned 
counters continue the
+    /// live session, and `video_keys` receives the key and nonce established 
for each connector.
+    pub(crate) fn send_cp_setup(
+        link: &UsbLink<'_>,
+        profile: &DockProfile,
+        session: &mut Session,
+        // Scratch slot filled by reply drains and moved into the selected 
connector's EDID cache.
+        edid_out: &mut Option<KVec<u8>>,
+        edid_connectors: &mut [Option<KVec<u8>>; Self::CP_SETUP_CONNECTORS],
+        video_keys: &mut [kernel::crypto::Secret<32>; 
Self::CP_SETUP_CONNECTORS],
+        connectors_present: &mut [bool; Self::CP_SETUP_CONNECTORS],
+        discovery_deferred: &mut [bool; Self::CP_SETUP_CONNECTORS],
+        // Which connectors had their video stream opened in this burst, as a 
connector bitmask. A
+        // connector with no sink yet is skipped here and owes its open to 
whatever drives it later.
+        stream_opened: &mut u32,
+    ) -> Result<(usize, u32, u16)> {
+        let connector_count =
+            
usize::from(profile.topology.connectors).min(Self::CP_SETUP_CONNECTORS);
+        // 16 KiB so the dock's ~5787 B capability block is read whole (see 
[`EP84_BUF`]).
+        let mut resp = KVec::from_elem(0u8, EP84_BUF, GFP_KERNEL)?;
+        let mut drained = 0usize;
+        let mut acks = 0usize;
+        let mut rejects = 0usize;
+        let mut sent = 0usize;
+        // Match each display-capability response to the stream-open counter 
of its connector.
+        let mut stream_open_ctr: [Option<u16>; Self::CP_SETUP_CONNECTORS] =
+            [None; Self::CP_SETUP_CONNECTORS];
+
+        // Plaintext `type=2 sub=0x24`+`0x45` stream-open arm marker -- the 
mandatory gate
+        // before the first encrypted frame.
+        const STREAM_OPEN: [u8; 64] = [
+            0x00, 0x00, 0x1c, 0x00, 0x02, 0x00, 0x00, 0x00, // pad, size, type
+            0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // wsub=0x24, 
aux=0, seq=0
+            0x04, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, // payload
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // padding
+            0x00, 0x00, 0x1c, 0x00, 0x02, 0x00, 0x00, 0x00, // pad, size, type
+            0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // wsub=0x45, 
aux=0, seq=0
+            0x05, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, // payload
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // padding
+        ];
+
+        // Post the persistent EP84 reader before arming so asynchronous 
replies cannot fill the
+        // dock's IN FIFO while the host submits control traffic.
+        let ep84_depth = profile.protocol.ep84_queue_depth;
+        let mut ep84_q = match link.ctrl_in_queue(ep84_depth, EP84_BUF) {
+            Ok(q) => {
+                vino_debug!("vino: EP84 async IN queue opened 
(depth={ep84_depth})\n");
+                Some(q)
+            }
+            Err(e) => {
+                vino_debug!("vino: EP84 queue unavailable ({e:?}); using 
synchronous reads\n");
+                None
+            }
+        };
+
+        let mut out_q = match link.ctrl_out_queue(4, 1024) {
+            Ok(q) => {
+                vino_debug!("vino: EP02 async OUT queue opened (depth=4)\n");
+                Some(q)
+            }
+            Err(e) => {
+                vino_debug!("vino: EP02 async OUT queue open failed 
({e:?})\n");
+                None
+            }
+        };
+
+        // Submit and flush the arm before sealing the first encrypted message.
+        let arm_res = match out_q.as_mut() {
+            Some(q) => q
+                .send(link.io(), &STREAM_OPEN, timeout())
+                .and_then(|()| q.flush(link.io(), timeout())),
+            None => link
+                .ctrl_send(&STREAM_OPEN, timeout(), GFP_KERNEL)
+                .map(|_| ()),
+        };
+        arm_res?;
+        // The first live message continues the AKE inner counter and starts 
the encrypted wire
+        // block counter at zero. Every following message advances both 
counters from its true size.
+        let mut cp_ctr: u16 = session.next_ctr;
+        let mut wseq: u32 = 0;
+
+        let content = cp::session_hello(cp_ctr);
+        let body_len = content.len() + 16; // AES-CTR ciphertext + 16-byte 
Dl3Cmac
+        let size = ((16 + body_len) - 4) as u16;
+        let aux = cp::aux_for_id(0x14, body_len);
+        let mut hdr = [0u8; 16];
+        hdr[2..4].copy_from_slice(&size.to_le_bytes());
+        hdr[4..8].copy_from_slice(&4u32.to_le_bytes()); // type=4
+        hdr[8..10].copy_from_slice(&0x24u16.to_le_bytes()); // sub=0x24 
(interactive CP)
+        hdr[10..12].copy_from_slice(&aux.to_le_bytes());
+        // Running AES-CTR block index, initially zero.
+        hdr[12..16].copy_from_slice(&wseq.to_le_bytes());
+        let frame = cp::seal_livemac(&session.ks, &session.riv, &hdr, 
&content)?;
+
+        match out_q.as_mut() {
+            Some(q) => {
+                q.send(link.io(), &frame, timeout())?;
+                q.flush(link.io(), timeout())?;
+                if profile.protocol.reply_discipline == 
profile::ReplyDiscipline::Lockstep {
+                    // DLM advances as soon as the dock authenticates msg0. 
The old eight-drain
+                    // loop waited for seven empty 10-ms windows after that 
reply and moved every
+                    // following setup transition roughly 90 ms later on the 
wire.
+                    let d = Self::lockstep_reply(
+                        link,
+                        ep84_q.as_mut(),
+                        &mut resp,
+                        session,
+                        cp_ctr,
+                        edid_out,
+                    );
+                    drained += d.reads;
+                    acks += d.acks;
+                    rejects += d.rejects;
+                } else {
+                    for _ in 0..8 {
+                        let d = Self::drain_ep84(
+                            link,
+                            ep84_q.as_mut(),
+                            &mut resp,
+                            session,
+                            edid_out,
+                            Delta::from_millis(10),
+                        );
+                        drained += d.reads;
+                        acks += d.acks;
+                        rejects += d.rejects;
+                    }
+                }
+            }
+            None => {
+                // A NAK transfers no bytes, so cancel and retry are safe.
+                // Between attempts drain EP84 so the dock can push/drain its 
IN queue. Bounded.
+                const TRIES: usize = 40;
+                let mut last_err = ETIMEDOUT;
+                let mut accepted = false;
+                // Name what the dock is refusing. This path returns `control 
session failed ...
+                // (ETIMEDOUT)`, which is otherwise indistinguishable from a 
reply that never
+                // arrived; the distinction matters because this is the send 
side, where the dock
+                // has stopped taking EP02 writes at all.
+                let mut nak_reported = false;
+                for _ in 0..TRIES {
+                    match link.ctrl_send(&frame, Delta::from_millis(5), 
GFP_KERNEL) {
+                        Ok(_) => {
+                            accepted = true;
+                            break;
+                        }
+                        // OUT NAK'd (nothing transferred) -- let the dock 
push on EP84, then retry.
+                        Err(e) => {
+                            last_err = e;
+                            if !nak_reported {
+                                nak_reported = true;
+                                vino_debug!(
+                                    "vino: EP02 NAKed a {} B control frame 
(cp_ctr={cp_ctr}, wseq={wseq}); retrying up to {TRIES}x\n",
+                                    frame.len()
+                                );
+                            }
+                            let d = Self::drain_ep84(
+                                link,
+                                ep84_q.as_mut(),
+                                &mut resp,
+                                session,
+                                edid_out,
+                                Delta::from_millis(10),
+                            );
+                            drained += d.reads;
+                            acks += d.acks;
+                            rejects += d.rejects;
+                        }
+                    }
+                }
+                if !accepted {
+                    pr_err!(
+                        "vino: EP02 refused {TRIES} submissions of a {} B 
frame (cp_ctr={cp_ctr}, wseq={wseq}) -- giving up\n",
+                        frame.len()
+                    );
+                    return Err(last_err);
+                }
+            }
+        }
+        sent += 1;
+        cp_ctr += 1; // past msg0
+        wseq += 2; // msg0 content is 32 B = 2 AES blocks
+
+        // Initialization continues with two dock-wide records and one 
`0x16/0x2a` record for
+        // every physical connector. The authenticated Navarro transcript 
carries selectors
+        // 0,1,2,3 here; emitting Ridge's historical pair left all later 
counters four AES blocks
+        // behind DLM and never initialized Navarro's last two connector slots.
+        macro_rules! send_init {
+            ($id:expr, $sub:expr, $fixed_prefix:expr) => {{
+                let id: u16 = $id;
+                let sub: u16 = $sub;
+                let fixed_prefix: &[u8] = $fixed_prefix;
+                let mut c = [0u8; 32];
+                c[0..2].copy_from_slice(&id.to_le_bytes());
+                c[2..4].copy_from_slice(&sub.to_le_bytes());
+                c[4..6].copy_from_slice(&cp_ctr.to_le_bytes());
+                rng::fill(&mut c[22..32]);
+                c[22..22 + fixed_prefix.len()].copy_from_slice(fixed_prefix);
+                let frame = cp::seal_interactive(&session.ks, &session.riv, 
id, wseq, &c)?;
+                Self::submit_cp_frame(link, &mut out_q, &frame)?;
+                sent += 1;
+                // Navarro's reference transaction is reply-lockstep: the next 
operation follows
+                // the matching authenticated counter immediately. A generic 
burst drain adds an
+                // empty 10-ms read after every acknowledgment and changes the 
EP0/EP02 ordering.
+                let d = if profile.protocol.reply_discipline == 
profile::ReplyDiscipline::Lockstep {
+                    Self::lockstep_reply(
+                        link,
+                        ep84_q.as_mut(),
+                        &mut resp,
+                        session,
+                        cp_ctr,
+                        edid_out,
+                    )
+                } else {
+                    Self::drain_ep84(
+                        link,
+                        ep84_q.as_mut(),
+                        &mut resp,
+                        session,
+                        edid_out,
+                        Delta::from_millis(10),
+                    )
+                };
+                drained += d.reads;
+                acks += d.acks;
+                rejects += d.rejects;
+                cp_ctr += 1;
+                wseq += 2; // every initialization message is 32 B = 2 AES 
blocks
+            }};
+        }
+        // See `DockProfile::dock_wide_init`: a dock that does not expect 
these is left every
+        // later inner counter and AES block out of step by them.
+        if profile.protocol.dock_wide_init {
+            send_init!(0x0014, 0x0030, &[]);
+            send_init!(0x0015, 0x000b, &[0x01]);
+        }
+
+        if profile.protocol.video_commit_point == 
profile::VideoCommitPoint::BeforeConnectorRecords
+        {
+            // The working DLM transaction places the video-engine transition 
at this exact
+            // authenticated boundary: after the reply to 0x15/0x0b (counter 
11), before the four
+            // connector-selecting 0x16/0x2a records (counters 12..15). 
Measured submit times are
+            // EP08 clear, +12.647 ms EP0a clear, +143 us vendor commit, 
+2.941 ms first 0x16/0x2a.
+            // Performing the same requests after finalization moved them 53 
messages later.
+            link.clear_video_halt_wire(0)?;
+            fsleep(Delta::from_millis(13));
+            link.clear_video_halt_wire(1)?;
+            link.control_send(
+                0x24,
+                0x40, /* VENDOR_OUT */
+                0,
+                0,
+                &[],
+                timeout(),
+                GFP_KERNEL,
+            )?;
+            let mut state2 = [0u8; 28];
+            link.control_recv(0x22, 0xc1, 1, 0, &mut state2, timeout(), 
GFP_KERNEL)?;
+            fsleep(Delta::from_millis(3));
+        }
+        if profile.protocol.dock_wide_init {
+            for connector in 0..connector_count {
+                let prefix = [connector as u8, 0x01];
+                send_init!(0x0016, 0x002a, &prefix);
+            }
+        }
+
+        // Drain pending replies before starting the per-connector 
authentication blocks. Each block
+        // mirrors the HDCP AKE layout and ends by opening that connector's 
stream.
+        {
+            let d = Self::drain_ep84(
+                link,
+                ep84_q.as_mut(),
+                &mut resp,
+                session,
+                edid_out,
+                Delta::from_millis(10),
+            );
+            drained += d.reads;
+            acks += d.acks;
+            rejects += d.rejects;
+            if let Some(c) = d.display_cap_ctr {
+                for h in 0..Self::CP_SETUP_CONNECTORS {
+                    if stream_open_ctr[h] == Some(c) {
+                        connectors_present[h] = true;
+                    }
+                }
+            }
+        }
+        // Which connectors completed their downstream authentication. A 
connector with nothing
+        // plugged into it never runs one, so this is not expected to be all 
of them.
+        let mut connector_ok = [false; Self::CP_SETUP_CONNECTORS];
+        let mut heads_authenticated = 0usize;
+        'per_head: for connector in 0..connector_count {
+            // The socket number printed on the dock's case; the wire counts 
connectors from zero.
+            let socket = connector + 1;
+            // Derive an independent HDCP 2.2 authentication chain for this 
downstream connector.
+            let mut rtx_h = [0u8; drm_hdcp::RTX_LEN];
+            rng::fill(&mut rtx_h);
+            let mut km_h =
+                kernel::crypto::Secret::<{ drm_hdcp::ENCRYPTED_SESSION_KEY_LEN 
}>::zeroed();
+            rng::fill(&mut km_h[..]);
+            let mut rn_h = [0u8; drm_hdcp::RN_LEN];
+            rng::fill(&mut rn_h);
+            let mut ske_ks_h =
+                kernel::crypto::Secret::<{ drm_hdcp::ENCRYPTED_SESSION_KEY_LEN 
}>::zeroed();
+            rng::fill(&mut ske_ks_h[..]);
+            let mut riv_h = [0u8; drm_hdcp::RIV_LEN];
+            rng::fill(&mut riv_h);
+            let ekpub_h = hdcp::oaep_encrypt_km(&mut session.rsa, &km_h)?;
+            let mut kd_h: Option<kernel::crypto::Secret<32>> = None;
+            let mut edkey_h = None;
+            let mut v_h = None;
+            let mut fresh_rrx: Option<[u8; drm_hdcp::RRX_LEN]> = None;
+            let mut per_connector_repeater: Option<bool> = None;
+            let mut rrx_applied = false;
+            // SKE_Send_Eks establishes this connector's video key. Store the 
whitened key and the
+            // video nonce derived from the delivered RIV for the scanout arm 
burst. Layout: key(16)
+            // || nonce(8) || pad(8).
+            let stream_id = profile.geometry().stream_id(connector as u8);
+            video_keys[connector] = kernel::crypto::Secret::zeroed();
+            // Whether the dock applies the control-plane whitening constant 
to a per-connector SKE
+            // key is proven only for the link stream; the per-connector rule 
is carried over from
+            // Ridge, and both docks accept the sealed records it produces.
+            let video_key = cp::cp_session_key(&ske_ks_h);
+            video_keys[connector][..16].copy_from_slice(&video_key[..]);
+            let vnonce = cp::stream_content_nonce(&riv_h, stream_id);
+            video_keys[connector][16..24].copy_from_slice(&vnonce);
+            for (i, (id, sub, content_len)) in 
cp::CP_SETUP_PER_HEAD.iter().copied().enumerate() {
+                // The per-connector `rrx` arrives with the response to 
AKE_No_Stored_km. It is
+                // mandatory for deriving this connector's kd and Edkey before 
the consuming
+                // messages. V is not computed until the connector's own 
ReceiverID_List/V' has been
+                // received and verified.
+                if i >= 3 && !rrx_applied {
+                    let Some(rrx_h) = fresh_rrx else {
+                        // No `rrx` means this connector never began a 
downstream authentication,
+                        // which is what an empty DisplayPort connector looks 
like -- DLM does not
+                        // run a per-connector burst for a connector with no 
sink either, as a
+                        // capture of it driving a monitorless dock shows: one 
AKE for the dock,
+                        // none per connector.
+                        //
+                        // Skip the connector rather than failing the device. 
Aborting here took the
+                        // whole dock down whenever a single connector was 
empty, so a two-connector
+                        // dock with one monitor never came up at all, and a 
dock with none was
+                        // unreachable even for EDID and hotplug.
+                        vino_debug!(
+                            "vino: socket {socket} has no downstream sink (no 
AKE_Send_Rrx); skipping its authentication\n"
+                        );
+                        Self::announce_stream(link, &mut out_q, profile, 
connector)?;
+                        continue 'per_head;
+                    };
+                    let kd = hdcp::derive_kd(&km_h, &rtx_h, &rrx_h)?;
+                    edkey_h = Some(hdcp::compute_eks(&km_h, &rtx_h, &rrx_h, 
&rn_h, &ske_ks_h)?);
+                    // Ridge retains the older reply-drain path and has only 
the dock-wide list
+                    // available here. Navarro replaces this after SKE with 
the verified list from
+                    // this exact connector.
+                    if !profile.protocol.per_connector_onehot {
+                        let vf = hdcp::compute_v_full(&kd, &session.rxid_list);
+                        let mut ack = [0u8; drm_hdcp::V_PRIME_HALF_LEN];
+                        ack.copy_from_slice(&vf[drm_hdcp::V_PRIME_HALF_LEN..]);
+                        v_h = Some(ack);
+                    }
+                    kd_h = Some(kd);
+                    rrx_applied = true;
+                }
+                // id=0x26 (Stream_Manage restatement) is fully decoded -- 
deterministic content,
+                // not the generic path below. See 
`cp::stream_manage_restatement`'s doc comment.
+                if id == 0x0026 {
+                    let content = cp::stream_manage_restatement(
+                        cp_ctr,
+                        connector as u8,
+                        stream_id,
+                        profile.protocol.per_connector_onehot,
+                    )?;
+                    let frame =
+                        cp::seal_interactive(&session.ks, &session.riv, id, 
wseq, &content)?;
+                    Self::submit_cp_frame(link, &mut out_q, &frame)?;
+                    sent += 1;
+                    let d = if profile.protocol.per_connector_onehot {
+                        Self::wait_per_connector_push(
+                            link,
+                            ep84_q.as_mut(),
+                            &mut resp,
+                            session,
+                            ake::id::REPEATERAUTH_STREAM_READY,
+                            Delta::from_millis(30),
+                        )
+                    } else {
+                        Self::drain_ep84(
+                            link,
+                            ep84_q.as_mut(),
+                            &mut resp,
+                            session,
+                            edid_out,
+                            Delta::from_millis(10),
+                        )
+                    };
+                    drained += d.reads;
+                    acks += d.acks;
+                    rejects += d.rejects;
+                    fresh_rrx = fresh_rrx.or(d.per_connector_rrx);
+                    if profile.protocol.per_connector_onehot && 
d.per_connector_mprime.is_none() {
+                        pr_err!(
+                            "vino: socket {socket} downstream HDCP never 
returned M'/Stream_Ready\n"
+                        );
+                        return Err(EPROTO);
+                    }
+                    cp_ctr += 1;
+                    wseq += ((content_len + 15) / 16) as u32;
+                    // The vendor names the connector's content stream here, 
between the HDCP
+                    // restatement and the stream-open control record. The 
position is part of the
+                    // sequence: carried ahead of the first frame instead, 
this dock stops
+                    // answering altogether.
+                    Self::announce_stream(link, &mut out_q, profile, 
connector)?;
+                    continue;
+                }
+                let mut c = KVec::from_elem(0u8, content_len, GFP_KERNEL)?;
+                // Shared header (id / sub=0x10 / inner counter), identical to 
the plaintext AKE
+                // body layout (`ake::body`). The buffer is already zeroed by 
`from_elem`.
+                c[0..2].copy_from_slice(&id.to_le_bytes());
+                c[2..4].copy_from_slice(&sub.to_le_bytes());
+                c[4..6].copy_from_slice(&cp_ctr.to_le_bytes());
+                // Per-connector AKE messages carry the platform-specific 
connector marker, HDCP
+                // message id at offset 27 and the standard HDCP payload at 
offset 28.
+                match i {
+                    // AKE restatements: connector marker @23, HDCP msg-id tag 
@27, HDCP field @28..
+                    0 | 1 | 2 | 3 | 4 | 5 => {
+                        cp::connector_marker(
+                            &mut c,
+                            connector as u8,
+                            profile.protocol.per_connector_onehot,
+                        );
+                        c[27] = match i {
+                            0 => 0x02, // AKE_Init (rtx)
+                            1 => 0x13, // AKE_Transmitter_Info
+                            2 => 0x04, // AKE_No_Stored_km (Ekpub)
+                            3 => 0x09, // LC_Init (rn)
+                            4 => 0x0b, // SKE_Send_Eks (edkey+riv)
+                            _ => 0x0f, // 5: RepeaterAuth_Send_Ack (V)
+                        };
+                        match i {
+                            0 => {
+                                // AKE_Init carries Rtx and a fresh 
proprietary suffix.
+                                c[28..36].copy_from_slice(&rtx_h);
+                                rng::fill(&mut c[36..48]);
+                            }
+                            1 => {
+                                c[28..33].copy_from_slice(&[0x00, 0x06, 0x02, 
0x00, 0x02]);
+                                rng::fill(&mut c[33..48]);
+                            }
+                            2 => {
+                                c[28..156].copy_from_slice(&ekpub_h);
+                                rng::fill(&mut c[156..160]);
+                            }
+                            3 => {
+                                c[28..36].copy_from_slice(&rn_h); // LC_Init: 
rn
+                                rng::fill(&mut c[36..48]);
+                            }
+                            4 => {
+                                let Some(ed) = edkey_h.as_ref() else {
+                                    return Err(EPROTO);
+                                };
+                                c[28..44].copy_from_slice(ed);
+                                c[44..52].copy_from_slice(&riv_h);
+                                rng::fill(&mut c[52..64]);
+                            }
+                            _ => {
+                                let Some(v) = v_h else {
+                                    return Err(EPROTO);
+                                };
+                                c[28..44].copy_from_slice(&v); // 
RepeaterAuth_Send_Ack: V
+                                rng::fill(&mut c[44..48]);
+                            }
+                        }
+                    }
+                    // Stream-open control: header + zero[8..22] + 10 
host-random bytes[22..32]; no
+                    // connector marker, no tag (confirmed genuinely fully 
random across both
+                    // connectors). Record this connector's request counter. 
The display-capability
+                    // reply echoes it only when this connector has a monitor.
+                    7 => {
+                        if connector < stream_open_ctr.len() {
+                            stream_open_ctr[connector] = Some(cp_ctr);
+                        }
+                        rng::fill(&mut c[22..]);
+                    }
+                    // strm2: connector index @22, then the `<marker> 
[connector*4] 04` triple
+                    // @24..27, then a fresh 5-byte host-random tail. The 
marker is a per-platform
+                    // constant, not a connector count: Ridge sends 0x06 and 
Navarro 0x0c for two
+                    // and four connectors, but DL-3x00 sends 0x10 with two.
+                    8 => {
+                        c[22] = connector as u8;
+                        c[24] = profile.protocol.strm2_marker;
+                        c[25] = (connector as u8) * 4;
+                        c[26] = 0x04;
+                        rng::fill(&mut c[27..]);
+                    }
+                    _ => {}
+                }
+                let send_at = Instant::<Monotonic>::now();
+                let frame = cp::seal_interactive(&session.ks, &session.riv, 
id, wseq, &c)?;
+                Self::submit_cp_frame(link, &mut out_q, &frame)?;
+                sent += 1;
+                let mut d = if profile.protocol.per_connector_onehot && i <= 5 
{
+                    let want = match i {
+                        0 => ake::id::AKE_SEND_CERT,
+                        1 => 0x14, // DisplayLink AKE_Receiver_Info
+                        2 => ake::id::AKE_SEND_RRX,
+                        3 => ake::id::LC_SEND_L_PRIME,
+                        4 => ake::id::REPEATERAUTH_SEND_RECEIVERID_LIST,
+                        _ => ake::id::RECEIVER_AUTH_STATUS,
+                    };
+                    Self::wait_per_connector_push(
+                        link,
+                        ep84_q.as_mut(),
+                        &mut resp,
+                        session,
+                        want,
+                        Delta::from_millis(30),
+                    )
+                } else if profile.protocol.per_connector_onehot {
+                    Self::lockstep_reply(
+                        link,
+                        ep84_q.as_mut(),
+                        &mut resp,
+                        session,
+                        cp_ctr,
+                        edid_out,
+                    )
+                } else {
+                    Self::drain_ep84(
+                        link,
+                        ep84_q.as_mut(),
+                        &mut resp,
+                        session,
+                        edid_out,
+                        Delta::from_millis(10),
+                    )
+                };
+                per_connector_repeater = 
per_connector_repeater.or(d.per_connector_repeater);
+                fresh_rrx = fresh_rrx.or(d.per_connector_rrx);
+
+                // AKE_No_Stored_km starts the receiver's H' calculation. Rrx 
is the immediate
+                // result; H' and pairing info are later milestones, and DLM 
sends no LC_Init
+                // (i == 3) until both have crossed EP84. Both platforms need 
the wait and the
+                // drain, or the connector authenticates on material that 
never arrived.
+                if i == 2 && !profile.protocol.per_connector_onehot {
+                    hold_until(send_at, HDCP_HPRIME_WAIT_US);
+                    // Wait for the rrx rather than sampling for it. A fixed 
drain window returns
+                    // whatever has already arrived, so a connector whose 
receiver answers a little
+                    // late reads as a connector with no sink and loses the 
whole rest of its burst
+                    // -- including the key exchange that gives the dock a key 
for its content
+                    // stream. How long a receiver may take to answer is a 
property of the dock,
+                    // so the bound comes from its profile.
+                    let dh = Self::wait_per_connector_push(
+                        link,
+                        ep84_q.as_mut(),
+                        &mut resp,
+                        session,
+                        ake::id::AKE_SEND_RRX,
+                        
Delta::from_millis(profile.protocol.perhead_rrx_wait_ms),
+                    );
+                    fresh_rrx = fresh_rrx.or(dh.per_connector_rrx);
+                    per_connector_repeater = 
per_connector_repeater.or(dh.per_connector_repeater);
+                    d.add(dh);
+                }
+                if i == 2 && profile.protocol.per_connector_onehot {
+                    let Some(rrx_h) = fresh_rrx else {
+                        cp_ctr += 1;
+                        wseq += ((content_len + 15) / 16) as u32;
+                        vino_debug!(
+                            "vino: socket {socket} has no downstream sink (no 
AKE_Send_Rrx); skipping its authentication\n"
+                        );
+                        Self::announce_stream(link, &mut out_q, profile, 
connector)?;
+                        continue 'per_head;
+                    };
+                    hold_until(send_at, HDCP_HPRIME_WAIT_US);
+                    let dh = Self::wait_per_connector_push(
+                        link,
+                        ep84_q.as_mut(),
+                        &mut resp,
+                        session,
+                        ake::id::AKE_SEND_H_PRIME,
+                        Delta::from_millis(50),
+                    );
+                    d.add(dh);
+                    let dp = Self::wait_per_connector_push(
+                        link,
+                        ep84_q.as_mut(),
+                        &mut resp,
+                        session,
+                        0x08, // AKE_Send_Pairing_Info
+                        Delta::from_millis(30),
+                    );
+                    d.add(dp);
+                    let Some(hprime) = d.per_connector_hprime else {
+                        pr_err!(
+                            "vino: socket {socket} downstream HDCP returned no 
H'\n",
+                            socket = connector + 1
+                        );
+                        return Err(EPROTO);
+                    };
+                    let kd = hdcp::derive_kd(&km_h, &rtx_h, &rrx_h)?;
+                    let want_h =
+                        hdcp::compute_h(&kd, &rtx_h, 
per_connector_repeater.unwrap_or(true));
+                    if want_h != hprime {
+                        pr_err!(
+                            "vino: socket {socket} downstream H' mismatch\n",
+                            socket = connector + 1
+                        );
+                        return Err(EPROTO);
+                    }
+                    edkey_h = Some(hdcp::compute_eks(&km_h, &rtx_h, &rrx_h, 
&rn_h, &ske_ks_h)?);
+                    kd_h = Some(kd);
+                    rrx_applied = true;
+                    vino_debug!(
+                        "vino: socket {socket} downstream H' verified\n",
+                        socket = connector + 1
+                    );
+                }
+
+                if i == 3 && profile.protocol.per_connector_onehot {
+                    let Some(lprime) = d.per_connector_lprime else {
+                        pr_err!(
+                            "vino: socket {socket} downstream HDCP returned no 
L'\n",
+                            socket = connector + 1
+                        );
+                        return Err(EPROTO);
+                    };
+                    let (Some(kd), Some(rrx_h)) = (kd_h.as_ref(), 
fresh_rrx.as_ref()) else {
+                        return Err(EPROTO);
+                    };
+                    if hdcp::compute_l(kd, rrx_h, &rn_h) != lprime {
+                        pr_err!(
+                            "vino: socket {socket} downstream L' mismatch\n",
+                            socket = connector + 1
+                        );
+                        return Err(EPROTO);
+                    }
+                    vino_debug!(
+                        "vino: socket {socket} downstream L' verified\n",
+                        socket = connector + 1
+                    );
+                }
+
+                if i == 4 && profile.protocol.per_connector_onehot {
+                    let Some((list_header, vprime)) = d.per_connector_v else {
+                        pr_err!(
+                            "vino: socket {socket} downstream HDCP returned no 
ReceiverID_List/V'\n"
+                        );
+                        return Err(EPROTO);
+                    };
+                    let Some(kd) = kd_h.as_ref() else {
+                        return Err(EPROTO);
+                    };
+                    let vf = hdcp::compute_v_full(kd, &list_header);
+                    if vf[..drm_hdcp::V_PRIME_HALF_LEN] != vprime {
+                        pr_err!(
+                            "vino: socket {socket} downstream V' mismatch\n",
+                            socket = connector + 1
+                        );
+                        return Err(EPROTO);
+                    }
+                    let mut ack = [0u8; drm_hdcp::V_PRIME_HALF_LEN];
+                    ack.copy_from_slice(&vf[drm_hdcp::V_PRIME_HALF_LEN..]);
+                    v_h = Some(ack);
+                    if session.rxid_list.as_slice() != list_header {
+                        vino_debug!(
+                            "vino: socket {socket} ReceiverID list differs 
from dock-wide list\n",
+                            socket = connector + 1
+                        );
+                    }
+                    vino_debug!(
+                        "vino: socket {socket} downstream V' verified\n",
+                        socket = connector + 1
+                    );
+                }
+
+                if i == 5 && profile.protocol.per_connector_onehot {
+                    let Some(status) = d.per_connector_auth_status else {
+                        pr_err!(
+                            "vino: socket {socket} downstream HDCP returned no 
receiver-auth status\n"
+                        );
+                        return Err(EPROTO);
+                    };
+                    if status != 0x04 {
+                        pr_err!(
+                            "vino: socket {socket} downstream receiver-auth 
status {status:#x}, expected 0x04\n"
+                        );
+                        return Err(EPROTO);
+                    }
+                }
+                drained += d.reads;
+                acks += d.acks;
+                rejects += d.rejects;
+                // Attribute a display-capability reply by its echoed 
stream-open counter.
+                if let Some(c) = d.display_cap_ctr {
+                    for h in 0..Self::CP_SETUP_CONNECTORS {
+                        if stream_open_ctr[h] == Some(c) {
+                            connectors_present[h] = true;
+                        }
+                    }
+                }
+                cp_ctr += 1;
+                wseq += ((content_len + 15) / 16) as u32;
+            }
+            // Collect replies before moving to the next connector without 
adding another phase
+            // delay.
+            {
+                let d = Self::drain_ep84(
+                    link,
+                    ep84_q.as_mut(),
+                    &mut resp,
+                    session,
+                    edid_out,
+                    Delta::from_millis(10),
+                );
+                drained += d.reads;
+                acks += d.acks;
+                rejects += d.rejects;
+                if let Some(c) = d.display_cap_ctr {
+                    for h in 0..Self::CP_SETUP_CONNECTORS {
+                        if stream_open_ctr[h] == Some(c) {
+                            connectors_present[h] = true;
+                        }
+                    }
+                }
+            }
+
+            if trace_crypto_enabled() {
+                pr_info!(
+                    "vino-crypto: video connector={connector} raw_key={:02x?} 
delivered_riv={riv_h:02x?} key={:02x?} nonce={:02x?}\n",
+                    &ske_ks_h[..],
+                    &video_keys[connector][..16],
+                    &video_keys[connector][16..24]
+                );
+            }
+            connector_ok[connector] = true;
+            heads_authenticated += 1;
+        }
+        vino_debug!(
+            "vino: {heads_authenticated}/{} connector(s) authenticated\n",
+            connector_count
+        );
+
+        // Navarro performs one dock-wide state transition after the last 
per-connector AKE and
+        // before any connector finalizer. This message was absent from vino 
even though its
+        // authenticated DLM reply reports state 2. Keep it Navarro-only until 
a Ridge transcript
+        // establishes that platform's behavior.
+        if profile.protocol.per_connector_onehot {
+            let state = cp::post_auth_state_req(cp_ctr)?;
+            let frame = cp::seal_interactive(&session.ks, &session.riv, 0x15, 
wseq, &state)?;
+            Self::submit_cp_frame(link, &mut out_q, &frame)?;
+            sent += 1;
+            let d = Self::drain_ep84(
+                link,
+                ep84_q.as_mut(),
+                &mut resp,
+                session,
+                edid_out,
+                Delta::from_millis(10),
+            );
+            drained += d.reads;
+            acks += d.acks;
+            rejects += d.rejects;
+            cp_ctr += 1;
+            wseq += ((state.len() + 15) / 16) as u32;
+        }
+
+        // Finalize the streams of the connectors that authenticated, before 
entering the
+        // steady-state heartbeat.
+        //
+        // Only those connectors: finalizing a stream whose downstream 
authentication never ran
+        // makes the dock hard-reset a few seconds later and re-enumerate, 
which reads as a
+        // spontaneous dock reset rather than as a message it refused. The 
sequence is per
+        // connector, so it follows the dock's connector count rather than a 
fixed pair: a
+        // four-connector dock finalizes four, and DLM does the same. A dock 
that shares its control
+        // pipe is never finalized: DLM sends neither `0x16/0x4c` nor 
`0x15/0x4a` to it in a whole
+        // session, and every message it has not been asked to handle is one 
more it must answer on
+        // the pipe it is about to take pixels on.
+        let finalize = (0..connector_count)
+            .filter(|_| !profile.topology.video_on_ctrl_pipe)
+            .flat_map(|c| {
+                cp::CP_SETUP_FINALIZE_STEPS
+                    .iter()
+                    .map(move |&(id, sub)| (id, sub, c as u8))
+            });
+        for (id, sub, off22) in finalize {
+            if (off22 as usize) < Self::CP_SETUP_CONNECTORS && 
!connector_ok[off22 as usize] {
+                continue;
+            }
+            // Offset 22 selects the connector or step; sub 0x4c also carries 
1 at offset 23.
+            let mut c = [0u8; 32];
+            c[0..2].copy_from_slice(&id.to_le_bytes());
+            c[2..4].copy_from_slice(&sub.to_le_bytes());
+            c[4..6].copy_from_slice(&cp_ctr.to_le_bytes());
+            c[22] = off22;
+            if sub == 0x004c {
+                c[23] = 0x01;
+                rng::fill(&mut c[24..]);
+            } else {
+                rng::fill(&mut c[23..]);
+            }
+            let frame = cp::seal_interactive(&session.ks, &session.riv, id, 
wseq, &c)?;
+            Self::submit_cp_frame(link, &mut out_q, &frame)?;
+            sent += 1;
+            let d = Self::drain_ep84(
+                link,
+                ep84_q.as_mut(),
+                &mut resp,
+                session,
+                edid_out,
+                Delta::from_millis(10),
+            );
+            drained += d.reads;
+            acks += d.acks;
+            rejects += d.rejects;
+            cp_ctr += 1;
+            wseq += 2; // 32 B content = 2 AES blocks
+        }
+        if let Some(queue) = out_q.as_mut() {
+            // A flush timeout is not fatal: `reap()` leaves a slow slot 
posted for a later call,
+            // and the session is otherwise complete. A dock with an empty 
connector never drains
+            // that connector's last EP02 write, which would otherwise fail 
the whole setup.
+            if let Err(e) = queue.flush(link.io(), timeout()) {
+                vino_debug!(
+                    "vino: EP02 queue flush timed out ({e:?}); session already 
acknowledged, continuing\n"
+                );
+            }
+        }
+
+        // Ridge commits after finalization. Navarro has already committed at 
its measured
+        // post-0x15/pre-0x16 boundary above.
+        if profile.protocol.video_commit_point == 
profile::VideoCommitPoint::AfterFinalize {
+            link.control_send(
+                0x24,
+                0x40, /* VENDOR_OUT */
+                0,
+                0,
+                &[],
+                timeout(),
+                GFP_KERNEL,
+            )?;
+            // Refresh the interface state after the render/commit request.
+            let mut state2 = [0u8; 28];
+            link.control_recv(0x22, 0xc1, 1, 0, &mut state2, timeout(), 
GFP_KERNEL)?;
+        }
+
+        // Read the dock's reply: a VERIFIED `wsub=0x45` ack means the cipher 
engaged on our frame.
+        let ls = Self::lockstep_reply(link, ep84_q.as_mut(), &mut resp, 
session, 0x08, edid_out);
+        drained += ls.reads;
+        acks += ls.acks;
+        rejects += ls.rejects;
+
+        const MAX_ROUNDS: usize = 16;
+        for _ in 0..MAX_ROUNDS {
+            let d = Self::drain_ep84(
+                link,
+                ep84_q.as_mut(),
+                &mut resp,
+                session,
+                edid_out,
+                Delta::from_millis(10),
+            );
+            drained += d.reads;
+            acks += d.acks;
+            rejects += d.rejects;
+            if d.reads == 0 {
+                break;
+            }
+        }
+
+        if acks == 0 {
+            pr_err!(
+                "vino: encrypted session not acknowledged (reads={drained}, 
rejects={rejects})\n"
+            );
+            return Err(EPROTO);
+        }
+
+        // Complete downstream discovery on the authenticated counter stream.
+        if profile.protocol.per_connector_onehot {
+            // Navarro's authenticated DLM transcript is a compact, ordered 
transaction. Keep it
+            // separate from Ridge's older retry-heavy discovery below: 
hundreds of extra status
+            // polls delayed Navarro's first mode by ~23 seconds and changed 
every live counter.
+            macro_rules! navarro_send {
+                ($id:expr, $body:expr) => {{
+                    let id: u16 = $id;
+                    let body = $body;
+                    let e = Self::send_live_cp(
+                        link,
+                        session,
+                        ep84_q.as_mut(),
+                        &mut resp,
+                        edid_out,
+                        id,
+                        wseq,
+                        &body,
+                    )?;
+                    drained += e.reads;
+                    acks += e.acks;
+                    rejects += e.rejects;
+                    sent += 1;
+                    wseq = wseq.wrapping_add(((body.len() + 15) / 16) as u32);
+                    cp_ctr += 1;
+                }};
+            }
+
+            // DLM first seeks every physical connector, including empty 
sockets.
+            for connector in 0..connector_count {
+                let probe = cp::get_edid_req_sub(cp_ctr, 0x20, connector as 
u8)?;
+                navarro_send!(0x15, probe);
+            }
+            let devq = cp::device_query_req(cp_ctr, 0x0000)?;
+            navarro_send!(0x14, devq);
+
+            let mut active = [false; Self::CP_SETUP_CONNECTORS];
+            for connector in 0..connector_count {
+                // Navarro exposes four connector numbers on two physical 
video pipes, and the
+                // reference DLM session opened only the first connector on 
each pipe -- but that
+                // session had its two panels in sockets 1 and 2. Sockets 3 
and 4 are not aliases
+                // of them: they are separate physical connectors that happen 
to share a bulk
+                // endpoint, with their own selector, EDID and stream. 
Skipping them by endpoint
+                // made a monitor in socket 3 or 4 invisible, with the dock 
never even probed for
+                // it. Read every socket; engagement below is what stays 
selective.
+                active[connector] = true;
+                *edid_out = None;
+                let hu8 = connector as u8;
+                let kick = cp::edid_readiness_kick(cp_ctr, hu8)?;
+                navarro_send!(0x16, kick);
+                let probe = cp::get_edid_req_sub(cp_ctr, 0x20, hu8)?;
+                navarro_send!(0x15, probe);
+                let fetch = cp::get_edid_req(cp_ctr, hu8)?;
+                navarro_send!(0x15, fetch);
+                // The fetch drain carries the asynchronous EDID in the 
working DLM cadence.
+                edid_connectors[connector] = edid_out.take();
+                discovery_deferred[connector] = 
edid_connectors[connector].is_none();
+            }
+            // Engage each connector that answered with an EDID, once, after 
all kick/probe/fetch
+            // triplets. Gated on a recovered EDID because that is the 
presence signal on this
+            // platform, and because driving setup at a socket with nothing in 
it is exactly what
+            // makes this dock hard-reset a few seconds later.
+            for connector in 0..connector_count {
+                if active[connector] && edid_connectors[connector].is_some() {
+                    let engage = cp::edid_engage_req(cp_ctr, connector as u8)?;
+                    navarro_send!(0x16, engage);
+                }
+            }
+
+            // Two status samples bracket the one-shot RTC synchronization, 
followed by thirteen
+            // more samples in the same-day working capture.
+            for _ in 0..2 {
+                let status = cp::device_query_req(cp_ctr, 0x000c)?;
+                navarro_send!(0x14, status);
+            }
+            let now = kernel::time::ktime_get_real_seconds();
+            let rtc = cp::rtc_sync_req(
+                cp_ctr,
+                now,
+                *crate::module_parameters::rtc_utc_offset_minutes.value(),
+            )?;
+            navarro_send!(0x1e, rtc);
+            for _ in 0..13 {
+                let status = cp::device_query_req(cp_ctr, 0x000c)?;
+                navarro_send!(0x14, status);
+            }
+            // The last discovery records immediately preceding DLM's 
clear-mode pair re-seek
+            // each active connector once.
+            for connector in 0..connector_count {
+                if active[connector] {
+                    let probe = cp::get_edid_req_sub(cp_ctr, 0x20, connector 
as u8)?;
+                    navarro_send!(0x15, probe);
+                }
+            }
+        } else if profile.topology.video_on_ctrl_pipe {
+            // Discovery on a dock that shares its control pipe walks all its 
connectors through
+            // each phase before starting the next, rather than taking one 
connector end to end.
+            // Running a connector to completion first leaves the connector 
that went first without
+            // an EDID while the second streams, which is what a 
single-monitor dock looks like from
+            // userspace.
+            //
+            // It is also short: no reader kick, one engage rather than two, 
and no readiness
+            // polling. Every extra transaction here is one the dock has to 
answer on the pipe it
+            // will shortly be asked to take pixels on.
+            macro_rules! walk_send {
+                ($id:expr, $body:expr) => {{
+                    let id: u16 = $id;
+                    let body = $body;
+                    let e = Self::send_live_cp(
+                        link,
+                        session,
+                        ep84_q.as_mut(),
+                        &mut resp,
+                        edid_out,
+                        id,
+                        wseq,
+                        &body,
+                    )?;
+                    drained += e.reads;
+                    acks += e.acks;
+                    rejects += e.rejects;
+                    sent += 1;
+                    wseq = wseq.wrapping_add(((body.len() + 15) / 16) as u32);
+                    cp_ctr += 1;
+                }};
+            }
+
+            for connector in 0..connector_count {
+                let hu8 = connector as u8;
+                *edid_out = None;
+                let probe = cp::get_edid_req_sub(cp_ctr, 0x20, hu8)?;
+                walk_send!(0x15, probe);
+                let fetch = cp::get_edid_req(cp_ctr, hu8)?;
+                walk_send!(0x15, fetch);
+                // The EDID arrives asynchronously, a few messages behind the 
fetch it answers, and
+                // the push names no connector -- so a connector's answer has 
to be collected before
+                // the next connector is asked, even though DLM issues both 
fetches back to back.
+                let waited = Instant::<Monotonic>::now();
+                while edid_out.is_none() && Instant::<Monotonic>::now() - 
waited < EDID_REPLY_WAIT {
+                    let d = Self::drain_ep84(
+                        link,
+                        ep84_q.as_mut(),
+                        &mut resp,
+                        session,
+                        edid_out,
+                        Delta::from_millis(20),
+                    );
+                    drained += d.reads;
+                    acks += d.acks;
+                    rejects += d.rejects;
+                }
+                edid_connectors[connector] = edid_out.take();
+                vino_debug!(
+                    "vino: socket {socket} EDID fetch {}\n",
+                    if edid_connectors[connector].is_some() {
+                        "succeeded"
+                    } else {
+                        "returned no EDID"
+                    },
+                    socket = connector + 1
+                );
+            }
+            // Both probes are answered before either sink is engaged, and the 
last message before
+            // the engages restates the session hello.
+            let hello = cp::session_hello(cp_ctr);
+            walk_send!(0x14, hello);
+            // Every connector, not only the ones that answered with an EDID. 
This dock reports no
+            // EDID for a sink it is nonetheless driving, so an EDID is 
evidence of presence and
+            // its absence is evidence of nothing; the vendor engages both 
connectors before any
+            // pixels and leaves an unoccupied one engaged for the life of the 
session.
+            for connector in 0..connector_count {
+                let engage = cp::edid_engage_req(cp_ctr, connector as u8)?;
+                walk_send!(0x16, engage);
+            }
+
+            // Open every connector's sealed video stream, and announce its 
video plane.
+            //
+            // This is where DLM does it: after the sinks are engaged, before 
the capability
+            // queries, and long before any mode is set. The sealed record 
takes block zero of the
+            // stream so the decoder configuration that follows it at prologue 
time takes block
+            // one; `set_video_keys` is told as much.
+            //
+            // Every connector, not only the ones that answered with an EDID: 
a monitor that arrives
+            // after setup would otherwise be driven on a stream the dock was 
never told about, and
+            // this dock has no video pipe to open one on later. The vendor 
opens both.
+            //
+            // A dock with a video pipe of its own is opened by the scanout 
path instead,
+            // immediately ahead of the frame that needs it -- it has a pipe 
to do that on.
+            // The vendor spaces the stages of this burst with status polls; 
see `SetupPolls`.
+            // They are round trips, so a stage that follows three of them is 
a stage the dock was
+            // given three acknowledged messages' worth of time to reach. 
Opening both streams back
+            // to back puts the same records on the wire and gives it none of 
that.
+            macro_rules! settle_polls {
+                ($n:expr) => {{
+                    for _ in 0..$n {
+                        let status = cp::device_query_req(cp_ctr, 0x000c)?;
+                        walk_send!(0x14, status);
+                    }
+                }};
+            }
+
+            for connector in 0..connector_count {
+                
settle_polls!(profile.protocol.setup_polls.before_open(connector as u8));
+                let stream_id = profile.geometry().stream_id(connector as u8);
+                let mut vkey = kernel::crypto::Secret::<16>::zeroed();
+                vkey.copy_from_slice(&video_keys[connector][..16]);
+                let mut vnonce = [0u8; 8];
+                vnonce.copy_from_slice(&video_keys[connector][16..24]);
+                let content = 
cp::stream_open(profile.protocol.stream_marker_kind);
+                let open = cp::seal_video_arm(&vkey, &vnonce, stream_id, 
0x000a, 0, &content)?;
+                link.ctrl_send(&open, timeout(), GFP_KERNEL)?;
+                let announce = cp::stream_announce(
+                    u16::from(profile.geometry().connector_selector(connector 
as u8)),
+                    0,
+                );
+                link.ctrl_send(&announce, timeout(), GFP_KERNEL)?;
+                sent += 2;
+                *stream_opened |= 1u32 << connector;
+                let d = Self::drain_ep84(
+                    link,
+                    ep84_q.as_mut(),
+                    &mut resp,
+                    session,
+                    edid_out,
+                    Delta::from_millis(10),
+                );
+                drained += d.reads;
+                acks += d.acks;
+                rejects += d.rejects;
+            }
+
+            settle_polls!(profile.protocol.setup_polls.after_stream_opens);
+
+            // Every connector again, for the same reason the engages are.
+            for connector in 0..connector_count {
+                let query = cp::post_edid_query(cp_ctr, connector as u8)?;
+                walk_send!(0x15, query);
+            }
+        } else {
+            // Open discovery with a heartbeat and the one-shot 
device-capability query.
+            let hb = cp::heartbeat(cp_ctr)?;
+            let e = Self::send_live_cp(
+                link,
+                session,
+                ep84_q.as_mut(),
+                &mut resp,
+                edid_out,
+                0x16,
+                wseq,
+                &hb,
+            )?;
+            drained += e.reads;
+            acks += e.acks;
+            rejects += e.rejects;
+            wseq = wseq.wrapping_add(((hb.len() + 15) / 16) as u32);
+            cp_ctr += 1;
+
+            let devq = cp::device_query_req(cp_ctr, 0x0000)?;
+            let e = Self::send_live_cp(
+                link,
+                session,
+                ep84_q.as_mut(),
+                &mut resp,
+                edid_out,
+                0x14,
+                wseq,
+                &devq,
+            )?;
+            drained += e.reads;
+            acks += e.acks;
+            rejects += e.rejects;
+            wseq = wseq.wrapping_add(((devq.len() + 15) / 16) as u32);
+            cp_ctr += 1;
+
+            // EDID discovery is a probe/kick/fetch sequence followed by two 
engage messages. A
+            // cold receiver then needs bounded status polling until the 
readiness bit is set.
+            const EDID_STEP_DELAY: Delta = Delta::from_millis(100);
+            const EDID_EARLY_ROUNDS: usize = 1;
+            // Bound both the poll count and wall-clock duration.
+            const EDID_POLL_ITERS: usize = 250;
+            const EDID_POLL_DELAY: Delta = Delta::from_millis(20);
+            const EDID_POLL_PROBE_EVERY: usize = 8;
+            // Offset 22 selects the downstream connector. A connector the 
display-capability
+            // transaction reported empty need not be probed, but that 
inference only holds once
+            // some connector has reported one: a dock that pushed no 
capability at all has said
+            // nothing about any socket, and treating silence as "empty" 
leaves every connector past
+            // the first undiscovered.
+            let capabilities_reported = 
connectors_present.iter().any(|&present| present);
+            for connector in 0..connector_count {
+                if connector != 0 && capabilities_reported && 
!connectors_present[connector] {
+                    continue;
+                }
+                let hu8 = connector as u8;
+                let socket = connector + 1;
+                *edid_out = None;
+                let mut edid_ready = false;
+                let mut transport_error = None;
+                'discovery: {
+                    macro_rules! edid_send {
+                        ($ep:expr, $body:expr, $tag:expr) => {{
+                            match Self::send_live_cp(
+                                link,
+                                session,
+                                ep84_q.as_mut(),
+                                &mut resp,
+                                edid_out,
+                                $ep,
+                                wseq,
+                                &$body,
+                            ) {
+                                Ok(e) => {
+                                    drained += e.reads;
+                                    acks += e.acks;
+                                    rejects += e.rejects;
+                                    wseq = wseq.wrapping_add((($body.len() + 
15) / 16) as u32);
+                                    cp_ctr += 1;
+                                    edid_ready |= e.edid_ready;
+                                    vino_debug!(
+                                        "vino: live connector {} {} sent\n",
+                                        connector,
+                                        $tag
+                                    );
+                                }
+                                Err(e) => {
+                                    transport_error = Some(e);
+                                    break 'discovery;
+                                }
+                            }
+                        }};
+                    }
+                    // A block that arrives before the dock reports its 
downstream read complete
+                    // describes the dock's own bridge rather than the 
monitor, and publishing it
+                    // drives the panel at a timing it never advertised. 
Offset 26 bit 7 of the
+                    // presence reply is that report; drop anything offered 
ahead of it and let the
+                    // engage below produce the real one.
+                    let gate_on_ready = profile.quirks.edid_ready_reported;
+                    macro_rules! edid_settled {
+                        () => {{
+                            if gate_on_ready && !edid_ready && 
edid_out.is_some() {
+                                vino_debug!(
+                                    "vino: socket {socket} discarding an EDID 
offered before the \
+                                     downstream read completed\n"
+                                );
+                                *edid_out = None;
+                            }
+                            edid_out.is_some()
+                        }};
+                    }
+                    'early: for cycle in 0..EDID_EARLY_ROUNDS {
+                        if edid_settled!() {
+                            break;
+                        }
+                        vino_debug!(
+                            "vino: live get-EDID socket {socket} early round 
{cycle}\n",
+                            socket = connector + 1
+                        );
+                        for _ in 0..2 {
+                            let probe = cp::get_edid_req_sub(cp_ctr, 0x20, 
hu8)?;
+                            edid_send!(0x15, probe, "get-EDID probe (id=0x15 
sub=0x20)");
+                            fsleep(EDID_STEP_DELAY);
+                        }
+                        // Start or continue the selected connector's 
downstream DDC read.
+                        let kick = cp::edid_readiness_kick(cp_ctr, hu8)?;
+                        edid_send!(0x16, kick, "get-EDID kick (id=0x16 
sub=0x4b)");
+                        fsleep(EDID_STEP_DELAY);
+                        let req = cp::get_edid_req(cp_ctr, hu8)?;
+                        edid_send!(0x15, req, "get-EDID fetch (id=0x15 
sub=0x21)");
+                        if edid_settled!() {
+                            break 'early;
+                        }
+                        fsleep(EDID_STEP_DELAY);
+                        // EDID arrives asynchronously after the fetch 
acknowledgment.
+                        let reply_wait = Instant::<Monotonic>::now();
+                        while Instant::<Monotonic>::now() - reply_wait < 
Delta::from_secs(2) {
+                            let d = Self::drain_ep84(
+                                link,
+                                ep84_q.as_mut(),
+                                &mut resp,
+                                session,
+                                edid_out,
+                                Delta::from_millis(20),
+                            );
+                            drained += d.reads;
+                            acks += d.acks;
+                            rejects += d.rejects;
+                            edid_ready |= d.edid_ready;
+                            if edid_settled!() {
+                                break 'early;
+                            }
+                        }
+                    }
+                    // Engage is required twice even if the EDID push arrived 
early.
+                    for _ in 0..2 {
+                        let engage = cp::edid_engage_req(cp_ctr, hu8)?;
+                        edid_send!(0x16, engage, "get-EDID engage (id=0x16 
sub=0x0023)");
+                        fsleep(EDID_STEP_DELAY);
+                    }
+                    if edid_out.is_none() {
+                        // Bound wall-clock time independently of the iteration
+                        // count because each failed send has its own USB 
timeout.
+                        const EDID_POLL_MAX: Delta = Delta::from_secs(6);
+                        let poll_start = Instant::<Monotonic>::now();
+                        'poll: for i in 0..EDID_POLL_ITERS {
+                            if edid_out.is_some() || edid_ready {
+                                break 'poll;
+                            }
+                            if Instant::<Monotonic>::now() - poll_start > 
EDID_POLL_MAX {
+                                vino_debug!(
+                                "vino: get-EDID socket {socket} readiness poll 
hit wall-clock cap\n"
+                            );
+                                break 'poll;
+                            }
+                            let status = cp::device_query_req(cp_ctr, 0x000c)?;
+                            edid_send!(0x14, status, "device-status poll 
(id=0x14 sub=0x000c)");
+                            if i % EDID_POLL_PROBE_EVERY == 
EDID_POLL_PROBE_EVERY - 1 {
+                                let probe = cp::get_edid_req_sub(cp_ctr, 0x20, 
hu8)?;
+                                edid_send!(
+                                    0x15,
+                                    probe,
+                                    "get-EDID readiness probe (id=0x15 
sub=0x20)"
+                                );
+                            }
+                            if edid_out.is_some() || edid_ready {
+                                break 'poll;
+                            }
+                            fsleep(EDID_POLL_DELAY);
+                        }
+                        vino_debug!(
+                        "vino: get-EDID socket {socket} readiness poll 
finished (ready={edid_ready})\n"
+                    );
+                        // The asynchronous `id=0x194` EDID can follow the 
fetch
+                        // acknowledgment by several messages.
+                        for _ in 0..24 {
+                            if edid_settled!() {
+                                break;
+                            }
+                            let req = cp::get_edid_req(cp_ctr, hu8)?;
+                            edid_send!(0x15, req, "get-EDID retry (id=0x15 
sub=0x21)");
+                            let d = Self::drain_ep84(
+                                link,
+                                ep84_q.as_mut(),
+                                &mut resp,
+                                session,
+                                edid_out,
+                                Delta::from_millis(10),
+                            );
+                            drained += d.reads;
+                            acks += d.acks;
+                            rejects += d.rejects;
+                            fsleep(EDID_POLL_DELAY);
+                        }
+                    }
+                    // Complete this connector with its post-EDID capability 
query.
+                    let query = cp::post_edid_query(cp_ctr, hu8)?;
+                    edid_send!(0x15, query, "post-EDID capability query 
(id=0x15 sub=0x53)");
+                    // `edid_send!` folds the drain's readiness bit into 
`edid_ready`. This is the
+                    // last statement of the per-connector iteration and the 
next connector
+                    // re-derives it, so that update is deliberately not read 
again here.
+                    let _ = edid_ready;
+                }
+
+                discovery_deferred[connector] = transport_error.is_some();
+                if let Some(e) = transport_error {
+                    // The encrypted session is already authenticated. Keep it 
running and recover
+                    // this connector independently after a connector-local 
discovery timeout.
+                    *edid_out = None;
+                    pr_warn!(
+                        "vino: socket {socket} discovery timed out ({e:?}); 
deferring to runtime \
+                         recovery\n",
+                        socket = connector + 1
+                    );
+                }
+                edid_connectors[connector] = edid_out.take();
+                vino_debug!(
+                    "vino: socket {socket} EDID fetch {}\n",
+                    if edid_connectors[connector].is_some() {
+                        "succeeded"
+                    } else {
+                        "returned no EDID"
+                    },
+                    socket = connector + 1
+                );
+            }
+
+            // KMS is the sole owner of mode selection; setup only discovers 
connector state.
+        }
+
+        if rejects > 0 {
+            pr_warn!("vino: dock returned {rejects} undecryptable control 
frame(s)\n");
+        }
+        vino_debug!("vino: control setup tx={sent} rx={drained} ack={acks} 
reject={rejects}\n");
+        // Hand the caller the running counters: the next free AES-CTR block 
(`wseq`) and inner
+        // message counter (`cp_ctr`), so runtime KMS sends (mode-set/cursor) 
continue the sequence.
+        Ok((sent, wseq, cp_ctr))
+    }
+}

Reply via email to