[PATCH v3 4/13] drm/vino: add the encrypted control plane

From: Mike Lothian

Date: Wed Aug 26 2026 - 12:49:52 EST


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 <mike@xxxxxxxxxxxxxx>
---
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(())
+ }
+}