Add the video module: the RawRl ("Raw/RLX" mode-2) encoder, clean-room
from the AArch64 reference-driver decompile, which emits packed-RGB565
frames the dock decodes without the impractical Vino Walsh-Hadamard
entropy codec. The encode/decode round-trip is unit-tested (keyframe,
differential, >256-pixel multi-block and >255 RLE run-splits all
reconstruct byte-exact); that round-trip is the correctness anchor, since
no real mode-2 capture exists to diff against.This is the codec library only; the DRM/KMS sink that drives it (vmap the framebuffer, encode, push to the EP08 video endpoint on each page-flip) is added in the next patch. Signed-off-by: Mike Lothian <[email protected]> Assisted-by: Claude:claude-opus-4-8 [Claude-Code] --- drivers/gpu/drm/vino/video.rs | 348 ++++++++++++++++++++++++++++++++++ drivers/gpu/drm/vino/vino.rs | 1 + 2 files changed, 349 insertions(+) create mode 100644 drivers/gpu/drm/vino/video.rs diff --git a/drivers/gpu/drm/vino/video.rs b/drivers/gpu/drm/vino/video.rs new file mode 100644 index 000000000000..bb5ea893575f --- /dev/null +++ b/drivers/gpu/drm/vino/video.rs @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: GPL-2.0 + +//! RawRl (Raw/RLX) **mode-2 video encoder** -- clean-room from the AArch64 DLM +//! decompile (sec 8.4 + `docs/decompile/arm64-blockencoder`/`-frame-markers`). +//! Emits packed-RGB565 frames the dock decodes WITHOUT the impractical Vino +//! Walsh-Hadamard entropy codec (sec 7.11). This is a **verbatim port** of the +//! `vino-codec::rawrl` oracle, whose encode/decode round-trip is unit-tested +//! (keyframe, differential, >256-pixel multi-block and >255 RLE run-splits all +//! reconstruct byte-exact); keep the two in lockstep. No real mode-2 capture +//! exists to diff against (sec 7.4), so that round-trip is the correctness anchor. +//! NOT yet wired into `probe()`: sending a frame the dock rejects USB-resets the +//! dock, so EP08 streaming is a supervised bring-up step. +#![allow(dead_code)] // Encoder/Mode variants validated by KUnit; live scanout uses the RLE path + +use super::*; + +pub(super) const MAGIC_RAW16: u16 = 0x68af; +pub(super) const MAGIC_RLE16: u16 = 0x69af; +/// Frame-init `0x40af` (`FUN_003330fc`: u32 `0xaf0440af` + u16 `0x0840`). +pub(super) const FRAME_INIT: [u8; 6] = [0xaf, 0x40, 0x04, 0xaf, 0x40, 0x08]; +/// Bare `0xa0af` sync (`FUN_00332a38`). +pub(super) const SYNC: [u8; 2] = [0xaf, 0xa0]; +/// Frame-end section->code table `DAT_005b7860`, indexed by `mode - 1`. +pub(super) const SECTION_CODE: [u8; 7] = [0x01, 0x00, 0x03, 0x00, 0x05, 0x07, 0x07]; +pub(super) const MAX_BLOCK_PIXELS: usize = 256; + +/// Per-run strategy: mode 0 raw-only, 1 RLE-only, 2 adaptive (sec 8.4). +#[derive(Clone, Copy)] +pub(super) enum Mode { + Raw = 0, + Rle = 1, + Adaptive = 2, +} + +/// Pack 8-bit RGB into RGB565 (the XRGB framebuffer reduced for the +/// `0x68af`/`0x69af` path). +pub(super) fn rgb565(r: u8, g: u8, b: u8) -> u16 { + ((r as u16 >> 3) << 11) | ((g as u16 >> 2) << 5) | (b as u16 >> 3) +} + +/// 6-byte block header: magic LE, 24-bit coord BE, count u8 (256 -> 0). +fn block_header(out: &mut KVec<u8>, magic: u16, coord: u32, count: usize) -> Result { + out.extend_from_slice(&magic.to_le_bytes(), GFP_KERNEL)?; + out.push(((coord >> 16) & 0xff) as u8, GFP_KERNEL)?; + out.push(((coord >> 8) & 0xff) as u8, GFP_KERNEL)?; + out.push((coord & 0xff) as u8, GFP_KERNEL)?; + out.push((count & 0xff) as u8, GFP_KERNEL)?; + Ok(()) +} + +fn encode_raw_into(out: &mut KVec<u8>, coord: u32, pix: &[u16]) -> Result { + block_header(out, MAGIC_RAW16, coord, pix.len())?; + for &p in pix { + out.extend_from_slice(&p.to_be_bytes(), GFP_KERNEL)?; + } + Ok(()) +} + +fn encode_rle_into(out: &mut KVec<u8>, coord: u32, pix: &[u16]) -> Result { + block_header(out, MAGIC_RLE16, coord, pix.len())?; + let mut i = 0; + while i < pix.len() { + let v = pix[i]; + let mut run = 1; + while i + run < pix.len() && pix[i + run] == v && run < 255 { + run += 1; + } + out.push(run as u8, GFP_KERNEL)?; + out.extend_from_slice(&v.to_be_bytes(), GFP_KERNEL)?; + i += run; + } + Ok(()) +} + +fn run_count(pix: &[u16]) -> usize { + let mut c = 0; + let mut i = 0; + while i < pix.len() { + let v = pix[i]; + let mut j = i + 1; + while j < pix.len() && pix[j] == v { + j += 1; + } + c += 1; + i = j; + } + c +} + +fn encode_run_into(out: &mut KVec<u8>, mode: Mode, coord: u32, pix: &[u16]) -> Result { + match mode { + Mode::Raw => encode_raw_into(out, coord, pix), + Mode::Rle => encode_rle_into(out, coord, pix), + Mode::Adaptive => { + let l = pix.len(); + let c = run_count(pix); + if 2 * l < 3 * c + 1 { + encode_raw_into(out, coord, pix) + } else { + encode_rle_into(out, coord, pix) + } + } + } +} + +/// Mode-2 frame encoder holding the shadow (previous-frame) buffer. +pub(super) struct Encoder { + width: usize, + height: usize, + mode: Mode, + // vmalloc-backed: a `width*height` u16 buffer is ~4 MiB at 1080p, far above the + // contiguous-kmalloc order limit (the page allocator WARNs and fails on it). + shadow: VVec<u16>, +} + +impl Encoder { + pub(super) fn new(width: usize, height: usize, mode: Mode) -> Result<Self> { + let shadow = VVec::from_elem(0u16, width * height, GFP_KERNEL)?; + Ok(Self { width, height, mode, shadow }) + } + + /// Encode `cur` (RGB565) into a mode-2 marker stream; updates the shadow. + /// Change-detection is per row; changed runs chunk into <=256-px blocks. + pub(super) fn encode(&mut self, cur: &[u16]) -> Result<KVec<u8>> { + let mut s = KVec::new(); + self.encode_into(cur, &mut s)?; + Ok(s) + } + + /// Like [`encode`](Self::encode) but appends the marker stream to a caller-owned + /// `out` instead of allocating a fresh `KVec`. The hot scanout path + /// ([`encode_and_send`](super::drm_sink::encode_and_send)) uses this to encode + /// straight into a buffer that already reserves the EP08 transport header, so a + /// frame costs one allocation with no separate framing copy. + pub(super) fn encode_into(&mut self, cur: &[u16], s: &mut KVec<u8>) -> Result { + s.extend_from_slice(&FRAME_INIT, GFP_KERNEL)?; + for y in 0..self.height { + let row = y * self.width; + let mut x = 0; + while x < self.width { + while x < self.width && cur[row + x] == self.shadow[row + x] { + x += 1; + } + if x >= self.width { + break; + } + let run_start = x; + while x < self.width && cur[row + x] != self.shadow[row + x] { + x += 1; + } + let run_end = x; + let mut p = run_start; + while p < run_end { + let n = (run_end - p).min(MAX_BLOCK_PIXELS); + let coord = (((row + p) * 2) & 0xff_ffff) as u32; + encode_run_into(s, self.mode, coord, &cur[row + p..row + p + n])?; + p += n; + } + for k in run_start..run_end { + self.shadow[row + k] = cur[row + k]; + } + } + } + let code = SECTION_CODE[(self.mode as usize).saturating_sub(1).min(6)]; + s.extend_from_slice(&SYNC, GFP_KERNEL)?; + s.extend_from_slice(&[0xaf, 0x20, 0x1f, code], GFP_KERNEL)?; + s.extend_from_slice(&[0xaf, 0x20, 0xff, 0x00], GFP_KERNEL)?; + s.extend_from_slice(&SYNC, GFP_KERNEL)?; + Ok(()) + } +} + +/// Vino (`0x2801`) Walsh-Hadamard codec -- the bandwidth-constrained / 4K path (the RLE path +/// above is what the dock currently runs; this is the lossy transform codec DLM uses when raw/ +/// RLE won't fit the USB budget). See `docs/WHT-CODEC.md` + `docs/VIDEO.md`. +/// +/// **Scope.** The colour transform, the quantizer, and the 2-level Walsh-Hadamard transform +/// are reverse-engineered and **validated offline** (`white -> Y_DC=16320 -> quantized 1020`; +/// achromatic -> `Cb=Cr=0`; uniform block -> DC=mean, AC=0). The token *bit format* (5-bit +/// short +/// 0..=30 / 17-bit long, MSB-first) and the **token-value mapping** are confirmed against DLM's +/// own frida token trace (`captures/02-solid-white/tokens.jsonl`): the **token value is the +/// quantized coefficient, directly** -- pure-white strips emit `L,1020` exactly where +/// `quantize(16320, DC) = 1020`, so the rumoured "entropy codebook" is just this direct value +/// encoding, not a lookup table (the 1641-byte expression-tree coder is the bit-packer). **What +/// is still NOT generated here:** the per-strip *framing* -- a uniform strip wraps the DC in a +/// constant prefix/suffix of framing tokens (`L,2048 L,3072 ... L,3 ... S,19 S,16 ...`) plus +/// zero-run +/// AC coding, and the dock's exact sequency ordering -- so a complete `Mode::Wht` would replay +/// the recovered uniform-strip template with the DC substituted (the `docs/WHT-CODEC.md` +/// structural model, ~90% desktop coverage). Until that framing is generalized + wired, the +/// scanout path keeps using RLE. +// Not yet wired into the scanout path (the per-strip framing template is recovered for white +// but not yet generalized to arbitrary uniform colour / non-uniform content) -- RLE stays the +// active codec; this module is validated by its KUnit tests + the frida-trace value mapping. +#[allow(dead_code)] // Walsh-Hadamard codec: KUnit-validated, not yet on the live scanout path +pub(super) mod wht { + use super::*; + + /// 4x8 transform block geometry (`docs/VIDEO.md`): 4 rows x 8 columns = 32 samples. + pub(super) const ROWS: usize = 4; + pub(super) const COLS: usize = 8; + pub(super) const BLOCK: usize = ROWS * COLS; + + /// Vino colour transform (`docs/VIDEO.md`, exact integer form, no rounding): + /// `Y = 16R + 32G + 16B`, `Cb = 64(R-G)`, `Cr = 64(B-G)`. Achromatic (R=G=B) -> + /// Cb=Cr=0. + pub(super) fn colour(r: u8, g: u8, b: u8) -> (i32, i32, i32) { + let (r, g, b) = (r as i32, g as i32, b as i32); + (16 * r + 32 * g + 16 * b, 64 * (r - g), 64 * (b - g)) + } + + /// Per-coefficient `(bias, step)` quantization table (`docs/VIDEO.md` `FUN_0077b140`), + /// keyed by coefficient position `0..64`. + fn bias_step(i: usize) -> (i32, i32) { + match i { + 0..=2 => (8, 16), + 3 => (16, 32), + 4..=11 => (2, 4), + 12..=15 => (4, 8), + 16..=47 => (1, 2), + _ => (2, 4), // 48..=63 + } + } + + /// Quantize coefficient `coeff` at position `i`: `(coeff + bias) * (65536/step) >> 16`, + /// the fixed-point form of `(coeff + bias) / step` (`docs/VIDEO.md`). Clamped to the + /// 12-bit signed long-token range (the DC is wider than the +/-127 AC clip -- the + /// documented + /// `white -> 1020` vector is a 12-bit long token, not a +/-127 value). + pub(super) fn quantize(coeff: i32, i: usize) -> i32 { + let (bias, step) = bias_step(i); + let scale = 65536 / step; + (((coeff + bias) * scale) >> 16).clamp(-2048, 2047) + } + + /// In-place 1-D Walsh-Hadamard (natural/Hadamard order) on a power-of-two slice, + /// unnormalized (pairwise sums/differences); the 2-D transform normalizes afterwards. + fn hadamard_1d(v: &mut [i32]) { + let n = v.len(); + let mut h = 1; + while h < n { + let mut i = 0; + while i < n { + for j in i..i + h { + let (a, b) = (v[j], v[j + h]); + v[j] = a + b; + v[j + h] = a - b; + } + i += 2 * h; + } + h *= 2; + } + } + + /// 2-level separable Walsh-Hadamard transform of a 4x8 `block` (row-major), normalized so + /// the DC coefficient equals the block **mean** -- i.e. a uniform block yields `DC = the + /// per-pixel value` and all AC = 0 (`docs/VIDEO.md`). Returns 32 coefficients row-major. + /// (Natural Hadamard order; the dock's sequency reorder is not bit-matched -- see the + /// module note.) + pub(super) fn transform(block: &[i32; BLOCK]) -> [i32; BLOCK] { + let mut m = *block; + for r in 0..ROWS { + hadamard_1d(&mut m[r * COLS..r * COLS + COLS]); + } + let mut col = [0i32; ROWS]; + for c in 0..COLS { + for r in 0..ROWS { + col[r] = m[r * COLS + c]; + } + hadamard_1d(&mut col); + for r in 0..ROWS { + m[r * COLS + c] = col[r]; + } + } + // Normalize by the block size (/32 = >>5) so DC = mean (uniform block -> DC = value). + for x in m.iter_mut() { + *x >>= 5; + } + m + } + + /// MSB-first bit packer for the Vino token stream (`docs/VIDEO.md`): a 16-bit zero pad at + /// the start, then codewords packed most-significant-bit first across byte boundaries. + pub(super) struct TokenWriter { + out: KVec<u8>, + acc: u32, + nbits: u32, + } + + impl TokenWriter { + pub(super) fn new() -> Result<Self> { + let mut w = Self { out: KVec::new(), acc: 0, nbits: 0 }; + w.put(0, 16)?; // 16-bit zero pad at stream start + Ok(w) + } + + /// Append the low `n` bits of `val` (n <= 24), MSB-first. + fn put(&mut self, val: u32, n: u32) -> Result { + self.acc = (self.acc << n) | (val & ((1u32 << n) - 1)); + self.nbits += n; + while self.nbits >= 8 { + self.nbits -= 8; + self.out.push(((self.acc >> self.nbits) & 0xff) as u8, GFP_KERNEL)?; + } + Ok(()) + } + + /// Write one token *value* in the Vino short/long encoding: a 5-bit short token for + /// `0..=30`, else the 17-bit long token `0b11111` escape + 12-bit value. (The mapping + /// from a quantized coefficient to this `value` is the un-RE'd entropy codebook -- see + /// the module note -- so callers can only pack values they already know.) + pub(super) fn token(&mut self, value: u16) -> Result { + if value <= 30 { + self.put(value as u32, 5) + } else { + self.put(0b11111, 5)?; + self.put((value & 0x0fff) as u32, 12) + } + } + + /// Flush any partial byte (zero-padded) and return the packed stream. + pub(super) fn finish(mut self) -> Result<KVec<u8>> { + if self.nbits > 0 { + let pad = 8 - self.nbits; + self.put(0, pad)?; + } + Ok(self.out) + } + } +} + +/// Length of the EP08 transport header ([`write_ep08_header`]). +pub(super) const EP08_HDR_LEN: usize = 16; + +/// Write the 16-byte EP08 transport header into `hdr` for a `payload_len`-byte codec +/// stream: `type=4 sub=0x30 sub_len_dw=0` sec 3 framing (matches the live capture). +/// `size = payload_len + 12`. Used by the in-place scanout path. `hdr` must be at +/// least 16 bytes. +pub(super) fn write_ep08_header(hdr: &mut [u8], payload_len: usize, seq: u32) { + hdr[0] = 0; + hdr[1] = 0; + hdr[2..4].copy_from_slice(&((payload_len + 12) as u16).to_le_bytes()); + hdr[4..8].copy_from_slice(&4u32.to_le_bytes()); + hdr[8..10].copy_from_slice(&0x30u16.to_le_bytes()); + hdr[10..12].copy_from_slice(&0u16.to_le_bytes()); + hdr[12..16].copy_from_slice(&seq.to_le_bytes()); +} diff --git a/drivers/gpu/drm/vino/vino.rs b/drivers/gpu/drm/vino/vino.rs index ef44a625cb70..e9e6324b717b 100644 --- a/drivers/gpu/drm/vino/vino.rs +++ b/drivers/gpu/drm/vino/vino.rs @@ -86,6 +86,7 @@ fn timeout() -> Delta { mod ake; mod golden; mod cp; +mod video; /// The shared secrets a completed HDCP 2.2 AKE leaves behind: the SKE session key /// `ks` and content IV `riv` key the AES-CTR control plane (sec 6), and `kd` is kept -- 2.54.0
