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