Once the AKE completes, the dock takes only sealed messages: AES-CTR under
one nonce, authenticated by a CMAC under another, in host-driven lockstep
with an acknowledgment per message and an authenticated counter that a
message sent out of turn desynchronises for the rest of the session.

Add the sealed message layer and the builders that use it, grouped by
subject: the mode set and its timing derivation, the EDID probe, fetch and
downstream-sink engage, and the cursor. The tails these messages carry are
not padding -- they are HDCP payloads at fixed offsets -- and the KUnit
tests here compare each builder byte for byte against a decrypted capture,
because a malformed message is acknowledged like any other and then simply
never acted on.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Mike Lothian <[email protected]>
---
 drivers/gpu/drm/vino/cp.rs        | 1590 +++++++++++++++++++++++++++++
 drivers/gpu/drm/vino/cp/cursor.rs |  158 +++
 drivers/gpu/drm/vino/cp/edid.rs   |  464 +++++++++
 drivers/gpu/drm/vino/cp/mode.rs   |  874 ++++++++++++++++
 4 files changed, 3086 insertions(+)
 create mode 100644 drivers/gpu/drm/vino/cp.rs
 create mode 100644 drivers/gpu/drm/vino/cp/cursor.rs
 create mode 100644 drivers/gpu/drm/vino/cp/edid.rs
 create mode 100644 drivers/gpu/drm/vino/cp/mode.rs

diff --git a/drivers/gpu/drm/vino/cp.rs b/drivers/gpu/drm/vino/cp.rs
new file mode 100644
index 000000000000..0afbf64974db
--- /dev/null
+++ b/drivers/gpu/drm/vino/cp.rs
@@ -0,0 +1,1590 @@
+// SPDX-License-Identifier: GPL-2.0
+//! Encrypted-control-plane message builders (the inner plaintext of the type=4
+//! sub=0x24 AES-CTR frames) plus the AES-CTR `seal` that encrypts and frames 
them.
+use super::*;
+
+mod cursor;
+mod edid;
+mod mode;
+
+pub(crate) use cursor::*;
+pub(crate) use edid::*;
+pub(crate) use mode::*;
+
+/// DisplayLink key whitening applied to the raw SKE session key:
+/// ```text
+///   cp_session_key = ske_ks XOR CP_KEY_WHITEN
+/// ```
+///
+/// The whitened key is used by both the AES-CTR content cipher and Dl3Cmac. 
The raw key is wrapped
+/// in `Edkey` and delivered to the dock.
+pub(super) const CP_KEY_WHITEN: [u8; 16] = [
+    0x26, 0xab, 0xee, 0x38, 0x93, 0xd0, 0xc4, 0x32, 0x61, 0x43, 0xa4, 0xbf, 
0x5b, 0x45, 0xd6, 0xec,
+];
+
+/// Derive the live CP session key from the raw SKE key.
+///
+/// The result of `ske_ks XOR `[`CP_KEY_WHITEN`] keys the AES-CTR content
+/// cipher and the Dl3Cmac in [`seal_livemac`]. The input is wrapped into
+/// `Edkey`; the dock applies the same XOR.
+pub(super) fn cp_session_key(ske_ks: &[u8; 16]) -> kernel::crypto::Secret<16> {
+    let mut key = *ske_ks;
+    for i in 0..16 {
+        key[i] ^= CP_KEY_WHITEN[i];
+    }
+    kernel::crypto::Secret::new(key)
+}
+
+/// Derive a stream's AES-CTR content nonce from the RIV its `SKE_Send_Eks` 
restatement
+/// (`id=0x32`) delivered.
+///
+/// Byte 7 is xored with the stream's content-stream id: the value the stream's
+/// `RepeaterAuth_Stream_Manage` restatement declares, which is also the wire 
`sub` of that
+/// stream's control records. The control channel is stream `0x04`, Ridge's 
video streams are
+/// `0x08 | connector`, and Navarro's are `(connector << 3) | 7`.
+pub(super) fn stream_content_nonce(riv: &[u8; 8], stream_id: u16) -> [u8; 8] {
+    let mut nonce = *riv;
+    nonce[7] ^= stream_id as u8;
+    nonce
+}
+
+/// Common CP inner header: `[id u16][sub u16][counter u16][00 00]` (sec 
6.1/sec 8.6.4).
+fn header(out: &mut KVec<u8>, id: u16, sub: u16, counter: u16) -> Result {
+    out.extend_from_slice(&id.to_le_bytes(), GFP_KERNEL)?;
+    out.extend_from_slice(&sub.to_le_bytes(), GFP_KERNEL)?;
+    out.extend_from_slice(&counter.to_le_bytes(), GFP_KERNEL)?;
+    out.extend_from_slice(&[0, 0], GFP_KERNEL)?;
+    Ok(())
+}
+fn pad_to(out: &mut KVec<u8>, len: usize) -> Result {
+    while out.len() < len {
+        out.push(0, GFP_KERNEL)?;
+    }
+    Ok(())
+}
+/// OUT session heartbeat: `id=0x16 sub=0x75`, two AES blocks.
+///
+/// ```text
+/// 16 00 75 00 [ctr:2] 00 00   14x 00   e0 2e   [8-byte host-random token]
+/// ```
+///
+/// Offset 22 contains `0x2ee0`; offsets 24..32 are ignored and emitted as 
zero. The heartbeat runs
+/// throughout the streaming session.
+pub(super) fn heartbeat(counter: u16) -> Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(32, GFP_KERNEL)?;
+    header(&mut b, 0x16, 0x75, counter)?;
+    pad_to(&mut b, 22)?; // block0 tail + block1[0..6]
+    b.extend_from_slice(&[0xe0, 0x2e], GFP_KERNEL)?;
+    pad_to(&mut b, 32)?;
+    Ok(b)
+}
+/// Stream enable markers (`id=0x16`, sub `0x2e` or `0x2f`) bracket each mode 
set:
+///   `2f(1) 2e(3)` -> mode-set -> `2f(1) 2e(0) 2f(1) 2e(0) 2f(0) 2e(0)`
+///
+/// Offset 22 selects the connector and offset 23 carries the state.
+pub(super) fn stream_marker(counter: u16, connector: u8, sub: u16, state: u8) 
-> Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(32, GFP_KERNEL)?;
+    header(&mut b, 0x16, sub, counter)?;
+    pad_to(&mut b, 22)?;
+    b.push(connector, GFP_KERNEL)?; // off22: downstream connector selector
+    b.push(state, GFP_KERNEL)?; // off23: state byte
+    let mut token = [0u8; 8];
+    rng::fill(&mut token);
+    b.extend_from_slice(&token, GFP_KERNEL)?;
+    Ok(b)
+}
+
+pub(super) fn stream_commit(counter: u16, connector: u8) -> Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(32, GFP_KERNEL)?;
+    header(&mut b, 0x16, 0x4c, counter)?;
+    pad_to(&mut b, 22)?;
+    b.push(if connector == 0 { 0 } else { 1 }, GFP_KERNEL)?; // off22: 
per-connector flag
+    b.push(0, GFP_KERNEL)?; // off23
+    let mut token = [0u8; 8];
+    rng::fill(&mut token);
+    b.extend_from_slice(&token, GFP_KERNEL)?;
+    Ok(b)
+}
+/// OUT device-status/capability query: `id=0x14`. Subcommand 0 performs 
initial capability
+/// discovery; subcommand `0x0c` polls runtime status.
+pub(super) fn device_query_req(counter: u16, sub: u16) -> Result<KVec<u8>> {
+    random_tail_msg(0x14, sub, counter)
+}
+
+/// DL7400 post-authentication state query (`id=0x15 sub=0x78`).
+///
+/// The authenticated same-day DLM transcript sends this exactly once after 
all four per-connector
+/// authentication blocks and before the first `0x16/0x4c` finalizer. Its 
request has the ordinary
+/// 32-byte random-tail shape; the dock replies `0x14/0x78` with state `2` at 
offset 22. The
+/// handler's semantic name is not known, so keep the builder descriptive 
rather than assigning a
+/// guessed protocol meaning to that state.
+pub(super) fn post_auth_state_req(counter: u16) -> Result<KVec<u8>> {
+    random_tail_msg(0x15, 0x0078, counter)
+}
+/// DL7400 real-time-clock synchronization (`id=0x1e sub=0x94`).
+///
+/// The ten-byte payload at offset 22 is a compact broken-down local time:
+/// `[year LE16, month, day, hour, minute, second, weekday, yday LE16]`. The 
authenticated
+/// A capture carrying Monday as weekday 1 and 214 as the zero-based day of 
year proves the last
+/// three bytes are calendar fields rather than an opaque random tail.
+pub(super) fn rtc_sync_req(
+    counter: u16,
+    unix_seconds: i64,
+    utc_offset_minutes: i32,
+) -> Result<KVec<u8>> {
+    let local = unix_seconds.saturating_add(i64::from(utc_offset_minutes) * 
60);
+    let days = local.div_euclid(86_400);
+    let second_of_day = local.rem_euclid(86_400);
+
+    // Gregorian civil date from days since 1970-01-01 (Howard Hinnant's 
civil_from_days).
+    let z = days + 719_468;
+    let era = if z >= 0 { z } else { z - 146_096 }.div_euclid(146_097);
+    let doe = z - era * 146_097;
+    let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
+    let mut year = yoe + era * 400;
+    let doy_march = doe - (365 * yoe + yoe / 4 - yoe / 100);
+    let mp = (5 * doy_march + 2) / 153;
+    let day = doy_march - (153 * mp + 2) / 5 + 1;
+    let month = mp + if mp < 10 { 3 } else { -9 };
+    if month <= 2 {
+        year += 1;
+    }
+    if !(0..=u16::MAX as i64).contains(&year) {
+        return Err(EINVAL);
+    }
+    let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
+    let month_starts = [0u16, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 
334];
+    let mut yday = month_starts[(month - 1) as usize] + day as u16 - 1;
+    if leap && month > 2 {
+        yday += 1;
+    }
+    let weekday = (days + 4).rem_euclid(7) as u8; // 1970-01-01 was Thursday 
(4).
+
+    let mut b = KVec::with_capacity(32, GFP_KERNEL)?;
+    header(&mut b, 0x001e, 0x0094, counter)?;
+    pad_to(&mut b, 22)?;
+    b.extend_from_slice(&[0u8; 10], GFP_KERNEL)?;
+    b[22..24].copy_from_slice(&(year as u16).to_le_bytes());
+    b[24] = month as u8;
+    b[25] = day as u8;
+    b[26] = (second_of_day / 3_600) as u8;
+    b[27] = ((second_of_day % 3_600) / 60) as u8;
+    b[28] = (second_of_day % 60) as u8;
+    b[29] = weekday;
+    b[30..32].copy_from_slice(&yday.to_le_bytes());
+    Ok(b)
+}
+/// Shared builder for the many CP messages that share one wire shape: the 
standard 8-byte
+/// `[id][sub][counter][00 00]` header, 14 zero bytes, then a fresh 10-byte 
host-random tail the
+/// dock treats as an opaque token.
+fn random_tail_msg(id: u16, sub: u16, counter: u16) -> Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(32, GFP_KERNEL)?;
+    header(&mut b, id, sub, counter)?;
+    pad_to(&mut b, 22)?;
+    let mut tail = [0u8; 10];
+    rng::fill(&mut tail);
+    b.extend_from_slice(&tail, GFP_KERNEL)?;
+    Ok(b)
+}
+
+/// OUT `id=0x14 sub=0x0000`: an inner header and a fresh ten-byte token.
+///
+/// The first sealed message of a session, and on a dock that carries video on 
the control pipe
+/// also the last message before the sinks are engaged. The token is 
host-random and the dock has
+/// no way to validate it, so what the message states is the counter it 
carries.
+pub(super) fn session_hello(counter: u16) -> [u8; 32] {
+    let mut content = [0u8; 32];
+    content[0..2].copy_from_slice(&0x0014u16.to_le_bytes());
+    content[4..6].copy_from_slice(&counter.to_le_bytes());
+    rng::fill(&mut content[22..32]);
+    content
+}
+
+/// Pixel granularity the render stride is quantised to.
+const STRIDE_ALIGN: u32 = 128;
+
+/// Offset 42 is not a polarity field but a flags word, and DLM decodes every 
bit of it in its own
+/// `setupVideo` log line. Read out of the bit tests around DLM 3.4.26 
`0x576b26`, which select
+/// between an empty string and one of these:
+///
+/// | bit | mask | DLM's name |
+/// |---|---|---|
+/// | 0 | `0x0001` | `Interlace` |
+/// | 1 | `0x0002` | `Cross-connector synchronized` |
+/// | 2 | `0x0004` | `Dual NIVO` |
+/// | 3 | `0x0008` | `Just-in-time decode` |
+/// | 5 | `0x0020` | `DSC On`/`DSC Off` |
+/// | 6 | `0x0040` | `ST2084 colorspace used (HDR)` |
+/// | 7 | `0x0080` | `SingleDisplayMode enabled` |
+/// | 8 | `0x0100` | `Horizontal Sync Inverted` |
+/// | 9 | `0x0200` | `Vertical Syncs Inverted` |
+/// | 12 | `0x1000` | `ReducedQuantizationRange On`/`Off` |
+/// | 14 | `0x4000` | `Enable Timing for Gamma` |
+/// | 15 | `0x8000` | `(Disabled)` |
+///
+/// Bits 8, 9 and 15 land exactly where the decrypted corpus had already put 
them, which is what
+/// makes the rest of the table trustworthy. Bits 4, 10, 11 and 13 are not 
logged; bit 10 is the
+/// base below, always set and still unexplained.
+///
+/// Base bit of the offset-42 flags word, set in every message the corpus 
contains.
+const SYNC_FLAGS_BASE: u16 = 0x0400;
+/// `hSyncInv`: horizontal sync is active low.
+const SYNC_FLAG_HSYNC_INV: u16 = 0x0100;
+/// `vSyncInv`: vertical sync is active low.
+const SYNC_FLAG_VSYNC_INV: u16 = 0x0200;
+/// `ST2084 colorspace used (HDR)`: the connector's pixels are PQ-encoded 
rather than SDR.
+///
+/// This is the transfer-function selector that no capture could settle -- the 
Windows HDR A/B
+/// corpus has a sealed control plane, and DLM's Linux build never toggled HDR 
on this hardware --
+/// and it turns out not to need a capture at all. There is exactly one HDR 
flag: the colour
+/// primaries are not carried here, because the dock derives the downstream 
infoframe itself.
+const SYNC_FLAG_ST2084: u16 = 0x0040;
+/// `Dual NIVO`: this connector's video endpoint is carrying a second 
connector's stream too.
+///
+/// The DL-7400 multiplexes four connectors onto two video bulk endpoints -- 
`0x08` owns connectors
+/// {0, 2} and `0x0a` owns {1, 3} -- so any two monitors in sockets one apart 
share an endpoint.
+/// The dock drives only one of the two streams unless both mode sets declare 
the sharing here.
+/// DLM's `setupVideo` flag decode names this bit `Dual NIVO`, matching the 
`TiledNivoViewer`
+/// strings in its binary.
+const SYNC_FLAG_DUAL_NIVO: u16 = 0x0004;
+/// The offset-42 word a teardown carries in place of any polarity.
+const SYNC_FLAGS_TEARDOWN: u16 = 0x8000;
+
+/// Picture aspect of CTA VICs 1 through 59, one bit per VIC: set for 16:9, 
clear for 4:3.
+///
+/// The CTA table pairs most timings, one 4:3 and one 16:9 over the same 
signal -- VIC 2 and 3 are
+/// both 720x480p60, VIC 6 and 7 both 720x480i60 -- so the aspect cannot be 
recovered from the
+/// timing and has to be carried per VIC.
+const VIC_ASPECT_16_9: u64 = 0x055_575e_beaa_ed55c;
+
+/// Offset-66 high byte: the mode's picture aspect ratio.
+const ASPECT_16_9: u16 = 0x2800;
+const ASPECT_4_3: u16 = 0x1800;
+/// Sent for a timing with no CTA VIC, which has no CTA aspect to name.
+const ASPECT_NONE: u16 = 0x0800;
+
+/// Offset-68 of the `0x48/0x22` message: the colour depth, in the high byte.
+///
+/// The dock takes a depth enum, not a bit count: 16bpp is 1, 24bpp 2, 30bpp 
3, 36bpp 4 and 48bpp
+/// 5, and an unrecognised depth falls back to 24bpp. The low byte is a 
separate field that every
+/// capture carries as zero. The three values above 24bpp are 10, 12 and 16 
bits per channel --
+/// the deep-colour ladder -- and vino drives none of them.
+const COLOUR_DEPTH_24BPP: u16 = 0x0200;
+/// Offset-68 for 30 bpp: the same enum, one step up the deep-colour ladder 
(10 bits per channel).
+const COLOUR_DEPTH_30BPP: u16 = 0x0300;
+
+/// Offset-23 of the `0x48/0x22` message: the DMA buffer format the connector 
scans out.
+///
+/// The dock indexes a four-entry table with this, giving 2, 4, 3 and 4 bytes 
per pixel for formats
+/// 0 through 3, and rejects anything above 3. DLM names all four: the same 
value selects a string
+/// in the helper at 3.4.26 `0x62ecb0`, whose four arms point at the plaintext 
`NM16`, `NM32`,
+/// `NM24` and `NM30`, and the bytes-per-pixel table at `0x8dc320` reads `{2, 
4, 3, 4}` in exactly
+/// that order.
+///
+/// | value | name | bytes/px |
+/// |---|---|---|
+/// | 0 | `NM16` | 2 |
+/// | 1 | `NM32` | 4 |
+/// | 2 | `NM24` | 3 |
+/// | 3 | `NM30` | 4 |
+///
+/// A teardown writes no timing at all and leaves the field zero.
+const DMA_FORMAT_NM24: u8 = 2;
+const DMA_FORMAT_NONE: u8 = 0;
+/// Offset-23 for a 10-bit connector: `NM30`, the second of the table's two 
four-byte formats.
+///
+/// No capture on either dock generation carries anything but `NM24`, so the 
name has to settle
+/// the choice between the table's two four-byte formats: 30 bits per pixel 
packed into four bytes
+/// is what a 2:10:10:10 sample is, and `NM32` is the 8-bit-with-padding 
format vino has no use for.
+const DMA_FORMAT_NM30: u8 = 3;
+
+/// Known CP `sub` identifiers used to validate a decrypted header.
+fn is_known_sub(sub: u16) -> bool {
+    matches!(
+        sub,
+        0x00 | 0x04
+            | 0x0b
+            | 0x0c
+            | 0x10
+            | 0x20
+            | 0x21
+            | 0x22
+            | 0x24
+            | 0x25
+            | 0x2a
+            | 0x30
+            | 0x31
+            | 0x41
+            | 0x42
+            | 0x43
+            | 0x45
+            | 0x4a
+            | 0x4b
+            | 0x4c
+            | 0x75
+            | 0x84
+            | 0x86
+    )
+}
+
+/// Return the supported dock-to-host RIVs in reply-preference order.
+///
+/// The first pair uses the direction bit preferred by interactive replies. 
The second pair covers
+/// firmware which replies using the outgoing RIV. Within each pair, byte 0 
bit 7 selects the
+/// connector.
+fn inbound_reply_rivs(out_riv: &[u8; 8]) -> [[u8; 8]; 4] {
+    let in_head0 = in_riv(out_riv);
+    let mut in_head1 = in_head0;
+    in_head1[0] ^= 0x80;
+    let out_head0 = *out_riv;
+    let mut out_head1 = out_head0;
+    out_head1[0] ^= 0x80;
+    [in_head0, in_head1, out_head0, out_head1]
+}
+
+/// Try the supported RIV variants and return the best-scoring inner header 
and prefix.
+///
+/// Interactive replies use [`in_riv`], while capability replies can use the 
outgoing RIV.
+/// Flipping bit 7 of byte 0 selects the second connector.
+pub(super) fn decode_any(
+    ks: &[u8; 16],
+    out_riv: &[u8; 8],
+    wire: &[u8],
+) -> Option<(&'static str, u16, u16, u16, [u8; 24])> {
+    if wire.len() <= 16 {
+        return None;
+    }
+    let seq = u32::from_le_bytes([wire[12], wire[13], wire[14], wire[15]]);
+    let body = &wire[16..];
+    let rivs = inbound_reply_rivs(out_riv);
+    let variants: [(&'static str, [u8; 8]); 4] = [
+        ("out/h0", rivs[2]),
+        ("in/h0", rivs[0]),
+        ("out/h1", rivs[3]),
+        ("in/h1", rivs[1]),
+    ];
+    let mut best: Option<(i32, &'static str, u16, u16, u16, [u8; 24])> = None;
+    for (tag, riv) in variants {
+        let Ok(plaintext) = open_in(ks, &riv, seq, body) else {
+            continue;
+        };
+        if plaintext.len() < 8 {
+            continue;
+        }
+        let id = u16::from_le_bytes([plaintext[0], plaintext[1]]);
+        let sub = u16::from_le_bytes([plaintext[2], plaintext[3]]);
+        let ctr = u16::from_le_bytes([plaintext[4], plaintext[5]]);
+        let pad = u16::from_le_bytes([plaintext[6], plaintext[7]]);
+        let mut sc = 0i32;
+        if is_known_sub(sub) {
+            sc += 50;
+        }
+        if pad == 0 {
+            sc += 10;
+        }
+        if ctr < 0x400 {
+            sc += 5;
+        }
+        if best.map_or(true, |b| sc > b.0) {
+            // Retain enough plaintext to identify the decoded message class.
+            let mut sample = [0u8; 24];
+            let n = plaintext.len().min(24);
+            sample[..n].copy_from_slice(&plaintext[..n]);
+            best = Some((sc, tag, id, sub, ctr, sample));
+        }
+    }
+    best.map(|(_, tag, id, sub, ctr, sample)| (tag, id, sub, ctr, sample))
+}
+/// Verify a dock-to-host `sub=0x45` acknowledgment for the active session.
+///
+/// The wire tag alone is insufficient because status frames also use 
`sub=0x45`. A valid
+/// acknowledgment must decrypt to a small id, a known sub-id and a zero 
header pad.
+///
+/// Firmware revisions use both the outgoing RIV and its byte-7-bit-0 variant 
for replies, with
+/// byte-0-bit-7 selecting the connector, so all four combinations are checked.
+pub(super) fn verify_in_ack(
+    ks: &[u8; 16],
+    out_riv: &[u8; 8],
+    wire: &[u8],
+) -> Option<(u16, u16, u16)> {
+    if wire.len() <= 16 || u16::from_le_bytes([wire[8], wire[9]]) != 0x45 {
+        return None;
+    }
+    let seq = u32::from_le_bytes([wire[12], wire[13], wire[14], wire[15]]);
+    let body = &wire[16..];
+    for riv in inbound_reply_rivs(out_riv) {
+        let Ok(plaintext) = open_in(ks, &riv, seq, body) else {
+            continue;
+        };
+        if plaintext.len() < 8 {
+            continue;
+        }
+        let id = u16::from_le_bytes([plaintext[0], plaintext[1]]);
+        let sub = u16::from_le_bytes([plaintext[2], plaintext[3]]);
+        let ctr = u16::from_le_bytes([plaintext[4], plaintext[5]]);
+        let pad = u16::from_le_bytes([plaintext[6], plaintext[7]]);
+        if id < 0x400 && is_known_sub(sub) && pad == 0 {
+            return Some((id, sub, ctr));
+        }
+    }
+    None
+}
+
+/// Lenient sibling of [`verify_in_ack`] that also accepts uncatalogued 
sub-ids.
+///
+/// This distinguishes a valid message using a newly observed sub-id from a 
frame that cannot be
+/// decrypted under any supported RIV variant.
+/// Recover a dock->host frame's inner plaintext, whichever framing it used.
+///
+/// Ridge seals every reply as wire `sub=0x45`. Navarro also pushes frames 
framed in the clear as
+/// wire `sub=0x25`, with the inner message at offset 16 and nothing to 
decrypt.
+pub(super) fn inner_plaintext(ks: &[u8; 16], out_riv: &[u8; 8], wire: &[u8]) 
-> Option<KVec<u8>> {
+    if wire.len() <= 16 {
+        return None;
+    }
+    match u16::from_le_bytes([wire[8], wire[9]]) {
+        0x25 => {
+            let mut plaintext = KVec::with_capacity(wire.len() - 16, 
GFP_KERNEL).ok()?;
+            plaintext.extend_from_slice(&wire[16..], GFP_KERNEL).ok()?;
+            Some(plaintext)
+        }
+        0x45 => {
+            let seq = u32::from_le_bytes([wire[12], wire[13], wire[14], 
wire[15]]);
+            for riv in inbound_reply_rivs(out_riv) {
+                let Ok(plaintext) = open_in(ks, &riv, seq, &wire[16..]) else {
+                    continue;
+                };
+                // The verified Dl3Cmac identifies a genuine frame. Inner 
offsets 6..7 must not be
+                // tested as padding: Navarro stores connector selector bits 
there for the third
+                // and fourth per-connector HDCP bursts, and rejecting on them 
dropped those
+                // connectors' authentic pushes.
+                if plaintext.len() >= 8 {
+                    return Some(plaintext);
+                }
+            }
+            None
+        }
+        _ => None,
+    }
+}
+
+/// The dock's own log line carried by a `sub=0x0c` push, as printable ASCII.
+///
+/// The dock reports what it is doing, and what it refuses, on this channel. 
Recovering it costs
+/// one pass over an already-decrypted frame and is the only account of a 
fault the dock does not
+/// otherwise report.
+pub(super) fn dock_trace_line(inner: &[u8]) -> Option<KVec<u8>> {
+    if inner.len() < 10 || u16::from_le_bytes([inner[2], inner[3]]) != 0x000c {
+        return None;
+    }
+    let mut out = KVec::new();
+    for &b in &inner[8..] {
+        if b == 0 {
+            continue;
+        }
+        if !(0x20..0x7f).contains(&b) {
+            continue;
+        }
+        out.push(b, GFP_KERNEL).ok()?;
+    }
+    if out.len() < 4 {
+        return None;
+    }
+    Some(out)
+}
+
+pub(super) fn decode_in_lenient(
+    ks: &[u8; 16],
+    out_riv: &[u8; 8],
+    wire: &[u8],
+) -> Option<(u16, u16, u16)> {
+    if wire.len() <= 16 || u16::from_le_bytes([wire[8], wire[9]]) != 0x45 {
+        return None;
+    }
+    let seq = u32::from_le_bytes([wire[12], wire[13], wire[14], wire[15]]);
+    let body = &wire[16..];
+    for riv in inbound_reply_rivs(out_riv) {
+        let Ok(plaintext) = open_in(ks, &riv, seq, body) else {
+            continue;
+        };
+        if plaintext.len() < 8 {
+            continue;
+        }
+        let id = u16::from_le_bytes([plaintext[0], plaintext[1]]);
+        let sub = u16::from_le_bytes([plaintext[2], plaintext[3]]);
+        let ctr = u16::from_le_bytes([plaintext[4], plaintext[5]]);
+        // Navarro's device-log/status replies use session-varying IDs beyond 
the old catalogued
+        // range (the captured transaction boundary replies with 
id=0x0405/sub=0x000c). Its
+        // per-connector HDCP pushes also use bytes 4--7 as a one-hot 32-bit 
selector, so `ctr` is
+        // only an echo counter for actual request/reply classes and bytes 
6..7 need not be zero --
+        // `open_in` has already authenticated the whole ciphertext, so no 
plaintext plausibility
+        // restriction is needed or wanted here.
+        return Some((id, sub, ctr));
+    }
+    None
+}
+/// One decoded downstream-HDCP push carried inside the interactive control 
session.
+///
+/// The vendor wrapper pads all of the short HDCP messages to a fixed inner 
size, so callers must
+/// interpret the payload according to `msg_id`; `payload_len` is the 
available padded region, not
+/// a claim that every byte belongs to the HDCP message.  The largest value 
needed by the current
+/// authentication verifier is H'/L'/M' (32 bytes).
+#[derive(Clone, Copy)]
+pub(super) struct PerheadHdcpPush {
+    pub msg_id: u8,
+    pub payload: [u8; 38],
+    pub payload_len: usize,
+}
+
+/// Decode a per-connector HDCP push from either of the two observed vendor 
framings.
+///
+/// Ridge can send the inner body directly in `wsub=0x25`; Navarro seals it as 
`wsub=0x45` with the
+/// live control key. One parser covers both, so L', ReceiverID/V', 
receiver-auth status and M' are
+/// decoded alongside Rrx rather than falling through as generic traffic.
+pub(super) fn per_connector_hdcp_push(
+    ks: &[u8; 16],
+    out_riv: &[u8; 8],
+    wire: &[u8],
+) -> Option<PerheadHdcpPush> {
+    if wire.len() <= 16 {
+        return None;
+    }
+    const SUB_HDCP_RESP: u16 = 0x25;
+    const SUB_SEALED: u16 = 0x45;
+    let wsub = u16::from_le_bytes([wire[8], wire[9]]);
+
+    let copy_push = |inner: &[u8]| -> Option<PerheadHdcpPush> {
+        if inner.len() < 10 {
+            return None;
+        }
+        let sub = u16::from_le_bytes([inner[2], inner[3]]);
+        if sub != 0x84 {
+            return None;
+        }
+        let src = &inner[10..];
+        let n = src.len().min(38);
+        let mut payload = [0u8; 38];
+        payload[..n].copy_from_slice(&src[..n]);
+        Some(PerheadHdcpPush {
+            msg_id: inner[9],
+            payload,
+            payload_len: n,
+        })
+    };
+
+    if wsub == SUB_HDCP_RESP {
+        return copy_push(&wire[16..]);
+    }
+    if wsub != SUB_SEALED {
+        return None;
+    }
+    let seq = u32::from_le_bytes([wire[12], wire[13], wire[14], wire[15]]);
+    let body = &wire[16..];
+    for riv in inbound_reply_rivs(out_riv) {
+        let Ok(inner) = open_in(ks, &riv, seq, body) else {
+            continue;
+        };
+        if let Some(push) = copy_push(&inner) {
+            return Some(push);
+        }
+    }
+    None
+}
+
+// All three cursor messages share one 32-byte inner layout:
+// off0..7 id/sub/counter header
+// off8..21 zero
+// off22 0x02 constant marker
+// off23 connector_id (0 / 1 across the cold-ref's two monitors)
+// off24..25 field1 LE u16 (create: width / move: X / image: 0)
+// off26..27 field2 LE u16 (create: height / move: Y / image: 0)
+// off28..31 zero
+// Cursor images append their w*h*4 BGRA bitmap at off32 and set the high-byte 
flag in id 0x401c.
+
+/// off23 is the cursor's visible flag, not a message-kind tag: set to show 
the cursor, clear to
+/// hide it. The bitmap-bearing messages carry it clear because an upload is 
not itself a show.
+/// Offset-23 visibility flag of the cursor messages.
+const CURSOR_VISIBLE: u8 = 0x01;
+const CURSOR_HIDDEN: u8 = 0x00;
+
+/// Compute the 16-byte DisplayLink Dl3Cmac control-message integrity tag:
+/// `tag = AES-CMAC(ks, mac_nonce(8) || BE64(wire_seq) || ciphertext)` where
+/// - `mac_nonce` = the AES-CTR content nonce (`riv`) with `byte0 ^= 0x80`. 
Pass the CTR `riv`
+///   and this function applies the byte-0 transform.
+/// - `wire_seq` = the AES-CTR block counter (frame header off-12), 
zero-extended to BE64,
+/// - `ciphertext` = the AES-CTR ciphertext content (encrypt-then-MAC), tag 
appended IN CLEAR.
+///
+/// The Dl3Cmac key is the session key `ks`; the CTR and CMAC nonces differ by 
byte-0 bit 7.
+pub(super) fn dl3cmac_tag(
+    ks: &[u8; 16],
+    riv: &[u8; 8],
+    wire_seq: u64,
+    ciphertext: &[u8],
+) -> Result<[u8; 16]> {
+    let mut mac_nonce = *riv;
+    mac_nonce[0] ^= 0x80;
+    let mut buf = KVec::with_capacity(16 + ciphertext.len(), GFP_KERNEL)?;
+    buf.extend_from_slice(&mac_nonce, GFP_KERNEL)?;
+    buf.extend_from_slice(&wire_seq.to_be_bytes(), GFP_KERNEL)?;
+    buf.extend_from_slice(ciphertext, GFP_KERNEL)?;
+    Ok(crypto::aes_cmac(ks, &buf))
+}
+/// Seal a CP message with AES-CTR followed by a freshly computed Dl3Cmac.
+///
+/// `content_pt` excludes the 16-byte tag. The clear wire header supplies the 
sequence counter.
+pub(super) fn seal_livemac(
+    ks: &[u8; 16],
+    riv: &[u8; 8],
+    header: &[u8],
+    content_pt: &[u8],
+) -> Result<KVec<u8>> {
+    let seq = u32::from_le_bytes([header[12], header[13], header[14], 
header[15]]);
+    let cipher = crypto::Aes128::new(ks)?;
+    let mut ct = KVec::with_capacity(content_pt.len(), GFP_KERNEL)?;
+    for (i, chunk) in content_pt.chunks(16).enumerate() {
+        let mut iv = [0u8; 16];
+        iv[..8].copy_from_slice(riv);
+        iv[12..].copy_from_slice(&seq.wrapping_add(i as u32).to_be_bytes());
+        let ksb = cipher.encrypt_block(&iv);
+        for (j, &p) in chunk.iter().enumerate() {
+            ct.push(p ^ ksb[j], GFP_KERNEL)?;
+        }
+    }
+    let tag = dl3cmac_tag(ks, riv, seq as u64, &ct)?;
+    let mut frame = KVec::with_capacity(16 + ct.len() + 16, GFP_KERNEL)?;
+    frame.extend_from_slice(&header[..16], GFP_KERNEL)?;
+    frame.extend_from_slice(&ct, GFP_KERNEL)?;
+    frame.extend_from_slice(&tag, GFP_KERNEL)?;
+    Ok(frame)
+}
+/// Build a fully sealed interactive CP frame (`type=4 sub=0x24`) at 
`wire_seq` over `content`
+/// (the inner plaintext, WITHOUT any appended 16-byte tag placeholder): the 
16-byte wire
+/// header -- size, `type=4`, `sub=0x24`, the per-`id` [`aux_for_id`] field, 
and `wire_seq` --
+/// followed by [`seal_livemac`] (AES-CTR ciphertext + appended live Dl3Cmac). 
Shared by the
+/// bring-up live loop ([`VinoDriver::send_live_cp`]) and the runtime KMS 
senders
+/// ([`drm_sink::VinoDrmData::send_cp`]) so both produce a byte-identical wire 
frame.
+pub(super) fn seal_interactive(
+    ks: &[u8; 16],
+    riv: &[u8; 8],
+    id: u16,
+    wire_seq: u32,
+    content: &[u8],
+) -> Result<KVec<u8>> {
+    let body_len = content.len() + 16; // AES-CTR ciphertext + 16-byte Dl3Cmac
+    let size = ((16 + body_len) - 4) as u16;
+    let aux = aux_for_id(id, 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());
+    hdr[12..16].copy_from_slice(&wire_seq.to_le_bytes());
+    seal_livemac(ks, riv, &hdr, content)
+}
+/// Return the wire-header auxiliary value for an inner message id.
+///
+/// Known ids use protocol constants rather than the message length. Unknown 
ids fall back to the
+/// body length in dwords.
+pub(super) fn aux_for_id(id: u16, body_len: usize) -> u16 {
+    match id {
+        0x14 => 0x0a,
+        0x15 => 0x09,
+        0x16 => 0x08,
+        0x19 => 0x05,
+        0x1a => 0x04, // cursor move
+        0x1b => 0x03, // cursor create
+        0x1c => 0x02, // cursor image
+        0x1e => 0x00, // Navarro RTC synchronization
+        0x1f => 0x0f,
+        0x22 => 0x0c,
+        0x26 => 0x08,
+        0x2a => 0x04,
+        0x32 => 0x0c,
+        0x36 => 0x08, // DDC/CI write
+        0x48 => 0x06,
+        0x9a => 0x04,
+        _ => (body_len / 4) as u16,
+    }
+}
+/// Per-connector downstream repeater authentication and stream-open sequence.
+///
+/// Each entry is `(id, sub, plaintext length)` before [`seal_interactive`] 
appends the Dl3Cmac.
+/// The AKE entries carry the HDCP message id at offset 27 and its payload at 
offset 28. The driver
+/// derives a self-consistent HDCP 2.2 chain independently for each connector.
+///
+/// [`VinoDriver::send_cp_setup`]: super::VinoDriver::send_cp_setup
+pub(super) const CP_SETUP_PER_HEAD: [(u16, u16, usize); 9] = [
+    (0x0022, 0x0010, 48),  // AKE_Init -- msg-id 0x02 @off27, 20B random 
payload
+    (0x001f, 0x0010, 48),  // AKE_Transmitter_Info -- msg-id 0x13, fixed 00 06 
02 00 02 prefix
+    (0x009a, 0x0010, 160), // AKE_No_Stored_km -- msg-id 0x04, 132B payload 
(10 AES blocks)
+    (0x0022, 0x0010, 48),  // LC_Init -- msg-id 0x09 @off27, 20B random payload
+    (0x0032, 0x0010, 64),  // per-connector VIDEO KEY -- msg-id 0x0b, fresh 
32B key @off28, stashed
+    (0x002a, 0x0010, 48),  // LC_Send_L_prime -- msg-id 0x0f @off27, 20B 
random payload
+    // RepeaterAuth_Stream_Manage -- built by `stream_manage_restatement`.
+    (0x0026, 0x0010, 48),
+    (0x0014, 0x0030, 32), // per-connector stream-open ctl -- no marker/tag, 
10B random @off22
+    (0x0019, 0x0031, 32), // per-connector strm2 -- connector @off22, fixed 06 
[connector*4] 04 @off24
+];
+/// Layout of a restatement record, as the vendor's own message assembler 
writes it.
+///
+/// It allocates the record, stores a connector selector as a `u32`, a flag 
byte, and then copies
+/// an HDCP message -- its id byte first, its payload after -- to a fixed 
offset. Naming the four
+/// positions once keeps every builder below describing the same record rather 
than each repeating
+/// a different set of literals.
+pub(super) mod restatement {
+    /// `u32` connector selector. The upstream authentication uses `0x30`; a 
downstream connector
+    /// uses its one-based index, which puts `1` or `2` in the selector's 
second byte.
+    pub(super) const SELECTOR: usize = 22;
+    /// The HDCP message id, the first byte of the copied message.
+    pub(super) const HDCP_ID: usize = 27;
+    /// The HDCP payload, everything the message carries after its id.
+    pub(super) const PAYLOAD: usize = 28;
+
+    /// How far an HDCP message with a `payload_len`-byte payload reaches into 
the record.
+    ///
+    /// This is where the message *ends*, not how long the record is. The 
vendor assembles the
+    /// message into an allocation of exactly this size and then sends it 
inside a larger fixed
+    /// record, so everything past this offset is untouched allocation -- on 
its side heap
+    /// metadata, on ours a fresh token. The record length itself is per 
message class and comes
+    /// from the wire.
+    pub(super) const fn message_end(payload_len: usize) -> usize {
+        PAYLOAD + payload_len
+    }
+}
+
+/// Build a `RepeaterAuth_Stream_Manage` restatement for one connector.
+///
+/// The payload is the HDCP one: a zero `seq_num_M`, a stream count, and that 
many content-stream
+/// ids. One stream per connector, so the record ends after the first id -- 
there is nothing after
+/// it to fill, and appending anything makes the record longer than the 
message it carries.
+pub(super) fn stream_manage_restatement(
+    counter: u16,
+    connector: u8,
+    stream_id: u16,
+    onehot: bool,
+) -> Result<KVec<u8>> {
+    use restatement::*;
+    // seq_num_M, stream count, one stream id.
+    const PAYLOAD_LEN: usize = 4 + 4 + 4;
+    // The record is 48 bytes on the wire whatever the message inside it 
needs; the vendor's own
+    // is the same size and carries whatever its allocation held past 
`message_end`.
+    let mut b = KVec::from_elem(0u8, 48, GFP_KERNEL)?;
+    b[0..2].copy_from_slice(&0x0026u16.to_le_bytes());
+    b[2..4].copy_from_slice(&0x0010u16.to_le_bytes());
+    b[4..6].copy_from_slice(&counter.to_le_bytes());
+    connector_marker(&mut b, connector, onehot);
+    b[HDCP_ID] = ake::id::REPEATERAUTH_STREAM_MANAGE;
+    // `seq_num_M` stays zero at PAYLOAD..PAYLOAD + 4.
+    b[PAYLOAD + 4..PAYLOAD + 8].copy_from_slice(&1u32.to_le_bytes());
+    b[PAYLOAD + 8..PAYLOAD + 
12].copy_from_slice(&u32::from(stream_id).to_le_bytes());
+    let mut past_message = [0u8; 48 - message_end(PAYLOAD_LEN)];
+    rng::fill(&mut past_message);
+    b[message_end(PAYLOAD_LEN)..].copy_from_slice(&past_message);
+    Ok(b)
+}
+
+/// Write a per-connector record's connector selector.
+///
+/// Ridge names the connector by a one-based connector number at offset 23. 
Navarro sets a one-hot
+/// bit at offset `22 + connector`, which is why it can address four 
connectors where Ridge
+/// addresses two.
+pub(super) fn connector_marker(content: &mut [u8], connector: u8, onehot: 
bool) {
+    if onehot {
+        if let Some(byte) = content.get_mut(restatement::SELECTOR + connector 
as usize) {
+            *byte = 0x80;
+        }
+    } else if let Some(byte) = content.get_mut(restatement::SELECTOR + 1) {
+        *byte = connector + 1;
+    }
+}
+/// Stream-finalization sequence sent after both [`CP_SETUP_PER_HEAD`] blocks.
+///
+/// Each tuple is `(id, sub, value at offset 22)`. Finalization messages are 
32 bytes, use
+/// `0x01` at offset 23 for `sub=0x4c`, and end with a fresh token.
+pub(super) const CP_SETUP_FINALIZE_STEPS: [(u16, u16); 3] =
+    [(0x0016, 0x004c), (0x0015, 0x004a), (0x0016, 0x004c)];
+
+/// Video-channel arm sequence prepended to the first frame on each 
connector's bulk endpoint.
+///
+/// Entries are `(wire type, connector-0 sub-id, auxiliary value, body 
length)`; the connector index
+/// is added to the sub-id. Entries 0, 1, 4 and 5 are plaintext. Entries 6 and 
7 are fixed type-4
+/// records containing a tag over an empty payload. Entries 2, 3, 8 and 9 are 
sealed with the
+/// per-connector video key and share one block-counter sequence. The final 
pair carries the decoder
+/// configuration.
+///
+/// The complete arm sequence and the first encoded frame must be submitted in 
one URB. Splitting
+/// them leaves the video endpoint unarmed.
+pub(super) const VIDEO_ARM_BURST: [(u32, u16, u16, usize); 10] = [
+    (2, 0x0008, 0x0000, 16),   // #0 plaintext: body 08 00 06
+    (2, 0x0018, 0x0000, 16),   // #1 plaintext: body 08 00 16
+    (4, 0x0008, 0x000a, 16),   // #2 SEALED 16B, per-connector video key, seq 0
+    (4, 0x0018, 0x000a, 16),   // #3 SEALED 16B, per-connector video key, seq 1
+    (2, 0x0000, 0x0000, 16),   // #4 plaintext: body 00
+    (2, 0x0010, 0x0000, 16),   // #5 plaintext: body 00 00 10
+    (4, 0x0000, 0x0004, 16),   // #6 type=4 FIXED plaintext 0a 00 04 ... (sub 
0x00, unsealed)
+    (4, 0x0010, 0x0004, 16),   // #7 type=4 FIXED plaintext 0a 00 04 ... (sub 
0x10, unsealed)
+    (4, 0x0008, 0x000e, 1104), // #8 sealed decoder configuration, seq 2
+    (4, 0x0018, 0x000e, 1104), // #9 sealed decoder configuration, seq 71
+];
+
+/// Build the fully-known 16-byte plaintext body for one of 
[`VIDEO_ARM_BURST`]'s `wire_type==2`
+/// entries at table index `i`, for connector `h`.
+pub(super) fn video_arm_plaintext_body(i: usize, h: u16) -> [u8; 16] {
+    let mut b = [0u8; 16];
+    match i {
+        0 => {
+            b[0..2].copy_from_slice(&(0x0008u16 + h).to_le_bytes());
+            b[2..4].copy_from_slice(&0x0006u16.to_le_bytes());
+        }
+        1 => {
+            b[0..2].copy_from_slice(&(0x0008u16 + h).to_le_bytes());
+            b[2..4].copy_from_slice(&0x0016u16.to_le_bytes());
+        }
+        4 => {
+            b[0..2].copy_from_slice(&h.to_le_bytes());
+        }
+        5 => {
+            b[0..2].copy_from_slice(&h.to_le_bytes());
+            b[2..4].copy_from_slice(&0x0010u16.to_le_bytes());
+        }
+        // Entries 6 and 7 are type-4 records built directly by 
`build_arm_burst_buf`.
+        _ => {}
+    }
+    b
+}
+
+/// Build a fixed 32-byte `wire_type=2` (plaintext) video-arm-burst frame: 
16-byte header
+/// (`size=0x1c`, `type=2`, `sub`, `aux=0`, `seq=0`) + the 16-byte `body`. 
Matches
+/// [`VIDEO_ARM_BURST`]'s plaintext entries byte-exact.
+pub(super) fn video_arm_plain_frame(sub: u16, body: &[u8; 16]) -> [u8; 32] {
+    let mut f = [0u8; 32];
+    super::video::haar::record_header(&mut f, 2, sub, 0, 0);
+    f[16..32].copy_from_slice(body);
+    f
+}
+
+/// Build the plaintext record that announces one stream or video plane to the 
dock.
+///
+/// The body names the `sub` a second time and carries a marker: 6 on a 
connector's content-stream
+/// id, 0 on its video `sub`. Every generation sends this pair; they differ 
only in when. A dock
+/// with a video pipe of its own takes them immediately ahead of the first 
frame, and a dock that
+/// shares its control pipe takes them during CP setup, interleaved with the 
per-connector blocks.
+pub(super) fn stream_announce(sub: u16, marker: u16) -> [u8; 32] {
+    let mut body = [0u8; 16];
+    body[0..2].copy_from_slice(&sub.to_le_bytes());
+    body[2..4].copy_from_slice(&marker.to_le_bytes());
+    video_arm_plain_frame(sub, &body)
+}
+
+/// The marker a record announcing a content stream carries; see 
[`stream_announce`].
+pub(super) const STREAM_ANNOUNCE_MARKER: u16 = 6;
+
+/// Build a sealed type-4 video-arm frame from its header fields and plaintext 
content.
+/// The fixed 14-byte stream marker that opens every Navarro video stream 
record.
+///
+/// It is not a normal CP header. The connector is carried solely by the 
*wire* sub, never here:
+/// all four connectors send these same fourteen bytes.
+pub(super) const NAVARRO_STREAM_MARKER: [u8; 14] = [
+    0x04, 0x00, 0x08, 0x04, 0x05, 0x00, 0x06, 0x00, 0x07, 0x01, 0x08, 0x02, 
0x07, 0x00,
+];
+
+/// Build the 16-byte plaintext of a Navarro video stream-open, sent once per 
connector on that
+/// connector's video endpoint before any pixels.
+///
+/// The content is [`NAVARRO_STREAM_MARKER`] followed by a two-byte opaque 
tail. The tail is host
+/// random and differs between observed opens; it is covered by the Dl3Cmac, 
so its length matters
+/// and its value does not.
+pub(super) fn navarro_stream_open() -> [u8; 16] {
+    let mut open = [0u8; 16];
+    open[..14].copy_from_slice(&NAVARRO_STREAM_MARKER);
+    rng::fill(&mut open[14..]);
+    open
+}
+
+/// Build the 16-byte plaintext that opens a connector's sealed video stream 
on a dock whose marker
+/// is six bytes long.
+///
+/// The first four bytes are shared with [`NAVARRO_STREAM_MARKER`]; `kind` is 
the fifth, and is the
+/// only part that differs between generations. The rest is a host-random 
token, which the dock
+/// cannot validate but which the Dl3Cmac covers, so its length is what 
matters.
+pub(super) fn stream_open(kind: u8) -> [u8; 16] {
+    let mut open = [0u8; 16];
+    open[..6].copy_from_slice(&[0x04, 0x00, 0x08, 0x04, kind, 0x00]);
+    rng::fill(&mut open[6..]);
+    open
+}
+
+/// Build the 32-byte plaintext of a per-frame stream report that carries 
nothing but the mode.
+///
+/// A dock that shares its control pipe restates the mode on every report 
rather than only around a
+/// mode change, and has no equivalent of the DL7400's longer report body.
+pub(super) fn stream_report_mode_only(mode_header: &[u8; 26]) -> [u8; 32] {
+    let mut out = [0u8; 32];
+    out[..26].copy_from_slice(mode_header);
+    rng::fill(&mut out[26..]);
+    out
+}
+
+/// Fixed leader of one slot record in a DL7400 pipe descriptor, observed at 
2560x1440.
+const NAVARRO_SLOT_HEADER: [u8; 12] = [
+    0x00, 0x10, 0xb4, 0x00, 0x14, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x00,
+];
+
+/// Fixed trailer of one slot record.
+const NAVARRO_SLOT_TRAILER: [u8; 10] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 
0x00, 0x80, 0x01, 0x09];
+
+/// Slot records per connector, and the connector stride in the dock's slot-id 
space.
+const NAVARRO_SLOTS_PER_CONNECTOR: u16 = 6;
+const NAVARRO_SLOT_STRIDE: u16 = 8;
+
+/// Dock-side addresses each slot record names, as `base - n * step`.
+///
+/// The ring index counts in slot ids, so it skips the two ids each connector 
leaves unused; the
+/// two CFB pools count in allocated slots and do not. Both forms are fixed by 
twelve records
+/// across two independently keyed connectors.
+const NAVARRO_RING_BASE: u32 = 0x6fcc;
+const NAVARRO_RING_STEP: u32 = 0x21c;
+const NAVARRO_CFB0_BASE: u32 = 0x71fb_9000;
+const NAVARRO_CFB0_STEP: u32 = 0x5000;
+const NAVARRO_CFB1_BASE: u32 = 0x7216_6000;
+const NAVARRO_CFB1_STEP: u32 = 0x8000;
+
+/// The dock's slot id for one of a connector's pipe buffers.
+pub(super) fn navarro_pipe_slot(connector: u8, index: u16) -> u16 {
+    (connector as u16) * NAVARRO_SLOT_STRIDE + index
+}
+
+/// The ring address a connector's pipe buffer is given.
+pub(super) fn navarro_pipe_ring(connector: u8, index: u16) -> u32 {
+    NAVARRO_RING_BASE - u32::from(navarro_pipe_slot(connector, index)) * 
NAVARRO_RING_STEP
+}
+
+/// The quiescent body of a DL7400 per-frame stream report, as 
`[len=0x0052][kind=0x000a]` and
+/// thirty-five `u16` values.
+///
+/// DLM sends one of these on a connector's *stream* sub for every frame it 
sends on the frame sub
+/// -- 165 and 306 of them across a 4.3 s and a 4.7 s session, a median 9-19 
ms apart and never
+/// more than ~1.0 s apart. vino sent none, and the dock tore the link down a 
few seconds after
+/// its first frame.
+///
+/// The five-value preamble (`1, 1, 0, 64, 64`) and the trailing zero are 
fixed. The thirty
+/// values between them are three blocks of three `(a, a, b)` triples 
separated by `(1, 1, 1)`,
+/// where the third triple of each block carries twice the `a` of the first 
two. These are the
+/// values DLM sends on a quiescent stream, identical on both connectors in 
both captures; under
+/// load `a` and `b` grow with the frame's cost, but the mapping from a frame 
to them is not
+/// established, so this reports the quiescent set.
+const NAVARRO_STREAM_REPORT: [u16; 42] = [
+    0x0052, 0x000a, // len, kind
+    1, 1, 0, 64, 64, // fixed preamble
+    16, // per-report scalar: 16 quiescent, larger under load
+    16, 16, 16, 16, 16, 16, 32, 32, 32, // block A
+    1, 1, 1, //
+    16, 16, 4, 16, 16, 4, 32, 32, 8, // block B
+    1, 1, 1, //
+    32, 32, 2, 32, 32, 2, 64, 64, 4, // block C
+    0,
+];
+
+/// Build the 84-byte body shared by both forms of the DL7400 per-frame stream 
report.
+fn navarro_stream_report_body(out: &mut [u8; 84]) {
+    for (i, v) in NAVARRO_STREAM_REPORT.iter().enumerate() {
+        out[i * 2..i * 2 + 2].copy_from_slice(&v.to_le_bytes());
+    }
+}
+
+/// Build the 96-byte plaintext of the DL7400's ordinary per-frame stream 
report (`aux=0x000c`).
+///
+/// The report body followed by a 12-byte host-random tail. This is the form 
DLM sends for all but
+/// a handful of frames: 159 of 164 on one connector, 304 of 306 on the other.
+pub(super) fn navarro_stream_report() -> [u8; 96] {
+    let mut out = [0u8; 96];
+    let mut body = [0u8; 84];
+    navarro_stream_report_body(&mut body);
+    out[..84].copy_from_slice(&body);
+    rng::fill(&mut out[84..]);
+    out
+}
+
+/// Build the 112-byte plaintext of the DL7400's mode-restating stream report 
(`aux=0x0002`).
+///
+/// The same body, prefixed by the 26-byte mode header that also opens the 
decoder configuration,
+/// and followed by a two-byte host-random tail. DLM sends this form only a 
handful of times per
+/// session, around a mode change.
+pub(super) fn navarro_stream_report_mode(mode_header: &[u8; 26]) -> [u8; 112] {
+    let mut out = [0u8; 112];
+    out[..26].copy_from_slice(mode_header);
+    let mut body = [0u8; 84];
+    navarro_stream_report_body(&mut body);
+    out[26..110].copy_from_slice(&body);
+    rng::fill(&mut out[110..]);
+    out
+}
+
+/// Build a DL7400 pipe descriptor for one connector.
+///
+/// The 304-byte plaintext is [`NAVARRO_STREAM_MARKER`] twice, then six
+/// `[len=0x002c][kind=0x000e][slot]` records of 40 configuration bytes. 
Records advance by
+/// `len + 2`. Each configuration names the connector's slot id and the three 
dock-side addresses
+/// that slot is given. 14 + 14 + 6 * 46 = 304 exactly, so there is no padding 
and no tail.
+///
+/// The marker count is not a settled constant: one capture has it once 
followed by the six records
+/// and fourteen unexplained bytes, while a capture taken while DLM was 
driving both panels has it
+/// twice and no trailing bytes. Both plaintexts are 304 bytes. This follows 
the capture that was
+/// working, and it is the reason the fourteen bytes must not be dismissed as 
AES padding for *this*
+/// record: in the working capture they are consumed by a second marker at the 
front.
+///
+/// Only 2560x1440 has been observed, and the fixed header carries 
mode-derived bytes, so callers
+/// must not use this for another mode.
+pub(super) fn navarro_pipe_descriptor(connector: u8) -> Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(304, GFP_KERNEL)?;
+    b.extend_from_slice(&NAVARRO_STREAM_MARKER, GFP_KERNEL)?;
+    b.extend_from_slice(&NAVARRO_STREAM_MARKER, GFP_KERNEL)?;
+    for index in 0..NAVARRO_SLOTS_PER_CONNECTOR {
+        let alloc = u32::from((connector as u16) * NAVARRO_SLOTS_PER_CONNECTOR 
+ index);
+        b.extend_from_slice(&0x002cu16.to_le_bytes(), GFP_KERNEL)?;
+        b.extend_from_slice(&0x000eu16.to_le_bytes(), GFP_KERNEL)?;
+        b.extend_from_slice(
+            &navarro_pipe_slot(connector, index).to_le_bytes(),
+            GFP_KERNEL,
+        )?;
+        b.extend_from_slice(&NAVARRO_SLOT_HEADER, GFP_KERNEL)?;
+        b.extend_from_slice(
+            &navarro_pipe_ring(connector, index).to_le_bytes(),
+            GFP_KERNEL,
+        )?;
+        b.extend_from_slice(&[0, 0], GFP_KERNEL)?;
+        let cfb0 = NAVARRO_CFB0_BASE - alloc * NAVARRO_CFB0_STEP;
+        b.extend_from_slice(&cfb0.to_le_bytes(), GFP_KERNEL)?;
+        b.extend_from_slice(&[0, 0, 0, 0], GFP_KERNEL)?;
+        let cfb1 = NAVARRO_CFB1_BASE - alloc * NAVARRO_CFB1_STEP;
+        b.extend_from_slice(&cfb1.to_le_bytes(), GFP_KERNEL)?;
+        b.extend_from_slice(&NAVARRO_SLOT_TRAILER, GFP_KERNEL)?;
+    }
+    debug_assert_eq!(b.len(), 304);
+    debug_assert_eq!(b.len(), 304);
+    Ok(b)
+}
+
+pub(super) fn seal_video_arm(
+    key: &[u8; 16],
+    riv: &[u8; 8],
+    sub: u16,
+    aux: u16,
+    seq: u32,
+    content: &[u8],
+) -> Result<KVec<u8>> {
+    let body_len = content.len() + 16; // AES-CTR ciphertext + 16-byte Dl3Cmac
+    let size = ((16 + body_len) - 4) as u16;
+    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(&sub.to_le_bytes());
+    hdr[10..12].copy_from_slice(&aux.to_le_bytes());
+    hdr[12..16].copy_from_slice(&seq.to_le_bytes());
+    seal_livemac(key, riv, &hdr, content)
+}
+/// Derive the primary dock-to-host CP RIV from the host-to-dock RIV.
+///
+/// The two directions differ by bit 0 of byte 7 on current dock firmware.
+pub(super) fn in_riv(out_riv: &[u8; 8]) -> [u8; 8] {
+    let mut riv = *out_riv;
+    riv[7] ^= 0x01;
+    riv
+}
+/// Authenticate and decrypt a dock->host CP frame body.
+///
+/// `body` is everything after the 16-byte clear wire header: AES-CTR 
ciphertext followed by the
+/// 16-byte clear Dl3Cmac. Inbound messages use the same encrypt-then-MAC 
construction as
+/// [`seal_livemac`]. Verifying the tag is important on Navarro because bytes 
6--7 of the inner
+/// header are not invariably padding: per-connector HDCP pushes put the high 
half of their
+/// one-hot selector there (`00 80` / `80 00`). A zero-padding heuristic 
therefore rejects two
+/// connectors' authentic messages, while accepting arbitrary unauthenticated 
ciphertext with a
+/// chance plaintext prefix would be unsafe.
+pub(super) fn open_in(ks: &[u8; 16], in_riv: &[u8; 8], seq: u32, body: &[u8]) 
-> Result<KVec<u8>> {
+    // Both platforms authenticate an inbound frame with a trailing Dl3Cmac 
over the whole body.
+    // Verifying it is what lets callers read the plaintext without also 
testing it for
+    // plausibility -- and that matters, because Navarro's per-connector HDCP 
pushes carry a
+    // one-hot selector in inner bytes 6..7 that the old "those bytes are zero 
padding" heuristic
+    // rejected.
+    if body.len() < 16 {
+        return Err(EINVAL);
+    }
+    let (ct, wire_tag) = body.split_at(body.len() - 16);
+    let expected = dl3cmac_tag(ks, in_riv, seq as u64, ct)?;
+    // Accumulate the difference so a tag mismatch does not reveal the first 
differing byte.
+    let mut different = 0u8;
+    for (&actual, &want) in wire_tag.iter().zip(expected.iter()) {
+        different |= actual ^ want;
+    }
+    if different != 0 {
+        return Err(EINVAL);
+    }
+
+    let cipher = crypto::Aes128::new(ks)?;
+    let mut plaintext = KVec::with_capacity(ct.len(), GFP_KERNEL)?;
+    for (i, chunk) in ct.chunks(16).enumerate() {
+        let mut iv = [0u8; 16];
+        iv[..8].copy_from_slice(in_riv);
+        iv[12..].copy_from_slice(&seq.wrapping_add(i as u32).to_be_bytes());
+        let ksb = cipher.encrypt_block(&iv);
+        for (j, &c) in chunk.iter().enumerate() {
+            plaintext.push(c ^ ksb[j], GFP_KERNEL)?;
+        }
+    }
+    Ok(plaintext)
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_cp)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn seal_livemac_roundtrip() -> Result {
+        // A sealed CP frame must decrypt back to its content under the IN 
riv, and its
+        // appended tag must equal a fresh Dl3Cmac over the ciphertext 
(encrypt-then-MAC).
+        let ks = [
+            0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 
0xbb, 0xcc, 0xdd,
+            0xee, 0xff,
+        ];
+        let riv = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17];
+        let content = [0xa5u8; 32];
+        let mut hdr = [0u8; 16];
+        hdr[12..16].copy_from_slice(&4u32.to_le_bytes()); // wire_seq = 4
+        let frame = seal_livemac(&ks, &riv, &hdr, &content)?;
+        assert_eq!(frame.len(), 16 + 32 + 16);
+        let body = &frame[16..];
+        let ct = &frame[16..16 + 32];
+        // `open_in` verifies the appended Dl3Cmac, then applies AES-CTR with 
the supplied nonce.
+        assert_eq!(&open_in(&ks, &riv, 4, body)?[..], &content[..]);
+        // And pin that contract rather than leaving it implicit: the IN nonce 
really is different,
+        // so both its MAC nonce and content keystream reject this fixture.
+        assert_ne!(in_riv(&riv), riv);
+        assert!(open_in(&ks, &in_riv(&riv), 4, body).is_err());
+        assert_eq!(&frame[16 + 32..], &dl3cmac_tag(&ks, &riv, 4, ct)?[..]);
+
+        let mut damaged = KVec::new();
+        damaged.extend_from_slice(body, GFP_KERNEL)?;
+        let last = damaged.len() - 1;
+        damaged[last] ^= 1;
+        assert!(open_in(&ks, &riv, 4, &damaged).is_err());
+        Ok(())
+    }
+
+    #[test]
+    fn reply_decoders_accept_all_supported_rivs() -> Result {
+        let ks = [0x5au8; 16];
+        let out_head0 = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17];
+        let in_head0 = in_riv(&out_head0);
+        let mut out_head1 = out_head0;
+        out_head1[0] ^= 0x80;
+        let mut in_head1 = in_head0;
+        in_head1[0] ^= 0x80;
+
+        let mut header = [0u8; 16];
+        header[8..10].copy_from_slice(&0x45u16.to_le_bytes());
+        header[12..16].copy_from_slice(&7u32.to_le_bytes());
+        let inner = [0x14, 0, 0x30, 0, 9, 0, 0, 0];
+
+        for riv in [in_head0, in_head1, out_head0, out_head1] {
+            let frame = seal_livemac(&ks, &riv, &header, &inner)?;
+            assert_eq!(
+                verify_in_ack(&ks, &out_head0, &frame),
+                Some((0x14, 0x30, 9))
+            );
+            assert_eq!(
+                decode_in_lenient(&ks, &out_head0, &frame),
+                Some((0x14, 0x30, 9))
+            );
+        }
+
+        // Navarro's connector 2/3 HDCP pushes carry the upper half of their 
one-hot selector at
+        // inner offsets 6--7. They are authenticated messages, not malformed 
zero-pad headers.
+        let selector_push = [0x10, 0, 0x84, 0, 0, 0, 0, 0x80];
+        let frame = seal_livemac(&ks, &in_head0, &header, &selector_push)?;
+        assert_eq!(
+            decode_in_lenient(&ks, &out_head0, &frame),
+            Some((0x10, 0x84, 0))
+        );
+        assert_eq!(
+            &inner_plaintext(&ks, &out_head0, &frame).unwrap()[..],
+            &selector_push
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn stream_content_nonce_matches_golden_vectors() {
+        // Ridge: each connector's video stream is `0x08 | connector`.
+        let h0 = stream_content_nonce(&[0xa1, 0x2b, 0xaa, 0xb7, 0x0e, 0x0b, 
0x02, 0x74], 0x08);
+        assert_eq!(h0, [0xa1, 0x2b, 0xaa, 0xb7, 0x0e, 0x0b, 0x02, 0x7c]);
+
+        let h1 = stream_content_nonce(&[0xd0, 0x2a, 0xc0, 0x83, 0xb6, 0x42, 
0x72, 0x57], 0x09);
+        assert_eq!(h1, [0xd0, 0x2a, 0xc0, 0x83, 0xb6, 0x42, 0x72, 0x5e]);
+
+        // Navarro: the RIV each connector's SKE_Send_Eks delivered, and the 
AES-CTR nonce the
+        // dock then expects for that connector's stream.
+        let riv = [0x7d, 0x2c, 0xb6, 0x6b, 0x2c, 0xd1, 0x75, 0x7c];
+        let link = stream_content_nonce(&riv, 0x04);
+        assert_eq!(link, [0x7d, 0x2c, 0xb6, 0x6b, 0x2c, 0xd1, 0x75, 0x78]);
+
+        let c0 = stream_content_nonce(&[0xc3, 0x45, 0xfe, 0x55, 0x93, 0x61, 
0x39, 0x01], 0x07);
+        assert_eq!(c0, [0xc3, 0x45, 0xfe, 0x55, 0x93, 0x61, 0x39, 0x06]);
+
+        let c1 = stream_content_nonce(&[0x94, 0x46, 0xc8, 0x3d, 0xa5, 0xfa, 
0x39, 0xe3], 0x0f);
+        assert_eq!(c1, [0x94, 0x46, 0xc8, 0x3d, 0xa5, 0xfa, 0x39, 0xec]);
+    }
+
+    #[test]
+    fn aux_for_id_constants() {
+        // The CP header `aux` field is a per-inner-id constant, not 
body_len/4.
+        assert_eq!(aux_for_id(0x14, 48), 0x0a);
+        assert_eq!(aux_for_id(0x15, 32), 0x09);
+        assert_eq!(aux_for_id(0x36, 80), 0x08);
+        assert_eq!(aux_for_id(0x48, 96), 0x06);
+        // Cursor message IDs have fixed auxiliary fields; deriving them as 
`body_len / 4` would
+        // produce 0x0c for all three.
+        assert_eq!(aux_for_id(0x1a, 48), 0x04); // cursor move
+        assert_eq!(aux_for_id(0x1b, 48), 0x03); // cursor create
+        assert_eq!(aux_for_id(0x1c, 48), 0x02); // cursor image
+        assert_eq!(aux_for_id(0x99, 40), 10); // unknown id falls back to 
body_len/4
+    }
+
+    #[test]
+    fn cp_setup_burst_table_framing() -> Result {
+        // Pin the post-msg0 `(aux, body_len)` wire profile. `body_len` 
includes the encrypted
+        // content and its 16-byte Dl3Cmac tag.
+        const PER_HEAD_FINGERPRINT: [(u16, usize); 9] = [
+            (0x0c, 64),
+            (0x0f, 64),
+            (0x04, 176),
+            (0x0c, 64),
+            (0x0c, 80),
+            (0x04, 64),
+            (0x08, 64),
+            (0x0a, 48),
+            (0x05, 48),
+        ];
+        // Finalization bodies contain 32 bytes of content and a 16-byte tag. 
Keep one fingerprint
+        // per step so table growth cannot cause an out-of-bounds test access.
+        const FINALIZE_FINGERPRINT: [(u16, usize); 3] = [(0x08, 48), (0x09, 
48), (0x08, 48)];
+        // Keep the fingerprint table and the step table in lockstep: growing 
one without the
+        // other is exactly the defect above.
+        build_assert!(FINALIZE_FINGERPRINT.len() == 
CP_SETUP_FINALIZE_STEPS.len());
+
+        let ks = [0x5au8; 16];
+        let riv = [0x11u8; 8];
+        for (i, &(id, _sub, content_len)) in 
CP_SETUP_PER_HEAD.iter().enumerate() {
+            let content = KVec::from_elem(0u8, content_len, GFP_KERNEL)?;
+            let frame = seal_interactive(&ks, &riv, id, 0, &content)?;
+            let (want_aux, want_body) = PER_HEAD_FINGERPRINT[i];
+            assert_eq!(frame.len(), 16 + want_body);
+            assert_eq!(u16::from_le_bytes([frame[10], frame[11]]), want_aux);
+        }
+        for (i, &(id, _sub)) in CP_SETUP_FINALIZE_STEPS.iter().enumerate() {
+            let frame = seal_interactive(&ks, &riv, id, 0, &[0u8; 32])?;
+            let (want_aux, want_body) = FINALIZE_FINGERPRINT[i];
+            assert_eq!(frame.len(), 16 + want_body);
+            assert_eq!(u16::from_le_bytes([frame[10], frame[11]]), want_aux);
+        }
+        Ok(())
+    }
+
+    #[test]
+    fn stream_manage_restatement_matches_dlm() -> Result {
+        // All deterministic fields must match the captured plaintext for both 
connectors. The
+        // connector marker is at offset 23, the HDCP message ID at offset 27, 
and the final three
+        // u32 fields contain `0`, `1`, and `connector + 8`.
+        const WANT: [[u8; 40]; 2] = [
+            [
+                0x26, 0x00, 0x10, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
0, 0, 0, 0x00, 0x00,
+                0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 
0x01, 0x00, 0x00, 0x00,
+                0x08, 0x00, 0x00, 0x00,
+            ],
+            [
+                0x26, 0x00, 0x10, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
0, 0, 0, 0x00, 0x00,
+                0x00, 0x02, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 
0x01, 0x00, 0x00, 0x00,
+                0x09, 0x00, 0x00, 0x00,
+            ],
+        ];
+        for connector in 0..2u8 {
+            // Ridge: the connector is a one-based connector number at offset 
23, and the
+            // content-stream id at offset 36 is 8 for connector 0 and 9 for 
connector 1.
+            let c = stream_manage_restatement(0, connector, 8 + 
u16::from(connector), false)?;
+            assert_eq!(c.len(), 48);
+            // Bytes 4..6 are the live counter (passed as 0 here, so already 
covered); the last
+            // 8 bytes (offset 40..48) are host-random.
+            assert_eq!(&c[..40], &WANT[connector as usize][..]);
+        }
+        Ok(())
+    }
+
+    #[test]
+    fn video_arm_burst_table_framing() -> Result {
+        // Pin every video-arm entry's type, sub-ID, auxiliary value, and body 
length to captured
+        // traffic. Head 0 uses the table's base sub-IDs; the builders add one 
for connector 1. The
+        // compile-time length check prevents the fixture and production table 
from drifting.
+        const FINGERPRINT_H0: [(u32, u16, u16, usize); 10] = [
+            (2, 0x0008, 0x0000, 16),
+            (2, 0x0018, 0x0000, 16),
+            (4, 0x0008, 0x000a, 16),
+            (4, 0x0018, 0x000a, 16),
+            (2, 0x0000, 0x0000, 16),
+            (2, 0x0010, 0x0000, 16),
+            (4, 0x0000, 0x0004, 16),
+            (4, 0x0010, 0x0004, 16),
+            (4, 0x0008, 0x000e, 1104),
+            (4, 0x0018, 0x000e, 1104),
+        ];
+        build_assert!(FINGERPRINT_H0.len() == VIDEO_ARM_BURST.len());
+        let ks = [0x5au8; 16];
+        let riv = [0x11u8; 8];
+        for (i, &(wire_type, sub_base, aux, body_len)) in 
VIDEO_ARM_BURST.iter().enumerate() {
+            let (want_type, want_sub, want_aux, want_body) = FINGERPRINT_H0[i];
+            assert_eq!(
+                (wire_type, sub_base, aux, body_len),
+                (want_type, want_sub, want_aux, want_body)
+            );
+            if wire_type == 2 {
+                let body = video_arm_plaintext_body(i, 0);
+                let frame = video_arm_plain_frame(sub_base, &body);
+                assert_eq!(frame.len(), 32);
+                assert_eq!(u16::from_le_bytes([frame[8], frame[9]]), want_sub);
+            } else {
+                let content = KVec::from_elem(0u8, body_len, GFP_KERNEL)?;
+                let frame = seal_video_arm(&ks, &riv, sub_base, aux, 0, 
&content)?;
+                assert_eq!(frame.len(), 16 + body_len + 16);
+                assert_eq!(u16::from_le_bytes([frame[8], frame[9]]), want_sub);
+                assert_eq!(u16::from_le_bytes([frame[10], frame[11]]), 
want_aux);
+            }
+        }
+        Ok(())
+    }
+
+    #[test]
+    fn navarro_stream_open_matches_the_wire() -> Result {
+        // The connector lives solely in the wire sub, never in the content: 
all four connectors
+        // send the same marker, followed by a two-byte opaque tail.
+        let open = navarro_stream_open();
+        assert_eq!(open.len(), 16);
+        assert_eq!(open[..14], NAVARRO_STREAM_MARKER);
+
+        // Sealing it produces the 48-byte frame the dock is sent: a 16-byte 
header, the 16-byte
+        // ciphertext and a 16-byte Dl3Cmac, with `size` covering all but the 
first four bytes.
+        let frame = seal_video_arm(&[0u8; 16], &[0u8; 8], 0x0007, 0x0002, 0, 
&open)?;
+        assert_eq!(frame.len(), 48);
+        assert_eq!(u16::from_le_bytes([frame[2], frame[3]]), 0x002c);
+        assert_eq!(u16::from_le_bytes([frame[8], frame[9]]), 0x0007);
+        assert_eq!(u16::from_le_bytes([frame[10], frame[11]]), 0x0002);
+        Ok(())
+    }
+
+    /// Where each message stops meaning something and starts being filler.
+    ///
+    /// This boundary is the one that has actually cost hardware runs: the 
DL-3x00 cold-activation
+    /// gate was a random tail that began one byte early and buried the 
`0x16/0x23` connector
+    /// selector at offset 23, and nothing on the wire says no -- the dock 
acknowledges the message
+    /// either way and simply does not act on it. Every class below is checked 
against the vendor's
+    /// own corpus, where a byte the vendor varies over two or three values is 
a field and a byte it
+    /// varies uniformly is filler.
+    ///
+    /// Asserts the structured prefix and the total length. The tail itself 
cannot be asserted,
+    /// which is exactly why its start offset has to be.
+    #[test]
+    fn random_tails_begin_where_the_vendor_stops_meaning_something() -> Result 
{
+        // `0x14/0x0c`: nothing after the header; the tail is the whole of 
offsets 22..32.
+        let poll = device_query_req(0x1234, 0x000c)?;
+        assert_eq!(poll.len(), 32);
+        assert!(poll[8..22].iter().all(|&b| b == 0));
+
+        // `0x16/0x2e` and `0x16/0x2f`: connector at 22, state at 23, tail 
from 24. The vendor's
+        // corpus shows exactly two values at each -- connector, and the sink 
state.
+        for (sub, state) in [(0x2eu16, 3u8), (0x2e, 0), (0x2f, 1), (0x2f, 0)] {
+            for connector in 0..2u8 {
+                let m = stream_marker(0x1234, connector, sub, state)?;
+                assert_eq!(m.len(), 32);
+                assert!(m[8..22].iter().all(|&b| b == 0));
+                assert_eq!(m[22], connector);
+                assert_eq!(m[23], state);
+            }
+        }
+
+        // `0x15/0x20` and `0x15/0x21`: connector at 22 alone, tail from 23.
+        for sub in [0x20u16, 0x21] {
+            for connector in 0..2u8 {
+                let m = cp::get_edid_req_sub(0x1234, sub, connector)?;
+                assert_eq!(m.len(), 32);
+                assert_eq!(m[22], connector);
+            }
+        }
+
+        // `0x16/0x23`: the one that was wrong. Both bytes are selectors, and 
a tail that starts
+        // at 22 instead of 24 silently disables the downstream sink enable.
+        for connector in 0..2u8 {
+            let m = cp::edid_engage_req(0x1234, connector)?;
+            assert_eq!(m.len(), 32);
+            assert_eq!(m[22], connector);
+            assert_eq!(m[23], connector);
+        }
+        Ok(())
+    }
+
+    #[test]
+    fn stream_marker_routes_the_selected_head() -> Result {
+        let h0 = stream_marker(0x1234, 0, 0x2f, 1)?;
+        let h1 = stream_marker(0x1235, 1, 0x2e, 3)?;
+        assert_eq!(&h0[0..6], &[0x16, 0, 0x2f, 0, 0x34, 0x12]);
+        assert_eq!(&h0[22..24], &[0, 1]);
+        assert_eq!(&h1[0..6], &[0x16, 0, 0x2e, 0, 0x35, 0x12]);
+        assert_eq!(&h1[22..24], &[1, 3]);
+        Ok(())
+    }
+
+    #[test]
+    fn navarro_pipe_descriptor_matches_authenticated_capture() -> Result {
+        // Slot ids and the three dock-side addresses of every record, for 
both connectors of the
+        // authenticated capture.
+        for (connector, slots) in [
+            (
+                0u8,
+                [
+                    (0x0000u16, 0x6fccu32, 0x71fb_9000u32, 0x7216_6000u32),
+                    (0x0001, 0x6db0, 0x71fb_4000, 0x7215_e000),
+                    (0x0002, 0x6b94, 0x71fa_f000, 0x7215_6000),
+                    (0x0003, 0x6978, 0x71fa_a000, 0x7214_e000),
+                    (0x0004, 0x675c, 0x71fa_5000, 0x7214_6000),
+                    (0x0005, 0x6540, 0x71fa_0000, 0x7213_e000),
+                ],
+            ),
+            (
+                1u8,
+                [
+                    (0x0008, 0x5eec, 0x71f9_b000, 0x7213_6000),
+                    (0x0009, 0x5cd0, 0x71f9_6000, 0x7212_e000),
+                    (0x000a, 0x5ab4, 0x71f9_1000, 0x7212_6000),
+                    (0x000b, 0x5898, 0x71f8_c000, 0x7211_e000),
+                    (0x000c, 0x567c, 0x71f8_7000, 0x7211_6000),
+                    (0x000d, 0x5460, 0x71f8_2000, 0x7210_e000),
+                ],
+            ),
+        ] {
+            let descriptor = navarro_pipe_descriptor(connector)?;
+            assert_eq!(descriptor.len(), 304);
+            // The marker is present twice before the slot records; 14 + 14 + 
6 * 46 = 304. Assert
+            // both copies, so the records are read from 28 rather than from 
the second marker.
+            assert_eq!(&descriptor[..14], &NAVARRO_STREAM_MARKER);
+            assert_eq!(&descriptor[14..28], &NAVARRO_STREAM_MARKER);
+            for (index, &(slot, ring, plane0, plane1)) in 
slots.iter().enumerate() {
+                let at = 28 + index * 46;
+                assert_eq!(&descriptor[at..at + 4], &[0x2c, 0x00, 0x0e, 0x00]);
+                assert_eq!(
+                    u16::from_le_bytes([descriptor[at + 4], descriptor[at + 
5]]),
+                    slot
+                );
+                let cfg = &descriptor[at + 6..at + 46];
+                let word =
+                    |o: usize| u32::from_le_bytes([cfg[o], cfg[o + 1], cfg[o + 
2], cfg[o + 3]]);
+                assert_eq!(word(12), ring);
+                assert_eq!(word(18), plane0);
+                assert_eq!(word(26), plane1);
+            }
+        }
+
+        // The decoder configuration is the same message Ridge sends, with the 
DL7400's layout word.
+        let tail = [0x5a; 14];
+        let header = video_arm::mode_header(2560, 1440, 0x2100);
+        let config = video_arm::build_config(video_arm::CodeTables::Wide, 
&header, &tail)?;
+        assert_eq!(config.len(), 1104);
+        assert_eq!(
+            &config[..26],
+            &[
+                0x18, 0x00, 0x0b, 0x03, 0x04, 0x02, 0x02, 0x00, 0x02, 0x00, 
0x00, 0x0a, 0xa0, 0x05,
+                0x00, 0x21, 0x02, 0x00, 0x00, 0x0a, 0xa0, 0x05, 0x00, 0x21, 
0x00, 0x00,
+            ]
+        );
+        assert_eq!(&config[1090..], &tail);
+        Ok(())
+    }
+
+    #[test]
+    fn ella_stream_records_match_the_captured_bytes() -> Result {
+        // The three records that open a DL-3x00 stream, each pinned to the 
bytes DLM sends. A
+        // stream opened with any of them wrong is a stream the dock accepts 
every frame of and
+        // presents none of, with nothing on the wire to say so -- so these 
are checked here rather
+        // than on hardware, where each attempt costs a replug.
+        let geometry = video::haar::Geometry::new(8, true, false, 0, 0x08, 3);
+
+        // Announcing the content stream, then the video plane. Both 
connectors, both markers.
+        for (connector, stream, plane) in [(0u8, 0x08u16, 0x00u16), (1, 0x09, 
0x01)] {
+            let announce = stream_announce(stream, STREAM_ANNOUNCE_MARKER);
+            assert_eq!(geometry.stream_id(connector), stream);
+            assert_eq!(
+                &announce[..12],
+                &[0, 0, 0x1c, 0, 2, 0, 0, 0, stream as u8, 0, 0, 0]
+            );
+            assert_eq!(&announce[16..20], &[stream as u8, 0, 6, 0]);
+            assert_eq!(announce[20..], [0u8; 12]);
+
+            let announce = stream_announce(plane, 0);
+            assert_eq!(u16::from(geometry.connector_selector(connector)), 
plane);
+            assert_eq!(
+                &announce[..12],
+                &[0, 0, 0x1c, 0, 2, 0, 0, 0, plane as u8, 0, 0, 0]
+            );
+            assert_eq!(
+                announce[16..],
+                [plane as u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+            );
+        }
+
+        // The sealed open. Only the first six bytes are fixed; the rest is a 
host-random token
+        // that the Dl3Cmac covers, so its length is what matters.
+        let open = stream_open(0x01);
+        assert_eq!(open.len(), 16);
+        assert_eq!(&open[..6], &[0x04, 0x00, 0x08, 0x04, 0x01, 0x00]);
+
+        // The decoder configuration, in full. 1920x1080 is stated as 1088 
lines: the surface the
+        // dock is told about is the padded one the codec actually produces.
+        let header = video_arm::mode_header(1920, 1088, 0x1800);
+        let config = video_arm::build_config(video_arm::CodeTables::Narrow, 
&header, &[])?;
+        assert_eq!(config.len(), 304);
+        assert_eq!(
+            &config[..26],
+            &[
+                0x18, 0x00, 0x0b, 0x03, 0x04, 0x02, 0x02, 0x00, 0x02, 0x00, 
0x80, 0x07, 0x40, 0x04,
+                0x00, 0x18, 0x02, 0x00, 0x80, 0x07, 0x40, 0x04, 0x00, 0x18, 
0x00, 0x00,
+            ]
+        );
+        assert_eq!(
+            &config[26..],
+            &[
+                0x28, 0x00, 0x09, 0x00, 0x12, 0x00, 0x01, 0x00, 0x00, 0x00, 
0x02, 0x00, 0x00, 0x00,
+                0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x10, 0x00, 
0x00, 0x00, 0x20, 0x00,
+                0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 
0x00, 0x01, 0x00, 0x02,
+                0x2c, 0x00, 0x09, 0x01, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 
0x02, 0x00, 0x00, 0x00,
+                0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x10, 0x00, 
0x00, 0x00, 0x20, 0x00,
+                0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 
0x00, 0x01, 0x00, 0x00,
+                0x00, 0x02, 0x00, 0x04, 0x2c, 0x00, 0x09, 0x02, 0x14, 0x00, 
0x01, 0x00, 0x00, 0x00,
+                0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 
0x00, 0x00, 0x10, 0x00,
+                0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 
0x80, 0x00, 0x00, 0x00,
+                0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x04, 0x16, 0x00, 
0x09, 0x03, 0x09, 0x00,
+                0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 
0x00, 0x00, 0x08, 0x00,
+                0x0f, 0x00, 0x02, 0x00, 0x22, 0x00, 0x09, 0x04, 0x0f, 0x00, 
0x01, 0x00, 0x00, 0x00,
+                0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 
0x00, 0x00, 0x10, 0x00,
+                0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x40, 0x00, 0x7f, 0x00, 
0x02, 0x00, 0x52, 0x00,
+                0x0a, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 
0x40, 0x00, 0x10, 0x00,
+                0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 
0x10, 0x00, 0x20, 0x00,
+                0x20, 0x00, 0x20, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 
0x10, 0x00, 0x10, 0x00,
+                0x04, 0x00, 0x10, 0x00, 0x10, 0x00, 0x04, 0x00, 0x20, 0x00, 
0x20, 0x00, 0x08, 0x00,
+                0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x20, 0x00, 0x20, 0x00, 
0x02, 0x00, 0x20, 0x00,
+                0x20, 0x00, 0x02, 0x00, 0x40, 0x00, 0x40, 0x00, 0x04, 0x00, 
0x00, 0x00,
+            ]
+        );
+
+        // The per-frame report on this dock is the mode header and a six-byte 
token, nothing else.
+        let report = stream_report_mode_only(&header);
+        assert_eq!(report.len(), 32);
+        assert_eq!(&report[..26], &header);
+        Ok(())
+    }
+}
diff --git a/drivers/gpu/drm/vino/cp/cursor.rs 
b/drivers/gpu/drm/vino/cp/cursor.rs
new file mode 100644
index 000000000000..036aa66239ba
--- /dev/null
+++ b/drivers/gpu/drm/vino/cp/cursor.rs
@@ -0,0 +1,158 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! The dock-composited cursor.
+//!
+//! A cursor image is one control message carrying the whole 64x64 
premultiplied bitmap. Only a
+//! dock with a video pipe of its own is offered the plane; where control and 
pixels share an
+//! endpoint the vendor sends no cursor message at all and draws the pointer 
into the frame.
+
+use super::*;
+
+/// The dock's connector id at off22 of every cursor message, indexed by 
vino's connector number.
+///
+/// Cursor wire layout (sec 8.6.1). All three messages share the 32-byte inner 
header built by
+/// [`cursor_header`], with the connector selector at off22 and a flag at 
off23.
+///
+/// The selector is a connector bitmask, `1 << connector`; the dock numbers 
its connectors from one,
+/// so `0` is never valid. The two measured entries were `[0x01, 0x02]`, which 
is both `1 <<
+/// connector` and `connector + 1`, so they do not distinguish the two 
readings. They diverge from
+/// connector 2 on, and a connector sent `connector + 1` draws no cursor.
+fn cursor_head_id(connector: u8) -> Result<u8> {
+    if usize::from(connector) >= crate::drm_sink::MAX_CONNECTORS {
+        return Err(EINVAL);
+    }
+    Ok(1u8 << connector)
+}
+/// Common prologue of the cursor messages: the dock-side connector id at 
offset 22 and the
+/// visibility flag at offset 23.
+fn cursor_header(
+    b: &mut KVec<u8>,
+    id: u16,
+    sub: u16,
+    counter: u16,
+    dock_connector: u8,
+    visible: u8,
+) -> Result {
+    header(b, id, sub, counter)?;
+    pad_to(b, 22)?;
+    b.push(dock_connector, GFP_KERNEL)?;
+    b.push(visible, GFP_KERNEL)?;
+    Ok(())
+}
+/// cursor create: `id=0x1b sub=0x42`, advertises `w x h`. Sent once per 
bitmap geometry.
+pub(crate) fn cursor_create(counter: u16, connector: u8, w: u16, h: u16) -> 
Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(32, GFP_KERNEL)?;
+    let dock_connector = cursor_head_id(connector)?;
+    cursor_header(&mut b, 0x1b, 0x42, counter, dock_connector, CURSOR_HIDDEN)?;
+    b.extend_from_slice(&w.to_le_bytes(), GFP_KERNEL)?; // off24..25
+    b.extend_from_slice(&h.to_le_bytes(), GFP_KERNEL)?; // off26..27
+    pad_to(&mut b, 32)?; // off28..31 reserved
+    Ok(b)
+}
+/// cursor move: `id=0x1a sub=0x43`, X at off24 and Y at off26 (LE), for one 
connector.
+pub(crate) fn cursor_move(
+    counter: u16,
+    connector: u8,
+    x: u16,
+    y: u16,
+    visible: bool,
+) -> Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(32, GFP_KERNEL)?;
+    let dock_connector = cursor_head_id(connector)?;
+    let visible_flag = if visible {
+        CURSOR_VISIBLE
+    } else {
+        CURSOR_HIDDEN
+    };
+    cursor_header(&mut b, 0x1a, 0x43, counter, dock_connector, visible_flag)?;
+    b.extend_from_slice(&x.to_le_bytes(), GFP_KERNEL)?; // off24..25
+    b.extend_from_slice(&y.to_le_bytes(), GFP_KERNEL)?; // off26..27
+    pad_to(&mut b, 32)?; // off28..31 reserved
+    Ok(b)
+}
+/// cursor image: inner `id=0x401c sub=0x41` (the `0x40` high-byte flag marks 
the bitmap-bearing
+/// message), a 32-byte header then the bitmap. `w`/`h` come from 
[`cursor_create`].
+///
+/// Pixels are DRM `ARGB8888` (`[B, G, R, A]`, premultiplied) and start at 
off34: off32..33 are
+/// zero and the final pixel is truncated, so the message stays `32 + w*h*4` 
bytes.
+pub(crate) fn cursor_image(
+    counter: u16,
+    connector: u8,
+    w: u16,
+    h: u16,
+    bgra: &[u8],
+) -> Result<KVec<u8>> {
+    // `w*h*4` can wrap a 32-bit `usize` (max ~1.7e10 > u32::MAX), which would 
let an
+    // undersized `bgra` pass the check; compute it with checked arithmetic so 
an
+    // overflow is rejected as a mismatch rather than silently bypassing 
validation.
+    let expected = (w as usize)
+        .checked_mul(h as usize)
+        .and_then(|n| n.checked_mul(4));
+    if expected != Some(bgra.len()) {
+        return Err(EINVAL);
+    }
+    let mut b = KVec::with_capacity(32 + bgra.len(), GFP_KERNEL)?;
+    let dock_connector = cursor_head_id(connector)?;
+    cursor_header(&mut b, 0x401c, 0x41, counter, dock_connector, 
CURSOR_HIDDEN)?;
+    pad_to(&mut b, 32)?; // off24..31 zero (no w/h here)
+    b.extend_from_slice(&[0, 0], GFP_KERNEL)?; // off32..33
+    b.extend_from_slice(&bgra[..bgra.len() - 2], GFP_KERNEL)?; // pixels @ 
off34
+    Ok(b)
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_cp_cursor)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn cursor_messages_structure() -> Result {
+        // Shared 32-byte cursor layout: the dock's connector selector at 22, 
the visible flag at
+        // 23, and two little-endian u16 fields at 24 and 26. Check the 
selector against more than
+        // one connector: `1 << connector` and a fixed byte agree for a single 
connector and diverge
+        // past it. Create (connector 0): id=0x1b sub=0x42, fields = w,h. An 
upload is not a show,
+        // so hidden.
+        let c = cursor_create(7, 0, 64, 64)?;
+        assert_eq!(c.len(), 32);
+        assert_eq!(&c[0..6], &[0x1b, 0x00, 0x42, 0x00, 0x07, 0x00]); // id, 
sub, counter (LE)
+        assert_eq!(c[22], 0x01); // connector 0 -> dock connector 1
+        assert_eq!(c[23], 0x00); // not visible
+        assert_eq!(u16::from_le_bytes([c[24], c[25]]), 64); // width
+        assert_eq!(u16::from_le_bytes([c[26], c[27]]), 64); // height
+
+        // Move (connector 1): id=0x1a sub=0x43, connector@22, visible@23, 
X@24, Y@26 (LE).
+        let m = cursor_move(9, 1, 0x0140, 0x00f0, true)?;
+        assert_eq!(m.len(), 32);
+        assert_eq!(&m[0..4], &[0x1a, 0x00, 0x43, 0x00]); // id, sub
+        assert_eq!(m[22], 0x02); // connector 1 -> dock connector 2
+        assert_eq!(m[23], 0x01); // visible
+        assert_eq!(u16::from_le_bytes([m[24], m[25]]), 0x0140); // X
+        assert_eq!(u16::from_le_bytes([m[26], m[27]]), 0x00f0); // Y
+
+        // Every connector this driver exposes must produce a message. A 
two-entry lookup table left
+        // the DL7400's third and fourth connectors returning `EINVAL`, which 
`cmd_work` drops
+        // rather than retries -- so a monitor in socket 3 or 4 had no 
hardware cursor at all. The
+        // selector is a bitmask, not a one-based index: the original 
two-entry table was `[0x01,
+        // 0x02]`, which is `1 << connector` for the only two connectors that 
dock had.
+        for connector in 0..drm_sink::MAX_CONNECTORS as u8 {
+            let m = cursor_move(1, connector, 0, 0, true)?;
+            assert_eq!(m[22], 1u8 << connector);
+        }
+        assert!(cursor_move(1, drm_sink::MAX_CONNECTORS as u8, 0, 0, 
true).is_err());
+
+        // Image: 32-byte header (inner id 0x401c, the 0x40 bitmap flag) + 
w*h*4 BGRA at off32;
+        // wrong-size input rejected.
+        let bitmap = KVec::from_elem(0xabu8, 64 * 64 * 4, GFP_KERNEL)?;
+        let img = cursor_image(3, 0, 64, 64, &bitmap)?;
+        assert_eq!(img.len(), 32 + 64 * 64 * 4);
+        assert_eq!(&img[0..4], &[0x1c, 0x40, 0x41, 0x00]); // inner id 0x401c, 
sub 0x41
+        assert_eq!(img[22], 0x01); // connector 0 -> dock connector 1
+                                   // The bitmap begins at off34, not off32: 
offsets 32..33 are zero
+                                   // and the last pixel is truncated so the 
message still measures
+                                   // `32 + w*h*4`.
+        assert_eq!(&img[32..34], &[0x00, 0x00]);
+        assert_eq!(img[34], 0xab);
+        assert!(cursor_image(3, 0, 64, 64, &[0u8; 16]).is_err()); // wrong 
bitmap length
+        Ok(())
+    }
+}
diff --git a/drivers/gpu/drm/vino/cp/edid.rs b/drivers/gpu/drm/vino/cp/edid.rs
new file mode 100644
index 000000000000..0e7b765f3546
--- /dev/null
+++ b/drivers/gpu/drm/vino/cp/edid.rs
@@ -0,0 +1,464 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Asking the dock what is plugged into a connector.
+//!
+//! The EDID path is where the dock is least forgiving: the selector at offset 
22 names the
+//! connector, the reply's own id carries its length, and a fetch issued 
before the handler is
+//! engaged returns a block the dock synthesises for itself.
+
+use super::*;
+
+/// OUT `id=0x16 sub=0x0023` downstream-sink state request. Offset 22 selects 
the connector and
+/// offset 23 carries the state. Navarro's cold transcript uses `0xff` to tear 
the sink down, then
+/// the connector selector itself (`0` or `1`) to re-engage it.
+/// Vendor and product id of the descriptor the dock serves for itself.
+///
+/// A fetch the dock cannot answer from the monitor is answered from here, so 
this pair is the only
+/// thing separating that block from a real one.
+const BRIDGE_ID: [u8; 4] = [0x3a, 0xd4, 0x9c, 0x07];
+
+pub(crate) fn edid_sink_state(counter: u16, connector: u8, state: u8) -> 
Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(32, GFP_KERNEL)?;
+    header(&mut b, 0x16, 0x0023, counter)?;
+    pad_to(&mut b, 22)?;
+    b.extend_from_slice(&[connector, state], GFP_KERNEL)?;
+    let mut tail = [0u8; 8];
+    rng::fill(&mut tail);
+    b.extend_from_slice(&tail, GFP_KERNEL)?;
+    Ok(b)
+}
+/// Engage one downstream sink after its EDID exchange.
+pub(crate) fn edid_engage_req(counter: u16, connector: u8) -> Result<KVec<u8>> 
{
+    edid_sink_state(counter, connector, connector)
+}
+/// OUT `id=0x15 sub=0x0053` post-EDID capability query. Offset 22 is a 
connector bitmask.
+///
+/// `connector + 1` and `1 << connector` are the same byte for connectors 0 
and 1, so a capture with
+/// both monitors in the first two sockets cannot distinguish them. DLM sends 
`4` for connector 2,
+/// where a one-based index would send 3.
+pub(crate) fn post_edid_query(counter: u16, connector: u8) -> Result<KVec<u8>> 
{
+    let mut b = KVec::with_capacity(32, GFP_KERNEL)?;
+    header(&mut b, 0x15, 0x0053, counter)?;
+    pad_to(&mut b, 22)?;
+    b.push(1u8 << connector, GFP_KERNEL)?;
+    let mut tail = [0u8; 9];
+    rng::fill(&mut tail);
+    b.extend_from_slice(&tail, GFP_KERNEL)?;
+    Ok(b)
+}
+/// OUT `id=0x16 sub=0x004b` downstream EDID-reader state request.
+pub(crate) fn edid_readiness_state(counter: u16, connector: u8, state: u8) -> 
Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(32, GFP_KERNEL)?;
+    header(&mut b, 0x16, 0x4b, counter)?;
+    pad_to(&mut b, 22)?;
+    // Offset 22 selects the downstream connector and offset 23 stops/starts 
the reader.
+    b.extend_from_slice(&[connector, state], GFP_KERNEL)?;
+    let mut tail = [0u8; 8];
+    rng::fill(&mut tail);
+    b.extend_from_slice(&tail, GFP_KERNEL)?;
+    Ok(b)
+}
+/// Start one downstream EDID read.
+pub(crate) fn edid_readiness_kick(counter: u16, connector: u8) -> 
Result<KVec<u8>> {
+    edid_readiness_state(counter, connector, 1)
+}
+/// OUT get-EDID request (`id=0x15 sub=0x21`). A `sub=0x20` probe must precede 
each fetch attempt.
+/// The dock may initially return an internal placeholder, so callers retry 
until a downstream EDID
+/// arrives.
+pub(crate) fn get_edid_req(counter: u16, connector: u8) -> Result<KVec<u8>> {
+    get_edid_req_sub(counter, 0x21, connector)
+}
+/// Build an `id=0x15` EDID-family request with an explicit `sub` (`0x20` = 
probe/seek,
+/// `0x21` = fetch -- see [`get_edid_req`]'s doc comment). Same 32-byte wire 
shape for both.
+pub(crate) fn get_edid_req_sub(counter: u16, sub: u16, connector: u8) -> 
Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(32, GFP_KERNEL)?;
+    header(&mut b, 0x15, sub, counter)?;
+    pad_to(&mut b, 22)?;
+    // Offset 22 selects the downstream connector; the remaining bytes are an 
opaque token.
+    b.push(connector, GFP_KERNEL)?;
+    let mut tail = [0u8; 9];
+    rng::fill(&mut tail);
+    b.extend_from_slice(&tail, GFP_KERNEL)?;
+    Ok(b)
+}
+/// How many EDID bytes a reply id says it carries, if it names an EDID reply 
at all.
+///
+/// There is no single id for an EDID reply. The field is `0x14` -- the dock's 
generic reply -- plus
+/// the number of EDID bytes behind it, so a monitor whose EDID is one block 
answers `0x94`, a
+/// two-block one `0x114` and a three-block one `0x194`. Matching a fixed 
value makes every monitor
+/// with a different extension count invisible: the fetch is answered, the 
answer is discarded, and
+/// the connector is reported as having no sink at all.
+pub(crate) fn edid_reply_len(id: u16) -> Option<usize> {
+    let n = usize::from(id).checked_sub(0x14)?;
+    (n >= 128 && n % 128 == 0).then_some(n)
+}
+/// Whether a reply carries a connector's downstream display capability.
+///
+/// The inner sub names the message; the id is `0x14` plus the payload length, 
exactly as for an
+/// EDID reply (see [`edid_reply_len`]), so it moves with the descriptor the 
attached monitor
+/// produces. Pinning it to one observed length makes a monitor answering a 
shorter descriptor read
+/// as an empty socket, and that connector is then never probed for an EDID.
+pub(crate) fn is_display_cap_reply(id: u16, sub: u16) -> bool {
+    sub == 0x30 && id > 0x14
+}
+/// Decrypt an EDID reply and return its complete base block and extensions.
+///
+/// EDID replies use wire `sub=0x45` and inner `sub=0x21`, with the id naming 
the payload length
+/// (see [`edid_reply_len`]). The EDID starts at inner offset 22 and its 
base-block extension count
+/// determines the returned length. All supported direction and connector RIV 
variants are checked.
+pub(crate) fn parse_edid_from_reply(
+    ks: &[u8; 16],
+    out_riv: &[u8; 8],
+    wire: &[u8],
+) -> Result<Option<KVec<u8>>> {
+    // Wire header: [.. type@4 u32 .. sub@8 u16 .. seq@12 u32]; body at off16.
+    if wire.len() <= 16 || u16::from_le_bytes([wire[8], wire[9]]) != 0x45 {
+        return Ok(None);
+    }
+    let seq = u32::from_le_bytes([wire[12], wire[13], wire[14], wire[15]]);
+    let body = &wire[16..];
+    for riv in inbound_reply_rivs(out_riv) {
+        let Ok(inner) = open_in(ks, &riv, seq, body) else {
+            continue;
+        };
+        // Inner header: [id u16][sub u16][counter u16][00 00]; EDID payload 
at off22.
+        const EDID_OFF: usize = 22;
+        if inner.len() < EDID_OFF + 128 {
+            continue;
+        }
+        let id = u16::from_le_bytes([inner[0], inner[1]]);
+        let sub = u16::from_le_bytes([inner[2], inner[3]]);
+        let Some(declared) = edid_reply_len(id).filter(|_| sub == 0x21) else {
+            continue;
+        };
+        let edid = &inner[EDID_OFF..];
+        // Say what arrived, not just that nothing valid did. "no EDID came 
back" is true of a
+        // sink that answered with a block this rejected and of one that never 
answered at all,
+        // and those want opposite fixes.
+        if crate::debug_enabled() {
+            vino_debug!(
+                "vino: EDID reply candidate: inner {} B, payload {} B, first 8 
{:02x?}\n",
+                inner.len(),
+                edid.len(),
+                &edid[..8.min(edid.len())]
+            );
+        }
+        // Validate the EDID base-block magic `00 FF FF FF FF FF FF 00`.
+        const MAGIC: [u8; 8] = [0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
0x00];
+        if edid[..8] != MAGIC {
+            if crate::debug_enabled() {
+                vino_debug!("vino: EDID reply rejected: bad base-block 
magic\n");
+            }
+            continue;
+        }
+        // ...and its checksum. The magic is only eight bytes and a dock with 
an empty port can
+        // return a block that carries it, which is enough to be mistaken for 
a monitor: the
+        // connector is then declared connected, a hotplug is raised for a 
sink that is not there,
+        // and the dock resets. A real base block sums to zero modulo 256.
+        if edid.len() < 128 {
+            continue;
+        }
+        if edid[..128].iter().fold(0u8, |a, b| a.wrapping_add(*b)) != 0 {
+            if crate::debug_enabled() {
+                vino_debug!("vino: EDID reply rejected: base block 
checksum\n");
+            }
+            continue;
+        }
+        // A fetch the dock cannot yet answer from the monitor is answered 
from itself: a block
+        // describing a 1920x1080 panel under the bridge's own vendor and 
product id. It passes the
+        // magic and the checksum, so nothing above catches it, and publishing 
it drives the sink at
+        // a timing it never advertised. Refuse it and let the caller ask 
again.
+        if edid[8..12] == BRIDGE_ID {
+            if crate::debug_enabled() {
+                vino_debug!("vino: EDID reply rejected: the dock's own bridge 
descriptor\n");
+            }
+            continue;
+        }
+        if crate::debug_enabled() {
+            vino_debug!(
+                "vino: EDID base block accepted: {} extension block(s) 
declared, {} B available\n",
+                edid[126],
+                edid.len()
+            );
+        }
+        // The reply says how much EDID it carries; the base block says how 
much the monitor has.
+        // Take the smaller, so a truncated reply is never read past its end 
and a base block
+        // claiming more extensions than arrived cannot manufacture them.
+        let total = ((1 + edid[126] as usize) * 128)
+            .min(edid.len())
+            .min(declared);
+        // Keep only extension blocks that are wholly present and sum to zero. 
The core validates
+        // the whole blob, so one bad extension costs the monitor every mode 
it described --
+        // the connector then falls back to a synthesised list and the sink is 
driven at a timing
+        // it never advertised. A base block alone is a valid EDID and still 
carries the native
+        // mode, so salvage what checks out.
+        let mut blocks = 1;
+        while blocks * 128 + 128 <= total {
+            let ext = &edid[blocks * 128..blocks * 128 + 128];
+            if ext.iter().fold(0u8, |a, b| a.wrapping_add(*b)) != 0 {
+                break;
+            }
+            blocks += 1;
+        }
+        let kept = blocks * 128;
+        if crate::debug_enabled() && kept != total {
+            vino_debug!(
+                "vino: EDID extension blocks: {} of {} kept, rest failed 
checksum\n",
+                blocks - 1,
+                total / 128 - 1
+            );
+        }
+        let mut out = KVec::with_capacity(kept, GFP_KERNEL)?;
+        out.extend_from_slice(&edid[..kept], GFP_KERNEL)?;
+        // The extension count and the base-block checksum have to agree with 
what is actually
+        // being handed over, or the core rejects a blob whose blocks are 
individually sound.
+        if out[126] != (blocks - 1) as u8 {
+            out[126] = (blocks - 1) as u8;
+            out[127] = 0;
+            let sum = out[..128].iter().fold(0u8, |a, b| a.wrapping_add(*b));
+            out[127] = (0u8).wrapping_sub(sum);
+        }
+        return Ok(Some(out));
+    }
+    Ok(None)
+}
+/// Decode the downstream status carried by an EDID probe reply.
+///
+/// Returns the inner message id, the little-endian status at offsets 22 
through 25 and the ready
+/// bit at offset 26. `None` means no matching reply was decrypted.
+pub(crate) fn probe_reply_status(
+    ks: &[u8; 16],
+    out_riv: &[u8; 8],
+    wire: &[u8],
+) -> Option<(u16, u32, bool)> {
+    if wire.len() <= 16 || u16::from_le_bytes([wire[8], wire[9]]) != 0x45 {
+        return None;
+    }
+    let seq = u32::from_le_bytes([wire[12], wire[13], wire[14], wire[15]]);
+    let body = &wire[16..];
+    for riv in inbound_reply_rivs(out_riv) {
+        let Ok(inner) = open_in(ks, &riv, seq, body) else {
+            continue;
+        };
+        if inner.len() < 8 {
+            continue;
+        }
+        let id = u16::from_le_bytes([inner[0], inner[1]]);
+        let sub = u16::from_le_bytes([inner[2], inner[3]]);
+        let pad = u16::from_le_bytes([inner[6], inner[7]]);
+        if id >= 0x400 || pad != 0 {
+            continue;
+        }
+        // Ignore unrelated traffic: only a downstream capability/EDID handler 
response or a
+        // generic negative acknowledgment can answer this probe. The 
handler's id is `0x14` plus
+        // its payload length (see `edid_reply_len`), so it names a descriptor 
size rather than a
+        // message type and cannot be matched against a list of the sizes seen 
so far.
+        if !(id > 0x14 && sub == 0x0020) && id != 0x14 {
+            continue;
+        }
+        // A short generic ack (the `id=0x14` the dock sends when it cannot 
route the probe)
+        // carries no status region at all; report zeros rather than refusing 
to decode, so the
+        // caller still learns the id.
+        let status = if inner.len() >= 26 {
+            u32::from_le_bytes([inner[22], inner[23], inner[24], inner[25]])
+        } else {
+            0
+        };
+        let ready = inner.len() >= 27 && inner[26] & 0x80 != 0;
+        return Some((id, status, ready));
+    }
+    None
+}
+/// Decode an EDID-readiness probe reply.
+///
+/// Inner offset 26 bit 7 indicates that the downstream DDC read has 
completed. `None` distinguishes
+/// an unrelated or undecipherable frame from a matching reply that is not 
ready.
+pub(crate) fn edid_poll_ready(ks: &[u8; 16], out_riv: &[u8; 8], wire: &[u8]) 
-> Option<bool> {
+    if wire.len() <= 16 || u16::from_le_bytes([wire[8], wire[9]]) != 0x45 {
+        return None;
+    }
+    let seq = u32::from_le_bytes([wire[12], wire[13], wire[14], wire[15]]);
+    let body = &wire[16..];
+    for riv in inbound_reply_rivs(out_riv) {
+        let Ok(inner) = open_in(ks, &riv, seq, body) else {
+            continue;
+        };
+        if inner.len() < 27 {
+            continue;
+        }
+        let id = u16::from_le_bytes([inner[0], inner[1]]);
+        let sub = u16::from_le_bytes([inner[2], inner[3]]);
+        if id != 0x44 || sub != 0x20 {
+            continue;
+        }
+        return Some(inner[26] & 0x80 != 0);
+    }
+    None
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_cp_edid)]
+mod tests {
+    #[test]
+    fn the_docks_own_bridge_descriptor_is_never_published() {
+        // Built from a block the dock served on a warm plug: valid magic and 
checksum, so only the
+        // vendor and product id separate it from a monitor's.
+        let mut block = [0u8; 128];
+        block[..8].copy_from_slice(&[0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
0x00]);
+        block[8..12].copy_from_slice(&[0x3a, 0xd4, 0x9c, 0x07]);
+        let sum = block[..127].iter().fold(0u8, |a, b| a.wrapping_add(*b));
+        block[127] = 0u8.wrapping_sub(sum);
+        assert_eq!(block.iter().fold(0u8, |a, b| a.wrapping_add(*b)), 0);
+        assert_eq!(block[8..12], super::BRIDGE_ID);
+    }
+
+    use super::*;
+
+    #[test]
+    fn edid_reply_guards() -> Result {
+        // The pre-decrypt guards reject non-EDID frames without touching the 
cipher.
+        let ks = [0u8; 16];
+        let riv = [0u8; 8];
+        assert!(parse_edid_from_reply(&ks, &riv, &[0u8; 10])?.is_none());
+        let mut wrong_sub = [0u8; 20];
+        wrong_sub[8] = 0x44; // wire sub != 0x45
+        assert!(parse_edid_from_reply(&ks, &riv, &wrong_sub)?.is_none());
+        Ok(())
+    }
+
+    /// An EDID reply's id is its payload length, not a message type.
+    ///
+    /// Both values below are off the wire in one session: the dock answered 
one connector's fetch
+    /// with `0x114` and the other's with `0x194`, and the difference is 
exactly the 128 bytes of
+    /// one extension block. Accepting only the larger left a monitor that the 
vendor drives
+    /// reported as an empty socket for the whole life of the driver.
+    #[test]
+    fn edid_reply_id_is_the_payload_length() {
+        assert_eq!(edid_reply_len(0x94), Some(128));
+        assert_eq!(edid_reply_len(0x114), Some(256));
+        assert_eq!(edid_reply_len(0x194), Some(384));
+        assert_eq!(edid_reply_len(0x214), Some(512));
+        // The generic reply itself carries no EDID, and neither does anything 
off the 128-byte
+        // grid: a status or capability id must never be read as a base block.
+        assert_eq!(edid_reply_len(0x14), None);
+        assert_eq!(edid_reply_len(0x44), None);
+        assert_eq!(edid_reply_len(0x78), None);
+        assert_eq!(edid_reply_len(0x0), None);
+        assert_eq!(edid_reply_len(0x95), None);
+    }
+
+    #[test]
+    fn get_edid_req_matches_dlm_wire_shape() -> Result {
+        // The captured request is 32 bytes: an 8-byte header, 14 zero bytes, 
and a 10-byte random
+        // tail at offset 22.
+        let req = get_edid_req(0x2c, 0)?;
+        assert_eq!(req.len(), 32);
+        assert_eq!(
+            &req[0..8],
+            &[0x15, 0x00, 0x21, 0x00, 0x2c, 0x00, 0x00, 0x00]
+        );
+        assert_eq!(&req[8..22], &[0u8; 14]);
+        // Pin the complete wire framing as well:
+        // aux=0x09 (cp::aux_for_id(0x15, ..)), body = 32 + 16 (tag) = 48 
bytes.
+        let frame = cp::seal_interactive(&[0x5au8; 16], &[0x11u8; 8], 0x15, 0, 
&req)?;
+        assert_eq!(frame.len(), 16 + 32 + 16);
+        assert_eq!(u16::from_le_bytes([frame[10], frame[11]]), 0x09);
+        Ok(())
+    }
+
+    #[test]
+    fn edid_engage_req_matches_dlm_wire_shape() -> Result {
+        // Independent captures agree on `id=0x16 sub=0x0023` with the same 
32-byte shape as
+        // `get_edid_req`: an 8-byte header, 14 zero bytes, and a 10-byte 
random tail.
+        let req = edid_engage_req(0x30, 0)?;
+        assert_eq!(req.len(), 32);
+        assert_eq!(
+            &req[0..8],
+            &[0x16, 0x00, 0x23, 0x00, 0x30, 0x00, 0x00, 0x00]
+        );
+        assert_eq!(&req[8..22], &[0u8; 14]);
+        let frame = cp::seal_interactive(&[0x5au8; 16], &[0x11u8; 8], 0x16, 0, 
&req)?;
+        assert_eq!(frame.len(), 16 + 32 + 16);
+        assert_eq!(u16::from_le_bytes([frame[10], frame[11]]), 0x08); // 
cp::aux_for_id(0x16, ..)
+        Ok(())
+    }
+
+    #[test]
+    fn edid_poll_ready_byte_matches_golden_replies() -> Result {
+        // Golden dock-to-host `id=0x0044 sub=0x0020` replies pin the 
readiness bit at inner offset
+        // 26. The first precedes a placeholder `id=0x114` fetch and the 
second precedes a real
+        // `id=0x194` EDID fetch.
+        const KS: [u8; 16] = [
+            0xd9, 0xec, 0x1f, 0xbc, 0x8b, 0x5a, 0xb3, 0xd8, 0x71, 0x0f, 0xd3, 
0xbd, 0x42, 0x04,
+            0x06, 0x55,
+        ];
+        const OUT_RIV: [u8; 8] = [0xf6, 0x21, 0xdc, 0x0d, 0x22, 0x7e, 0xf4, 
0xaf];
+        #[rustfmt::skip]
+        const NOT_READY: [u8; 112] = [
+            0x00, 0x00, 0x6c, 0x00, 0x04, 0x00, 0x00, 0x00, 0x45, 0x00, 0x0a, 
0x00, 0xcd, 0x00,
+            0x00, 0x00, 0xa5, 0xea, 0x5d, 0x51, 0xf6, 0xa8, 0x6b, 0xb6, 0x89, 
0x88, 0x01, 0xa2,
+            0x47, 0x30, 0xbd, 0x6c, 0x84, 0xb8, 0xaf, 0x9f, 0x85, 0xf2, 0x8a, 
0x20, 0xc8, 0xec,
+            0x51, 0x9e, 0x8d, 0xeb, 0xef, 0x5a, 0x3a, 0x1d, 0xb5, 0xc7, 0x80, 
0x02, 0xfe, 0x1e,
+            0xed, 0x07, 0xdd, 0x71, 0x00, 0x7f, 0x45, 0x77, 0x6c, 0x82, 0xf6, 
0xe9, 0xc3, 0x0d,
+            0xdf, 0x67, 0x82, 0xac, 0xa8, 0x23, 0xd5, 0x5a, 0x1c, 0xce, 0xcb, 
0x89, 0xb5, 0x98,
+            0x65, 0xba, 0xbb, 0xb6, 0x2d, 0x0e, 0x9b, 0x55, 0xee, 0xfd, 0x46, 
0x0c, 0x22, 0x35,
+            0x6f, 0x84, 0xe5, 0x36, 0x95, 0xd0, 0xdc, 0xfc, 0x6f, 0x8a, 0x57, 
0xda, 0xa2, 0xae,
+        ];
+        #[rustfmt::skip]
+        const READY: [u8; 112] = [
+            0x00, 0x00, 0x6c, 0x00, 0x04, 0x00, 0x00, 0x00, 0x45, 0x00, 0x0a, 
0x00, 0x59, 0x03,
+            0x00, 0x00, 0xf7, 0x8e, 0x70, 0xb2, 0xa3, 0x24, 0xe2, 0x6f, 0x9f, 
0xb6, 0xe9, 0x8e,
+            0x32, 0x55, 0x11, 0x21, 0x99, 0x74, 0xf6, 0xfb, 0xea, 0x97, 0xd5, 
0x7f, 0xa6, 0x45,
+            0x9d, 0x35, 0xf0, 0xa7, 0xbe, 0xd3, 0x9b, 0x19, 0x24, 0x8c, 0x98, 
0xa6, 0x0c, 0xa2,
+            0x4d, 0x8e, 0x83, 0xaa, 0x74, 0xd5, 0x8b, 0xe0, 0x6f, 0xb1, 0x9f, 
0xa4, 0xb9, 0xae,
+            0x39, 0xc6, 0x0a, 0x9c, 0x63, 0x70, 0xdb, 0x49, 0x74, 0xe5, 0x85, 
0x42, 0x07, 0x7e,
+            0xc2, 0x49, 0xfb, 0x67, 0x54, 0xd5, 0x47, 0x72, 0xb7, 0x19, 0x24, 
0x8f, 0xb1, 0xb0,
+            0xb2, 0x83, 0x89, 0x62, 0x4b, 0xcb, 0x59, 0x15, 0x1f, 0x8f, 0x85, 
0xc3, 0xa5, 0x9d,
+        ];
+        assert_eq!(edid_poll_ready(&KS, &OUT_RIV, &NOT_READY), Some(false));
+        assert_eq!(edid_poll_ready(&KS, &OUT_RIV, &READY), Some(true));
+        Ok(())
+    }
+
+    #[test]
+    fn per_head_selectors_match_dlm_in_the_far_sockets() -> Result {
+        // `connector`, `connector + 1` and `1 << connector` agree for 
connectors 0 and 1, so only a
+        // capture with a monitor in a later socket separates them. These are 
the bytes DLM sends
+        // for connectors 1 and 2.
+
+        // `id=0x16 sub=0x23` names the connector twice, at offset 22 and 
offset 23.
+        for connector in 0..drm_sink::MAX_CONNECTORS as u8 {
+            let req = edid_engage_req(0x30, connector)?;
+            assert_eq!(req[22], connector);
+            assert_eq!(req[23], connector);
+        }
+
+        // `id=0x15 sub=0x53` carries a connector bitmask at offset 22: 2 for 
connector 1 and 4 for
+        // connector 2, where a one-based index would send 3.
+        assert_eq!(post_edid_query(0x30, 1)?[22], 2);
+        assert_eq!(post_edid_query(0x30, 2)?[22], 4);
+        for connector in 0..drm_sink::MAX_CONNECTORS as u8 {
+            assert_eq!(post_edid_query(0x30, connector)?[22], 1u8 << 
connector);
+        }
+        Ok(())
+    }
+
+    #[test]
+    fn display_capability_reply_reports_presence() -> Result {
+        let key = [0x5au8; 16];
+        let riv = [0x33u8; 8];
+        let mut inner = [0u8; 32];
+        inner[0..2].copy_from_slice(&0x78u16.to_le_bytes());
+        inner[2..4].copy_from_slice(&0x20u16.to_le_bytes());
+        inner[22..26].copy_from_slice(&0x1234u32.to_le_bytes());
+        inner[26] = 0x80;
+        let mut wire = cp::seal_interactive(&key, &riv, 0x78, 11, &inner)?;
+        wire[8..10].copy_from_slice(&0x45u16.to_le_bytes());
+
+        assert_eq!(
+            probe_reply_status(&key, &riv, &wire),
+            Some((0x78, 0x1234, true))
+        );
+        Ok(())
+    }
+}
diff --git a/drivers/gpu/drm/vino/cp/mode.rs b/drivers/gpu/drm/vino/cp/mode.rs
new file mode 100644
index 000000000000..91f4fc5d3625
--- /dev/null
+++ b/drivers/gpu/drm/vino/cp/mode.rs
@@ -0,0 +1,874 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Describing a mode to the dock.
+//!
+//! The dock is not told a DRM mode. It is told a pixel clock, a set of 
totals, a sync polarity
+//! and an allocation, and it programs its downstream link from those. A mode 
it accepts but
+//! cannot carry lights nothing, so what is sent is bounded by the profile 
rather than by what
+//! the compositor asked for.
+
+use super::*;
+
+/// A video timing as carried by the `0x48/0x22` set-mode message.
+///
+/// Field names follow the vendor's own vocabulary, which it logs as `hActive 
hBlanking
+/// hFrontPorch hSyncWidth hSyncInv vActive vBlanking vFrontPorch vSyncWidth 
vSyncInv vic
+/// pixelClock`. That is this payload in order: eight geometry words at 
offsets 26 through 40, the
+/// two sync-inversion flags packed into [`Timing::sync_flags`] at offset 42, 
the CTA VIC in the
+/// low byte of [`Timing::vic_word`] at offset 66, and the pixel clock at 
offset 70.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub(crate) struct Timing {
+    pub hactive: u16,
+    pub hblank: u16,
+    pub hsync_front: u16,
+    pub hsync_width: u16,
+    pub vactive: u16,
+    pub vblank: u16,
+    pub vsync_front: u16,
+    pub vsync_width: u16,
+    pub refresh_hz: u16,
+    /// Pixel clock in 10 kHz units, serialized as a `u32` at offsets 70 
through 73.
+    ///
+    /// It is a full 32-bit field. No Ridge capture
+    /// could show that, because Ridge is never driven above 497.75 MHz and 
the high half is
+    /// always zero there -- but the DL7400 sends `0x0001113d` (699.49 MHz) 
for 2560x1440p165,
+    /// so the upper word is real. Truncating to `u16` made every mode past 
655.35 MHz fail the
+    /// conversion and never reach the dock at all.
+    pub pixel_clock_10khz: u32,
+    /// Sync-polarity flags at offset 42; see [`sync_flags`].
+    pub sync_flags: u16,
+    /// Render stride at offset 46, in pixels; see [`render_stride`].
+    pub stride: u16,
+    /// Row count at offset 48; see [`profile::Allocation`].
+    pub total_rows: u16,
+    /// Picture aspect and CTA VIC at offset 66; see [`vic_word`].
+    pub vic_word: u16,
+    /// Whether this connector scans out 10 bits per channel, which selects 
the offset-68 colour
+    /// depth and the offset-23 DMA buffer format together. They are one 
decision: the dock sizes
+    /// its own buffer from the format's bytes-per-pixel and interprets the 
samples by the depth, so
+    /// a mismatched pair mis-sizes the allocation.
+    pub ten_bit: bool,
+    /// Whether the pixels this connector carries are encoded with the SMPTE 
ST 2084 (PQ) transfer
+    /// function, which sets [`SYNC_FLAG_ST2084`] in the offset-42 flags word.
+    ///
+    /// Independent of [`Timing::ten_bit`]: the depth says how many bits a 
sample has, this says
+    /// what curve those bits are on. A compositor can drive a 10-bit SDR 
output, and PQ in 8 bits
+    /// is merely a bad idea rather than a contradiction, so the dock is told 
the two separately --
+    /// exactly as DLM tells it.
+    pub st2084: bool,
+    /// This connector's video endpoint also carries another connector; see 
[`SYNC_FLAG_DUAL_NIVO`].
+    pub dual_nivo: bool,
+}
+/// The render stride for `hactive`, in pixels: quantised up to 
[`STRIDE_ALIGN`], then one whole
+/// unit more.
+///
+/// The trailing unit is added after the quantisation, not as slack for it, so 
a width that is
+/// already a multiple of 128 still gains 128. Both decrypted DL7400 widths 
are such multiples,
+/// which is why `0x0a80` at 2560 and `0x0300` at 640 both read as a plain 
`hactive + 128`.
+pub(crate) fn render_stride(hactive: u16) -> u16 {
+    let quantised = (u32::from(hactive) + STRIDE_ALIGN - 1) / STRIDE_ALIGN;
+    (((quantised + 1) * STRIDE_ALIGN) & 0xffff) as u16
+}
+/// Build the offset-42 flags word from the mode's sync polarity.
+///
+/// This is the vendor's `hSyncInv`/`vSyncInv` pair packed into one word, over 
a base bit that is
+/// set in every observed message and whose own meaning is unknown. Every 
decrypted mode set on
+/// both dock generations agrees:
+///
+/// | mode | polarity | off42 |
+/// |---|---|---|
+/// | 1280x720p60, 1920x1080p60/p120 (CTA) | `+h +v` | `0x0400` |
+/// | 2560x1440p60/p120/p165 (CVT-RB) | `+h -v` | `0x0600` |
+/// | 640x480p60 (DMT) | `-h -v` | `0x0700` |
+///
+/// The last row also fixes the assignment within the pair: 2560x1440 is `+h 
-v` and carries
+/// `0x0600`, so `0x0200` is the vertical flag and swapping the two would 
predict `0x0500`.
+/// DLM's own bit test confirms both independently; see [`SYNC_FLAGS_BASE`].
+fn sync_flags(mode: &kernel::drm::kms::modes::DisplayMode) -> u16 {
+    type ModeFlags = kernel::drm::kms::modes::ModeFlags;
+
+    let flags = mode.flags();
+    let mut word = SYNC_FLAGS_BASE;
+    if flags.contains(ModeFlags::NHSYNC) {
+        word |= SYNC_FLAG_HSYNC_INV;
+    }
+    if flags.contains(ModeFlags::NVSYNC) {
+        word |= SYNC_FLAG_VSYNC_INV;
+    }
+    word
+}
+/// Build the offset-66 word: the picture aspect in the high byte, the CTA VIC 
in the low.
+///
+/// The low byte is the VIC, or zero for a timing that has none. Measured: 
`0x10` for 1920x1080p60
+/// (VIC 16), `0x3f` for 1920x1080p120 (VIC 63), `0x00` for the VIC-less 
2560x1440 CVT-RB timings.
+///
+/// The aspect is looked up in [`VIC_ASPECT_16_9`], which covers VICs 1 
through 59. A VIC outside
+/// that range gets [`ASPECT_NONE`] rather than being clamped into it -- that 
is what makes
+/// 1920x1080p120 (VIC 63) carry `0x083f` while 1920x1080p60 (VIC 16) carries 
`0x2810`.
+pub(crate) fn vic_word(vic: u8) -> u16 {
+    let vic = u16::from(vic);
+    let aspect = match vic.checked_sub(1) {
+        Some(bit) if bit < 59 => {
+            if VIC_ASPECT_16_9 & (1u64 << bit) != 0 {
+                ASPECT_16_9
+            } else {
+                ASPECT_4_3
+            }
+        }
+        _ => ASPECT_NONE,
+    };
+    aspect | vic
+}
+/// A mode's offset-42 and offset-66 set-mode words, and how they were 
obtained.
+pub(crate) struct ModeProfile {
+    pub sync_flags: u16,
+    pub vic_word: u16,
+    /// True when these bytes are reproduced from a decrypted DLM set-mode 
message.
+    pub measured: bool,
+}
+/// Return the two mode-dependent set-mode words at offsets 42 and 66.
+///
+/// Both words are derived by [`sync_flags`] and [`vic_word`], which between 
them reproduce every
+/// decrypted message byte-exactly, so an unsampled timing is driven rather 
than refused. The
+/// envelope the dock stays inside -- refresh ceiling, per-connector clock and 
the shared pixel
+/// budget -- is enforced by `drm_sink`'s `mode_valid`, not here.
+pub(crate) fn mode_profile(mode: &kernel::drm::kms::modes::DisplayMode) -> 
Option<ModeProfile> {
+    let clock = mode.clock();
+    if clock <= 0 {
+        return None;
+    }
+    if mode.vrefresh() <= 0 {
+        return None;
+    }
+
+    // The whole decrypted DLM corpus: 1920x1080p60 and p120 (CTA), 
2560x1440p60 and p120
+    // (CVT-RB), with the two words each carries on the wire.
+    //
+    // These are taken from the capture rather than derived, because the 
derivation reads the
+    // sync polarity and the CTA VIC off the DRM mode and a mode built from 
the fallback list
+    // carries neither: a 1920x1080p60 with exactly these timings arrives with 
both syncs marked
+    // negative and no VIC at all, which sends `0x0700`/`0x0800` where the 
vendor sends
+    // `0x0400`/`0x2810`. The timings identify the mode; the flags on the 
struct do not.
+    let captured = match (
+        clock,
+        mode.hdisplay(),
+        mode.hsync_start(),
+        mode.hsync_end(),
+        mode.htotal(),
+        mode.vdisplay(),
+        mode.vsync_start(),
+        mode.vsync_end(),
+        mode.vtotal(),
+    ) {
+        (148_500, 1920, 2008, 2052, 2200, 1080, 1084, 1089, 1125) => 
Some((0x0400, 0x2810)),
+        (297_000, 1920, 2008, 2052, 2200, 1080, 1084, 1089, 1125) => 
Some((0x0400, 0x083f)),
+        (241_500, 2560, 2608, 2640, 2720, 1440, 1443, 1448, 1481) => 
Some((0x0600, 0x0800)),
+        (497_750, 2560, 2608, 2640, 2720, 1440, 1443, 1448, 1525) => 
Some((0x0600, 0x0800)),
+        _ => None,
+    };
+
+    Some(ModeProfile {
+        sync_flags: captured.map_or_else(|| sync_flags(mode), |(s, _)| s),
+        vic_word: captured.map_or_else(|| vic_word(mode.cea_vic()), |(_, v)| 
v),
+        measured: captured.is_some(),
+    })
+}
+/// Whether the dock can be given a mode profile for `mode`.
+pub(crate) fn mode_supported(mode: &kernel::drm::kms::modes::DisplayMode) -> 
bool {
+    mode_profile(mode).is_some()
+}
+/// Build the set-mode message's teardown form: every timing word zero and
+/// [`SYNC_FLAGS_TEARDOWN`] at offset 42.
+///
+/// The dock expects this for a connector before that connector's real mode. 
DLM sends two rounds
+/// of `(conn 0, conn 1)` teardowns 3.1 s and 1.2 s ahead of the real pair, 
which itself lands
+/// 0.12 s before the first video byte.
+pub(crate) fn clear_mode(counter: u16, connector: u8) -> Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(80, GFP_KERNEL)?;
+    header(&mut b, 0x48, 0x22, counter)?;
+    pad_to(&mut b, 22)?;
+    b.push(connector, GFP_KERNEL)?; // off22: connector
+    b.push(DMA_FORMAT_NONE, GFP_KERNEL)?;
+    pad_to(&mut b, 42)?;
+    b.extend_from_slice(&SYNC_FLAGS_TEARDOWN.to_le_bytes(), GFP_KERNEL)?;
+    pad_to(&mut b, 74)?;
+    let mut tail = [0u8; 6];
+    rng::fill(&mut tail);
+    b.extend_from_slice(&tail, GFP_KERNEL)?; // off74..79: pad to the AES block
+    Ok(b)
+}
+/// Set-mode (`id=0x48 sub=0x22`): an 80-byte inner message carrying the 
target connector and a
+/// timing record. Offsets 26 through 48 hold the geometry, sync flags, 
refresh and the
+/// resolution-keyed pair; offset 66 carries the VIC word, offset 68 is fixed, 
offset 70 the pixel
+/// clock, and offsets 74 through 79 a fresh token.
+pub(crate) fn set_mode(counter: u16, connector: u8, t: &Timing) -> 
Result<KVec<u8>> {
+    let mut b = KVec::with_capacity(80, GFP_KERNEL)?;
+    header(&mut b, 0x48, 0x22, counter)?;
+    pad_to(&mut b, 22)?;
+    b.push(connector, GFP_KERNEL)?; // off22: downstream connector selector
+    b.push(
+        if t.ten_bit {
+            DMA_FORMAT_NM30
+        } else {
+            DMA_FORMAT_NM24
+        },
+        GFP_KERNEL,
+    )?;
+    pad_to(&mut b, 26)?; // off24..25 zero; timing begins at off26
+                         // The transfer function rides in the same word as 
the sync polarity; see
+                         // `SYNC_FLAG_ST2084`.
+    let flags = t.sync_flags
+        | if t.st2084 { SYNC_FLAG_ST2084 } else { 0 }
+        | if t.dual_nivo { SYNC_FLAG_DUAL_NIVO } else { 0 };
+    for v in [
+        t.hactive,
+        t.hblank,
+        t.hsync_front,
+        t.hsync_width,
+        t.vactive,
+        t.vblank,
+        t.vsync_front,
+        t.vsync_width,
+        flags,
+        t.refresh_hz,
+        t.stride,
+        t.total_rows,
+    ] {
+        b.extend_from_slice(&v.to_le_bytes(), GFP_KERNEL)?;
+    }
+    pad_to(&mut b, 58)?;
+    b.extend_from_slice(&0x0080u16.to_le_bytes(), GFP_KERNEL)?; // off58: 
profile constant
+    b.extend_from_slice(&0x00ffu16.to_le_bytes(), GFP_KERNEL)?; // off60: 
profile constant
+    pad_to(&mut b, 66)?;
+    b.extend_from_slice(&t.vic_word.to_le_bytes(), GFP_KERNEL)?; // off66: see 
`vic_word`
+    b.extend_from_slice(
+        &if t.ten_bit {
+            COLOUR_DEPTH_30BPP
+        } else {
+            COLOUR_DEPTH_24BPP
+        }
+        .to_le_bytes(),
+        GFP_KERNEL,
+    )?;
+
+    // off70..73: pixel clock in 10 kHz units, a full u32. Ridge only ever 
fills the low half, so
+    // this is byte-identical there to the old u16 followed by two zero bytes.
+    b.extend_from_slice(&t.pixel_clock_10khz.to_le_bytes(), GFP_KERNEL)?;
+    pad_to(&mut b, 74)?;
+    let mut tail = [0u8; 6];
+    rng::fill(&mut tail);
+    b.extend_from_slice(&tail, GFP_KERNEL)?; // off74..79: fresh per-message 
token
+    Ok(b)
+}
+/// Convert a DRM display mode into the dock's set-mode timing representation.
+pub(crate) fn timing_from_drm_mode(
+    mode: &kernel::drm::kms::modes::DisplayMode,
+    allocation: &profile::Allocation,
+    ten_bit: bool,
+) -> Result<Timing> {
+    let refresh = mode.vrefresh() as u16;
+    let sub = |a: u16, b: u16| a.saturating_sub(b);
+    let profile = mode_profile(mode).ok_or(EINVAL)?;
+    let clock = mode.clock();
+    if clock <= 0 {
+        return Err(EINVAL);
+    }
+    // A dark panel on a mode with no decrypted message is far more likely to 
be these two words
+    // than anything else in the pipeline, so name them in the log.
+    if !profile.measured {
+        vino_debug!(
+            "vino: {}x{}@{} has no decrypted DLM profile; inferring 
sync_flags={:#06x} \
+             vic_word={:#06x}\n",
+            mode.hdisplay(),
+            mode.vdisplay(),
+            refresh,
+            profile.sync_flags,
+            profile.vic_word
+        );
+    }
+    let pixel_clock_10khz = (clock as u32) / 10;
+    let (stride, total_rows, known) = allocation.words(mode.hdisplay(), 
mode.vdisplay(), ten_bit);
+    // A dock with nowhere to put the second frame stops consuming and says 
nothing, so name an
+    // allocation no capture covers.
+    if !known {
+        vino_debug!(
+            "vino: {}x{} has no stated allocation; sending this dock's default 
{:#06x} rows\n",
+            mode.hdisplay(),
+            mode.vdisplay(),
+            total_rows
+        );
+    }
+    Ok(Timing {
+        hactive: mode.hdisplay(),
+        hblank: sub(mode.htotal(), mode.hdisplay()),
+        hsync_front: sub(mode.hsync_start(), mode.hdisplay()),
+        hsync_width: sub(mode.hsync_end(), mode.hsync_start()),
+        vactive: mode.vdisplay(),
+        vblank: sub(mode.vtotal(), mode.vdisplay()),
+        vsync_front: sub(mode.vsync_start(), mode.vdisplay()),
+        vsync_width: sub(mode.vsync_end(), mode.vsync_start()),
+        refresh_hz: refresh,
+        pixel_clock_10khz,
+        sync_flags: profile.sync_flags,
+        stride,
+        total_rows,
+        vic_word: profile.vic_word,
+        // The depth is an argument because the allocation above divides by 
it, so the pair cannot
+        // disagree: a connector told 30 bpp is told the row count that goes 
with 30 bpp.
+        ten_bit,
+        // Filled by the caller. Both describe the pixels a connector will 
actually carry, which a
+        // DRM mode does not know: `atomic_enable` reads them from the 
committed framebuffer and the
+        // connector's HDR properties.
+        st2084: false,
+        dual_nivo: false,
+    })
+}
+
+#[cfg(CONFIG_DRM_VINO_KUNIT_TEST)]
+#[kunit_tests(vino_cp_mode)]
+mod tests {
+    use super::*;
+    use kernel::drm::kms::modes::{DisplayMode, ModeFlags, ModeTimings};
+
+    #[test]
+    fn dual_nivo_rides_the_flags_word() -> Result {
+        // Bit 2 of offset 42 declares that this connector's video endpoint 
carries a second
+        // connector. It must not disturb anything else in the word.
+        let base = Timing {
+            hactive: 2560,
+            hblank: 160,
+            hsync_front: 48,
+            hsync_width: 32,
+            vactive: 1440,
+            vblank: 85,
+            vsync_front: 3,
+            vsync_width: 8,
+            pixel_clock_10khz: 49775,
+            sync_flags: 0x0600,
+            refresh_hz: 120,
+            stride: 0x0a80,
+            total_rows: 0x66db,
+            vic_word: 0x0800,
+            ten_bit: false,
+            st2084: false,
+            dual_nivo: false,
+        };
+        let plain = set_mode(0x30, 0, &base)?;
+        let shared = set_mode(
+            0x30,
+            0,
+            &Timing {
+                dual_nivo: true,
+                ..base
+            },
+        )?;
+        assert_eq!(plain.len(), shared.len());
+        let f_plain = u16::from_le_bytes([plain[42], plain[43]]);
+        let f_shared = u16::from_le_bytes([shared[42], shared[43]]);
+        assert_eq!(f_shared, f_plain | 0x0004);
+        // Nothing else in the timing block moves. Compare only up to the end 
of the timing: this
+        // message carries a random tail like every other, so a byte-for-byte 
comparison of the
+        // whole thing compares two different random draws and fails for the 
wrong reason.
+        assert_eq!(plain[..42], shared[..42]);
+        assert_eq!(plain[44..74], shared[44..74]);
+        Ok(())
+    }
+
+    /// Verify set-mode geometry and profile words against the decrypted DLM 
corpus.
+    ///
+    /// The middle four cases are byte-exact DLM messages (1920x1080p60/p120, 
2560x1440p60/p120); no
+    /// capture backs the 1280x720p60 and 3840x2160p60 cases, which the 
derivation supplies.
+    #[test]
+    fn set_mode_matches_dlm_corpus() -> Result {
+        // hact, htotal, hsync_start, hsync_end, vact, vtotal, vsync_start, 
vsync_end, clock kHz,
+        // refresh, sync flags, off42, off66.
+        type Case = (
+            u16,
+            u16,
+            u16,
+            u16,
+            u16,
+            u16,
+            u16,
+            u16,
+            i32,
+            u16,
+            ModeFlags,
+            u16,
+            u16,
+        );
+        let cta = ModeFlags::PHSYNC | ModeFlags::PVSYNC;
+        let cvt_rb = ModeFlags::PHSYNC | ModeFlags::NVSYNC;
+        let cases: [Case; 6] = [
+            (
+                1280, 1650, 1390, 1430, 720, 750, 725, 730, 74_250, 60, cta, 
0x0400, 0x2804,
+            ),
+            (
+                1920, 2200, 2008, 2052, 1080, 1125, 1084, 1089, 148_500, 60, 
cta, 0x0400, 0x2810,
+            ),
+            (
+                1920, 2200, 2008, 2052, 1080, 1125, 1084, 1089, 297_000, 120, 
cta, 0x0400, 0x083f,
+            ),
+            (
+                2560, 2720, 2608, 2640, 1440, 1481, 1443, 1448, 241_500, 60, 
cvt_rb, 0x0600, 0x0800,
+            ),
+            (
+                2560, 2720, 2608, 2640, 1440, 1525, 1443, 1448, 497_750, 120, 
cvt_rb, 0x0600,
+                0x0800,
+            ),
+            (
+                3840, 4000, 3888, 3920, 2160, 2222, 2163, 2168, 533_120, 60, 
cvt_rb, 0x0600, 0x0800,
+            ),
+        ];
+        for (hact, htotal, hss, hse, vact, vtotal, vss, vse, clock, refresh, 
flags, off42, off66) in
+            cases
+        {
+            let mode = DisplayMode::from_timings(ModeTimings {
+                clock_khz: clock,
+                hdisplay: hact,
+                hsync_start: hss,
+                hsync_end: hse,
+                htotal,
+                vdisplay: vact,
+                vsync_start: vss,
+                vsync_end: vse,
+                vtotal,
+                flags,
+            })?;
+            let t =
+                timing_from_drm_mode(&mode, 
&profile::PROFILE_RIDGE.protocol.allocation, false)?;
+            let w = set_mode(7, 1, &t)?;
+            assert_eq!(w.len(), 80);
+            let u16_at = |off: usize| u16::from_le_bytes([w[off], w[off + 1]]);
+            assert_eq!(u16_at(26), hact); // hactive
+            assert_eq!(u16_at(28), htotal - hact); // hblank
+            assert_eq!(u16_at(30), hss - hact); // hsync front porch
+            assert_eq!(u16_at(32), hse - hss); // hsync width
+            assert_eq!(u16_at(34), vact); // vactive
+            assert_eq!(u16_at(36), vtotal - vact); // vblank
+            assert_eq!(u16_at(38), vss - vact); // vsync front porch
+            assert_eq!(u16_at(40), vse - vss); // vsync width
+            assert_eq!(u16_at(42), off42);
+            assert_eq!(u16_at(44), refresh);
+            assert_eq!(u16_at(66), off66);
+            assert_eq!(u16_at(68), 0x0200);
+            assert_eq!(u16_at(70), (clock as u32 / 10) as u16); // pixel clock 
/ 10 kHz
+            assert_eq!(&w[72..74], &[0, 0]);
+        }
+        Ok(())
+    }
+
+    /// The DL-3x00 set-mode, byte for byte against DLM's own, both connectors.
+    ///
+    /// Offsets 46 and 48 state the dock's framebuffer allocation, and nothing 
on the wire reports
+    /// them wrong: the dock accepts the set-mode, accepts the first frame, 
and then stops
+    /// consuming because it has nowhere to put the next one. Ridge's 
device-level override is a
+    /// different pair entirely, so a dock of one generation carrying 
another's allocation is the
+    /// failure this pins.
+    #[test]
+    fn ella_set_mode_matches_the_dlm_capture() -> Result {
+        let mode = DisplayMode::from_timings(ModeTimings {
+            clock_khz: 148_500,
+            hdisplay: 1920,
+            hsync_start: 2008,
+            hsync_end: 2052,
+            htotal: 2200,
+            vdisplay: 1080,
+            vsync_start: 1084,
+            vsync_end: 1089,
+            vtotal: 1125,
+            flags: ModeFlags::PHSYNC | ModeFlags::PVSYNC,
+        })?;
+        let t = timing_from_drm_mode(&mode, 
&profile::PROFILE_ELLA.protocol.allocation, false)?;
+        for connector in 0..2u8 {
+            let w = set_mode(0, connector, &t)?;
+            // Everything DLM sends, except its message counter at offset 4, 
the connector at 22 and
+            // the six-byte token at 74.
+            let want: [u8; 74] = [
+                0x48, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00,
+                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, connector, 
0x02, 0x00, 0x00, 0x80,
+                0x07, 0x18, 0x01, 0x58, 0x00, 0x2c, 0x00, 0x38, 0x04, 0x2d, 
0x00, 0x04, 0x00, 0x05,
+                0x00, 0x00, 0x04, 0x3c, 0x00, 0x00, 0x08, 0x00, 0x20, 0x00, 
0x00, 0x00, 0x00, 0x00,
+                0x00, 0x00, 0x00, 0x80, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x10, 0x28, 0x00,
+                0x02, 0x02, 0x3a, 0x00, 0x00,
+            ];
+            assert_eq!(w.len(), 80);
+            assert_eq!(&w[..4], &want[..4]);
+            assert_eq!(&w[6..74], &want[6..74]);
+        }
+        Ok(())
+    }
+
+    /// Pin offsets 46 and 66 to the rules the vendor's own serializer applies.
+    ///
+    /// The stride quantises the width up to 128 pixels and then adds one 
whole unit; both decrypted
+    /// DL7400 widths are already multiples of 128, so they cannot tell that 
apart from a plain
+    /// `hactive + 128` and the cases below deliberately do.
+    ///
+    /// The offset-66 high byte is the CTA picture aspect, which pairs of VICs 
disagree on over
+    /// identical timings, so it is per VIC and not derivable from the mode.
+    #[test]
+    fn stride_and_vic_word_match_the_vendor_rules() -> Result {
+        // (hactive, expected offset-46 stride)
+        let strides: [(u16, u16); 6] = [
+            // The two decrypted DL7400 widths.
+            (2560, 0x0a80),
+            (640, 0x0300),
+            // Widths that are not multiples of 128: a plain `hactive + 128` 
would give 0x05d6 and
+            // 0x0740, and quantising without the trailing unit would give 
0x0580 and 0x06c0.
+            (1366, 0x0600),
+            // 1600 is 12.5 units, so it quantises to 13 and the trailing unit 
makes 14 x 128.
+            (1600, 0x0700),
+            // The boundaries of one quantisation step.
+            (129, 0x0180),
+            (128, 0x0100),
+        ];
+        for (hactive, expect) in strides {
+            assert_eq!(render_stride(hactive), expect);
+        }
+
+        // (vic, expected offset-66 word)
+        let words: [(u8, u16); 7] = [
+            // Measured: 16:9 with a VIC, and a VIC past the table.
+            (4, 0x2804),
+            (16, 0x2810),
+            (63, 0x083f),
+            // No VIC at all -- the CVT-RB timings the docks actually run.
+            (0, 0x0800),
+            // 4:3, which no capture covers and which a refresh rule would 
have called 16:9.
+            (1, 0x1801),
+            (2, 0x1802),
+            // 720x480p60 again, but the 16:9 half of the pair: same timing, 
different aspect.
+            (3, 0x2803),
+        ];
+        for (vic, expect) in words {
+            assert_eq!(vic_word(vic), expect);
+        }
+        Ok(())
+    }
+
+    /// Pin offset 42 to the mode's sync polarity across both dock generations.
+    ///
+    /// 640x480p60 is the case that separates polarity from any resolution 
rule: it is the
+    /// narrowest mode in the corpus and the only one with both syncs active 
low, and the DL7400
+    /// message carries `0x0700` where a width ladder predicts the `0x0400` of 
every other mode
+    /// below 1920.
+    #[test]
+    fn sync_flags_follow_mode_polarity() -> Result {
+        // hact, htotal, hss, hse, vact, vtotal, vss, vse, clock kHz, flags, 
off42.
+        type Case = (u16, u16, u16, u16, u16, u16, u16, u16, i32, ModeFlags, 
u16);
+        let cases: [Case; 4] = [
+            // 640x480p60 DMT, -h -v: the DL7400 capture.
+            (
+                640,
+                800,
+                656,
+                752,
+                480,
+                525,
+                490,
+                492,
+                25_175,
+                ModeFlags::NHSYNC | ModeFlags::NVSYNC,
+                0x0700,
+            ),
+            // 1920x1080p60 CTA, +h +v.
+            (
+                1920,
+                2200,
+                2008,
+                2052,
+                1080,
+                1125,
+                1084,
+                1089,
+                148_500,
+                ModeFlags::PHSYNC | ModeFlags::PVSYNC,
+                0x0400,
+            ),
+            // 2560x1440p120 CVT-RB, +h -v.
+            (
+                2560,
+                2720,
+                2608,
+                2640,
+                1440,
+                1525,
+                1443,
+                1448,
+                497_750,
+                ModeFlags::PHSYNC | ModeFlags::NVSYNC,
+                0x0600,
+            ),
+            // No sample carries -h +v; the packing says it is the base plus 
the horizontal flag.
+            // Stated on a timing the corpus does not cover, because the four 
it does cover carry
+            // the polarity the capture recorded rather than the one the mode 
struct claims.
+            (
+                640,
+                800,
+                656,
+                752,
+                480,
+                525,
+                490,
+                492,
+                25_175,
+                ModeFlags::NHSYNC | ModeFlags::PVSYNC,
+                0x0500,
+            ),
+        ];
+        for (hact, htotal, hss, hse, vact, vtotal, vss, vse, clock, flags, 
off42) in cases {
+            let mode = DisplayMode::from_timings(ModeTimings {
+                clock_khz: clock,
+                hdisplay: hact,
+                hsync_start: hss,
+                hsync_end: hse,
+                htotal,
+                vdisplay: vact,
+                vsync_start: vss,
+                vsync_end: vse,
+                vtotal,
+                flags,
+            })?;
+            assert_eq!(
+                timing_from_drm_mode(&mode, 
&profile::PROFILE_RIDGE.protocol.allocation, false)?
+                    .sync_flags,
+                off42
+            );
+            assert_eq!(
+                timing_from_drm_mode(&mode, 
&profile::PROFILE_NAVARRO.protocol.allocation, false)?
+                    .sync_flags,
+                off42
+            );
+        }
+        // The teardown form carries none of it.
+        let w = clear_mode(3, 0)?;
+        assert_eq!(u16::from_le_bytes([w[42], w[43]]), 0x8000);
+        assert_eq!(w[23], 0);
+        Ok(())
+    }
+
+    #[test]
+    fn unmeasured_mode_is_accepted_with_a_derived_profile() -> Result {
+        // 2560x1440@165: no decrypted message exists for it, but the profile 
is derived rather
+        // than refused, and the DL7400's ceilings admit it. This is the mode 
the dock really runs.
+        let mode = DisplayMode::from_timings(ModeTimings {
+            clock_khz: 699_500,
+            hdisplay: 2560,
+            hsync_start: 2608,
+            hsync_end: 2640,
+            htotal: 2720,
+            vdisplay: 1440,
+            vsync_start: 1443,
+            vsync_end: 1451,
+            vtotal: 1559,
+            flags: ModeFlags::PHSYNC | ModeFlags::NVSYNC,
+        })?;
+        assert!(mode_supported(&mode));
+        // Inside the DL7400's envelope, and exactly at its clock ceiling: the 
monitor's EDID DTD
+        // says 699.50 MHz where DLM's wire value rounds to its 10 kHz unit 
(699.49), so a ceiling
+        // taken from DLM's rounding would prune this mode by 10 kHz. It must 
not.
+        assert!(
+            mode.clock() as u32
+                <= profile::PROFILE_NAVARRO
+                    .capabilities
+                    .max_connector_clock_khz
+        );
+        // The clock field itself carries it fine: offsets 70..73 are a u32, 
as the DL7400's
+        // 2560x1440p165 mode set proves (0x0001113d = 699.49 MHz). Admission 
is the refresh
+        // limit's job, not a silent conversion failure.
+        let t = timing_from_drm_mode(&mode, 
&profile::PROFILE_RIDGE.protocol.allocation, false)?;
+        assert_eq!(t.pixel_clock_10khz, (mode.clock() as u32) / 10);
+        assert!(t.pixel_clock_10khz > u32::from(u16::MAX));
+        Ok(())
+    }
+
+    /// A resolution with no capture at all must still produce a usable 
profile, so a monitor whose
+    /// native mode was never sampled is driven rather than refused.
+    #[test]
+    fn derived_profile_covers_an_unsampled_resolution() -> Result {
+        // 1680x1050@60 CVT-RB: 119.00 MHz, no CTA VIC.
+        let mode = DisplayMode::from_timings(ModeTimings {
+            clock_khz: 119_000,
+            hdisplay: 1680,
+            hsync_start: 1728,
+            hsync_end: 1760,
+            htotal: 1840,
+            vdisplay: 1050,
+            vsync_start: 1053,
+            vsync_end: 1059,
+            vtotal: 1080,
+            flags: ModeFlags::PHSYNC | ModeFlags::NVSYNC,
+        })?;
+        let t = timing_from_drm_mode(&mode, 
&profile::PROFILE_RIDGE.protocol.allocation, false)?;
+        // CVT-RB, so vertical sync is active low and horizontal is not.
+        assert_eq!(t.sync_flags, 0x0600);
+        // No VIC, so the low byte is zero and the base is the common 0x0800.
+        assert_eq!(t.vic_word, 0x0800);
+        assert_eq!(t.pixel_clock_10khz, 11_900);
+        Ok(())
+    }
+
+    #[test]
+    fn set_mode_has_head_and_exact_dlm_plaintext_length() -> Result {
+        let timing = Timing {
+            hactive: 3840,
+            hblank: 160,
+            hsync_front: 48,
+            hsync_width: 32,
+            vactive: 2160,
+            vblank: 62,
+            vsync_front: 3,
+            vsync_width: 5,
+            refresh_hz: 60,
+            pixel_clock_10khz: 0xd040,
+            sync_flags: 0x0600,
+            stride: 0x4000,
+            total_rows: 0x6000,
+            vic_word: 0x0800,
+            ten_bit: false,
+            st2084: false,
+            dual_nivo: false,
+        };
+        let m = set_mode(0x1234, 1, &timing)?;
+        assert_eq!(m.len(), 80);
+        assert_eq!(&m[0..6], &[0x48, 0x00, 0x22, 0x00, 0x34, 0x12]);
+        assert!(m[6..22].iter().all(|&x| x == 0));
+        assert_eq!(&m[22..26], &[1, 2, 0, 0]);
+        assert_eq!(u16::from_le_bytes([m[26], m[27]]), 3840);
+        assert_eq!(u16::from_le_bytes([m[34], m[35]]), 2160);
+        assert_eq!(u32::from_le_bytes([m[70], m[71], m[72], m[73]]), 0xd040);
+        assert_eq!(u16::from_le_bytes([m[68], m[69]]), 0x0200);
+        Ok(())
+    }
+
+    /// The three fields that describe an HDR connector, against the values 
read out of DLM 3.4.26.
+    ///
+    /// Offset 23 is the DMA buffer format, whose four values DLM names 
`NM16`/`NM32`/`NM24`/`NM30`
+    /// against a bytes-per-pixel table of `{2, 4, 3, 4}`; 30 bpp is `NM30` = 
3. Offset 69 is the
+    /// colour-depth enum from DLM's own `depth` switch, where 24 -> 2 and 30 
-> 3, and offset 68 is
+    /// the byte below it, zero on every enable. Offset 42 bit 6 is `ST2084 
colorspace used (HDR)`,
+    /// which rides over the sync polarity in the same word.
+    ///
+    /// The SDR half is here too: an 8-bit connector must be byte-identical to 
what it sent before
+    /// any of this existed, which is what makes the HDR half safe to land.
+    #[test]
+    fn set_mode_carries_depth_and_transfer_function() -> Result {
+        let base = Timing {
+            hactive: 2560,
+            hblank: 160,
+            hsync_front: 48,
+            hsync_width: 32,
+            vactive: 1440,
+            vblank: 85,
+            vsync_front: 3,
+            vsync_width: 8,
+            refresh_hz: 165,
+            pixel_clock_10khz: 0x1113d,
+            sync_flags: 0x0600,
+            stride: 0x0a80,
+            total_rows: 0x66db,
+            vic_word: 0x0800,
+            ten_bit: false,
+            st2084: false,
+            dual_nivo: false,
+        };
+
+        let sdr = set_mode(0x1234, 1, &base)?;
+        // An 8-bit connector still sends NM24.
+        assert_eq!(sdr[23], 2);
+        assert_eq!(u16::from_le_bytes([sdr[42], sdr[43]]), 0x0600);
+        assert_eq!(u16::from_le_bytes([sdr[68], sdr[69]]), 0x0200);
+
+        // Ten bits per channel on its own: a 10-bit SDR output is a thing a 
compositor can ask
+        // for, and it must not set the HDR bit.
+        let deep = set_mode(
+            0x1234,
+            1,
+            &Timing {
+                ten_bit: true,
+                ..base
+            },
+        )?;
+        assert_eq!(deep[23], 3); // NM30
+        assert_eq!(u16::from_le_bytes([deep[42], deep[43]]), 0x0600);
+        assert_eq!(u16::from_le_bytes([deep[68], deep[69]]), 0x0300);
+
+        // PQ on its own: the transfer function is independent of the depth.
+        let pq8 = set_mode(
+            0x1234,
+            1,
+            &Timing {
+                st2084: true,
+                ..base
+            },
+        )?;
+        assert_eq!(pq8[23], 2);
+        assert_eq!(u16::from_le_bytes([pq8[42], pq8[43]]), 0x0640);
+        assert_eq!(u16::from_le_bytes([pq8[68], pq8[69]]), 0x0200);
+
+        // What a compositor driving HDR actually produces.
+        let hdr = set_mode(
+            0x1234,
+            1,
+            &Timing {
+                ten_bit: true,
+                st2084: true,
+                ..base
+            },
+        )?;
+        assert_eq!(hdr[23], 3);
+        assert_eq!(u16::from_le_bytes([hdr[42], hdr[43]]), 0x0640);
+        assert_eq!(u16::from_le_bytes([hdr[68], hdr[69]]), 0x0300);
+        // The timing itself is untouched by either flag.
+        assert_eq!(&hdr[26..42], &sdr[26..42]);
+        assert_eq!(&hdr[44..68], &sdr[44..68]);
+        assert_eq!(&hdr[70..74], &sdr[70..74]);
+        Ok(())
+    }
+
+    /// A teardown carries no colour description at all, whatever the 
connector was doing before it.
+    ///
+    /// Offset 42 bit 15 is `(Disabled)` in DLM's decode, and it is a real 
branch: the serializer
+    /// skips every timing write when it is set. Setting an HDR bit beside it 
would be describing
+    /// a signal that is being switched off.
+    #[test]
+    fn clear_mode_carries_no_colour_description() -> Result {
+        let m = clear_mode(0x1234, 1)?;
+        // No DMA format on a teardown.
+        assert_eq!(m[23], 0);
+        assert_eq!(u16::from_le_bytes([m[42], m[43]]), 0x8000);
+        assert!(m[44..74].iter().all(|&x| x == 0));
+        Ok(())
+    }
+
+    #[test]
+    fn timing_from_drm_mode_1080p60() -> Result {
+        // CEA 1920x1080@60: clock 148.5 MHz, h 2008/2052/2200, v 
1084/1089/1125.
+        let mode = DisplayMode::from_timings(ModeTimings {
+            clock_khz: 148_500,
+            hdisplay: 1920,
+            hsync_start: 2008,
+            hsync_end: 2052,
+            htotal: 2200,
+            vdisplay: 1080,
+            vsync_start: 1084,
+            vsync_end: 1089,
+            vtotal: 1125,
+            flags: ModeFlags::PHSYNC | ModeFlags::PVSYNC,
+        })?;
+        assert_eq!(mode.cea_vic(), 16);
+        let t = timing_from_drm_mode(&mode, 
&profile::PROFILE_RIDGE.protocol.allocation, false)?;
+        assert_eq!(t.hactive, 1920);
+        assert_eq!(t.hblank, 280); // htotal - hdisplay
+        assert_eq!(t.hsync_front, 88); // hsync_start - hdisplay
+        assert_eq!(t.hsync_width, 44); // hsync_end - hsync_start
+        assert_eq!(t.vactive, 1080);
+        assert_eq!(t.vblank, 45); // vtotal - vdisplay
+        assert_eq!(t.vsync_front, 4);
+        assert_eq!(t.vsync_width, 5);
+        assert_eq!(t.pixel_clock_10khz, 14_850); // clock(kHz) / 10
+        assert_eq!(t.refresh_hz, 60); // via drm_mode_vrefresh
+        Ok(())
+    }
+}

Reply via email to